From 413f8d43b67c408b981b6f01d4ba12d8b84e83a3 Mon Sep 17 00:00:00 2001 From: maaz519 Date: Tue, 21 Jul 2026 23:35:41 +0530 Subject: [PATCH] feat(messenger): channels + @mentions in the CRM SDK adapter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CrmMessagingAdapter now implements the full SDK contract: - browseChannels/createChannel/leaveChannel over the be-crm channel door (crm.messenger.channel.*); joinChannel is a governed public self-join over the socket (openThread) — the BFF has no join verb, OPA enforces it - listMembers over crm.messenger.members → drives @mention autocomplete - send forwards mentions[] on the socket path (→ IIOS MENTION inbox items) So the CRM messenger (rendered by the SDK) now gets sectioned Channels/DMs, browse+join+create, and @mention autocomplete/highlight — no embedded code. (SDK re-packed to the local tarball; publish for deploy.) tsc + next build green. Co-Authored-By: Claude Opus 4.8 --- package-lock.json | 2 +- src/lib/crm-messaging-adapter.ts | 54 +++++++++++++++++++++++++++++--- 2 files changed, 50 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 714de0f..d660907 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1030,7 +1030,7 @@ "node_modules/@insignia/iios-messaging-ui": { "version": "0.1.0", "resolved": "file:../iios/packages/iios-messaging-ui/insignia-iios-messaging-ui-0.1.0.tgz", - "integrity": "sha512-6pSVWACjex1sXWzVqx+42rM6vgWEJ+1ud5y3DLKogJiQmfgFZgCB8vTw0PN/5FECugXC7iGuc9rV9RsHsP+EDw==", + "integrity": "sha512-B6Vzb75lBv4W3ZuKE4muMSz/OyUrE0NxImUI6sHaCBby5xveHVyanPiwkxiaqg9zZPYQMYiZRMONYwlNqyJvig==", "peerDependencies": { "@insignia/iios-kernel-client": "*", "react": ">=18" diff --git a/src/lib/crm-messaging-adapter.ts b/src/lib/crm-messaging-adapter.ts index c340fdd..0ecb00e 100644 --- a/src/lib/crm-messaging-adapter.ts +++ b/src/lib/crm-messaging-adapter.ts @@ -6,11 +6,15 @@ // When no socket is available (token failed / demo), it degrades to a 4s history poll. import type { + ChannelSummary, + ChannelVisibility, Conversation, + CreateChannelInput, Membership, Message, MessageEvent, MessagingAdapter, + Person, Reaction, SendOpts, Unsubscribe, @@ -106,11 +110,11 @@ export class CrmMessagingAdapter implements MessagingAdapter { async send(threadId: string, content: string, opts?: SendOpts): Promise { if (this.socket) { - const m = await this.socket.sendMessage( - threadId, - content, - opts?.parentInteractionId ? { parentInteractionId: opts.parentInteractionId } : undefined, - ); + const sendOpts = { + ...(opts?.parentInteractionId ? { parentInteractionId: opts.parentInteractionId } : {}), + ...(opts?.mentions && opts.mentions.length ? { mentions: opts.mentions } : {}), + }; + const m = await this.socket.sendMessage(threadId, content, Object.keys(sendOpts).length ? sendOpts : undefined); return this.fromKernel(m); } const m = await this.sdk.command("crm.messenger.send", { threadId, content }); @@ -152,6 +156,46 @@ export class CrmMessagingAdapter implements MessagingAdapter { if (this.socket) await this.socket.markRead(threadId, messageId); } + // ── channels + members (BFF, except join which is a governed socket self-join) ── + async browseChannels(): Promise { + const rows = await this.sdk.query>( + "crm.messenger.channel.browse", + {}, + ); + return rows.map((c) => ({ + threadId: c.threadId, + name: c.name, + topic: c.topic, + visibility: (c.visibility === "private" ? "private" : "public") as ChannelVisibility, + memberCount: c.memberCount, + joined: c.joined, + })); + } + + async createChannel(input: CreateChannelInput): Promise<{ threadId: string }> { + return this.sdk.command<{ threadId: string }>("crm.messenger.channel.create", { + name: input.name, + ...(input.topic ? { topic: input.topic } : {}), + visibility: input.visibility, + }); + } + + async joinChannel(threadId: string): Promise { + // Governed public self-join over the socket (the BFF has no join verb; OPA enforces it). + if (!this.socket) throw new Error("joining a channel needs a live connection"); + await this.socket.openThread(threadId); + this.joined.add(threadId); + } + + async leaveChannel(threadId: string): Promise { + await this.sdk.command("crm.messenger.channel.leave", { threadId }); + } + + async listMembers(threadId: string): Promise { + const rows = await this.sdk.query>("crm.messenger.members", { threadId }); + return rows.map((r) => ({ id: r.userId, name: r.displayName, kind: "staff" as const })); + } + // ── polling fallback (no socket) ─────────────────────────────── private startPoll(threadId: string): void { if (this.polls.has(threadId)) return;