stream-chat 9.21.0 → 9.22.0
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/dist/cjs/index.browser.js +654 -37
- package/dist/cjs/index.browser.js.map +4 -4
- package/dist/cjs/index.node.js +656 -37
- package/dist/cjs/index.node.js.map +4 -4
- package/dist/esm/index.mjs +654 -37
- package/dist/esm/index.mjs.map +4 -4
- package/dist/types/channel.d.ts +11 -2
- package/dist/types/channel_state.d.ts +6 -0
- package/dist/types/client.d.ts +17 -5
- package/dist/types/events.d.ts +4 -4
- package/dist/types/index.d.ts +1 -0
- package/dist/types/messageDelivery/MessageDeliveryReporter.d.ts +74 -0
- package/dist/types/messageDelivery/MessageReceiptsTracker.d.ts +121 -0
- package/dist/types/messageDelivery/index.d.ts +2 -0
- package/dist/types/types.d.ts +11 -4
- package/dist/types/utils.d.ts +2 -1
- package/package.json +1 -1
- package/src/channel.ts +73 -7
- package/src/channel_state.ts +147 -14
- package/src/client.ts +60 -14
- package/src/events.ts +4 -6
- package/src/index.ts +1 -0
- package/src/messageDelivery/MessageDeliveryReporter.ts +259 -0
- package/src/messageDelivery/MessageReceiptsTracker.ts +417 -0
- package/src/messageDelivery/index.ts +2 -0
- package/src/thread.ts +1 -1
- package/src/types.ts +13 -4
- package/src/utils.ts +2 -1
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
import type { ReadResponse, UserResponse } from '../types';
|
|
2
|
+
|
|
3
|
+
type UserId = string;
|
|
4
|
+
type MessageId = string;
|
|
5
|
+
export type MsgRef = { timestampMs: number; msgId: MessageId };
|
|
6
|
+
export type OwnMessageReceiptsTrackerMessageLocator = (
|
|
7
|
+
timestampMs: number,
|
|
8
|
+
) => MsgRef | null;
|
|
9
|
+
export type UserProgress = {
|
|
10
|
+
user: UserResponse;
|
|
11
|
+
lastReadRef: MsgRef; // MIN_REF if none
|
|
12
|
+
lastDeliveredRef: MsgRef; // MIN_REF if none; always >= readRef
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// ---------- ordering utilities ----------
|
|
16
|
+
|
|
17
|
+
const MIN_REF: MsgRef = { timestampMs: Number.NEGATIVE_INFINITY, msgId: '' } as const;
|
|
18
|
+
|
|
19
|
+
const compareRefsAsc = (a: MsgRef, b: MsgRef) =>
|
|
20
|
+
a.timestampMs !== b.timestampMs ? a.timestampMs - b.timestampMs : 0;
|
|
21
|
+
|
|
22
|
+
const findIndex = <T>(arr: T[], target: MsgRef, keyOf: (x: T) => MsgRef): number => {
|
|
23
|
+
let lo = 0,
|
|
24
|
+
hi = arr.length;
|
|
25
|
+
while (lo < hi) {
|
|
26
|
+
const mid = (lo + hi) >>> 1;
|
|
27
|
+
if (compareRefsAsc(keyOf(arr[mid]), target) >= 0) hi = mid;
|
|
28
|
+
else lo = mid + 1;
|
|
29
|
+
}
|
|
30
|
+
return lo;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* For insertion after the last equal item. E.g. array [a] exists and b is being inserted -> we want [a,b], not [b,a].
|
|
35
|
+
* @param arr
|
|
36
|
+
* @param target
|
|
37
|
+
* @param keyOf
|
|
38
|
+
*/
|
|
39
|
+
const findUpperIndex = <T>(arr: T[], target: MsgRef, keyOf: (x: T) => MsgRef): number => {
|
|
40
|
+
let lo = 0,
|
|
41
|
+
hi = arr.length;
|
|
42
|
+
while (lo < hi) {
|
|
43
|
+
const mid = (lo + hi) >>> 1;
|
|
44
|
+
if (compareRefsAsc(keyOf(arr[mid]), target) > 0) hi = mid;
|
|
45
|
+
else lo = mid + 1;
|
|
46
|
+
}
|
|
47
|
+
return lo;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const insertByKey = (
|
|
51
|
+
arr: UserProgress[],
|
|
52
|
+
item: UserProgress,
|
|
53
|
+
keyOf: (x: UserProgress) => MsgRef,
|
|
54
|
+
) => arr.splice(findUpperIndex(arr, keyOf(item), keyOf), 0, item);
|
|
55
|
+
|
|
56
|
+
const removeByOldKey = (
|
|
57
|
+
arr: UserProgress[],
|
|
58
|
+
item: UserProgress,
|
|
59
|
+
oldKey: MsgRef,
|
|
60
|
+
keyOf: (x: UserProgress) => MsgRef,
|
|
61
|
+
) => {
|
|
62
|
+
// Find the plateau for oldKey, scan to match by user id
|
|
63
|
+
let i = findIndex(arr, oldKey, keyOf);
|
|
64
|
+
while (i < arr.length && compareRefsAsc(keyOf(arr[i]), oldKey) === 0) {
|
|
65
|
+
if (arr[i].user.id === item.user.id) {
|
|
66
|
+
arr.splice(i, 1);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
i++;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export type OwnMessageReceiptsTrackerOptions = {
|
|
74
|
+
locateMessage: OwnMessageReceiptsTrackerMessageLocator;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* MessageReceiptsTracker
|
|
79
|
+
* --------------------------------
|
|
80
|
+
* Tracks **other participants’** delivery/read progress toward **own (outgoing) messages**
|
|
81
|
+
* within a **single timeline** (one channel/thread).
|
|
82
|
+
*
|
|
83
|
+
* How it works
|
|
84
|
+
* ------------
|
|
85
|
+
* - Each user has a compact progress record:
|
|
86
|
+
* - `lastReadRef`: latest message they have **read**
|
|
87
|
+
* - `lastDeliveredRef`: latest message they have **received** (always `>= lastReadRef`)
|
|
88
|
+
* - Internally keeps two arrays sorted **ascending by timestamp**:
|
|
89
|
+
* - `readSorted` (by `lastReadRef`)
|
|
90
|
+
* - `deliveredSorted` (by `lastDeliveredRef`)
|
|
91
|
+
* - Queries like “who read message M?” become a **binary search + suffix slice**.
|
|
92
|
+
*
|
|
93
|
+
* Construction
|
|
94
|
+
* ------------
|
|
95
|
+
* `new MessageReceiptsTracker({locateMessage})`
|
|
96
|
+
* - `locateMessage(timestamp) => MsgRef | null` must resolve a message ref representation - `{ timestamp, msgId }`.
|
|
97
|
+
* - If `locateMessage` returns `null`, the event is ignored (message unknown locally).
|
|
98
|
+
*
|
|
99
|
+
* Event ingestion
|
|
100
|
+
* ---------------
|
|
101
|
+
* - `ingestInitial(rows: ReadResponse[])`: Builds initial state from server snapshot.
|
|
102
|
+
* If a user’s `last_read` is ahead of `last_delivered_at`, the tracker enforces
|
|
103
|
+
* the invariant `lastDeliveredRef >= lastReadRef`.
|
|
104
|
+
* - `onMessageRead(user, readAtISO)`:
|
|
105
|
+
* Advances the user’s read; also bumps delivered to match if needed.
|
|
106
|
+
* - `onMessageDelivered(user, deliveredAtISO)`:
|
|
107
|
+
* Advances the user’s delivered to `max(currentRead, deliveredAt)`.
|
|
108
|
+
*
|
|
109
|
+
* Queries
|
|
110
|
+
* -------
|
|
111
|
+
* - `readersForMessage(msgRef) : UserResponse[]` → users with `lastReadRef >= msgRef`
|
|
112
|
+
* - `deliveredForMessage(msgRef) : UserResponse[]` → users with `lastDeliveredRef >= msgRef`
|
|
113
|
+
* - `deliveredNotReadForMessage(msgRef): UserResponse[]` → delivered but `lastReadRef < msgRef`
|
|
114
|
+
* - `usersWhoseLastReadIs : UserResponse[]` → users for whom `msgRef` is their *last read* (exact match)
|
|
115
|
+
* - `usersWhoseLastDeliveredIs : UserResponse[]` → users for whom `msgRef` is their *last delivered* (exact match)
|
|
116
|
+
* - `groupUsersByLastReadMessage : Record<MsgId, UserResponse[]> → mapping of messages to their readers
|
|
117
|
+
* - `groupUsersByLastDeliveredMessage : Record<MsgId, UserResponse[]> → mapping of messages to their receivers
|
|
118
|
+
* - `hasUserRead(msgRef, userId) : boolean`
|
|
119
|
+
* - `hasUserDelivered(msgRef, userId) : boolean`
|
|
120
|
+
*
|
|
121
|
+
* Complexity
|
|
122
|
+
* ----------
|
|
123
|
+
* - Update on read/delivered: **O(log U)** (binary search + one splice) per event, where U is count of users stored by tracker.
|
|
124
|
+
* - Query lists: **O(log U + K)** where `K` is the number of returned users (suffix length).
|
|
125
|
+
* - Memory: **O(U)** - tracker’s memory grows linearly with the number of users in the channel/thread and does not depend on the number of messages.
|
|
126
|
+
*
|
|
127
|
+
* Scope & notes
|
|
128
|
+
* -------------
|
|
129
|
+
* - One tracker instance is **scoped to a single timeline**. Instantiate per channel/thread.
|
|
130
|
+
* - Ordering is by **ascending timestamp**; ties are kept stable by inserting at the end of the
|
|
131
|
+
* equal-timestamp plateau (upper-bound insertion), preserving intuitive arrival order.
|
|
132
|
+
* - This tracker models **others’ progress toward own messages**;
|
|
133
|
+
*/
|
|
134
|
+
export class MessageReceiptsTracker {
|
|
135
|
+
private byUser = new Map<UserId, UserProgress>();
|
|
136
|
+
private readSorted: UserProgress[] = []; // asc by lastReadRef
|
|
137
|
+
private deliveredSorted: UserProgress[] = []; // asc by lastDeliveredRef
|
|
138
|
+
private locateMessage: OwnMessageReceiptsTrackerMessageLocator;
|
|
139
|
+
|
|
140
|
+
constructor({ locateMessage }: OwnMessageReceiptsTrackerOptions) {
|
|
141
|
+
this.locateMessage = locateMessage;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Build initial state from server snapshots (single pass + sort). */
|
|
145
|
+
ingestInitial(responses: ReadResponse[]) {
|
|
146
|
+
this.byUser.clear();
|
|
147
|
+
this.readSorted = [];
|
|
148
|
+
this.deliveredSorted = [];
|
|
149
|
+
for (const r of responses) {
|
|
150
|
+
const lastReadTimestamp = r.last_read ? new Date(r.last_read).getTime() : null;
|
|
151
|
+
const lastDeliveredTimestamp = r.last_delivered_at
|
|
152
|
+
? new Date(r.last_delivered_at).getTime()
|
|
153
|
+
: null;
|
|
154
|
+
const lastReadRef = lastReadTimestamp
|
|
155
|
+
? (this.locateMessage(lastReadTimestamp) ?? MIN_REF)
|
|
156
|
+
: MIN_REF;
|
|
157
|
+
let lastDeliveredRef = lastDeliveredTimestamp
|
|
158
|
+
? (this.locateMessage(lastDeliveredTimestamp) ?? MIN_REF)
|
|
159
|
+
: MIN_REF;
|
|
160
|
+
const isReadAfterDelivered = compareRefsAsc(lastDeliveredRef, lastReadRef) < 0;
|
|
161
|
+
if (isReadAfterDelivered) lastDeliveredRef = lastReadRef;
|
|
162
|
+
|
|
163
|
+
const userProgress: UserProgress = { user: r.user, lastReadRef, lastDeliveredRef };
|
|
164
|
+
this.byUser.set(r.user.id, userProgress);
|
|
165
|
+
this.readSorted.splice(
|
|
166
|
+
findIndex(this.readSorted, lastReadRef, (up) => up.lastReadRef),
|
|
167
|
+
0,
|
|
168
|
+
userProgress,
|
|
169
|
+
);
|
|
170
|
+
this.deliveredSorted.splice(
|
|
171
|
+
findIndex(this.deliveredSorted, lastDeliveredRef, (up) => up.lastDeliveredRef),
|
|
172
|
+
0,
|
|
173
|
+
userProgress,
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** message.delivered — user device confirmed delivery up to and including messageId. */
|
|
179
|
+
onMessageDelivered({
|
|
180
|
+
user,
|
|
181
|
+
deliveredAt,
|
|
182
|
+
lastDeliveredMessageId,
|
|
183
|
+
}: {
|
|
184
|
+
user: UserResponse;
|
|
185
|
+
deliveredAt: string;
|
|
186
|
+
lastDeliveredMessageId?: string;
|
|
187
|
+
}) {
|
|
188
|
+
const timestampMs = new Date(deliveredAt).getTime();
|
|
189
|
+
const msgRef = lastDeliveredMessageId
|
|
190
|
+
? { timestampMs, msgId: lastDeliveredMessageId }
|
|
191
|
+
: this.locateMessage(new Date(deliveredAt).getTime());
|
|
192
|
+
if (!msgRef) return;
|
|
193
|
+
const userProgress = this.ensureUser(user);
|
|
194
|
+
|
|
195
|
+
const newDelivered =
|
|
196
|
+
compareRefsAsc(msgRef, userProgress.lastReadRef) < 0
|
|
197
|
+
? userProgress.lastReadRef
|
|
198
|
+
: msgRef; // max(read, loc)
|
|
199
|
+
// newly announced delivered is older than or equal what is already registered
|
|
200
|
+
if (compareRefsAsc(newDelivered, userProgress.lastDeliveredRef) <= 0) return;
|
|
201
|
+
|
|
202
|
+
removeByOldKey(
|
|
203
|
+
this.deliveredSorted,
|
|
204
|
+
userProgress,
|
|
205
|
+
userProgress.lastDeliveredRef,
|
|
206
|
+
(x) => x.lastDeliveredRef,
|
|
207
|
+
);
|
|
208
|
+
userProgress.lastDeliveredRef = newDelivered;
|
|
209
|
+
insertByKey(this.deliveredSorted, userProgress, (x) => x.lastDeliveredRef);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** message.read — user read up to and including messageId. */
|
|
213
|
+
onMessageRead({
|
|
214
|
+
user,
|
|
215
|
+
readAt,
|
|
216
|
+
lastReadMessageId,
|
|
217
|
+
}: {
|
|
218
|
+
user: UserResponse;
|
|
219
|
+
readAt: string;
|
|
220
|
+
lastReadMessageId?: string;
|
|
221
|
+
}) {
|
|
222
|
+
const timestampMs = new Date(readAt).getTime();
|
|
223
|
+
const msgRef = lastReadMessageId
|
|
224
|
+
? { timestampMs, msgId: lastReadMessageId }
|
|
225
|
+
: this.locateMessage(timestampMs);
|
|
226
|
+
if (!msgRef) return;
|
|
227
|
+
const userProgress = this.ensureUser(user);
|
|
228
|
+
// newly announced read message is older than or equal the already recorded last read message
|
|
229
|
+
if (compareRefsAsc(msgRef, userProgress.lastReadRef) <= 0) return;
|
|
230
|
+
|
|
231
|
+
// move in readSorted
|
|
232
|
+
removeByOldKey(
|
|
233
|
+
this.readSorted,
|
|
234
|
+
userProgress,
|
|
235
|
+
userProgress.lastReadRef,
|
|
236
|
+
(x) => x.lastReadRef,
|
|
237
|
+
);
|
|
238
|
+
userProgress.lastReadRef = msgRef;
|
|
239
|
+
insertByKey(this.readSorted, userProgress, (x) => x.lastReadRef);
|
|
240
|
+
|
|
241
|
+
// keep delivered >= read
|
|
242
|
+
if (compareRefsAsc(userProgress.lastDeliveredRef, userProgress.lastReadRef) < 0) {
|
|
243
|
+
removeByOldKey(
|
|
244
|
+
this.deliveredSorted,
|
|
245
|
+
userProgress,
|
|
246
|
+
userProgress.lastDeliveredRef,
|
|
247
|
+
(x) => x.lastDeliveredRef,
|
|
248
|
+
);
|
|
249
|
+
userProgress.lastDeliveredRef = userProgress.lastReadRef;
|
|
250
|
+
insertByKey(this.deliveredSorted, userProgress, (x) => x.lastDeliveredRef);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** notification.mark_unread — user marked messages unread starting at `first_unread_message_id`.
|
|
255
|
+
* Sets lastReadRef to the event’s last_read_* values. Delivery never moves backward.
|
|
256
|
+
* The event is sent only to the user that triggered the action (own user), so we will never adjust read ref
|
|
257
|
+
* for other users - we will not see changes in the UI for other users. However, this implementation does not
|
|
258
|
+
* take into consideration this fact and is ready to handle the mark-unread event for any user.
|
|
259
|
+
*/
|
|
260
|
+
onNotificationMarkUnread({
|
|
261
|
+
user,
|
|
262
|
+
lastReadAt,
|
|
263
|
+
lastReadMessageId,
|
|
264
|
+
}: {
|
|
265
|
+
user: UserResponse;
|
|
266
|
+
lastReadAt?: string;
|
|
267
|
+
lastReadMessageId?: string;
|
|
268
|
+
}) {
|
|
269
|
+
const userProgress = this.ensureUser(user);
|
|
270
|
+
|
|
271
|
+
const newReadRef: MsgRef = lastReadAt
|
|
272
|
+
? { timestampMs: new Date(lastReadAt).getTime(), msgId: lastReadMessageId ?? '' }
|
|
273
|
+
: { ...MIN_REF };
|
|
274
|
+
|
|
275
|
+
// If no change, exit early.
|
|
276
|
+
if (
|
|
277
|
+
compareRefsAsc(newReadRef, userProgress.lastReadRef) === 0 &&
|
|
278
|
+
newReadRef.msgId === userProgress.lastReadRef.msgId
|
|
279
|
+
) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
removeByOldKey(
|
|
284
|
+
this.readSorted,
|
|
285
|
+
userProgress,
|
|
286
|
+
userProgress.lastReadRef,
|
|
287
|
+
(x) => x.lastReadRef,
|
|
288
|
+
);
|
|
289
|
+
userProgress.lastReadRef = newReadRef;
|
|
290
|
+
insertByKey(this.readSorted, userProgress, (x) => x.lastReadRef);
|
|
291
|
+
|
|
292
|
+
// Maintain invariant delivered >= read.
|
|
293
|
+
if (compareRefsAsc(userProgress.lastDeliveredRef, userProgress.lastReadRef) < 0) {
|
|
294
|
+
removeByOldKey(
|
|
295
|
+
this.deliveredSorted,
|
|
296
|
+
userProgress,
|
|
297
|
+
userProgress.lastDeliveredRef,
|
|
298
|
+
(x) => x.lastDeliveredRef,
|
|
299
|
+
);
|
|
300
|
+
userProgress.lastDeliveredRef = userProgress.lastReadRef;
|
|
301
|
+
insertByKey(this.deliveredSorted, userProgress, (x) => x.lastDeliveredRef);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** All users who READ this message. */
|
|
306
|
+
readersForMessage(msgRef: MsgRef): UserResponse[] {
|
|
307
|
+
const index = findIndex(this.readSorted, msgRef, ({ lastReadRef }) => lastReadRef);
|
|
308
|
+
return this.readSorted.slice(index).map((x) => x.user);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** All users who have it DELIVERED (includes readers). */
|
|
312
|
+
deliveredForMessage(msgRef: MsgRef): UserResponse[] {
|
|
313
|
+
const pos = findIndex(
|
|
314
|
+
this.deliveredSorted,
|
|
315
|
+
msgRef,
|
|
316
|
+
({ lastDeliveredRef }) => lastDeliveredRef,
|
|
317
|
+
);
|
|
318
|
+
return this.deliveredSorted.slice(pos).map((x) => x.user);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Users who delivered but have NOT read. */
|
|
322
|
+
deliveredNotReadForMessage(msgRef: MsgRef): UserResponse[] {
|
|
323
|
+
const pos = findIndex(
|
|
324
|
+
this.deliveredSorted,
|
|
325
|
+
msgRef,
|
|
326
|
+
({ lastDeliveredRef }) => lastDeliveredRef,
|
|
327
|
+
);
|
|
328
|
+
const usersDeliveredNotRead: UserResponse[] = [];
|
|
329
|
+
for (let i = pos; i < this.deliveredSorted.length; i++) {
|
|
330
|
+
const userProgress = this.deliveredSorted[i];
|
|
331
|
+
if (compareRefsAsc(userProgress.lastReadRef, msgRef) < 0)
|
|
332
|
+
usersDeliveredNotRead.push(userProgress.user);
|
|
333
|
+
}
|
|
334
|
+
return usersDeliveredNotRead;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Users for whom `msgRef` is their *last read* (exact match). */
|
|
338
|
+
usersWhoseLastReadIs(msgRef: MsgRef): UserResponse[] {
|
|
339
|
+
if (!msgRef.msgId) return [];
|
|
340
|
+
const start = findIndex(this.readSorted, msgRef, (x) => x.lastReadRef);
|
|
341
|
+
const end = findUpperIndex(this.readSorted, msgRef, (x) => x.lastReadRef);
|
|
342
|
+
const users: UserResponse[] = [];
|
|
343
|
+
for (let i = start; i < end; i++) {
|
|
344
|
+
const up = this.readSorted[i];
|
|
345
|
+
if (up.lastReadRef.msgId === msgRef.msgId) users.push(up.user);
|
|
346
|
+
}
|
|
347
|
+
return users;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/** Users for whom `msgRef` is their *last delivered* (exact match). */
|
|
351
|
+
usersWhoseLastDeliveredIs(msgRef: MsgRef): UserResponse[] {
|
|
352
|
+
if (!msgRef.msgId) return [];
|
|
353
|
+
const start = findIndex(this.deliveredSorted, msgRef, (x) => x.lastDeliveredRef);
|
|
354
|
+
const end = findUpperIndex(this.deliveredSorted, msgRef, (x) => x.lastDeliveredRef);
|
|
355
|
+
const users: UserResponse[] = [];
|
|
356
|
+
for (let i = start; i < end; i++) {
|
|
357
|
+
const up = this.deliveredSorted[i];
|
|
358
|
+
if (up.lastDeliveredRef.msgId === msgRef.msgId) users.push(up.user);
|
|
359
|
+
}
|
|
360
|
+
return users;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// ---- queries: per-user status ----
|
|
364
|
+
|
|
365
|
+
hasUserRead(msgRef: MsgRef, userId: string): boolean {
|
|
366
|
+
const up = this.byUser.get(userId);
|
|
367
|
+
return !!up && compareRefsAsc(up.lastReadRef, msgRef) >= 0;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
hasUserDelivered(msgRef: MsgRef, userId: string): boolean {
|
|
371
|
+
const up = this.byUser.get(userId);
|
|
372
|
+
return !!up && compareRefsAsc(up.lastDeliveredRef, msgRef) >= 0;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
getUserProgress(userId: string): UserProgress | null {
|
|
376
|
+
const userProgress = this.byUser.get(userId);
|
|
377
|
+
if (!userProgress) return null;
|
|
378
|
+
return userProgress;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
groupUsersByLastReadMessage(): Record<MessageId, UserResponse[]> {
|
|
382
|
+
return Array.from(this.byUser.values()).reduce<Record<MessageId, UserResponse[]>>(
|
|
383
|
+
(acc, userProgress) => {
|
|
384
|
+
const msgId = userProgress.lastReadRef.msgId;
|
|
385
|
+
if (!msgId) return acc;
|
|
386
|
+
if (!acc[msgId]) acc[msgId] = [];
|
|
387
|
+
acc[msgId].push(userProgress.user);
|
|
388
|
+
return acc;
|
|
389
|
+
},
|
|
390
|
+
{},
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
groupUsersByLastDeliveredMessage(): Record<MessageId, UserResponse[]> {
|
|
395
|
+
return Array.from(this.byUser.values()).reduce<Record<MessageId, UserResponse[]>>(
|
|
396
|
+
(acc, userProgress) => {
|
|
397
|
+
const msgId = userProgress.lastDeliveredRef.msgId;
|
|
398
|
+
if (!msgId) return acc;
|
|
399
|
+
if (!acc[msgId]) acc[msgId] = [];
|
|
400
|
+
acc[msgId].push(userProgress.user);
|
|
401
|
+
return acc;
|
|
402
|
+
},
|
|
403
|
+
{},
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
private ensureUser(user: UserResponse): UserProgress {
|
|
408
|
+
let up = this.byUser.get(user.id);
|
|
409
|
+
if (!up) {
|
|
410
|
+
up = { user, lastReadRef: MIN_REF, lastDeliveredRef: MIN_REF };
|
|
411
|
+
this.byUser.set(user.id, up);
|
|
412
|
+
insertByKey(this.readSorted, up, (x) => x.lastReadRef);
|
|
413
|
+
insertByKey(this.deliveredSorted, up, (x) => x.lastDeliveredRef);
|
|
414
|
+
}
|
|
415
|
+
return up;
|
|
416
|
+
}
|
|
417
|
+
}
|
package/src/thread.ts
CHANGED
|
@@ -534,7 +534,7 @@ export class Thread extends WithSubscriptions {
|
|
|
534
534
|
return null;
|
|
535
535
|
}
|
|
536
536
|
|
|
537
|
-
return await this.
|
|
537
|
+
return await this.client.messageDeliveryReporter.markRead(this);
|
|
538
538
|
};
|
|
539
539
|
|
|
540
540
|
private throttledMarkAsRead = throttle(
|
package/src/types.ts
CHANGED
|
@@ -713,6 +713,7 @@ export type MessageResponseBase = MessageBase & {
|
|
|
713
713
|
status?: string;
|
|
714
714
|
thread_participants?: UserResponse[];
|
|
715
715
|
updated_at?: string;
|
|
716
|
+
deleted_for_me?: boolean;
|
|
716
717
|
};
|
|
717
718
|
|
|
718
719
|
export type ReactionGroupResponse = {
|
|
@@ -968,7 +969,7 @@ export type BanUserOptions = UnBanUserOptions & {
|
|
|
968
969
|
ip_ban?: boolean;
|
|
969
970
|
reason?: string;
|
|
970
971
|
timeout?: number;
|
|
971
|
-
delete_messages?:
|
|
972
|
+
delete_messages?: MessageDeletionStrategy;
|
|
972
973
|
};
|
|
973
974
|
|
|
974
975
|
export type ChannelOptions = {
|
|
@@ -1468,14 +1469,15 @@ export type Event = CustomEventData & {
|
|
|
1468
1469
|
ai_state?: AIState;
|
|
1469
1470
|
channel?: ChannelResponse;
|
|
1470
1471
|
channel_custom?: CustomChannelData;
|
|
1471
|
-
channel_member_count?: number;
|
|
1472
1472
|
channel_id?: string;
|
|
1473
|
+
channel_member_count?: number;
|
|
1473
1474
|
channel_type?: string;
|
|
1474
1475
|
cid?: string;
|
|
1475
1476
|
clear_history?: boolean;
|
|
1476
1477
|
connection_id?: string;
|
|
1477
1478
|
// event creation timestamp, format Date ISO string
|
|
1478
1479
|
created_at?: string;
|
|
1480
|
+
deleted_for_me?: boolean;
|
|
1479
1481
|
draft?: DraftResponse;
|
|
1480
1482
|
// id of the message that was marked as unread - all the following messages are considered unread. (notification.mark_unread)
|
|
1481
1483
|
first_unread_message_id?: string;
|
|
@@ -3602,14 +3604,21 @@ export type CustomCheckFlag = {
|
|
|
3602
3604
|
reason?: string;
|
|
3603
3605
|
};
|
|
3604
3606
|
|
|
3605
|
-
export type
|
|
3607
|
+
export type MessageDeletionStrategy = 'soft' | 'hard';
|
|
3608
|
+
// @deprecated use type MessageDeletionStrategy instead
|
|
3609
|
+
export type DeleteMessagesOptions = MessageDeletionStrategy;
|
|
3610
|
+
|
|
3611
|
+
export type DeleteMessageOptions = {
|
|
3612
|
+
deleteForMe?: boolean;
|
|
3613
|
+
hardDelete?: boolean;
|
|
3614
|
+
};
|
|
3606
3615
|
|
|
3607
3616
|
export type SubmitActionOptions = {
|
|
3608
3617
|
ban?: {
|
|
3609
3618
|
channel_ban_only?: boolean;
|
|
3610
3619
|
reason?: string;
|
|
3611
3620
|
timeout?: number;
|
|
3612
|
-
delete_messages?:
|
|
3621
|
+
delete_messages?: MessageDeletionStrategy;
|
|
3613
3622
|
};
|
|
3614
3623
|
delete_message?: {
|
|
3615
3624
|
hard_delete?: boolean;
|
package/src/utils.ts
CHANGED
|
@@ -748,7 +748,8 @@ export const debounce = <T extends (...args: any[]) => any>(
|
|
|
748
748
|
};
|
|
749
749
|
|
|
750
750
|
// works exactly the same as lodash.throttle
|
|
751
|
-
|
|
751
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
752
|
+
export const throttle = <T extends (...args: any[]) => any>(
|
|
752
753
|
fn: T,
|
|
753
754
|
timeout = 200,
|
|
754
755
|
{ leading = true, trailing = false }: { leading?: boolean; trailing?: boolean } = {},
|