Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2c38ce811f | |||
| 0c2b7ef132 | |||
| e04f1e6086 | |||
| ae27830893 | |||
| 84cb5c66b5 |
Generated
+4
-4
@@ -10,7 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@abe-kap/appshell-sdk": "^0.2.6",
|
"@abe-kap/appshell-sdk": "^0.2.6",
|
||||||
"@insignia/iios-kernel-client": "^0.1.4",
|
"@insignia/iios-kernel-client": "^0.1.4",
|
||||||
"@insignia/iios-messaging-ui": "^0.1.0",
|
"@insignia/iios-messaging-ui": "^0.1.4",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^1.21.0",
|
"lucide-react": "^1.21.0",
|
||||||
"next": "16.2.9",
|
"next": "16.2.9",
|
||||||
@@ -1028,9 +1028,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@insignia/iios-messaging-ui": {
|
"node_modules/@insignia/iios-messaging-ui": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.4",
|
||||||
"resolved": "https://git.lynkedup.cloud/api/packages/insignia/npm/%40insignia%2Fiios-messaging-ui/-/0.1.0/iios-messaging-ui-0.1.0.tgz",
|
"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-iGOh/lwJJm+QG2u5YP1Kj4gZyShRWO1o7/dxnl+DE1m/WbE0PDO+baTJ7NFhremubSB7Wc/KSLhf9yLdLbivAQ==",
|
"integrity": "sha512-xgwVPY//NFyCrGxuLpLlrXn89q2SMgQoKTiW46CWdLx6rkaaxSesVEijGEoYt25rVPVF9+RcDPbwIBQ2GDtHQw==",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@insignia/iios-kernel-client": "*",
|
"@insignia/iios-kernel-client": "*",
|
||||||
"react": ">=18",
|
"react": ">=18",
|
||||||
|
|||||||
+1
-1
@@ -11,7 +11,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@abe-kap/appshell-sdk": "^0.2.6",
|
"@abe-kap/appshell-sdk": "^0.2.6",
|
||||||
"@insignia/iios-kernel-client": "^0.1.4",
|
"@insignia/iios-kernel-client": "^0.1.4",
|
||||||
"@insignia/iios-messaging-ui": "^0.1.0",
|
"@insignia/iios-messaging-ui": "^0.1.4",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^1.21.0",
|
"lucide-react": "^1.21.0",
|
||||||
"next": "16.2.9",
|
"next": "16.2.9",
|
||||||
|
|||||||
@@ -93,8 +93,16 @@ export class CrmInboxAdapter implements InboxAdapter {
|
|||||||
return { contentRef: objectKey, mimeType: mime, sizeBytes: file.size, filename: file.name };
|
return { contentRef: objectKey, mimeType: mime, sizeBytes: file.size, filename: file.name };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async downloadAttachment(attachment: MailAttachment): Promise<string> {
|
||||||
|
const { url } = await this.sdk.command<{ url: string }>("crm.media.presignDownload", {
|
||||||
|
contentRef: attachment.contentRef,
|
||||||
|
...(attachment.mimeType ? { mime: attachment.mimeType } : {}),
|
||||||
|
});
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
async directory(): Promise<MailPerson[]> {
|
async directory(): Promise<MailPerson[]> {
|
||||||
const rows = await this.sdk.query<DirectoryDTO[]>("crm.messenger.directory", { kind: "all", limit: 200 });
|
const rows = await this.sdk.query<DirectoryDTO[]>("crm.messenger.directory", { kind: "all", limit: 100 });
|
||||||
return rows.map((d) => ({ id: d.id, name: d.displayName, kind: d.kind }));
|
return rows.map((d) => ({ id: d.id, name: d.displayName, kind: d.kind }));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
// When no socket is available (token failed / demo), it degrades to a 4s history poll.
|
// When no socket is available (token failed / demo), it degrades to a 4s history poll.
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
|
Attachment,
|
||||||
ChannelSummary,
|
ChannelSummary,
|
||||||
ChannelVisibility,
|
ChannelVisibility,
|
||||||
Conversation,
|
Conversation,
|
||||||
@@ -32,7 +33,7 @@ interface ConversationDTO {
|
|||||||
threadId: string; subject: string | null; membership: Membership | null;
|
threadId: string; subject: string | null; membership: Membership | null;
|
||||||
participants: string[]; unread: number; lastMessage?: string; lastAt?: string;
|
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 POLL_MS = 4000;
|
||||||
const REACTION = "reaction";
|
const REACTION = "reaction";
|
||||||
@@ -58,7 +59,7 @@ export class CrmMessagingAdapter implements MessagingAdapter {
|
|||||||
if (socket) {
|
if (socket) {
|
||||||
socket.on("message", (m) => {
|
socket.on("message", (m) => {
|
||||||
this.ingestReactions(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 }));
|
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.
|
// 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) {
|
if (this.socket) {
|
||||||
const res = await this.socket.openThread(threadId); // joins so live events flow
|
const res = await this.socket.openThread(threadId); // joins so live events flow
|
||||||
this.joined.add(threadId);
|
this.joined.add(threadId);
|
||||||
return res.history.map((m) => {
|
return Promise.all(
|
||||||
|
res.history.map((m) => {
|
||||||
this.ingestReactions(m);
|
this.ingestReactions(m);
|
||||||
return this.fromKernel(m);
|
return this.toKernelMessage(m);
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
const msgs = await this.sdk.query<MessageDTO[]>("crm.messenger.history", { threadId });
|
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> {
|
async send(threadId: string, content: string, opts?: SendOpts): Promise<Message> {
|
||||||
|
const att = opts?.attachment;
|
||||||
if (this.socket) {
|
if (this.socket) {
|
||||||
const sendOpts = {
|
const sendOpts = {
|
||||||
...(opts?.parentInteractionId ? { parentInteractionId: opts.parentInteractionId } : {}),
|
...(opts?.parentInteractionId ? { parentInteractionId: opts.parentInteractionId } : {}),
|
||||||
...(opts?.mentions && opts.mentions.length ? { mentions: opts.mentions } : {}),
|
...(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);
|
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 m = await this.sdk.command<MessageDTO>("crm.messenger.send", { threadId, content });
|
||||||
const msg = this.fromDto(m);
|
const msg = this.fromDto(m);
|
||||||
this.polls.get(threadId)?.seen.add(msg.id);
|
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 {
|
subscribe(threadId: string, cb: (e: MessageEvent) => void): Unsubscribe {
|
||||||
@@ -221,7 +237,7 @@ export class CrmMessagingAdapter implements MessagingAdapter {
|
|||||||
|
|
||||||
/** The org directory — people you can start a DM/group with. Drives the "New message" picker. */
|
/** The org directory — people you can start a DM/group with. Drives the "New message" picker. */
|
||||||
async directory(): Promise<Person[]> {
|
async directory(): Promise<Person[]> {
|
||||||
const dir = await this.sdk.query<DirectoryDTO[]>("crm.messenger.directory", { kind: "all", limit: 200 });
|
const dir = await this.sdk.query<DirectoryDTO[]>("crm.messenger.directory", { kind: "all", limit: 100 });
|
||||||
return dir.map((d) => ({ id: d.id, name: d.displayName, kind: d.kind }));
|
return dir.map((d) => ({ id: d.id, name: d.displayName, kind: d.kind }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,6 +281,31 @@ export class CrmMessagingAdapter implements MessagingAdapter {
|
|||||||
return { id: m.interactionId, actorId: m.actorId, text: m.text ?? "", at: m.occurredAt };
|
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 ─────────────────────────────────────────────
|
// ── reaction state ─────────────────────────────────────────────
|
||||||
private ingestReactions(m: KernelMessage): void {
|
private ingestReactions(m: KernelMessage): void {
|
||||||
for (const a of m.annotations ?? []) {
|
for (const a of m.annotations ?? []) {
|
||||||
|
|||||||
Reference in New Issue
Block a user