Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c38ce811f | |||
| 0c2b7ef132 |
Generated
+4
-4
@@ -10,7 +10,7 @@
|
||||
"dependencies": {
|
||||
"@abe-kap/appshell-sdk": "^0.2.6",
|
||||
"@insignia/iios-kernel-client": "^0.1.4",
|
||||
"@insignia/iios-messaging-ui": "^0.1.2",
|
||||
"@insignia/iios-messaging-ui": "^0.1.4",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.21.0",
|
||||
"next": "16.2.9",
|
||||
@@ -1028,9 +1028,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@insignia/iios-messaging-ui": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://git.lynkedup.cloud/api/packages/insignia/npm/%40insignia%2Fiios-messaging-ui/-/0.1.2/iios-messaging-ui-0.1.2.tgz",
|
||||
"integrity": "sha512-YsoTM9vmjQ+dk6Hch5nXuqMFAdybd3luAZepfaliOw727ylc8FPAtwTMMcIWQ3WOQPa2eKeGLiEpo61QCO0GrA==",
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://git.lynkedup.cloud/api/packages/insignia/npm/%40insignia%2Fiios-messaging-ui/-/0.1.4/iios-messaging-ui-0.1.4.tgz",
|
||||
"integrity": "sha512-xgwVPY//NFyCrGxuLpLlrXn89q2SMgQoKTiW46CWdLx6rkaaxSesVEijGEoYt25rVPVF9+RcDPbwIBQ2GDtHQw==",
|
||||
"peerDependencies": {
|
||||
"@insignia/iios-kernel-client": "*",
|
||||
"react": ">=18",
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@
|
||||
"dependencies": {
|
||||
"@abe-kap/appshell-sdk": "^0.2.6",
|
||||
"@insignia/iios-kernel-client": "^0.1.4",
|
||||
"@insignia/iios-messaging-ui": "^0.1.2",
|
||||
"@insignia/iios-messaging-ui": "^0.1.4",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.21.0",
|
||||
"next": "16.2.9",
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
// When no socket is available (token failed / demo), it degrades to a 4s history poll.
|
||||
|
||||
import type {
|
||||
Attachment,
|
||||
ChannelSummary,
|
||||
ChannelVisibility,
|
||||
Conversation,
|
||||
@@ -32,7 +33,7 @@ interface ConversationDTO {
|
||||
threadId: string; subject: string | null; membership: Membership | null;
|
||||
participants: string[]; unread: number; lastMessage?: string; lastAt?: string;
|
||||
}
|
||||
interface MessageDTO { interactionId: string; actorId: string | null; kind: string; occurredAt: string; text: string | null }
|
||||
interface MessageDTO { interactionId: string; actorId: string | null; kind: string; occurredAt: string; text: string | null; attachment?: { contentRef: string; mimeType: string; sizeBytes: number; filename: string | null } | null }
|
||||
|
||||
const POLL_MS = 4000;
|
||||
const REACTION = "reaction";
|
||||
@@ -58,7 +59,7 @@ export class CrmMessagingAdapter implements MessagingAdapter {
|
||||
if (socket) {
|
||||
socket.on("message", (m) => {
|
||||
this.ingestReactions(m);
|
||||
this.emit(m.threadId, { kind: "message", message: this.fromKernel(m) });
|
||||
void this.toKernelMessage(m).then((message) => this.emit(m.threadId, { kind: "message", message }));
|
||||
});
|
||||
socket.on("typing", (e) => this.emit(e.threadId, { kind: "typing", userId: e.userId }));
|
||||
// Receipts carry no threadId → fan to all open threads; the UI filters by messageId.
|
||||
@@ -99,28 +100,43 @@ export class CrmMessagingAdapter implements MessagingAdapter {
|
||||
if (this.socket) {
|
||||
const res = await this.socket.openThread(threadId); // joins so live events flow
|
||||
this.joined.add(threadId);
|
||||
return res.history.map((m) => {
|
||||
this.ingestReactions(m);
|
||||
return this.fromKernel(m);
|
||||
});
|
||||
return Promise.all(
|
||||
res.history.map((m) => {
|
||||
this.ingestReactions(m);
|
||||
return this.toKernelMessage(m);
|
||||
}),
|
||||
);
|
||||
}
|
||||
const msgs = await this.sdk.query<MessageDTO[]>("crm.messenger.history", { threadId });
|
||||
return msgs.map((m) => this.fromDto(m));
|
||||
return Promise.all(msgs.map((m) => this.toDtoMessage(m)));
|
||||
}
|
||||
|
||||
async send(threadId: string, content: string, opts?: SendOpts): Promise<Message> {
|
||||
const att = opts?.attachment;
|
||||
if (this.socket) {
|
||||
const sendOpts = {
|
||||
...(opts?.parentInteractionId ? { parentInteractionId: opts.parentInteractionId } : {}),
|
||||
...(opts?.mentions && opts.mentions.length ? { mentions: opts.mentions } : {}),
|
||||
...(att?.contentRef ? { attachment: { contentRef: att.contentRef, mimeType: att.mime, sizeBytes: att.sizeBytes ?? 0 } } : {}),
|
||||
};
|
||||
const m = await this.socket.sendMessage(threadId, content, Object.keys(sendOpts).length ? sendOpts : undefined);
|
||||
return this.fromKernel(m);
|
||||
const msg = this.fromKernel(m);
|
||||
// Reuse the staged attachment (already carries a display URL from upload) for instant render.
|
||||
return att ? { ...msg, attachment: att } : msg;
|
||||
}
|
||||
const m = await this.sdk.command<MessageDTO>("crm.messenger.send", { threadId, content });
|
||||
const msg = this.fromDto(m);
|
||||
this.polls.get(threadId)?.seen.add(msg.id);
|
||||
return msg;
|
||||
return att ? { ...msg, attachment: att } : msg;
|
||||
}
|
||||
|
||||
async upload(file: File): Promise<Attachment> {
|
||||
const mime = file.type || "application/octet-stream";
|
||||
const { objectKey, uploadUrl } = await this.sdk.command<{ objectKey: string; uploadUrl: string }>("crm.media.presignUpload", { mime, sizeBytes: file.size });
|
||||
const res = await fetch(uploadUrl, { method: "PUT", body: file });
|
||||
if (!res.ok) throw new Error(`Upload failed (${res.status}).`);
|
||||
const url = await this.downloadUrl(objectKey, mime);
|
||||
return { url, mime, name: file.name, contentRef: objectKey, sizeBytes: file.size };
|
||||
}
|
||||
|
||||
subscribe(threadId: string, cb: (e: MessageEvent) => void): Unsubscribe {
|
||||
@@ -265,6 +281,31 @@ export class CrmMessagingAdapter implements MessagingAdapter {
|
||||
return { id: m.interactionId, actorId: m.actorId, text: m.text ?? "", at: m.occurredAt };
|
||||
}
|
||||
|
||||
// ── attachments ────────────────────────────────────────────────
|
||||
/** A short-lived signed URL to display/download a stored object. */
|
||||
private async downloadUrl(contentRef: string, mime?: string): Promise<string> {
|
||||
const { url } = await this.sdk.command<{ url: string }>("crm.media.presignDownload", { contentRef, ...(mime ? { mime } : {}) });
|
||||
return url;
|
||||
}
|
||||
|
||||
private async resolveAttachment(a: { contentRef: string; mimeType: string; sizeBytes: number; filename?: string | null } | null | undefined): Promise<Attachment | undefined> {
|
||||
if (!a?.contentRef) return undefined;
|
||||
const url = await this.downloadUrl(a.contentRef, a.mimeType);
|
||||
return { url, mime: a.mimeType, name: a.filename ?? "attachment", contentRef: a.contentRef, sizeBytes: a.sizeBytes };
|
||||
}
|
||||
|
||||
private async toKernelMessage(m: KernelMessage): Promise<Message> {
|
||||
const base = this.fromKernel(m);
|
||||
const att = await this.resolveAttachment(m.attachment ?? null);
|
||||
return att ? { ...base, attachment: att } : base;
|
||||
}
|
||||
|
||||
private async toDtoMessage(m: MessageDTO): Promise<Message> {
|
||||
const base = this.fromDto(m);
|
||||
const att = await this.resolveAttachment(m.attachment ?? null);
|
||||
return att ? { ...base, attachment: att } : base;
|
||||
}
|
||||
|
||||
// ── reaction state ─────────────────────────────────────────────
|
||||
private ingestReactions(m: KernelMessage): void {
|
||||
for (const a of m.annotations ?? []) {
|
||||
|
||||
Reference in New Issue
Block a user