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