@pmate/chat-sdk 0.12.0-next.27 → 0.12.0-next.28
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/lib/runtime/chatSession.d.ts +3 -0
- package/lib/runtime/chatSession.js +7 -2
- package/lib/runtime/conversationReading.d.ts +28 -0
- package/lib/runtime/conversationReading.js +43 -0
- package/lib/runtime/createChat.js +10 -0
- package/lib/runtime/index.d.ts +1 -0
- package/lib/runtime/index.js +1 -0
- package/lib/runtime/readIndex.js +2 -1
- package/lib/runtime/readingContent.d.ts +2 -0
- package/lib/runtime/readingContent.js +9 -0
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ReadingCandidate, type MessageReadingState } from "./conversationReading.js";
|
|
1
2
|
import type { ConversationReadMessage } from "./conversationRead.js";
|
|
2
3
|
import { type ReadCountsResult } from "./readCounts.js";
|
|
3
4
|
import type { OnlineReachEvent } from "./reach.js";
|
|
@@ -16,6 +17,7 @@ export type ChatSessionOptions<T extends StoredMessage> = {
|
|
|
16
17
|
activeThreadId?: string;
|
|
17
18
|
reading: ReadingClientOptions;
|
|
18
19
|
decode: (message: ChatRuntimeMessage) => T | undefined;
|
|
20
|
+
readingCandidate?: (message: T) => ReadingCandidate | undefined;
|
|
19
21
|
onError: (error: unknown) => void;
|
|
20
22
|
};
|
|
21
23
|
export type ChatThreadSnapshot<T> = {
|
|
@@ -26,6 +28,7 @@ export type ChatThreadSnapshot<T> = {
|
|
|
26
28
|
complete: boolean;
|
|
27
29
|
readCounts?: ReadCountsResult;
|
|
28
30
|
unread: ReturnType<typeof unreadInConversation> | undefined;
|
|
31
|
+
messageReading: Readonly<Record<string, MessageReadingState>>;
|
|
29
32
|
};
|
|
30
33
|
export type ChatSummarySnapshot<T> = {
|
|
31
34
|
threads: readonly {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { projectConversationReading } from "./conversationReading.js";
|
|
1
2
|
import { ReadCountsClient } from "./readCounts.js";
|
|
2
3
|
import { WindowedTasks } from "./coalescer.js";
|
|
3
4
|
import { MessageReadersClient } from "./messageReaders.js";
|
|
@@ -27,7 +28,7 @@ export class ChatSession {
|
|
|
27
28
|
off = [];
|
|
28
29
|
globalListeners = new Set();
|
|
29
30
|
globalSnapshot;
|
|
30
|
-
closedThread = { messages: [], outgoing: [], hasEarlier: false, resetVersion: 0, complete: false, unread: undefined };
|
|
31
|
+
closedThread = { messages: [], outgoing: [], hasEarlier: false, resetVersion: 0, complete: false, unread: undefined, messageReading: {} };
|
|
31
32
|
closedSummary = { threads: [], unreadMessageCount: undefined, badgeCount: undefined, connection: "closed" };
|
|
32
33
|
connectionState = "closed";
|
|
33
34
|
catchUp;
|
|
@@ -430,7 +431,11 @@ export class ChatSession {
|
|
|
430
431
|
if (!snapshot) {
|
|
431
432
|
const messages = this.messages.getSnapshot(threadId);
|
|
432
433
|
const state = this.getReadingSnapshot();
|
|
433
|
-
|
|
434
|
+
const candidates = messages?.messages.flatMap(message => { const candidate = this.options.readingCandidate?.(message); return candidate ? [candidate] : []; }) ?? [];
|
|
435
|
+
const sources = state?.threads.filter(thread => (thread.conversationId ?? thread.id) === threadId) ?? [];
|
|
436
|
+
const kind = sources[0]?.kind ?? "dm";
|
|
437
|
+
const view = projectConversationReading(state, { id: threadId, kind }, candidates);
|
|
438
|
+
snapshot = { messages: messages?.messages ?? [], hasEarlier: messages?.hasEarlier ?? false, resetVersion: messages?.resetVersion ?? 0, complete: messages?.complete ?? false, outgoing: this.sender.snapshot(threadId), readCounts: this.countSnapshots.get(threadId), unread: view.unread, messageReading: view.messageReading };
|
|
434
439
|
this.snapshots.set(threadId, snapshot);
|
|
435
440
|
}
|
|
436
441
|
return snapshot;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { unreadInConversation, type ReadIndexState } from "./readIndex.js";
|
|
2
|
+
export type ReadingCandidate = {
|
|
3
|
+
id: string;
|
|
4
|
+
threadId: string;
|
|
5
|
+
from?: string;
|
|
6
|
+
mine?: boolean;
|
|
7
|
+
recalled?: boolean;
|
|
8
|
+
unread?: boolean;
|
|
9
|
+
attentionEligible?: boolean;
|
|
10
|
+
readPolicy?: "visible" | "explicit";
|
|
11
|
+
};
|
|
12
|
+
export type MessageReadingState = {
|
|
13
|
+
seq?: number;
|
|
14
|
+
read: boolean;
|
|
15
|
+
readPolicy: "visible" | "explicit";
|
|
16
|
+
};
|
|
17
|
+
export type ConversationReadingView = {
|
|
18
|
+
messageReading: Readonly<Record<string, MessageReadingState>>;
|
|
19
|
+
unread: ReturnType<typeof unreadInConversation> | undefined;
|
|
20
|
+
badgeCount: number | undefined;
|
|
21
|
+
};
|
|
22
|
+
/** One projection for indexed authority and observations awaiting indexing. A
|
|
23
|
+
* directory badge is optional compatibility metadata, never proof of coverage. */
|
|
24
|
+
export declare function projectConversationReading(state: ReadIndexState | undefined, conversation: {
|
|
25
|
+
id: string;
|
|
26
|
+
kind: "dm" | "group";
|
|
27
|
+
badgeCount?: number;
|
|
28
|
+
}, candidates?: readonly ReadingCandidate[]): ConversationReadingView;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { sourcesInConversation, unreadInConversation } from "./readIndex.js";
|
|
2
|
+
/** One projection for indexed authority and observations awaiting indexing. A
|
|
3
|
+
* directory badge is optional compatibility metadata, never proof of coverage. */
|
|
4
|
+
export function projectConversationReading(state, conversation, candidates = []) {
|
|
5
|
+
const sources = state ? sourcesInConversation(state, conversation.id) : [];
|
|
6
|
+
const sourceIds = new Set([conversation.id, ...sources.map(source => source.id)]);
|
|
7
|
+
const indexed = new Set(), messageReading = Object.create(null);
|
|
8
|
+
for (const source of sources)
|
|
9
|
+
for (const message of source.messages) {
|
|
10
|
+
indexed.add(message.id);
|
|
11
|
+
messageReading[message.id] = { seq: message.seq, readPolicy: message.readPolicy, read: message.recalled || message.from === state.profileId || !message.recipients.includes(state.profileId) || (message.readPolicy === "explicit" ? source.explicitReadMessageIds.includes(message.id) : message.seq <= source.readThroughSeq) };
|
|
12
|
+
}
|
|
13
|
+
const observed = new Map();
|
|
14
|
+
for (const intent of state?.observedReadIntents ?? [])
|
|
15
|
+
if (sourceIds.has(intent.threadId)) {
|
|
16
|
+
if (!observed.has(intent.messageId))
|
|
17
|
+
observed.set(intent.messageId, new Set());
|
|
18
|
+
observed.get(intent.messageId).add(intent.readPolicy);
|
|
19
|
+
}
|
|
20
|
+
const unique = new Map(candidates.filter(message => message.threadId === conversation.id).map(message => [message.id, message]));
|
|
21
|
+
let pendingMessages = 0, pendingMentions = 0, observedBadges = 0;
|
|
22
|
+
for (const message of unique.values()) {
|
|
23
|
+
const incoming = !message.mine && message.from !== state?.profileId && !message.recalled;
|
|
24
|
+
const attention = conversation.kind === "dm" || message.attentionEligible === true;
|
|
25
|
+
const policy = message.readPolicy ?? "visible";
|
|
26
|
+
const acknowledged = observed.get(message.id)?.has(policy) === true;
|
|
27
|
+
if (!indexed.has(message.id) && acknowledged)
|
|
28
|
+
messageReading[message.id] = { read: true, readPolicy: policy };
|
|
29
|
+
if (!incoming)
|
|
30
|
+
continue;
|
|
31
|
+
if (acknowledged && attention)
|
|
32
|
+
observedBadges++;
|
|
33
|
+
if (indexed.has(message.id) || acknowledged || message.unread !== true)
|
|
34
|
+
continue;
|
|
35
|
+
pendingMessages++;
|
|
36
|
+
if (message.attentionEligible === true)
|
|
37
|
+
pendingMentions++;
|
|
38
|
+
}
|
|
39
|
+
const authoritative = sources.length && state ? unreadInConversation(state, conversation.id) : undefined;
|
|
40
|
+
const unread = authoritative ? { unreadMessageCount: authoritative.unreadMessageCount + pendingMessages, unreadMentionCount: authoritative.unreadMentionCount + pendingMentions, badgeCount: authoritative.badgeCount + (conversation.kind === "group" ? pendingMentions : pendingMessages) } : undefined;
|
|
41
|
+
const pendingBadge = conversation.kind === "group" ? pendingMentions : pendingMessages;
|
|
42
|
+
return { messageReading, unread, badgeCount: unread?.badgeCount ?? (conversation.badgeCount === undefined ? undefined : Math.max(Math.max(0, conversation.badgeCount - observedBadges), pendingBadge)) };
|
|
43
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isReadingControl } from "./readingContent.js";
|
|
1
2
|
import { ChatSession } from "./chatSession.js";
|
|
2
3
|
import { SocialReadClient } from "./socialReadClient.js";
|
|
3
4
|
import { chatTeamId } from "./teamScope.js";
|
|
@@ -21,6 +22,15 @@ export function createChat(options) {
|
|
|
21
22
|
reading: { appId, profileId, sessionToken: token, hubHttpUrl: http, fetch: options.fetch, onError },
|
|
22
23
|
connection: { appId, profileId, teamId, hubUrl: ws, clientInstanceId: options.clientInstanceId, fetchImpl: options.fetch,
|
|
23
24
|
session: { token, identity: { accountId, app: appId, profileId } }, onNotification: options.onNotification, onReach: options.onReach },
|
|
25
|
+
readingCandidate: message => {
|
|
26
|
+
if (isReadingControl(message.body, appId))
|
|
27
|
+
return undefined;
|
|
28
|
+
const body = message.body;
|
|
29
|
+
if (!body || !["message", "quote"].includes(String(body.kind)) && !(body.kind === "contact" && ["friendRequest", "requestFriend"].includes(String(body.action))))
|
|
30
|
+
return undefined;
|
|
31
|
+
return { id: message.id, threadId: message.threadId, from: message.from, mine: message.from === profileId, recalled: message.recalled, unread: message.from !== profileId,
|
|
32
|
+
readPolicy: body?.contentKind === "voice" ? "explicit" : "visible", attentionEligible: Array.isArray(body.content?.mentions) && body.content.mentions.some(mention => mention?.id === profileId) };
|
|
33
|
+
},
|
|
24
34
|
decode: message => decodeIncoming(message, { appId, profileId, teamId }), onError,
|
|
25
35
|
});
|
|
26
36
|
}
|
package/lib/runtime/index.d.ts
CHANGED
|
@@ -30,3 +30,4 @@ export * from "./chatSession.js";
|
|
|
30
30
|
export * from "./messageReaders.js";
|
|
31
31
|
export * from "./createChat.js";
|
|
32
32
|
export * from "./conversationRead.js";
|
|
33
|
+
export { projectConversationReading, type ReadingCandidate, type MessageReadingState, type ConversationReadingView } from "./conversationReading.js";
|
package/lib/runtime/index.js
CHANGED
package/lib/runtime/readIndex.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Dexie } from "dexie";
|
|
2
|
+
import { isReadingControl } from "./readingContent.js";
|
|
2
3
|
import { isValidChatTeamId } from "./thread.js";
|
|
3
4
|
export class ReadSyncGap extends Error {
|
|
4
5
|
cursor;
|
|
@@ -158,7 +159,7 @@ export function unreadInThread(state, threadId) {
|
|
|
158
159
|
if (!thread)
|
|
159
160
|
throw new Error("UNKNOWN_READ_THREAD");
|
|
160
161
|
const explicit = new Set(thread.explicitReadMessageIds);
|
|
161
|
-
const messages = thread.messages.filter(message => !message.recalled && message.from !== state.profileId && message.recipients.includes(state.profileId)
|
|
162
|
+
const messages = thread.messages.filter(message => !message.recalled && !isReadingControl(message.payload, state.appId) && message.from !== state.profileId && message.recipients.includes(state.profileId)
|
|
162
163
|
&& (message.readPolicy === "explicit" ? !explicit.has(message.id) : message.seq > thread.readThroughSeq));
|
|
163
164
|
const unreadMessageCount = messages.length;
|
|
164
165
|
const unreadMentionCount = messages.filter(message => message.mentions.includes(state.profileId)).length;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Social control envelopes do not contribute to message unread counts. */
|
|
2
|
+
export function isReadingControl(payload, appId) {
|
|
3
|
+
if (!payload || typeof payload !== "object")
|
|
4
|
+
return false;
|
|
5
|
+
const value = payload;
|
|
6
|
+
if (value.body !== undefined)
|
|
7
|
+
return isReadingControl(value.body, appId);
|
|
8
|
+
return value.kind === "message" && value.content?.appId === appId && (value.content.reaction !== undefined || value.content.recall !== undefined);
|
|
9
|
+
}
|
package/package.json
CHANGED