@pmate/chat-sdk 0.12.0-next.64 → 0.12.0-next.66

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 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. Object references
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 conversation
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 { ChatGroup } from "./runtime/groupClient.js";
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: SessionThread<CachedChatMessage>["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: SessionThread<CachedChatMessage>["group"];
38
+ group: ChatGroup | undefined;
38
39
  unread: ChatThreadSnapshot["unread"];
39
40
  outgoingCount: number;
40
41
  }[];
@@ -47,6 +48,17 @@ export type MessageQuery = {
47
48
  all?: boolean;
48
49
  refresh?: boolean;
49
50
  };
51
+ export type ReadersQuery = {
52
+ afterProfileId?: string;
53
+ revision?: number;
54
+ };
55
+ export type GroupChanges = {
56
+ expectedRevision?: string;
57
+ addMembers?: readonly string[];
58
+ removeMembers?: readonly string[];
59
+ title?: string;
60
+ avatar?: string;
61
+ };
50
62
  export type SendOptions = {
51
63
  waitFor?: "accepted" | "delivered";
52
64
  };
@@ -62,6 +74,12 @@ type SubscribeOptions<T> = {
62
74
  export declare class Chat {
63
75
  readonly advanced: ChatSession<CachedChatMessage>;
64
76
  private options;
77
+ private groups;
78
+ private group;
79
+ private present;
80
+ private redactedQuotes;
81
+ private messageLists;
82
+ private messages;
65
83
  private views;
66
84
  private summary?;
67
85
  private targets;
@@ -83,31 +101,30 @@ export declare class Chat {
83
101
  subscribe(threadId: string, listener: (snapshot: ChatThreadSnapshot) => void, options?: SubscribeOptions<ChatThreadSnapshot>): () => void;
84
102
  private sessionFields;
85
103
  private notify;
86
- observeMessages(threadId: string, ids: string[], event: "visible" | "played"): Promise<import("./runtime/readIndex.js").ReadIndexState>;
87
- markRead(threadId: string): Promise<import("./runtime/readIndex.js").ReadIndexState>;
88
- getReaders(threadId: string, messageId: string, page?: Parameters<ChatSession<CachedChatMessage>["getReaders"]>[2], signal?: AbortSignal): Promise<import("./runtime/messageReaders.js").MessageReadersPage>;
104
+ observeMessages(threadId: string, ids: string[], event: "visible" | "played"): Promise<void>;
105
+ markRead(threadId: string): Promise<void>;
106
+ getReaders(threadId: string, messageId: string, page?: ReadersQuery, signal?: AbortSignal): Promise<import("./runtime/messageReaders.js").MessageReadersPage>;
89
107
  watchReaders(threadId: string | undefined, ids: string[], receiver?: string): Promise<void>;
90
108
  handleReach(event: Parameters<ChatSession<CachedChatMessage>["handleReach"]>[0]): Promise<"handled" | undefined>;
91
109
  getVoiceAttachment(threadId: string, messageId: string): Promise<import("./runtime/durableSender.js").VoiceAttachmentLease>;
92
110
  getGroup(threadId: string, options?: {
93
111
  refresh?: boolean;
94
- }): Promise<import("./chat.js").ChatGroup>;
112
+ }): Promise<Readonly<Omit<RuntimeGroup, "versionNumber">>>;
95
113
  createGroup(input: {
96
114
  groupId?: string;
97
115
  title: string;
98
116
  members: readonly string[];
99
117
  }): Promise<{
100
118
  threadId: string;
101
- groupId: string;
119
+ revision: string;
102
120
  teamId: string;
103
- ownerId: string;
104
121
  title: string;
122
+ groupId: string;
105
123
  avatar?: string;
124
+ ownerId: string;
106
125
  memberIds: readonly string[];
107
- revision: string;
108
- versionNumber: number;
109
126
  }>;
110
- updateGroup(threadId: string, changes: Parameters<ChatSession<CachedChatMessage>["updateGroup"]>[1]): Promise<import("./chat.js").ChatGroup>;
127
+ updateGroup(threadId: string, changes: GroupChanges): Promise<Readonly<Omit<RuntimeGroup, "versionNumber">>>;
111
128
  revokeAccess(): Promise<void>;
112
129
  close(): Promise<void>;
113
130
  }
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.lastItem, group: thread.group, unread: thread.unread, outgoingCount: thread.outgoingCount }));
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,29 @@ 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)); }
159
- getReaders(threadId, messageId, page = {}, signal) { return this.threadOperation(threadId, () => this.advanced.getReaders(threadId, messageId, page, signal)); }
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); }); }
194
+ getReaders(threadId, messageId, page = {}, signal) {
195
+ return this.threadOperation(threadId, async () => {
196
+ if (Object.keys(page).some(key => !["afterProfileId", "revision"].includes(key)))
197
+ throw new ChatError("invalid-input", "Unsupported reader query");
198
+ return this.advanced.getReaders(threadId, messageId, page, signal);
199
+ });
200
+ }
160
201
  watchReaders(threadId, ids, receiver) { return this.threadOperation(threadId, () => this.advanced.observeMessages(threadId, ids, "receipts", receiver)); }
161
202
  handleReach(event) { return this.operation(() => this.advanced.handleReach(event)); }
162
203
  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)); }
204
+ getGroup(threadId, options = {}) { return this.threadOperation(threadId, async () => this.group(await this.advanced.getGroup(threadId, options))); }
164
205
  createGroup(input) {
165
206
  return this.operation(async () => {
166
207
  const groupId = input.groupId ?? crypto.randomUUID(), threadId = await this.getConversation({ groupId });
167
208
  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 };
209
+ return { ...this.group(group), threadId };
169
210
  });
