stream-chat 9.21.0 → 9.22.1

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.
@@ -36,6 +36,12 @@ var require_https = __commonJS({
36
36
  }
37
37
  });
38
38
 
39
+ // (disabled):node_modules/jsonwebtoken/index.js
40
+ var require_jsonwebtoken = __commonJS({
41
+ "(disabled):node_modules/jsonwebtoken/index.js"() {
42
+ }
43
+ });
44
+
39
45
  // (disabled):crypto
40
46
  var require_crypto = __commonJS({
41
47
  "(disabled):crypto"() {
@@ -96,7 +102,9 @@ __export(index_exports, {
96
102
  MergedStateStore: () => MergedStateStore,
97
103
  MessageComposer: () => MessageComposer,
98
104
  MessageComposerMiddlewareExecutor: () => MessageComposerMiddlewareExecutor,
105
+ MessageDeliveryReporter: () => MessageDeliveryReporter,
99
106
  MessageDraftComposerMiddlewareExecutor: () => MessageDraftComposerMiddlewareExecutor,
107
+ MessageReceiptsTracker: () => MessageReceiptsTracker,
100
108
  MessageSearchSource: () => MessageSearchSource,
101
109
  MiddlewareExecutor: () => MiddlewareExecutor,
102
110
  MinPriority: () => MinPriority,
@@ -1147,6 +1155,21 @@ var runDetached = (callback, options) => {
1147
1155
  };
1148
1156
 
1149
1157
  // src/channel_state.ts
1158
+ var messageSetBounds = (a, b) => ({
1159
+ newestMessageA: new Date(a[0]?.created_at ?? 0),
1160
+ oldestMessageA: new Date(a.slice(-1)[0]?.created_at ?? 0),
1161
+ newestMessageB: new Date(b[0]?.created_at ?? 0),
1162
+ oldestMessageB: new Date(b.slice(-1)[0]?.created_at ?? 0)
1163
+ });
1164
+ var aContainsOrEqualsB = (a, b) => {
1165
+ const { newestMessageA, newestMessageB, oldestMessageA, oldestMessageB } = messageSetBounds(a, b);
1166
+ return newestMessageA >= newestMessageB && oldestMessageB >= oldestMessageA;
1167
+ };
1168
+ var aOverlapsB = (a, b) => {
1169
+ const { newestMessageA, newestMessageB, oldestMessageA, oldestMessageB } = messageSetBounds(a, b);
1170
+ return oldestMessageA < oldestMessageB && oldestMessageB < newestMessageA && newestMessageA < newestMessageB;
1171
+ };
1172
+ var messageSetsOverlapByTimestamp = (a, b) => aContainsOrEqualsB(a, b) || aContainsOrEqualsB(b, a) || aOverlapsB(a, b) || aOverlapsB(b, a);
1150
1173
  var ChannelState = class {
1151
1174
  constructor(channel) {
1152
1175
  /**
@@ -1225,6 +1248,22 @@ var ChannelState = class {
1225
1248
  deletedAt: deletedAt ?? null
1226
1249
  });
1227
1250
  };
1251
+ /**
1252
+ * Identifies the set index into which a message set would pertain if its first item's creation date corresponded to oldestTimestampMs.
1253
+ * @param oldestTimestampMs
1254
+ */
1255
+ this.findMessageSetByOldestTimestamp = (oldestTimestampMs) => {
1256
+ let lo = 0, hi = this.messageSets.length;
1257
+ while (lo < hi) {
1258
+ const mid = lo + hi >>> 1;
1259
+ const msgSet = this.messageSets[mid];
1260
+ if (msgSet.messages.length === 0) return -1;
1261
+ const oldestMessageTimestampInSet = msgSet.messages[0].created_at.getTime();
1262
+ if (oldestMessageTimestampInSet <= oldestTimestampMs) hi = mid;
1263
+ else lo = mid + 1;
1264
+ }
1265
+ return lo;
1266
+ };
1228
1267
  this._channel = channel;
1229
1268
  this.watcher_count = 0;
1230
1269
  this.typing = {};
@@ -1750,6 +1789,25 @@ var ChannelState = class {
1750
1789
  }
1751
1790
  return this.messageSets[messageSetIndex].messages.find((m) => m.id === messageId);
1752
1791
  }
1792
+ findMessageByTimestamp(timestampMs, parentMessageId, exactTsMatch = false) {
1793
+ if (parentMessageId && !this.threads[parentMessageId] || this.messageSets.length === 0)
1794
+ return null;
1795
+ const setIndex = this.findMessageSetByOldestTimestamp(timestampMs);
1796
+ const targetMsgSet = this.messageSets[setIndex]?.messages;
1797
+ if (!targetMsgSet?.length) return null;
1798
+ const firstMsgTimestamp = targetMsgSet[0].created_at.getTime();
1799
+ const lastMsgTimestamp = targetMsgSet.slice(-1)[0].created_at.getTime();
1800
+ const isOutOfBound = timestampMs < firstMsgTimestamp || lastMsgTimestamp < timestampMs;
1801
+ if (isOutOfBound && exactTsMatch) return null;
1802
+ let msgIndex = 0, hi = targetMsgSet.length - 1;
1803
+ while (msgIndex < hi) {
1804
+ const mid = msgIndex + hi >>> 1;
1805
+ if (timestampMs <= targetMsgSet[mid].created_at.getTime()) hi = mid;
1806
+ else msgIndex = mid + 1;
1807
+ }
1808
+ const foundMessage = targetMsgSet[msgIndex];
1809
+ return !exactTsMatch ? foundMessage : foundMessage.created_at.getTime() === timestampMs ? foundMessage : null;
1810
+ }
1753
1811
  switchToMessageSet(index) {
1754
1812
  const currentMessages = this.messageSets.find((s) => s.isCurrent);
1755
1813
  if (!currentMessages) {
@@ -1769,35 +1827,77 @@ var ChannelState = class {
1769
1827
  findTargetMessageSet(newMessages, addIfDoesNotExist = true, messageSetToAddToIfDoesNotExist = "current") {
1770
1828
  let messagesToAdd = newMessages;
1771
1829
  let targetMessageSetIndex;
1830
+ if (newMessages.length === 0)
1831
+ return { targetMessageSetIndex: 0, messagesToAdd: newMessages };
1772
1832
  if (addIfDoesNotExist) {
1773
- const overlappingMessageSetIndices = this.messageSets.map((_, i) => i).filter(
1833
+ const overlappingMessageSetIndicesByMsgIds = this.messageSets.map((_, i) => i).filter(
1774
1834
  (i) => this.areMessageSetsOverlap(this.messageSets[i].messages, newMessages)
1775
1835
  );
1836
+ const overlappingMessageSetIndicesByTimestamp = this.messageSets.map((_, i) => i).filter(
1837
+ (i) => messageSetsOverlapByTimestamp(
1838
+ this.messageSets[i].messages,
1839
+ newMessages.map(formatMessage)
1840
+ )
1841
+ );
1776
1842
  switch (messageSetToAddToIfDoesNotExist) {
1777
1843
  case "new":
1778
- if (overlappingMessageSetIndices.length > 0) {
1779
- targetMessageSetIndex = overlappingMessageSetIndices[0];
1844
+ if (overlappingMessageSetIndicesByMsgIds.length > 0) {
1845
+ targetMessageSetIndex = overlappingMessageSetIndicesByMsgIds[0];
1846
+ } else if (overlappingMessageSetIndicesByTimestamp.length > 0) {
1847
+ targetMessageSetIndex = overlappingMessageSetIndicesByTimestamp[0];
1780
1848
  } else if (newMessages.some((m) => !m.parent_id)) {
1781
- this.messageSets.push({
1782
- messages: [],
1783
- isCurrent: false,
1784
- isLatest: false,
1785
- pagination: DEFAULT_MESSAGE_SET_PAGINATION
1786
- });
1787
- targetMessageSetIndex = this.messageSets.length - 1;
1849
+ const setIngestIndex = this.findMessageSetByOldestTimestamp(
1850
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1851
+ new Date(newMessages[0].created_at).getTime()
1852
+ );
1853
+ if (setIngestIndex === -1) {
1854
+ this.messageSets.push({
1855
+ messages: [],
1856
+ isCurrent: false,
1857
+ isLatest: false,
1858
+ pagination: DEFAULT_MESSAGE_SET_PAGINATION
1859
+ });
1860
+ targetMessageSetIndex = this.messageSets.length - 1;
1861
+ } else {
1862
+ const isLatest = setIngestIndex === 0;
1863
+ this.messageSets.splice(setIngestIndex, 0, {
1864
+ messages: [],
1865
+ isCurrent: false,
1866
+ isLatest,
1867
+ pagination: DEFAULT_MESSAGE_SET_PAGINATION
1868
+ // fixme: it is problematic decide about pagination without having data
1869
+ });
1870
+ if (isLatest) {
1871
+ this.messageSets.slice(1).forEach((set) => {
1872
+ set.isLatest = false;
1873
+ });
1874
+ }
1875
+ targetMessageSetIndex = setIngestIndex;
1876
+ }
1788
1877
  }
1789
1878
  break;
1790
1879
  case "current":
1791
- targetMessageSetIndex = this.messageSets.findIndex((s) => s.isCurrent);
1880
+ if (overlappingMessageSetIndicesByTimestamp.length > 0) {
1881
+ targetMessageSetIndex = overlappingMessageSetIndicesByTimestamp[0];
1882
+ } else {
1883
+ targetMessageSetIndex = this.messageSets.findIndex((s) => s.isCurrent);
1884
+ }
1792
1885
  break;
1793
1886
  case "latest":
1794
- targetMessageSetIndex = this.messageSets.findIndex((s) => s.isLatest);
1887
+ if (overlappingMessageSetIndicesByTimestamp.length > 0) {
1888
+ targetMessageSetIndex = overlappingMessageSetIndicesByTimestamp[0];
1889
+ } else {
1890
+ targetMessageSetIndex = this.messageSets.findIndex((s) => s.isLatest);
1891
+ }
1795
1892
  break;
1796
1893
  default:
1797
1894
  targetMessageSetIndex = -1;
1798
1895
  }
1799
- const mergeTargetMessageSetIndex = overlappingMessageSetIndices.splice(0, 1)[0];
1800
- const mergeSourceMessageSetIndices = [...overlappingMessageSetIndices];
1896
+ const mergeTargetMessageSetIndex = overlappingMessageSetIndicesByMsgIds.splice(
1897
+ 0,
1898
+ 1
1899
+ )[0];
1900
+ const mergeSourceMessageSetIndices = [...overlappingMessageSetIndicesByMsgIds];
1801
1901
  if (mergeTargetMessageSetIndex !== void 0 && mergeTargetMessageSetIndex !== targetMessageSetIndex) {
1802
1902
  mergeSourceMessageSetIndices.push(targetMessageSetIndex);
1803
1903
  }
@@ -6330,7 +6430,7 @@ var Thread = class extends WithSubscriptions {
6330
6430
  if (this.ownUnreadCount === 0 && !force) {
6331
6431
  return null;
6332
6432
  }
6333
- return await this.channel.markRead({ thread_id: this.id });
6433
+ return await this.client.messageDeliveryReporter.markRead(this);
6334
6434
  };
6335
6435
  this.throttledMarkAsRead = throttle(
6336
6436
  () => this.markAsRead(),
@@ -7060,6 +7160,444 @@ var _MessageComposer = class _MessageComposer extends WithSubscriptions {
7060
7160
  _MessageComposer.generateId = generateUUIDv4;
7061
7161
  var MessageComposer = _MessageComposer;
7062
7162
 
7163
+ // src/messageDelivery/MessageDeliveryReporter.ts
7164
+ var MAX_DELIVERED_MESSAGE_COUNT_IN_PAYLOAD = 100;
7165
+ var MARK_AS_DELIVERED_BUFFER_TIMEOUT = 1e3;
7166
+ var MARK_AS_READ_THROTTLE_TIMEOUT2 = 1e3;
7167
+ var isChannel = (item) => item instanceof Channel;
7168
+ var isThread = (item) => item instanceof Thread;
7169
+ var MessageDeliveryReporter = class {
7170
+ constructor({ client }) {
7171
+ this.deliveryReportCandidates = /* @__PURE__ */ new Map();
7172
+ this.nextDeliveryReportCandidates = /* @__PURE__ */ new Map();
7173
+ this.markDeliveredRequestPromise = null;
7174
+ this.markDeliveredTimeout = null;
7175
+ /**
7176
+ * Retrieve the reference to the latest message in the state that is nor read neither reported as delivered
7177
+ * @param collection
7178
+ */
7179
+ this.getNextDeliveryReportCandidate = (collection) => {
7180
+ const ownUserId = this.client.user?.id;
7181
+ if (!ownUserId) return;
7182
+ let latestMessages = [];
7183
+ let lastDeliveredAt;
7184
+ let lastReadAt;
7185
+ let key = void 0;
7186
+ if (isChannel(collection)) {
7187
+ latestMessages = collection.state.latestMessages;
7188
+ const ownReadState = collection.state.read[ownUserId] ?? {};
7189
+ lastReadAt = ownReadState?.last_read;
7190
+ lastDeliveredAt = ownReadState?.last_delivered_at;
7191
+ key = collection.cid;
7192
+ } else if (isThread(collection)) {
7193
+ latestMessages = collection.state.getLatestValue().replies;
7194
+ const ownReadState = collection.state.getLatestValue().read[ownUserId] ?? {};
7195
+ lastReadAt = ownReadState?.lastReadAt;
7196
+ lastDeliveredAt = ownReadState?.lastDeliveredAt;
7197
+ key = `${collection.channel.cid}:${collection.id}`;
7198
+ return;
7199
+ } else {
7200
+ return;
7201
+ }
7202
+ if (!key) return;
7203
+ const [latestMessage] = latestMessages.slice(-1);
7204
+ const wholeCollectionIsRead = !latestMessage || lastReadAt >= latestMessage.created_at;
7205
+ if (wholeCollectionIsRead) return { key, id: null };
7206
+ const wholeCollectionIsMarkedDelivered = !latestMessage || (lastDeliveredAt ?? 0) >= latestMessage.created_at;
7207
+ if (wholeCollectionIsMarkedDelivered) return { key, id: null };
7208
+ return { key, id: latestMessage.id || null };
7209
+ };
7210
+ /**
7211
+ * Fires delivery announcement request followed by immediate delivery candidate buffer reset.
7212
+ * @param options
7213
+ */
7214
+ this.announceDelivery = (options) => {
7215
+ if (this.markDeliveredRequestInFlight || !this.hasDeliveryCandidates) return;
7216
+ const { latest_delivered_messages, sendBuffer } = this.confirmationsFromDeliveryReportCandidates();
7217
+ if (!latest_delivered_messages.length) return;
7218
+ const payload = { ...options, latest_delivered_messages };
7219
+ const postFlightReconcile = () => {
7220
+ this.markDeliveredRequestPromise = null;
7221
+ for (const [k, v] of this.nextDeliveryReportCandidates.entries()) {
7222
+ this.deliveryReportCandidates.set(k, v);
7223
+ }
7224
+ this.nextDeliveryReportCandidates = /* @__PURE__ */ new Map();
7225
+ this.announceDeliveryBuffered(options);
7226
+ };
7227
+ const handleError = () => {
7228
+ for (const [k, v] of Object.entries(sendBuffer)) {
7229
+ if (!this.deliveryReportCandidates.has(k)) {
7230
+ this.deliveryReportCandidates.set(k, v);
7231
+ }
7232
+ }
7233
+ postFlightReconcile();
7234
+ };
7235
+ this.markDeliveredRequestPromise = this.client.markChannelsDelivered(payload).then(postFlightReconcile, handleError);
7236
+ };
7237
+ this.announceDeliveryBuffered = (options) => {
7238
+ if (this.hasTimer || this.markDeliveredRequestInFlight || !this.hasDeliveryCandidates)
7239
+ return;
7240
+ this.markDeliveredTimeout = setTimeout(() => {
7241
+ this.markDeliveredTimeout = null;
7242
+ this.announceDelivery(options);
7243
+ }, MARK_AS_DELIVERED_BUFFER_TIMEOUT);
7244
+ };
7245
+ /**
7246
+ * Delegates the mark-read call to the Channel or Thread instance
7247
+ * @param collection
7248
+ * @param options
7249
+ */
7250
+ this.markRead = async (collection, options) => {
7251
+ let result = null;
7252
+ if (isChannel(collection)) {
7253
+ result = await collection.markAsReadRequest(options);
7254
+ } else if (isThread(collection)) {
7255
+ result = await collection.channel.markAsReadRequest({
7256
+ ...options,
7257
+ thread_id: collection.id
7258
+ });
7259
+ }
7260
+ this.removeCandidateFor(collection);
7261
+ return result;
7262
+ };
7263
+ /**
7264
+ * Throttles the MessageDeliveryReporter.markRead call
7265
+ * @param collection
7266
+ * @param options
7267
+ */
7268
+ this.throttledMarkRead = throttle(this.markRead, MARK_AS_READ_THROTTLE_TIMEOUT2, {
7269
+ leading: false,
7270
+ trailing: true
7271
+ });
7272
+ this.client = client;
7273
+ }
7274
+ get markDeliveredRequestInFlight() {
7275
+ return this.markDeliveredRequestPromise !== null;
7276
+ }
7277
+ get hasTimer() {
7278
+ return this.markDeliveredTimeout !== null;
7279
+ }
7280
+ get hasDeliveryCandidates() {
7281
+ return this.deliveryReportCandidates.size > 0;
7282
+ }
7283
+ /**
7284
+ * Build latest_delivered_messages payload from an arbitrary buffer (deliveryReportCandidates / nextDeliveryReportCandidates)
7285
+ */
7286
+ confirmationsFrom(map2) {
7287
+ return Array.from(map2.entries()).map(([key, messageId]) => {
7288
+ const [type, id, parent_id] = key.split(":");
7289
+ return parent_id ? { cid: `${type}:${id}`, id: messageId, parent_id } : { cid: key, id: messageId };
7290
+ });
7291
+ }
7292
+ confirmationsFromDeliveryReportCandidates() {
7293
+ const entries = Array.from(this.deliveryReportCandidates);
7294
+ const sendBuffer = new Map(entries.slice(0, MAX_DELIVERED_MESSAGE_COUNT_IN_PAYLOAD));
7295
+ this.deliveryReportCandidates = new Map(
7296
+ entries.slice(MAX_DELIVERED_MESSAGE_COUNT_IN_PAYLOAD)
7297
+ );
7298
+ return { latest_delivered_messages: this.confirmationsFrom(sendBuffer), sendBuffer };
7299
+ }
7300
+ /**
7301
+ * Generate candidate key for storing in the candidates buffer
7302
+ * @param collection
7303
+ * @private
7304
+ */
7305
+ candidateKeyFor(collection) {
7306
+ if (isChannel(collection)) return collection.cid;
7307
+ if (isThread(collection)) return `${collection.channel.cid}:${collection.id}`;
7308
+ }
7309
+ /**
7310
+ * Updates the delivery candidates buffer with the latest delivery candidates
7311
+ * @param collection
7312
+ */
7313
+ trackDeliveredCandidate(collection) {
7314
+ if (isChannel(collection) && !collection.getConfig()?.read_events) return;
7315
+ if (isThread(collection) && !collection.channel.getConfig()?.read_events) return;
7316
+ const candidate = this.getNextDeliveryReportCandidate(collection);
7317
+ if (!candidate?.key) return;
7318
+ const buffer = this.markDeliveredRequestInFlight ? this.nextDeliveryReportCandidates : this.deliveryReportCandidates;
7319
+ if (candidate.id === null) buffer.delete(candidate.key);
7320
+ else buffer.set(candidate.key, candidate.id);
7321
+ }
7322
+ /**
7323
+ * Removes candidate from the delivery report buffer
7324
+ * @param collection
7325
+ * @private
7326
+ */
7327
+ removeCandidateFor(collection) {
7328
+ const candidateKey = this.candidateKeyFor(collection);
7329
+ if (!candidateKey) return;
7330
+ this.deliveryReportCandidates.delete(candidateKey);
7331
+ this.nextDeliveryReportCandidates.delete(candidateKey);
7332
+ }
7333
+ /**
7334
+ * Records the latest message delivered for Channel or Thread instances and schedules the next report
7335
+ * if not already scheduled and candidates exist.
7336
+ * Should be used for WS handling (message.new) as well as for ingesting HTTP channel query results.
7337
+ * @param collections
7338
+ */
7339
+ syncDeliveredCandidates(collections) {
7340
+ if (this.client.user?.privacy_settings?.delivery_receipts?.enabled === false) return;
7341
+ for (const c of collections) this.trackDeliveredCandidate(c);
7342
+ this.announceDeliveryBuffered();
7343
+ }
7344
+ };
7345
+
7346
+ // src/messageDelivery/MessageReceiptsTracker.ts
7347
+ var MIN_REF = { timestampMs: Number.NEGATIVE_INFINITY, msgId: "" };
7348
+ var compareRefsAsc = (a, b) => a.timestampMs !== b.timestampMs ? a.timestampMs - b.timestampMs : 0;
7349
+ var findIndex = (arr, target, keyOf) => {
7350
+ let lo = 0, hi = arr.length;
7351
+ while (lo < hi) {
7352
+ const mid = lo + hi >>> 1;
7353
+ if (compareRefsAsc(keyOf(arr[mid]), target) >= 0) hi = mid;
7354
+ else lo = mid + 1;
7355
+ }
7356
+ return lo;
7357
+ };
7358
+ var findUpperIndex = (arr, target, keyOf) => {
7359
+ let lo = 0, hi = arr.length;
7360
+ while (lo < hi) {
7361
+ const mid = lo + hi >>> 1;
7362
+ if (compareRefsAsc(keyOf(arr[mid]), target) > 0) hi = mid;
7363
+ else lo = mid + 1;
7364
+ }
7365
+ return lo;
7366
+ };
7367
+ var insertByKey = (arr, item, keyOf) => arr.splice(findUpperIndex(arr, keyOf(item), keyOf), 0, item);
7368
+ var removeByOldKey = (arr, item, oldKey, keyOf) => {
7369
+ let i = findIndex(arr, oldKey, keyOf);
7370
+ while (i < arr.length && compareRefsAsc(keyOf(arr[i]), oldKey) === 0) {
7371
+ if (arr[i].user.id === item.user.id) {
7372
+ arr.splice(i, 1);
7373
+ return;
7374
+ }
7375
+ i++;
7376
+ }
7377
+ };
7378
+ var MessageReceiptsTracker = class {
7379
+ constructor({ locateMessage }) {
7380
+ this.byUser = /* @__PURE__ */ new Map();
7381
+ this.readSorted = [];
7382
+ // asc by lastReadRef
7383
+ this.deliveredSorted = [];
7384
+ this.locateMessage = locateMessage;
7385
+ }
7386
+ /** Build initial state from server snapshots (single pass + sort). */
7387
+ ingestInitial(responses) {
7388
+ this.byUser.clear();
7389
+ this.readSorted = [];
7390
+ this.deliveredSorted = [];
7391
+ for (const r of responses) {
7392
+ const lastReadTimestamp = r.last_read ? new Date(r.last_read).getTime() : null;
7393
+ const lastDeliveredTimestamp = r.last_delivered_at ? new Date(r.last_delivered_at).getTime() : null;
7394
+ const lastReadRef = lastReadTimestamp ? this.locateMessage(lastReadTimestamp) ?? MIN_REF : MIN_REF;
7395
+ let lastDeliveredRef = lastDeliveredTimestamp ? this.locateMessage(lastDeliveredTimestamp) ?? MIN_REF : MIN_REF;
7396
+ const isReadAfterDelivered = compareRefsAsc(lastDeliveredRef, lastReadRef) < 0;
7397
+ if (isReadAfterDelivered) lastDeliveredRef = lastReadRef;
7398
+ const userProgress = { user: r.user, lastReadRef, lastDeliveredRef };
7399
+ this.byUser.set(r.user.id, userProgress);
7400
+ this.readSorted.splice(
7401
+ findIndex(this.readSorted, lastReadRef, (up) => up.lastReadRef),
7402
+ 0,
7403
+ userProgress
7404
+ );
7405
+ this.deliveredSorted.splice(
7406
+ findIndex(this.deliveredSorted, lastDeliveredRef, (up) => up.lastDeliveredRef),
7407
+ 0,
7408
+ userProgress
7409
+ );
7410
+ }
7411
+ }
7412
+ /** message.delivered — user device confirmed delivery up to and including messageId. */
7413
+ onMessageDelivered({
7414
+ user,
7415
+ deliveredAt,
7416
+ lastDeliveredMessageId
7417
+ }) {
7418
+ const timestampMs = new Date(deliveredAt).getTime();
7419
+ const msgRef = lastDeliveredMessageId ? { timestampMs, msgId: lastDeliveredMessageId } : this.locateMessage(new Date(deliveredAt).getTime());
7420
+ if (!msgRef) return;
7421
+ const userProgress = this.ensureUser(user);
7422
+ const newDelivered = compareRefsAsc(msgRef, userProgress.lastReadRef) < 0 ? userProgress.lastReadRef : msgRef;
7423
+ if (compareRefsAsc(newDelivered, userProgress.lastDeliveredRef) <= 0) return;
7424
+ removeByOldKey(
7425
+ this.deliveredSorted,
7426
+ userProgress,
7427
+ userProgress.lastDeliveredRef,
7428
+ (x) => x.lastDeliveredRef
7429
+ );
7430
+ userProgress.lastDeliveredRef = newDelivered;
7431
+ insertByKey(this.deliveredSorted, userProgress, (x) => x.lastDeliveredRef);
7432
+ }
7433
+ /** message.read — user read up to and including messageId. */
7434
+ onMessageRead({
7435
+ user,
7436
+ readAt,
7437
+ lastReadMessageId
7438
+ }) {
7439
+ const timestampMs = new Date(readAt).getTime();
7440
+ const msgRef = lastReadMessageId ? { timestampMs, msgId: lastReadMessageId } : this.locateMessage(timestampMs);
7441
+ if (!msgRef) return;
7442
+ const userProgress = this.ensureUser(user);
7443
+ if (compareRefsAsc(msgRef, userProgress.lastReadRef) <= 0) return;
7444
+ removeByOldKey(
7445
+ this.readSorted,
7446
+ userProgress,
7447
+ userProgress.lastReadRef,
7448
+ (x) => x.lastReadRef
7449
+ );
7450
+ userProgress.lastReadRef = msgRef;
7451
+ insertByKey(this.readSorted, userProgress, (x) => x.lastReadRef);
7452
+ if (compareRefsAsc(userProgress.lastDeliveredRef, userProgress.lastReadRef) < 0) {
7453
+ removeByOldKey(
7454
+ this.deliveredSorted,
7455
+ userProgress,
7456
+ userProgress.lastDeliveredRef,
7457
+ (x) => x.lastDeliveredRef
7458
+ );
7459
+ userProgress.lastDeliveredRef = userProgress.lastReadRef;
7460
+ insertByKey(this.deliveredSorted, userProgress, (x) => x.lastDeliveredRef);
7461
+ }
7462
+ }
7463
+ /** notification.mark_unread — user marked messages unread starting at `first_unread_message_id`.
7464
+ * Sets lastReadRef to the event’s last_read_* values. Delivery never moves backward.
7465
+ * The event is sent only to the user that triggered the action (own user), so we will never adjust read ref
7466
+ * for other users - we will not see changes in the UI for other users. However, this implementation does not
7467
+ * take into consideration this fact and is ready to handle the mark-unread event for any user.
7468
+ */
7469
+ onNotificationMarkUnread({
7470
+ user,
7471
+ lastReadAt,
7472
+ lastReadMessageId
7473
+ }) {
7474
+ const userProgress = this.ensureUser(user);
7475
+ const newReadRef = lastReadAt ? { timestampMs: new Date(lastReadAt).getTime(), msgId: lastReadMessageId ?? "" } : { ...MIN_REF };
7476
+ if (compareRefsAsc(newReadRef, userProgress.lastReadRef) === 0 && newReadRef.msgId === userProgress.lastReadRef.msgId) {
7477
+ return;
7478
+ }
7479
+ removeByOldKey(
7480
+ this.readSorted,
7481
+ userProgress,
7482
+ userProgress.lastReadRef,
7483
+ (x) => x.lastReadRef
7484
+ );
7485
+ userProgress.lastReadRef = newReadRef;
7486
+ insertByKey(this.readSorted, userProgress, (x) => x.lastReadRef);
7487
+ if (compareRefsAsc(userProgress.lastDeliveredRef, userProgress.lastReadRef) < 0) {
7488
+ removeByOldKey(
7489
+ this.deliveredSorted,
7490
+ userProgress,
7491
+ userProgress.lastDeliveredRef,
7492
+ (x) => x.lastDeliveredRef
7493
+ );
7494
+ userProgress.lastDeliveredRef = userProgress.lastReadRef;
7495
+ insertByKey(this.deliveredSorted, userProgress, (x) => x.lastDeliveredRef);
7496
+ }
7497
+ }
7498
+ /** All users who READ this message. */
7499
+ readersForMessage(msgRef) {
7500
+ const index = findIndex(this.readSorted, msgRef, ({ lastReadRef }) => lastReadRef);
7501
+ return this.readSorted.slice(index).map((x) => x.user);
7502
+ }
7503
+ /** All users who have it DELIVERED (includes readers). */
7504
+ deliveredForMessage(msgRef) {
7505
+ const pos = findIndex(
7506
+ this.deliveredSorted,
7507
+ msgRef,
7508
+ ({ lastDeliveredRef }) => lastDeliveredRef
7509
+ );
7510
+ return this.deliveredSorted.slice(pos).map((x) => x.user);
7511
+ }
7512
+ /** Users who delivered but have NOT read. */
7513
+ deliveredNotReadForMessage(msgRef) {
7514
+ const pos = findIndex(
7515
+ this.deliveredSorted,
7516
+ msgRef,
7517
+ ({ lastDeliveredRef }) => lastDeliveredRef
7518
+ );
7519
+ const usersDeliveredNotRead = [];
7520
+ for (let i = pos; i < this.deliveredSorted.length; i++) {
7521
+ const userProgress = this.deliveredSorted[i];
7522
+ if (compareRefsAsc(userProgress.lastReadRef, msgRef) < 0)
7523
+ usersDeliveredNotRead.push(userProgress.user);
7524
+ }
7525
+ return usersDeliveredNotRead;
7526
+ }
7527
+ /** Users for whom `msgRef` is their *last read* (exact match). */
7528
+ usersWhoseLastReadIs(msgRef) {
7529
+ if (!msgRef.msgId) return [];
7530
+ const start = findIndex(this.readSorted, msgRef, (x) => x.lastReadRef);
7531
+ const end = findUpperIndex(this.readSorted, msgRef, (x) => x.lastReadRef);
7532
+ const users = [];
7533
+ for (let i = start; i < end; i++) {
7534
+ const up = this.readSorted[i];
7535
+ if (up.lastReadRef.msgId === msgRef.msgId) users.push(up.user);
7536
+ }
7537
+ return users;
7538
+ }
7539
+ /** Users for whom `msgRef` is their *last delivered* (exact match). */
7540
+ usersWhoseLastDeliveredIs(msgRef) {
7541
+ if (!msgRef.msgId) return [];
7542
+ const start = findIndex(this.deliveredSorted, msgRef, (x) => x.lastDeliveredRef);
7543
+ const end = findUpperIndex(this.deliveredSorted, msgRef, (x) => x.lastDeliveredRef);
7544
+ const users = [];
7545
+ for (let i = start; i < end; i++) {
7546
+ const up = this.deliveredSorted[i];
7547
+ if (up.lastDeliveredRef.msgId === msgRef.msgId) users.push(up.user);
7548
+ }
7549
+ return users;
7550
+ }
7551
+ // ---- queries: per-user status ----
7552
+ hasUserRead(msgRef, userId) {
7553
+ const up = this.byUser.get(userId);
7554
+ return !!up && compareRefsAsc(up.lastReadRef, msgRef) >= 0;
7555
+ }
7556
+ hasUserDelivered(msgRef, userId) {
7557
+ const up = this.byUser.get(userId);
7558
+ return !!up && compareRefsAsc(up.lastDeliveredRef, msgRef) >= 0;
7559
+ }
7560
+ getUserProgress(userId) {
7561
+ const userProgress = this.byUser.get(userId);
7562
+ if (!userProgress) return null;
7563
+ return userProgress;
7564
+ }
7565
+ groupUsersByLastReadMessage() {
7566
+ return Array.from(this.byUser.values()).reduce(
7567
+ (acc, userProgress) => {
7568
+ const msgId = userProgress.lastReadRef.msgId;
7569
+ if (!msgId) return acc;
7570
+ if (!acc[msgId]) acc[msgId] = [];
7571
+ acc[msgId].push(userProgress.user);
7572
+ return acc;
7573
+ },
7574
+ {}
7575
+ );
7576
+ }
7577
+ groupUsersByLastDeliveredMessage() {
7578
+ return Array.from(this.byUser.values()).reduce(
7579
+ (acc, userProgress) => {
7580
+ const msgId = userProgress.lastDeliveredRef.msgId;
7581
+ if (!msgId) return acc;
7582
+ if (!acc[msgId]) acc[msgId] = [];
7583
+ acc[msgId].push(userProgress.user);
7584
+ return acc;
7585
+ },
7586
+ {}
7587
+ );
7588
+ }
7589
+ ensureUser(user) {
7590
+ let up = this.byUser.get(user.id);
7591
+ if (!up) {
7592
+ up = { user, lastReadRef: MIN_REF, lastDeliveredRef: MIN_REF };
7593
+ this.byUser.set(user.id, up);
7594
+ insertByKey(this.readSorted, up, (x) => x.lastReadRef);
7595
+ insertByKey(this.deliveredSorted, up, (x) => x.lastDeliveredRef);
7596
+ }
7597
+ return up;
7598
+ }
7599
+ };
7600
+
7063
7601
  // src/channel.ts
7064
7602
  var Channel = class {
7065
7603
  /**
@@ -7141,6 +7679,12 @@ var Channel = class {
7141
7679
  client: this._client,
7142
7680
  compositionContext: this
7143
7681
  });
7682
+ this.messageReceiptsTracker = new MessageReceiptsTracker({
7683
+ locateMessage: (timestampMs) => {
7684
+ const msg = this.state.findMessageByTimestamp(timestampMs);
7685
+ return msg && { timestampMs, msgId: msg.id };
7686
+ }
7687
+ });
7144
7688
  }
7145
7689
  /**
7146
7690
  * getClient - Get the chat client for this channel. If client.disconnect() was called, this function will error
@@ -7950,15 +8494,24 @@ var Channel = class {
7950
8494
  return messageSlice[0];
7951
8495
  }
7952
8496
  /**
7953
- * markRead - Send the mark read event for this user, only works if the `read_events` setting is enabled
8497
+ * markRead - Send the mark read event for this user, only works if the `read_events` setting is enabled. Syncs the message delivery report candidates local state.
7954
8498
  *
7955
8499
  * @param {MarkReadOptions} data
7956
8500
  * @return {Promise<EventAPIResponse | null>} Description
7957
8501
  */
7958
8502
  async markRead(data = {}) {
8503
+ return await this.getClient().messageDeliveryReporter.markRead(this, data);
8504
+ }
8505
+ /**
8506
+ * markReadRequest - Send the mark read event for this user, only works if the `read_events` setting is enabled
8507
+ *
8508
+ * @param {MarkReadOptions} data
8509
+ * @return {Promise<EventAPIResponse | null>} Description
8510
+ */
8511
+ async markAsReadRequest(data = {}) {
7959
8512
  this._checkInitialized();
7960
8513
  if (!this.getConfig()?.read_events && !this.getClient()._isUsingServerAuth()) {
7961
- return Promise.resolve(null);
8514
+ return null;
7962
8515
  }
7963
8516
  return await this.getClient().post(this._channelURL() + "/read", {
7964
8517
  ...data
@@ -8266,6 +8819,7 @@ var Channel = class {
8266
8819
  }),
8267
8820
  { method: "upsertChannels" }
8268
8821
  );
8822
+ this.getClient().syncDeliveredCandidates([this]);
8269
8823
  return state;
8270
8824
  }
8271
8825
  /**
@@ -8537,17 +9091,44 @@ var Channel = class {
8537
9091
  break;
8538
9092
  case "message.read":
8539
9093
  if (event.user?.id && event.created_at) {
9094
+ const previousReadState = channelState.read[event.user.id];
8540
9095
  channelState.read[event.user.id] = {
9096
+ // in case we already have delivery information
9097
+ ...previousReadState,
8541
9098
  last_read: new Date(event.created_at),
8542
9099
  last_read_message_id: event.last_read_message_id,
8543
9100
  user: event.user,
8544
9101
  unread_messages: 0
8545
9102
  };
8546
- if (event.user?.id === this.getClient().user?.id) {
9103
+ this.messageReceiptsTracker.onMessageRead({
9104
+ user: event.user,
9105
+ readAt: event.created_at,
9106
+ lastReadMessageId: event.last_read_message_id
9107
+ });
9108
+ const client = this.getClient();
9109
+ const isOwnEvent = event.user?.id === client.user?.id;
9110
+ if (isOwnEvent) {
8547
9111
  channelState.unreadCount = 0;
9112
+ client.syncDeliveredCandidates([this]);
8548
9113
  }
8549
9114
  }
8550
9115
  break;
9116
+ case "message.delivered":
9117
+ if (event.user?.id && event.created_at) {
9118
+ const previousReadState = channelState.read[event.user.id];
9119
+ channelState.read[event.user.id] = {
9120
+ ...previousReadState,
9121
+ last_delivered_at: event.last_delivered_at ? new Date(event.last_delivered_at) : void 0,
9122
+ last_delivered_message_id: event.last_delivered_message_id,
9123
+ user: event.user
9124
+ };
9125
+ this.messageReceiptsTracker.onMessageDelivered({
9126
+ user: event.user,
9127
+ deliveredAt: event.created_at,
9128
+ lastDeliveredMessageId: event.last_delivered_message_id
9129
+ });
9130
+ }
9131
+ break;
8551
9132
  case "user.watching.start":
8552
9133
  case "user.updated":
8553
9134
  if (event.user?.id) {
@@ -8581,7 +9162,8 @@ var Channel = class {
8581
9162
  break;
8582
9163
  case "message.new":
8583
9164
  if (event.message) {
8584
- const ownMessage = event.user?.id === this.getClient().user?.id;
9165
+ const client = this.getClient();
9166
+ const ownMessage = event.user?.id === client.user?.id;
8585
9167
  const isThreadMessage = event.message.parent_id && !event.message.show_in_channel;
8586
9168
  if (this.state.isUpToDate || isThreadMessage) {
8587
9169
  channelState.addMessageSorted(event.message, ownMessage);
@@ -8597,7 +9179,9 @@ var Channel = class {
8597
9179
  channelState.read[event.user.id] = {
8598
9180
  last_read: new Date(event.created_at),
8599
9181
  user: event.user,
8600
- unread_messages: 0
9182
+ unread_messages: 0,
9183
+ last_delivered_at: new Date(event.created_at),
9184
+ last_delivered_message_id: event.message.id
8601
9185
  };
8602
9186
  } else {
8603
9187
  channelState.read[userId].unread_messages += 1;
@@ -8607,6 +9191,7 @@ var Channel = class {
8607
9191
  if (this._countMessageAsUnread(event.message)) {
8608
9192
  channelState.unreadCount = channelState.unreadCount + 1;
8609
9193
  }
9194
+ client.syncDeliveredCandidates([this]);
8610
9195
  }
8611
9196
  break;
8612
9197
  case "message.updated":
@@ -8689,9 +9274,12 @@ var Channel = class {
8689
9274
  break;
8690
9275
  case "notification.mark_unread": {
8691
9276
  const ownMessage = event.user?.id === this.getClient().user?.id;
8692
- if (!(ownMessage && event.user)) break;
9277
+ if (!ownMessage || !event.user) break;
8693
9278
  const unreadCount = event.unread_messages ?? 0;
9279
+ const currentState = channelState.read[event.user.id];
8694
9280
  channelState.read[event.user.id] = {
9281
+ // keep the message delivery info
9282
+ ...currentState,
8695
9283
  first_unread_message_id: event.first_unread_message_id,
8696
9284
  last_read: new Date(event.last_read_at),
8697
9285
  last_read_message_id: event.last_read_message_id,
@@ -8699,6 +9287,11 @@ var Channel = class {
8699
9287
  unread_messages: unreadCount
8700
9288
  };
8701
9289
  channelState.unreadCount = unreadCount;
9290
+ this.messageReceiptsTracker.onNotificationMarkUnread({
9291
+ user: event.user,
9292
+ lastReadAt: event.last_read_at,
9293
+ lastReadMessageId: event.last_read_message_id
9294
+ });
8702
9295
  break;
8703
9296
  }
8704
9297
  case "channel.updated":
@@ -8850,6 +9443,7 @@ var Channel = class {
8850
9443
  this.state.unreadCount = this.state.read[read.user.id].unread_messages;
8851
9444
  }
8852
9445
  }
9446
+ this.messageReceiptsTracker.ingestInitial(state.read);
8853
9447
  }
8854
9448
  return {
8855
9449
  messageSet
@@ -9459,7 +10053,7 @@ var StableWSConnection = class {
9459
10053
  };
9460
10054
 
9461
10055
  // src/signing.ts
9462
- var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
10056
+ var import_jsonwebtoken = __toESM(require_jsonwebtoken());
9463
10057
  var import_crypto = __toESM(require_crypto());
9464
10058
  function JWTUserToken(apiSecret, userId, extraData = {}, jwtOptions = {}) {
9465
10059
  if (typeof userId !== "string") {
@@ -12400,6 +12994,7 @@ var StreamChat = class _StreamChat {
12400
12994
  this.threads = new ThreadManager({ client: this });
12401
12995
  this.polls = new PollManager({ client: this });
12402
12996
  this.reminders = new ReminderManager({ client: this });
12997
+ this.messageDeliveryReporter = new MessageDeliveryReporter({ client: this });
12403
12998
  }
12404
12999
  static getInstance(key, secretOrOptions, options) {
12405
13000
  if (!_StreamChat._instance) {
@@ -13070,6 +13665,7 @@ var StreamChat = class _StreamChat {
13070
13665
  c.messageComposer.initStateFromChannelResponse(channelState);
13071
13666
  channels.push(c);
13072
13667
  }
13668
+ this.syncDeliveredCandidates(channels);
13073
13669
  return channels;
13074
13670
  }
13075
13671
  /**
@@ -13834,10 +14430,26 @@ var StreamChat = class _StreamChat {
13834
14430
  }
13835
14431
  );
13836
14432
  }
13837
- async deleteMessage(messageID, hardDelete) {
14433
+ /**
14434
+ * deleteMessage - Delete a message
14435
+ *
14436
+ * @param {string} messageID The id of the message to delete
14437
+ * @param {boolean | DeleteMessageOptions | undefined} [optionsOrHardDelete]
14438
+ * @return {Promise<APIResponse & { message: MessageResponse }>} The API response
14439
+ */
14440
+ // fixme: remove the signature with optionsOrHardDelete boolean with the next major release
14441
+ async deleteMessage(messageID, optionsOrHardDelete) {
14442
+ let options = {};
14443
+ if (typeof optionsOrHardDelete === "boolean") {
14444
+ options = optionsOrHardDelete ? { hardDelete: true } : {};
14445
+ } else if (optionsOrHardDelete?.deleteForMe) {
14446
+ options = { deleteForMe: true };
14447
+ } else if (optionsOrHardDelete?.hardDelete) {
14448
+ options = { hardDelete: true };
14449
+ }
13838
14450
  try {
13839
14451
  if (this.offlineDb) {
13840
- if (hardDelete) {
14452
+ if (options.hardDelete) {
13841
14453
  await this.offlineDb.hardDeleteMessage({ id: messageID });
13842
14454
  } else {
13843
14455
  await this.offlineDb.softDeleteMessage({ id: messageID });
@@ -13846,7 +14458,7 @@ var StreamChat = class _StreamChat {
13846
14458
  {
13847
14459
  task: {
13848
14460
  messageId: messageID,
13849
- payload: [messageID, hardDelete],
14461
+ payload: [messageID, options],
13850
14462
  type: "delete-message"
13851
14463
  }
13852
14464
  }
@@ -13858,17 +14470,27 @@ var StreamChat = class _StreamChat {
13858
14470
  error
13859
14471
  });
13860
14472
  }
13861
- return this._deleteMessage(messageID, hardDelete);
14473
+ return this._deleteMessage(messageID, options);
13862
14474
  }
13863
- async _deleteMessage(messageID, hardDelete) {
14475
+ // fixme: remove the signature with optionsOrHardDelete boolean with the next major release
14476
+ async _deleteMessage(messageID, optionsOrHardDelete) {
14477
+ const { deleteForMe, hardDelete } = typeof optionsOrHardDelete === "boolean" ? { hardDelete: optionsOrHardDelete } : optionsOrHardDelete ?? {};
13864
14478
  let params = {};
13865
14479
  if (hardDelete) {
13866
14480
  params = { hard: true };
13867
14481
  }
13868
- return await this.delete(
14482
+ if (deleteForMe) {
14483
+ params = { ...params, delete_for_me: true };
14484
+ }
14485
+ const result = await this.delete(
13869
14486
  this.baseURL + `/messages/${encodeURIComponent(messageID)}`,
13870
14487
  params
13871
14488
  );
14489
+ if (deleteForMe) {
14490
+ result.message.deleted_for_me = true;
14491
+ result.message.type = "deleted";
14492
+ }
14493
+ return result;
13872
14494
  }
13873
14495
  /**
13874
14496
  * undeleteMessage - Undelete a message
@@ -14004,7 +14626,7 @@ var StreamChat = class _StreamChat {
14004
14626
  if (this.userAgent) {
14005
14627
  return this.userAgent;
14006
14628
  }
14007
- const version = "9.21.0";
14629
+ const version = "9.22.1";
14008
14630
  const clientBundle = "browser-cjs";
14009
14631
  let userAgentString = "";
14010
14632
  if (this.sdkIdentifier) {
@@ -15153,19 +15775,21 @@ var StreamChat = class _StreamChat {
15153
15775
  return this.delete(`${this.baseURL}/uploads/image`, { url });
15154
15776
  }
15155
15777
  /**
15156
- * Send the mark delivered event for this user, only works if the `delivery_receipts` setting is enabled
15778
+ * Send the mark delivered event for this user
15157
15779
  *
15158
15780
  * @param {MarkDeliveredOptions} data
15159
15781
  * @return {Promise<EventAPIResponse | void>} Description
15160
15782
  */
15161
15783
  async markChannelsDelivered(data) {
15162
- const deliveryReceiptsEnabled = this.user?.privacy_settings?.delivery_receipts?.enabled;
15163
- if (!deliveryReceiptsEnabled) return;
15784
+ if (!data?.latest_delivered_messages?.length) return;
15164
15785
  return await this.post(
15165
15786
  this.baseURL + "/channels/delivered",
15166
15787
  data ?? {}
15167
15788
  );
15168
15789
  }
15790
+ syncDeliveredCandidates(collections) {
15791
+ this.messageDeliveryReporter.syncDeliveredCandidates(collections);
15792
+ }
15169
15793
  };
15170
15794
 
15171
15795
  // src/events.ts
@@ -15202,6 +15826,7 @@ var EVENT_MAP = {
15202
15826
  "notification.mark_unread": true,
15203
15827
  "notification.message_new": true,
15204
15828
  "notification.mutes_updated": true,
15829
+ "notification.reminder_due": true,
15205
15830
  "notification.removed_from_channel": true,
15206
15831
  "notification.thread_message_new": true,
15207
15832
  "poll.closed": true,
@@ -15212,6 +15837,9 @@ var EVENT_MAP = {
15212
15837
  "reaction.deleted": true,
15213
15838
  "reaction.new": true,
15214
15839
  "reaction.updated": true,
15840
+ "reminder.created": true,
15841
+ "reminder.deleted": true,
15842
+ "reminder.updated": true,
15215
15843
  "thread.updated": true,
15216
15844
  "typing.start": true,
15217
15845
  "typing.stop": true,
@@ -15236,12 +15864,7 @@ var EVENT_MAP = {
15236
15864
  "transport.changed": true,
15237
15865
  "capabilities.changed": true,
15238
15866
  "live_location_sharing.started": true,
15239
- "live_location_sharing.stopped": true,
15240
- // Reminder events
15241
- "reminder.created": true,
15242
- "reminder.updated": true,
15243
- "reminder.deleted": true,
15244
- "notification.reminder_due": true
15867
+ "live_location_sharing.stopped": true
15245
15868
  };
15246
15869
 
15247
15870
  // src/permissions.ts