@pmate/chat-sdk 0.12.0-next.64 → 0.12.0-next.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -2
- package/lib/chat.d.ts +19 -13
- package/lib/chat.js +43 -8
- package/lib/messageView.d.ts +36 -0
- package/lib/messageView.js +97 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -173,7 +173,12 @@ route entry and startup before the first read; it is not a cache-source indicato
|
|
|
173
173
|
`getSnapshot()` and `subscribe(listener)` provide conversation summaries, aggregate
|
|
174
174
|
unread counts and connection state. Unknown unread counts remain `undefined`.
|
|
175
175
|
Subscription always emits the current snapshot first, including with
|
|
176
|
-
`{fields:["messages","unread"]}`, then meaningful changes only.
|
|
176
|
+
`{fields:["messages","unread"]}`, then meaningful changes only. Messages expose `kind`, `text`, optional `image`,
|
|
177
|
+
`voice`, `quote`, explicit `richContent`, semantic `reactions` and reading/delivery
|
|
178
|
+
state. Apps do not inspect transport bodies or control-event revisions. Group
|
|
179
|
+
causal version numbers and raw message records remain under `advanced`.
|
|
180
|
+
`observeMessages` and `markRead` resolve with `void` after durable submission.
|
|
181
|
+
Object references
|
|
177
182
|
remain stable for unchanged results. Framework adapters can use
|
|
178
183
|
`{emitCurrent:false}`; React can import `useChatSnapshot(chat, threadId)` from
|
|
179
184
|
`@pmate/chat-sdk`. Establishing a subscription never starts a read or marks it read.
|
|
@@ -230,7 +235,8 @@ contract for compatibility. New Apps should import from `@pmate/chat-sdk/chat`
|
|
|
230
235
|
or the package root. Do not mix the two snapshot contracts.
|
|
231
236
|
|
|
232
237
|
For migration, replace `subscribe(id, () => render(getSnapshot(id).timeline))`
|
|
233
|
-
with `subscribe(id, state => render(state.messages))`; replace
|
|
238
|
+
with `subscribe(id, state => render(state.messages))`; replace
|
|
239
|
+
`item.message.body` parsing with `item.kind/text/image/voice/quote`; replace conversation
|
|
234
240
|
objects and App-supplied recipients with `threadId`; await `getMessages` before
|
|
235
241
|
opening a cold thread instead of checking `page.complete`. Replace `initialize()`
|
|
236
242
|
with `start()`; a shared connection is initialized without another connect call.
|
package/lib/chat.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import type { ChatGroup as RuntimeGroup } from "./runtime/groupClient.js";
|
|
2
|
+
import { type ChatMessage } from "./messageView.js";
|
|
3
|
+
export type { ChatMessage } from "./messageView.js";
|
|
1
4
|
import { type CreateChatOptions } from "./runtime/createChat.js";
|
|
2
5
|
import type { ChatSession, ChatSummarySnapshot as SessionSummary, ChatThreadSnapshot as SessionThread } from "./runtime/chatSession.js";
|
|
3
6
|
import type { CachedChatMessage } from "./runtime/cachedMessage.js";
|
|
4
|
-
import type { TimelineItem } from "./runtime/messageTimeline.js";
|
|
5
7
|
import type { ConversationTarget } from "./runtime/conversationDirectory.js";
|
|
6
8
|
import type { OutgoingContent as RuntimeContent } from "./runtime/outgoingContent.js";
|
|
7
9
|
import { ChatError } from "./runtime/chatError.js";
|
|
@@ -13,8 +15,7 @@ export type OutgoingContent = Exclude<RuntimeContent, {
|
|
|
13
15
|
} | {
|
|
14
16
|
groupUpdate: unknown;
|
|
15
17
|
}>;
|
|
16
|
-
export type
|
|
17
|
-
export type ChatMessage = Readonly<TimelineItem<CachedChatMessage>>;
|
|
18
|
+
export type ChatGroup = Readonly<Omit<RuntimeGroup, "versionNumber">>;
|
|
18
19
|
export type ConversationInfo = Readonly<{
|
|
19
20
|
kind: "dm" | "group";
|
|
20
21
|
targetId: string;
|
|
@@ -26,7 +27,7 @@ export type ChatThreadSnapshot = Readonly<{
|
|
|
26
27
|
hasEarlier: boolean;
|
|
27
28
|
conversation?: ConversationInfo;
|
|
28
29
|
unread: SessionThread<CachedChatMessage>["unread"];
|
|
29
|
-
group:
|
|
30
|
+
group: ChatGroup | undefined;
|
|
30
31
|
readCounts: SessionThread<CachedChatMessage>["readCounts"];
|
|
31
32
|
error?: ChatError;
|
|
32
33
|
}>;
|
|
@@ -34,7 +35,7 @@ export type ChatSnapshot = Readonly<{
|
|
|
34
35
|
threads: readonly {
|
|
35
36
|
threadId: string;
|
|
36
37
|
lastMessage?: ChatMessage;
|
|
37
|
-
group:
|
|
38
|
+
group: ChatGroup | undefined;
|
|
38
39
|
unread: ChatThreadSnapshot["unread"];
|
|
39
40
|
outgoingCount: number;
|
|
40
41
|
}[];
|
|
@@ -62,6 +63,12 @@ type SubscribeOptions<T> = {
|
|
|
62
63
|
export declare class Chat {
|
|
63
64
|
readonly advanced: ChatSession<CachedChatMessage>;
|
|
64
65
|
private options;
|
|
66
|
+
private groups;
|
|
67
|
+
private group;
|
|
68
|
+
private present;
|
|
69
|
+
private redactedQuotes;
|
|
70
|
+
private messageLists;
|
|
71
|
+
private messages;
|
|
65
72
|
private views;
|
|
66
73
|
private summary?;
|
|
67
74
|
private targets;
|
|
@@ -83,31 +90,30 @@ export declare class Chat {
|
|
|
83
90
|
subscribe(threadId: string, listener: (snapshot: ChatThreadSnapshot) => void, options?: SubscribeOptions<ChatThreadSnapshot>): () => void;
|
|
84
91
|
private sessionFields;
|
|
85
92
|
private notify;
|
|
86
|
-
observeMessages(threadId: string, ids: string[], event: "visible" | "played"): Promise<
|
|
87
|
-
markRead(threadId: string): Promise<
|
|
93
|
+
observeMessages(threadId: string, ids: string[], event: "visible" | "played"): Promise<void>;
|
|
94
|
+
markRead(threadId: string): Promise<void>;
|
|
88
95
|
getReaders(threadId: string, messageId: string, page?: Parameters<ChatSession<CachedChatMessage>["getReaders"]>[2], signal?: AbortSignal): Promise<import("./runtime/messageReaders.js").MessageReadersPage>;
|
|
89
96
|
watchReaders(threadId: string | undefined, ids: string[], receiver?: string): Promise<void>;
|
|
90
97
|
handleReach(event: Parameters<ChatSession<CachedChatMessage>["handleReach"]>[0]): Promise<"handled" | undefined>;
|
|
91
98
|
getVoiceAttachment(threadId: string, messageId: string): Promise<import("./runtime/durableSender.js").VoiceAttachmentLease>;
|
|
92
99
|
getGroup(threadId: string, options?: {
|
|
93
100
|
refresh?: boolean;
|
|
94
|
-
}): Promise<
|
|
101
|
+
}): Promise<Readonly<Omit<RuntimeGroup, "versionNumber">>>;
|
|
95
102
|
createGroup(input: {
|
|
96
103
|
groupId?: string;
|
|
97
104
|
title: string;
|
|
98
105
|
members: readonly string[];
|
|
99
106
|
}): Promise<{
|
|
100
107
|
threadId: string;
|
|
101
|
-
|
|
108
|
+
revision: string;
|
|
102
109
|
teamId: string;
|
|
103
|
-
ownerId: string;
|
|
104
110
|
title: string;
|
|
111
|
+
groupId: string;
|
|
105
112
|
avatar?: string;
|
|
113
|
+
ownerId: string;
|
|
106
114
|
memberIds: readonly string[];
|
|
107
|
-
revision: string;
|
|
108
|
-
versionNumber: number;
|
|
109
115
|
}>;
|
|
110
|
-
updateGroup(threadId: string, changes: Parameters<ChatSession<CachedChatMessage>["updateGroup"]>[1]): Promise<
|
|
116
|
+
updateGroup(threadId: string, changes: Parameters<ChatSession<CachedChatMessage>["updateGroup"]>[1]): Promise<Readonly<Omit<RuntimeGroup, "versionNumber">>>;
|
|
111
117
|
revokeAccess(): Promise<void>;
|
|
112
118
|
close(): Promise<void>;
|
|
113
119
|
}
|
package/lib/chat.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { createMessagePresenter } from "./messageView.js";
|
|
1
2
|
import { createChat as createSession } from "./runtime/createChat.js";
|
|
2
3
|
import { asChatError, ChatError } from "./runtime/chatError.js";
|
|
3
4
|
export { ChatError } from "./runtime/chatError.js";
|
|
@@ -6,6 +7,40 @@ const emptyMessages = Object.freeze([]);
|
|
|
6
7
|
export class Chat {
|
|
7
8
|
advanced;
|
|
8
9
|
options;
|
|
10
|
+
groups = new WeakMap();
|
|
11
|
+
group(value) {
|
|
12
|
+
if (!value)
|
|
13
|
+
return undefined;
|
|
14
|
+
let projected = this.groups.get(value);
|
|
15
|
+
if (!projected) {
|
|
16
|
+
const { versionNumber: _, ...visible } = value;
|
|
17
|
+
projected = visible;
|
|
18
|
+
this.groups.set(value, projected);
|
|
19
|
+
}
|
|
20
|
+
return projected;
|
|
21
|
+
}
|
|
22
|
+
present = createMessagePresenter();
|
|
23
|
+
redactedQuotes = new WeakMap();
|
|
24
|
+
messageLists = new WeakMap();
|
|
25
|
+
messages(items) {
|
|
26
|
+
let values = this.messageLists.get(items);
|
|
27
|
+
if (!values) {
|
|
28
|
+
const recalled = new Set(items.filter(item => item.recalled).map(item => item.id));
|
|
29
|
+
values = items.map(item => {
|
|
30
|
+
const value = this.present(item);
|
|
31
|
+
if (!value.quote?.messageId || !recalled.has(value.quote.messageId))
|
|
32
|
+
return value;
|
|
33
|
+
let hidden = this.redactedQuotes.get(value);
|
|
34
|
+
if (!hidden) {
|
|
35
|
+
hidden = { ...value, quote: { ...value.quote, content: "", recalled: true } };
|
|
36
|
+
this.redactedQuotes.set(value, hidden);
|
|
37
|
+
}
|
|
38
|
+
return hidden;
|
|
39
|
+
});
|
|
40
|
+
this.messageLists.set(items, values);
|
|
41
|
+
}
|
|
42
|
+
return values;
|
|
43
|
+
}
|
|
9
44
|
views = new Map();
|
|
10
45
|
summary;
|
|
11
46
|
targets = new Map();
|
|
@@ -66,7 +101,7 @@ export class Chat {
|
|
|
66
101
|
getSnapshot(threadId) {
|
|
67
102
|
if (threadId === undefined) {
|
|
68
103
|
const source = this.advanced.getSnapshot(), previous = this.summary;
|
|
69
|
-
const threads = source.threads.filter(thread => this.errors.get(thread.threadId)?.code !== "access-denied").map(thread => ({ threadId: thread.threadId, lastMessage: thread.
|
|
104
|
+
const threads = source.threads.filter(thread => this.errors.get(thread.threadId)?.code !== "access-denied").map(thread => ({ threadId: thread.threadId, lastMessage: this.messages(this.advanced.getSnapshot(thread.threadId).timeline).at(-1), group: this.group(thread.group), unread: thread.unread, outgoingCount: thread.outgoingCount }));
|
|
70
105
|
const unreadMessageCount = source.unreadMessageCount === undefined ? undefined : threads.reduce((sum, thread) => sum + (thread.unread?.unreadMessageCount ?? 0), 0);
|
|
71
106
|
const badgeCount = source.badgeCount === undefined ? undefined : threads.reduce((sum, thread) => sum + (thread.unread?.badgeCount ?? 0), 0);
|
|
72
107
|
if (previous && previous.connection === source.connection && previous.badgeCount === badgeCount && previous.unreadMessageCount === unreadMessageCount && previous.threads.length === threads.length && threads.every((thread, i) => { const old = previous.threads[i]; return thread.threadId === old.threadId && thread.lastMessage === old.lastMessage && thread.group === old.group && thread.unread === old.unread && thread.outgoingCount === old.outgoingCount; }))
|
|
@@ -78,7 +113,7 @@ export class Chat {
|
|
|
78
113
|
const target = this.closed || !this.advanced.active || denied ? undefined : this.advanced.getConversationTarget(threadId) ?? this.targets.get(threadId);
|
|
79
114
|
const conversation = previous?.conversation?.kind === target?.kind && previous?.conversation?.targetId === target?.targetId ? previous?.conversation : target ? { kind: target.kind, targetId: target.targetId } : undefined;
|
|
80
115
|
// Partial migrated rows never masquerade as a trusted recent window.
|
|
81
|
-
const next = { threadId, conversation, messages: !denied && source.complete ? source.timeline : emptyMessages, ready: !denied && source.complete, hasEarlier: !denied && source.hasEarlier, unread: denied ? undefined : source.unread, group: denied ? undefined : source.group, readCounts: denied ? undefined : source.readCounts, error };
|
|
116
|
+
const next = { threadId, conversation, messages: !denied && source.complete ? this.messages(source.timeline) : emptyMessages, ready: !denied && source.complete, hasEarlier: !denied && source.hasEarlier, unread: denied ? undefined : source.unread, group: denied ? undefined : this.group(source.group), readCounts: denied ? undefined : source.readCounts, error };
|
|
82
117
|
if (previous && Object.keys(next).every(key => next[key] === previous[key]))
|
|
83
118
|
return previous;
|
|
84
119
|
this.views.set(threadId, next);
|
|
@@ -154,23 +189,23 @@ export class Chat {
|
|
|
154
189
|
return fields ? [...new Set(fields.flatMap(field => field === "messages" ? ["timeline", "complete"] : field === "ready" ? ["complete"] : field === "conversation" ? ["messages", "group"] : field === "error" || field === "threadId" ? [] : [field]))] : undefined;
|
|
155
190
|
}
|
|
156
191
|
notify(threadId) { this.listeners.get(threadId)?.forEach(listener => listener()); this.listeners.get("")?.forEach(listener => listener()); }
|
|
157
|
-
observeMessages(threadId, ids, event) { return this.threadOperation(threadId, () => this.advanced.observeMessages(threadId, ids, event)); }
|
|
158
|
-
markRead(threadId) { return this.threadOperation(threadId, () => this.advanced.markRead(threadId)); }
|
|
192
|
+
observeMessages(threadId, ids, event) { return this.threadOperation(threadId, async () => { await this.advanced.observeMessages(threadId, ids, event); }); }
|
|
193
|
+
markRead(threadId) { return this.threadOperation(threadId, async () => { await this.advanced.markRead(threadId); }); }
|
|
159
194
|
getReaders(threadId, messageId, page = {}, signal) { return this.threadOperation(threadId, () => this.advanced.getReaders(threadId, messageId, page, signal)); }
|
|
160
195
|
watchReaders(threadId, ids, receiver) { return this.threadOperation(threadId, () => this.advanced.observeMessages(threadId, ids, "receipts", receiver)); }
|
|
161
196
|
handleReach(event) { return this.operation(() => this.advanced.handleReach(event)); }
|
|
162
197
|
getVoiceAttachment(threadId, messageId) { return this.threadOperation(threadId, () => this.advanced.getVoiceAttachment(threadId, messageId)); }
|
|
163
|
-
getGroup(threadId, options = {}) { return this.threadOperation(threadId, () => this.advanced.getGroup(threadId, options)); }
|
|
198
|
+
getGroup(threadId, options = {}) { return this.threadOperation(threadId, async () => this.group(await this.advanced.getGroup(threadId, options))); }
|
|
164
199
|
createGroup(input) {
|
|
165
200
|
return this.operation(async () => {
|
|
166
201
|
const groupId = input.groupId ?? crypto.randomUUID(), threadId = await this.getConversation({ groupId });
|
|
167
202
|
const group = await this.advanced.createGroup({ id: threadId, kind: "group", targetId: groupId, teamId: this.options.teamId }, { title: input.title, members: input.members });
|
|
168
|
-
return { ...group, threadId };
|
|
203
|
+
return { ...this.group(group), threadId };
|
|
169
204
|
});
|
|
170
205
|
}
|
|
171
|
-
updateGroup(threadId, changes) { return this.threadOperation(threadId, () => this.advanced.updateGroup(threadId, changes)); }
|
|
206
|
+
updateGroup(threadId, changes) { return this.threadOperation(threadId, async () => this.group(await this.advanced.updateGroup(threadId, changes))); }
|
|
172
207
|
revokeAccess() { return this.operation(() => this.advanced.revokeAccess()); }
|
|
173
|
-
close() { this.closed = true; return this.operation(() => this.advanced.close()).finally(() => { this.errors.clear(); this.targets.clear(); this.listeners.clear(); this.views.clear(); this.summary = undefined; }); }
|
|
208
|
+
close() { this.closed = true; return this.operation(() => this.advanced.close()).finally(() => { this.errors.clear(); this.targets.clear(); this.listeners.clear(); this.views.clear(); this.summary = undefined; this.present = createMessagePresenter(); this.messageLists = new WeakMap(); this.groups = new WeakMap(); this.redactedQuotes = new WeakMap(); }); }
|
|
174
209
|
}
|
|
175
210
|
export function createChat(options) {
|
|
176
211
|
const configured = { ...options, onError: (error) => options.onError(asChatError(error)) };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { TimelineItem } from "./runtime/messageTimeline.js";
|
|
2
|
+
import type { CachedChatMessage } from "./runtime/cachedMessage.js";
|
|
3
|
+
import type { MessageQuote } from "./runtime/outgoingContent.js";
|
|
4
|
+
import type { ChatMention } from "./mentions/model.js";
|
|
5
|
+
export type ChatMessage = Readonly<Omit<TimelineItem<CachedChatMessage>, "message" | "reactionEvents"> & {
|
|
6
|
+
kind: "text" | "image" | "voice" | "emoji" | "quote" | "invitation" | "system" | "attachment";
|
|
7
|
+
text: string;
|
|
8
|
+
senderName?: string;
|
|
9
|
+
senderAvatar?: string;
|
|
10
|
+
image?: Readonly<{
|
|
11
|
+
url: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
}>;
|
|
14
|
+
voice?: Readonly<{
|
|
15
|
+
url: string;
|
|
16
|
+
durationMs: number;
|
|
17
|
+
transcript?: string;
|
|
18
|
+
}>;
|
|
19
|
+
quote?: Readonly<MessageQuote & {
|
|
20
|
+
recalled?: boolean;
|
|
21
|
+
}>;
|
|
22
|
+
attachment?: Readonly<{
|
|
23
|
+
name?: string;
|
|
24
|
+
size?: string;
|
|
25
|
+
}>;
|
|
26
|
+
/** Explicit versioned content, interpreted with SDK readMessageContent. */
|
|
27
|
+
richContent?: unknown;
|
|
28
|
+
mentions?: readonly ChatMention[];
|
|
29
|
+
recipientCount?: number;
|
|
30
|
+
system?: Readonly<{
|
|
31
|
+
type: "group-created" | "group-updated" | "contact-updated";
|
|
32
|
+
title?: string;
|
|
33
|
+
}>;
|
|
34
|
+
}>;
|
|
35
|
+
/** Decode a body once; reading and delivery projections reuse its normalized content. */
|
|
36
|
+
export declare function createMessagePresenter(): (item: TimelineItem<CachedChatMessage>) => ChatMessage;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
const object = (value) => value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
2
|
+
const text = (value) => typeof value === "string" ? value : "";
|
|
3
|
+
function media(value) {
|
|
4
|
+
if (typeof value !== "string" || !value)
|
|
5
|
+
return undefined;
|
|
6
|
+
try {
|
|
7
|
+
const url = new URL(value);
|
|
8
|
+
return ["https:", "http:"].includes(url.protocol) && !url.username && !url.password ? value : undefined;
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function quote(value) {
|
|
15
|
+
const record = object(value);
|
|
16
|
+
if (typeof record.senderName !== "string" || typeof record.content !== "string" || record.content.length > 6000)
|
|
17
|
+
return;
|
|
18
|
+
return { senderName: record.senderName.slice(0, 100), content: record.content, ...(typeof record.messageId === "string" ? { messageId: record.messageId } : {}) };
|
|
19
|
+
}
|
|
20
|
+
function legacyDuration(value) {
|
|
21
|
+
if (typeof value !== "string" || !/^\d+:\d{2}$/.test(value))
|
|
22
|
+
return 0;
|
|
23
|
+
const [minutes, seconds] = value.split(":").map(Number);
|
|
24
|
+
return (minutes * 60 + seconds) * 1000;
|
|
25
|
+
}
|
|
26
|
+
function content(message) {
|
|
27
|
+
const body = object(message.body), outer = object(body.content);
|
|
28
|
+
const envelope = body.kind === "quote" && outer.content !== undefined ? outer : body;
|
|
29
|
+
const payload = object(envelope.content);
|
|
30
|
+
const legacy = object(message.legacyDisplay);
|
|
31
|
+
const validLegacy = legacy.id === message.id && legacy.threadId === message.threadId && legacy.orgId === message.teamId && legacy.from === message.from && typeof legacy.content === "string";
|
|
32
|
+
const hint = validLegacy ? legacy : {};
|
|
33
|
+
const contentKind = envelope.contentKind, voice = object(payload.voice);
|
|
34
|
+
const voiceHint = hint.kind === "voice";
|
|
35
|
+
const voiceUrl = media(voice.url) ?? (voiceHint ? media(hint.audioUrl) : undefined) ?? "";
|
|
36
|
+
const durationMs = typeof voice.durationMs === "number" && Number.isFinite(voice.durationMs) ? voice.durationMs : voiceHint ? legacyDuration(hint.duration) : 0;
|
|
37
|
+
const hasVoice = voiceHint || Object.keys(voice).length > 0 && (voiceUrl.length > 0 || durationMs > 0) || contentKind === "voice";
|
|
38
|
+
const imageUrl = media(payload.url) ?? (hint.kind === "image" ? media(hint.imageUrl) : undefined);
|
|
39
|
+
const messageQuote = quote(payload.quote) ?? quote(hint.quote);
|
|
40
|
+
const rawText = text(payload.text) || text(object(body.payload).message);
|
|
41
|
+
const result = { kind: "text", text: validLegacy ? text(hint.content) : rawText, senderName: text(hint.senderName) || undefined, senderAvatar: media(hint.senderAvatar),
|
|
42
|
+
recipientCount: message.threadKind === "dm" ? 1 : validLegacy ? typeof hint.recipientCount === "number" ? hint.recipientCount : undefined : Array.isArray(message.recipients) ? new Set(message.recipients.filter(id => id !== message.from)).size : undefined };
|
|
43
|
+
if (validLegacy && ["emoji", "system", "attachment", "quote"].includes(String(hint.kind)))
|
|
44
|
+
result.kind = hint.kind;
|
|
45
|
+
if (body.kind === "contact") {
|
|
46
|
+
result.kind = ["friendRequest", "requestFriend"].includes(String(body.action)) ? "invitation" : "system";
|
|
47
|
+
if (result.kind === "system")
|
|
48
|
+
result.system = { type: "contact-updated" };
|
|
49
|
+
}
|
|
50
|
+
else if (body.kind === "group") {
|
|
51
|
+
result.kind = "system";
|
|
52
|
+
result.system = { type: body.action === "groupCreate" ? "group-created" : "group-updated", title: text(object(body.payload).title) || undefined };
|
|
53
|
+
}
|
|
54
|
+
else if (hasVoice) {
|
|
55
|
+
result.kind = "voice";
|
|
56
|
+
result.voice = { url: voiceUrl, durationMs, ...(text(voice.transcript) || text(hint.transcript) ? { transcript: text(voice.transcript) || text(hint.transcript) } : {}) };
|
|
57
|
+
}
|
|
58
|
+
else if ((contentKind === "image" || hint.kind === "image") && imageUrl) {
|
|
59
|
+
result.kind = "image";
|
|
60
|
+
result.image = { url: imageUrl, name: text(payload.name) || text(hint.imageAlt) || undefined };
|
|
61
|
+
}
|
|
62
|
+
else if (contentKind === "emoji") {
|
|
63
|
+
result.kind = "emoji";
|
|
64
|
+
result.text = text(payload.code) || result.text;
|
|
65
|
+
}
|
|
66
|
+
if (messageQuote) {
|
|
67
|
+
result.quote = messageQuote;
|
|
68
|
+
if (result.kind === "text")
|
|
69
|
+
result.kind = "quote";
|
|
70
|
+
}
|
|
71
|
+
if (result.kind === "attachment")
|
|
72
|
+
result.attachment = { name: text(hint.attachmentName) || undefined, size: text(hint.attachmentSize) || undefined };
|
|
73
|
+
if (payload.format !== undefined)
|
|
74
|
+
result.richContent = payload;
|
|
75
|
+
else if (hint.richContent !== undefined)
|
|
76
|
+
result.richContent = hint.richContent;
|
|
77
|
+
if (Array.isArray(payload.mentions))
|
|
78
|
+
result.mentions = payload.mentions;
|
|
79
|
+
return result;
|
|
80
|
+
}
|
|
81
|
+
/** Decode a body once; reading and delivery projections reuse its normalized content. */
|
|
82
|
+
export function createMessagePresenter() {
|
|
83
|
+
const bodies = new WeakMap(), items = new WeakMap();
|
|
84
|
+
return (item) => {
|
|
85
|
+
const previous = items.get(item);
|
|
86
|
+
if (previous)
|
|
87
|
+
return previous;
|
|
88
|
+
let value = { kind: "system", text: "" };
|
|
89
|
+
if (!item.recalled && item.message) {
|
|
90
|
+
value = bodies.get(item.message) ?? content(item.message);
|
|
91
|
+
bodies.set(item.message, value);
|
|
92
|
+
}
|
|
93
|
+
const result = { id: item.id, threadId: item.threadId, timestamp: item.timestamp, from: item.from, mine: item.mine, status: item.status, recalled: item.recalled, unread: item.unread, attentionEligible: item.attentionEligible, reading: item.reading, reactions: item.reactions, ...value };
|
|
94
|
+
items.set(item, result);
|
|
95
|
+
return result;
|
|
96
|
+
};
|
|
97
|
+
}
|
package/package.json
CHANGED