170
211
  }
171
- updateGroup(threadId, changes) { return this.threadOperation(threadId, () => this.advanced.updateGroup(threadId, changes)); }
212
+ updateGroup(threadId, changes) { return this.threadOperation(threadId, async () => this.group(await this.advanced.updateGroup(threadId, changes))); }
172
213
  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; }); }
214
+ 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
215
  }
175
216
  export function createChat(options) {
176
217
  const configured = { ...options, onError: (error) => options.onError(asChatError(error)) };
package/lib/index.d.ts CHANGED
@@ -41,5 +41,5 @@ export * from "./runtime/socialReadClient";
41
41
  export * from "./ux/imagePaste";
42
42
  export * from "./hooks/useImageAttachments";
43
43
  export * from "./components/ImageAttachmentTray";
44
- export { createChat, Chat, ChatError, type ChatThreadSnapshot, type ChatMessage as MessageView, type ChatErrorCode, type ConversationTarget, type OutgoingContent, type CreateChatOptions, type ChatSnapshot, type SentMessage, type MessageQuery } from "./chat.js";
44
+ export { createChat, Chat, ChatError, type ChatThreadSnapshot, type ChatMessage as MessageView, type ChatErrorCode, type ConversationTarget, type OutgoingContent, type CreateChatOptions, type ChatSnapshot, type SentMessage, type MessageQuery, type ReadersQuery, type GroupChanges } from "./chat.js";
45
45
  export { useChatSnapshot } from "./hooks/useChatSnapshot.js";
@@ -0,0 +1,37 @@
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
+ timeLabel?: string;
11
+ image?: Readonly<{
12
+ url: string;
13
+ name?: string;
14
+ }>;
15
+ voice?: Readonly<{
16
+ url: string;
17
+ durationMs: number;
18
+ transcript?: string;
19
+ }>;
20
+ quote?: Readonly<MessageQuote & {
21
+ recalled?: boolean;
22
+ }>;
23
+ attachment?: Readonly<{
24
+ name?: string;
25
+ size?: string;
26
+ }>;
27
+ /** Explicit versioned content, interpreted with SDK readMessageContent. */
28
+ richContent?: unknown;
29
+ mentions?: readonly ChatMention[];
30
+ recipientCount?: number;
31
+ system?: Readonly<{
32
+ type: "group-created" | "group-updated" | "contact-updated";
33
+ title?: string;
34
+ }>;
35
+ }>;
36
+ /** Decode a body once; reading and delivery projections reuse its normalized content. */
37
+ 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), timeLabel: text(hint.createdAt) || undefined,
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
+ }
@@ -1,3 +1,4 @@
1
+ import { type ChatMessage } from "../messageView.js";
1
2
  import { type MessageScope } from "./messageStore.js";
2
3
  import { type CachedChatMessage } from "./cachedMessage.js";
3
4
  import { type TimelineItem } from "./messageTimeline.js";
@@ -12,6 +13,7 @@ export type MessageLaunchView = {
12
13
  threadId: string;
13
14
  metadata: MessageLaunchMetadata;
14
15
  images: Record<string, string>;
16
+ messages: readonly ChatMessage[]; /** @deprecated Advanced compatibility projection. */
15
17
  timeline: readonly TimelineItem<CachedChatMessage>[];
16
18
  };
17
19
  type Options = {
@@ -1,3 +1,4 @@
1
+ import { createMessagePresenter } from "../messageView.js";
1
2
  import { Dexie } from "dexie";
2
3
  import { IndexedDbMessageStore, messageScopeKey, mergeStoredMessage } from "./messageStore.js";
3
4
  import { DurableChatSender } from "./durableSender.js";
@@ -47,6 +48,7 @@ export async function saveMessageLaunch(input, options = {}) {
47
48
  export function watchMessageLaunch(input, onChange, onError, options = {}) {
48
49
  const { scope, openId, threadId } = input, key = messageScopeKey(scope), store = messages(scope, options), { db, rows } = tickets(options);
49
50
  let closed = false, generation = 0, ticket, timer;
51
+ const present = createMessagePresenter();
50
52
  const channels = [];
51
53
  const stop = () => { if (closed)
52
54
  return; closed = true; generation++; clearTimeout(timer); tasks.close(); channels.forEach(channel => channel.close()); store.close(); db.close(); };
@@ -89,7 +91,9 @@ export function watchMessageLaunch(input, onChange, onError, options = {}) {
89
91
  return;
90
92
  }
91
93
  const timeline = projectMessageTimeline({ appId: scope.appId, profileId: scope.profileId, threadId, messages: page.messages, outgoing, decode: message => decodeCachedChatMessage(message, scope), reading: {} }).slice(-30);
92
- onChange(timeline.length ? { openId, threadId, metadata: ticket.metadata, images: ticket.images, timeline } : undefined);
94
+ const normalized = timeline.map(present), visibleImages = new Set(normalized.flatMap(message => message.image ? [message.image.url] : []));
95
+ const images = Object.fromEntries(Object.entries(ticket.images).filter(([url]) => visibleImages.has(url)));
96
+ onChange(timeline.length ? { openId, threadId, metadata: ticket.metadata, images, messages: normalized, timeline } : undefined);
93
97
  };
94
98
  const tasks = new WindowedTasks(16, fail);
95
99
  if (typeof BroadcastChannel !== "undefined") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pmate/chat-sdk",
3
- "version": "0.12.0-next.64",
3
+ "version": "0.12.0-next.66",
4
4
  "description": "Local-first chat SDK, message subscriptions, reading state, and shared chat UI primitives",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",