4faf05fee9
Add a 'New message' + on the Direct messages section that opens a people picker
(NewConversation): pick one person -> DM, two or more -> group with an optional
name, via the adapter's existing openThread({participantIds, membership, subject}).
Expose directory() on MessagingAdapter (org people list); MockAdapter implements
it + dedupes 1:1 DMs. 72 tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
84 lines
3.2 KiB
TypeScript
84 lines
3.2 KiB
TypeScript
import type {
|
|
Attachment,
|
|
ChannelSummary,
|
|
Conversation,
|
|
CreateChannelInput,
|
|
Membership,
|
|
Message,
|
|
MessageEvent,
|
|
Person,
|
|
SendOpts,
|
|
Unsubscribe,
|
|
} from './types';
|
|
|
|
/**
|
|
* The one seam of this SDK. Hosts implement this; the SDK renders it.
|
|
*
|
|
* Lifted from lynkeduppro-crm's MessengerData/ThreadData, which already survived
|
|
* two implementations (live data-door + mock) — the minimum real evidence that a
|
|
* seam is genuine rather than imagined.
|
|
*
|
|
* Optional methods degrade gracefully: the UI hides the reaction picker when
|
|
* `react` is absent, and the attach button when `upload` is absent. That is how one
|
|
* component set serves both a full CRM messenger and a stripped-down widget with no
|
|
* `mode` prop.
|
|
*/
|
|
export interface MessagingAdapter {
|
|
listConversations(): Promise<Conversation[]>;
|
|
|
|
openThread(p: { participantIds: string[]; membership?: Membership; subject?: string }): Promise<{ threadId: string }>;
|
|
|
|
history(threadId: string): Promise<Message[]>;
|
|
|
|
send(threadId: string, content: string, opts?: SendOpts): Promise<Message>;
|
|
|
|
/** Returns an unsubscribe fn. Implementations MUST be idempotent on repeat unsubscribe. */
|
|
subscribe(threadId: string, cb: (e: MessageEvent) => void): Unsubscribe;
|
|
|
|
sendTyping(threadId: string): void;
|
|
|
|
markRead(threadId: string, messageId: string): Promise<void>;
|
|
|
|
/**
|
|
* The current user's actor id, or null if not yet known.
|
|
*
|
|
* MUST NOT be inferred from message history. The CRM's bug was exactly that:
|
|
* scanning for a sent message meant every message read as not-yours until you
|
|
* had spoken. Adapters derive this from auth/session.
|
|
*/
|
|
currentActorId(): string | null;
|
|
|
|
/** Absent => the UI hides reactions entirely. */
|
|
react?(threadId: string, messageId: string, emoji: string): Promise<void>;
|
|
|
|
/** Absent => the UI hides attachments. Storage/auth/limits are the host's concern. */
|
|
upload?(file: File): Promise<Attachment>;
|
|
|
|
/** Absent => treated as always connected (e.g. a pure-REST adapter). */
|
|
isConnected?(): boolean;
|
|
|
|
/** A thread's members (id + display name), for @mention autocomplete + highlighting.
|
|
* Absent => the composer offers no autocomplete (you can still type @text). */
|
|
listMembers?(threadId: string): Promise<Person[]>;
|
|
|
|
/** The people you can start a conversation with (org directory). Drives the "New message"
|
|
* people picker. Absent => the UI hides DM/group creation (you can still open channels). */
|
|
directory?(): Promise<Person[]>;
|
|
|
|
// ── Channels (optional capability) ──────────────────────────────
|
|
// A channel is just a third membership beyond dm/group: a discoverable, joinable room.
|
|
// Implement all four to enable the channels UI; absent => the UI hides channels entirely.
|
|
|
|
/** Discoverable channels in the caller's scope, each flagged `joined`. */
|
|
browseChannels?(): Promise<ChannelSummary[]>;
|
|
|
|
/** Create a channel; the creator joins as admin. Returns the new thread id. */
|
|
createChannel?(input: CreateChannelInput): Promise<{ threadId: string }>;
|
|
|
|
/** Join a (public) channel by id. */
|
|
joinChannel?(threadId: string): Promise<void>;
|
|
|
|
/** Leave a channel by id. */
|
|
leaveChannel?(threadId: string): Promise<void>;
|
|
}
|