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.
@@ -81,7 +81,9 @@ __export(index_exports, {
81
81
  MergedStateStore: () => MergedStateStore,
82
82
  MessageComposer: () => MessageComposer,
83
83
  MessageComposerMiddlewareExecutor: () => MessageComposerMiddlewareExecutor,
84
+ MessageDeliveryReporter: () => MessageDeliveryReporter,
84
85
  MessageDraftComposerMiddlewareExecutor: () => MessageDraftComposerMiddlewareExecutor,
86
+ MessageReceiptsTracker: () => MessageReceiptsTracker,
85
87
  MessageSearchSource: () => MessageSearchSource,
86
88
  MiddlewareExecutor: () => MiddlewareExecutor,
87
89
  MinPriority: () => MinPriority,
@@ -1132,6 +1134,21 @@ var runDetached = (callback, options) => {
1132
1134
  };
1133
1135
 
1134
1136
  // src/channel_state.ts
1137
+ var messageSetBounds = (a, b) => ({
1138
+ newestMessageA: new Date(a[0]?.created_at ?? 0),
1139
+ oldestMessageA: new Date(a.slice(-1)[0]?.created_at ?? 0),
1140
+ newestMessageB: new Date(b[0]?.created_at ?? 0),
1141
+ oldestMessageB: new Date(b.slice(-1)[0]?.created_at ?? 0)
1142
+ });
1143
+ var aContainsOrEqualsB = (a, b) => {
1144
+ const { newestMessageA, newestMessageB, oldestMessageA, oldestMessageB } = messageSetBounds(a, b);
1145
+ return newestMessageA >= newestMessageB && oldestMessageB >= oldestMessageA;
1146
+ };
1147
+ var aOverlapsB = (a, b) => {
1148
+ const { newestMessageA, newestMessageB, oldestMessageA, oldestMessageB } = messageSetBounds(a, b);
1149
+ return oldestMessageA < oldestMessageB && oldestMessageB < newestMessageA && newestMessageA < newestMessageB;
1150
+ };
1151
+ var messageSetsOverlapByTimestamp = (a, b) => aContainsOrEqualsB(a, b) || aContainsOrEqualsB(b, a) || aOverlapsB(a, b) || aOverlapsB(b, a);
1135
1152
  var ChannelState = class {
1136
1153
  constructor(channel) {
1137
1154
  /**
@@ -1210,6 +1227,22 @@ var ChannelState = class {
1210
1227
  deletedAt: deletedAt ?? null
1211
1228
  });
1212
1229
  };
1230
+ /**
1231
+ * Identifies the set index into which a message set would pertain if its first item's creation date corresponded to oldestTimestampMs.
1232
+ * @param oldestTimestampMs
1233
+ */
1234
+ this.findMessageSetByOldestTimestamp = (oldestTimestampMs) => {
1235
+ let lo = 0, hi = this.messageSets.length;
1236
+ while (lo < hi) {
1237
+ const mid = lo + hi >>> 1;
1238
+ const msgSet = this.messageSets[mid];
1239
+ if (msgSet.messages.length === 0) return -1;
1240
+ const oldestMessageTimestampInSet = msgSet.messages[0].created_at.getTime();
1241
+ if (oldestMessageTimestampInSet <= oldestTimestampMs) hi = mid;
1242
+ else lo = mid + 1;
1243
+ }
1244
+ return lo;
1245
+ };
1213
1246
  this._channel = channel;
1214
1247
  this.watcher_count = 0;
1215
1248
  this.typing = {};
@@ -1735,6 +1768,25 @@ var ChannelState = class {
1735
1768
  }
1736
1769
  return this.messageSets[messageSetIndex].messages.find((m) => m.id === messageId);
1737
1770
  }
1771
+ findMessageByTimestamp(timestampMs, parentMessageId, exactTsMatch = false) {
1772
+ if (parentMessageId && !this.threads[parentMessageId] || this.messageSets.length === 0)
1773
+ return null;
1774
+ const setIndex = this.findMessageSetByOldestTimestamp(timestampMs);
1775
+ const targetMsgSet = this.messageSets[setIndex]?.messages;
1776
+ if (!targetMsgSet?.length) return null;
1777
+ const firstMsgTimestamp = targetMsgSet[0].created_at.getTime();
1778
+ const lastMsgTimestamp = targetMsgSet.slice(-1)[0].created_at.getTime();
1779
+ const isOutOfBound = timestampMs < firstMsgTimestamp || lastMsgTimestamp < timestampMs;
1780
+ if (isOutOfBound && exactTsMatch) return null;
1781
+ let msgIndex = 0, hi = targetMsgSet.length - 1;
1782
+ while (msgIndex < hi) {
1783
+ const mid = msgIndex + hi >>> 1;
1784
+ if (timestampMs <= targetMsgSet[mid].created_at.getTime()) hi = mid;
1785
+ else msgIndex = mid + 1;
1786
+ }
1787
+ const foundMessage = targetMsgSet[msgIndex];
1788
+ return !exactTsMatch ? foundMessage : foundMessage.created_at.getTime() === timestampMs ? foundMessage : null;
1789
+ }
1738
1790
  switchToMessageSet(index) {
1739
1791
  const currentMessages = this.messageSets.find((s) => s.isCurrent);
1740
1792
  if (!currentMessages) {
@@ -1754,35 +1806,77 @@ var ChannelState = class {
1754
1806
  findTargetMessageSet(newMessages, addIfDoesNotExist = true, messageSetToAddToIfDoesNotExist = "current") {
1755
1807
  let messagesToAdd = newMessages;
1756
1808
  let targetMessageSetIndex;
1809
+ if (newMessages.length === 0)
1810
+ return { targetMessageSetIndex: 0, messagesToAdd: newMessages };
1757
1811
  if (addIfDoesNotExist) {
1758
- const overlappingMessageSetIndices = this.messageSets.map((_, i) => i).filter(
1812
+ const overlappingMessageSetIndicesByMsgIds = this.messageSets.map((_, i) => i).filter(
1759
1813
  (i) => this.areMessageSetsOverlap(this.messageSets[i].messages, newMessages)
1760
1814
  );
1815
+ const overlappingMessageSetIndicesByTimestamp = this.messageSets.map((_, i) => i).filter(
1816
+ (i) => messageSetsOverlapByTimestamp(
1817
+ this.messageSets[i].messages,
1818
+ newMessages.map(formatMessage)
1819
+ )
1820
+ );
1761
1821
  switch (messageSetToAddToIfDoesNotExist) {
1762
1822
  case "new":
1763
- if (overlappingMessageSetIndices.length > 0) {
1764
- targetMessageSetIndex = overlappingMessageSetIndices[0];
1823
+ if (overlappingMessageSetIndicesByMsgIds.length > 0) {
1824
+ targetMessageSetIndex = overlappingMessageSetIndicesByMsgIds[0];
1825
+ } else if (overlappingMessageSetIndicesByTimestamp.length > 0) {
1826
+ targetMessageSetIndex = overlappingMessageSetIndicesByTimestamp[0];
1765
1827
  } else if (newMessages.some((m) => !m.parent_id)) {
1766
- this.messageSets.push({
1767
- messages: [],
1768
- isCurrent: false,
1769
- isLatest: false,
1770
- pagination: DEFAULT_MESSAGE_SET_PAGINATION
1771
- });
1772
- targetMessageSetIndex = this.messageSets.length - 1;
1828
+ const setIngestIndex = this.findMessageSetByOldestTimestamp(
1829
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1830
+ new Date(newMessages[0].created_at).getTime()
1831
+ );
1832
+ if (setIngestIndex === -1) {
1833
+ this.messageSets.push({
1834
+ messages: [],
1835
+ isCurrent: false,
1836
+ isLatest: false,
1837
+ pagination: DEFAULT_MESSAGE_SET_PAGINATION
1838
+ });
1839
+ targetMessageSetIndex = this.messageSets.length - 1;
1840
+ } else {
1841
+ const isLatest = setIngestIndex === 0;
1842
+ this.messageSets.splice(setIngestIndex, 0, {
1843
+ messages: [],
1844
+ isCurrent: false,
1845
+ isLatest,
1846
+ pagination: DEFAULT_MESSAGE_SET_PAGINATION
1847
+ // fixme: it is problematic decide about pagination without having data
1848
+ });
1849
+ if (isLatest) {
1850
+ this.messageSets.slice(1).forEach((set) => {
1851
+ set.isLatest = false;
1852
+ });
1853
+ }
1854
+ targetMessageSetIndex = setIngestIndex;
1855
+ }
1773
1856
  }
1774
1857
  break;
1775
1858
  case "current":
1776
- targetMessageSetIndex = this.messageSets.findIndex((s) => s.isCurrent);
1859
+ if (overlappingMessageSetIndicesByTimestamp.length > 0) {
1860
+ targetMessageSetIndex = overlappingMessageSetIndicesByTimestamp[0];
1861
+ } else {
1862
+ targetMessageSetIndex = this.messageSets.findIndex((s) => s.isCurrent);
1863
+ }
1777
1864
  break;
1778
1865
  case "latest":
1779
- targetMessageSetIndex = this.messageSets.findIndex((s) => s.isLatest);
1866
+ if (overlappingMessageSetIndicesByTimestamp.length > 0) {
1867
+ targetMessageSetIndex = overlappingMessageSetIndicesByTimestamp[0];
1868
+ } else {
1869
+ targetMessageSetIndex = this.messageSets.findIndex((s) => s.isLatest);
1870
+ }
1780
1871
  break;
1781
1872
  default:
1782
1873
  targetMessageSetIndex = -1;
1783
1874
  }
1784
- const mergeTargetMessageSetIndex = overlappingMessageSetIndices.splice(0, 1)[0];
1785
- const mergeSourceMessageSetIndices = [...overlappingMessageSetIndices];
1875
+ const mergeTargetMessageSetIndex = overlappingMessageSetIndicesByMsgIds.splice(
1876
+ 0,
1877
+ 1
1878
+ )[0];
1879
+ const mergeSourceMessageSetIndices = [...overlappingMessageSetIndicesByMsgIds];
1786
1880
  if (mergeTargetMessageSetIndex !== void 0 && mergeTargetMessageSetIndex !== targetMessageSetIndex) {
1787
1881
  mergeSourceMessageSetIndices.push(targetMessageSetIndex);
1788
1882
  }
@@ -6315,7 +6409,7 @@ var Thread = class extends WithSubscriptions {
6315
6409
  if (this.ownUnreadCount === 0 && !force) {
6316
6410
  return null;
6317
6411
  }
6318
- return await this.channel.markRead({ thread_id: this.id });
6412
+ return await this.client.messageDeliveryReporter.markRead(this);
6319
6413
  };
6320
6414
  this.throttledMarkAsRead = throttle(
6321
6415
  () => this.markAsRead(),
@@ -7045,6 +7139,444 @@ var _MessageComposer = class _MessageComposer extends WithSubscriptions {
7045
7139
  _MessageComposer.generateId = generateUUIDv4;
7046
7140
  var MessageComposer = _MessageComposer;
7047
7141
 
7142
+ // src/messageDelivery/MessageDeliveryReporter.ts
7143
+ var MAX_DELIVERED_MESSAGE_COUNT_IN_PAYLOAD = 100;
7144
+ var MARK_AS_DELIVERED_BUFFER_TIMEOUT = 1e3;
7145
+ var MARK_AS_READ_THROTTLE_TIMEOUT2 = 1e3;
7146
+ var isChannel = (item) => item instanceof Channel;
7147
+ var isThread = (item) => item instanceof Thread;
7148
+ var MessageDeliveryReporter = class {
7149
+ constructor({ client }) {
7150
+ this.deliveryReportCandidates = /* @__PURE__ */ new Map();
7151
+ this.nextDeliveryReportCandidates = /* @__PURE__ */ new Map();
7152
+ this.markDeliveredRequestPromise = null;
7153
+ this.markDeliveredTimeout = null;
7154
+ /**
7155
+ * Retrieve the reference to the latest message in the state that is nor read neither reported as delivered
7156
+ * @param collection
7157
+ */
7158
+ this.getNextDeliveryReportCandidate = (collection) => {
7159
+ const ownUserId = this.client.user?.id;
7160
+ if (!ownUserId) return;
7161
+ let latestMessages = [];
7162
+ let lastDeliveredAt;
7163
+ let lastReadAt;
7164
+ let key = void 0;
7165
+ if (isChannel(collection)) {
7166
+ latestMessages = collection.state.latestMessages;
7167
+ const ownReadState = collection.state.read[ownUserId] ?? {};
7168
+ lastReadAt = ownReadState?.last_read;
7169
+ lastDeliveredAt = ownReadState?.last_delivered_at;
7170
+ key = collection.cid;
7171
+ } else if (isThread(collection)) {
7172
+ latestMessages = collection.state.getLatestValue().replies;
7173
+ const ownReadState = collection.state.getLatestValue().read[ownUserId] ?? {};
7174
+ lastReadAt = ownReadState?.lastReadAt;
7175
+ lastDeliveredAt = ownReadState?.lastDeliveredAt;
7176
+ key = `${collection.channel.cid}:${collection.id}`;
7177
+ return;
7178
+ } else {
7179
+ return;
7180
+ }
7181
+ if (!key) return;
7182
+ const [latestMessage] = latestMessages.slice(-1);
7183
+ const wholeCollectionIsRead = !latestMessage || lastReadAt >= latestMessage.created_at;
7184
+ if (wholeCollectionIsRead) return { key, id: null };
7185
+ const wholeCollectionIsMarkedDelivered = !latestMessage || (lastDeliveredAt ?? 0) >= latestMessage.created_at;
7186
+ if (wholeCollectionIsMarkedDelivered) return { key, id: null };
7187
+ return { key, id: latestMessage.id || null };
7188
+ };
7189
+ /**
7190
+ * Fires delivery announcement request followed by immediate delivery candidate buffer reset.
7191
+ * @param options
7192
+ */
7193
+ this.announceDelivery = (options) => {
7194
+ if (this.markDeliveredRequestInFlight || !this.hasDeliveryCandidates) return;
7195
+ const { latest_delivered_messages, sendBuffer } = this.confirmationsFromDeliveryReportCandidates();
7196
+ if (!latest_delivered_messages.length) return;
7197
+ const payload = { ...options, latest_delivered_messages };
7198
+ const postFlightReconcile = () => {
7199
+ this.markDeliveredRequestPromise = null;
7200
+ for (const [k, v] of this.nextDeliveryReportCandidates.entries()) {
7201
+ this.deliveryReportCandidates.set(k, v);
7202
+ }
7203
+ this.nextDeliveryReportCandidates = /* @__PURE__ */ new Map();
7204
+ this.announceDeliveryBuffered(options);
7205
+ };
7206
+ const handleError = () => {
7207
+ for (const [k, v] of Object.entries(sendBuffer)) {
7208
+ if (!this.deliveryReportCandidates.has(k)) {
7209
+ this.deliveryReportCandidates.set(k, v);
7210
+ }
7211
+ }
7212
+ postFlightReconcile();
7213
+ };
7214
+ this.markDeliveredRequestPromise = this.client.markChannelsDelivered(payload).then(postFlightReconcile, handleError);
7215
+ };
7216
+ this.announceDeliveryBuffered = (options) => {
7217
+ if (this.hasTimer || this.markDeliveredRequestInFlight || !this.hasDeliveryCandidates)
7218
+ return;
7219
+ this.markDeliveredTimeout = setTimeout(() => {
7220
+ this.markDeliveredTimeout = null;
7221
+ this.announceDelivery(options);
7222
+ }, MARK_AS_DELIVERED_BUFFER_TIMEOUT);
7223
+ };
7224
+ /**
7225
+ * Delegates the mark-read call to the Channel or Thread instance
7226
+ * @param collection
7227
+ * @param options
7228
+ */
7229
+ this.markRead = async (collection, options) => {
7230
+ let result = null;
7231
+ if (isChannel(collection)) {
7232
+ result = await collection.markAsReadRequest(options);
7233
+ } else if (isThread(collection)) {
7234
+ result = await collection.channel.markAsReadRequest({
7235
+ ...options,
7236
+ thread_id: collection.id
7237
+ });
7238
+ }
7239
+ this.removeCandidateFor(collection);
7240
+ return result;
7241
+ };
7242
+ /**
7243
+ * Throttles the MessageDeliveryReporter.markRead call
7244
+ * @param collection
7245
+ * @param options
7246
+ */
7247
+ this.throttledMarkRead = throttle(this.markRead, MARK_AS_READ_THROTTLE_TIMEOUT2, {
7248
+ leading: false,
7249
+ trailing: true
7250
+ });
7251
+ this.client = client;
7252
+ }
7253
+ get markDeliveredRequestInFlight() {
7254
+ return this.markDeliveredRequestPromise !== null;
7255
+ }
7256
+ get hasTimer() {
7257
+ return this.markDeliveredTimeout !== null;
7258
+ }
7259
+ get hasDeliveryCandidates() {
7260
+ return this.deliveryReportCandidates.size > 0;
7261
+ }
7262
+ /**
7263
+ * Build latest_delivered_messages payload from an arbitrary buffer (deliveryReportCandidates / nextDeliveryReportCandidates)
7264
+ */
7265
+ confirmationsFrom(map2) {
7266
+ return Array.from(map2.entries()).map(([key, messageId]) => {
7267
+ const [type, id, parent_id] = key.split(":");
7268
+ return parent_id ? { cid: `${type}:${id}`, id: messageId, parent_id } : { cid: key, id: messageId };
7269
+ });
7270
+ }
7271
+ confirmationsFromDeliveryReportCandidates() {
7272
+ const entries = Array.from(this.deliveryReportCandidates);
7273
+ const sendBuffer = new Map(entries.slice(0, MAX_DELIVERED_MESSAGE_COUNT_IN_PAYLOAD));
7274
+ this.deliveryReportCandidates = new Map(
7275
+ entries.slice(MAX_DELIVERED_MESSAGE_COUNT_IN_PAYLOAD)
7276
+ );
7277
+ return { latest_delivered_messages: this.confirmationsFrom(sendBuffer), sendBuffer };
7278
+ }
7279
+ /**
7280
+ * Generate candidate key for storing in the candidates buffer
7281
+ * @param collection
7282
+ * @private
7283
+ */
7284
+ candidateKeyFor(collection) {
7285
+ if (isChannel(collection)) return collection.cid;
7286
+ if (isThread(collection)) return `${collection.channel.cid}:${collection.id}`;
7287
+ }
7288
+ /**
7289
+ * Updates the delivery candidates buffer with the latest delivery candidates
7290
+ * @param collection
7291
+ */
7292
+ trackDeliveredCandidate(collection) {
7293
+ if (isChannel(collection) && !collection.getConfig()?.read_events) return;
7294
+ if (isThread(collection) && !collection.channel.getConfig()?.read_events) return;
7295
+ const candidate = this.getNextDeliveryReportCandidate(collection);
7296
+ if (!candidate?.key) return;
7297
+ const buffer = this.markDeliveredRequestInFlight ? this.nextDeliveryReportCandidates : this.deliveryReportCandidates;
7298
+ if (candidate.id === null) buffer.delete(candidate.key);
7299
+ else buffer.set(candidate.key, candidate.id);
7300
+ }
7301
+ /**
7302
+ * Removes candidate from the delivery report buffer
7303
+ * @param collection
7304
+ * @private
7305
+ */
7306
+ removeCandidateFor(collection) {
7307
+ const candidateKey = this.candidateKeyFor(collection);
7308
+ if (!candidateKey) return;
7309
+ this.deliveryReportCandidates.delete(candidateKey);
7310
+ this.nextDeliveryReportCandidates.delete(candidateKey);
7311
+ }
7312
+ /**
7313
+ * Records the latest message delivered for Channel or Thread instances and schedules the next report
7314
+ * if not already scheduled and candidates exist.
7315
+ * Should be used for WS handling (message.new) as well as for ingesting HTTP channel query results.
7316
+ * @param collections
7317
+ */
7318
+ syncDeliveredCandidates(collections) {
7319
+ if (this.client.user?.privacy_settings?.delivery_receipts?.enabled === false) return;
7320
+ for (const c of collections) this.trackDeliveredCandidate(c);
7321
+ this.announceDeliveryBuffered();
7322
+ }
7323
+ };
7324
+
7325
+ // src/messageDelivery/MessageReceiptsTracker.ts
7326
+ var MIN_REF = { timestampMs: Number.NEGATIVE_INFINITY, msgId: "" };
7327
+ var compareRefsAsc = (a, b) => a.timestampMs !== b.timestampMs ? a.timestampMs - b.timestampMs : 0;
7328
+ var findIndex = (arr, target, keyOf) => {
7329
+ let lo = 0, hi = arr.length;
7330
+ while (lo < hi) {
7331
+ const mid = lo + hi >>> 1;
7332
+ if (compareRefsAsc(keyOf(arr[mid]), target) >= 0) hi = mid;
7333
+ else lo = mid + 1;
7334
+ }
7335
+ return lo;
7336
+ };
7337
+ var findUpperIndex = (arr, target, keyOf) => {
7338
+ let lo = 0, hi = arr.length;
7339
+ while (lo < hi) {
7340
+ const mid = lo + hi >>> 1;
7341
+ if (compareRefsAsc(keyOf(arr[mid]), target) > 0) hi = mid;
7342
+ else lo = mid + 1;
7343
+ }
7344
+ return lo;
7345
+ };
7346
+ var insertByKey = (arr, item, keyOf) => arr.splice(findUpperIndex(arr, keyOf(item), keyOf), 0, item);
7347
+ var removeByOldKey = (arr, item, oldKey, keyOf) => {
7348
+ let i = findIndex(arr, oldKey, keyOf);
7349
+ while (i < arr.length && compareRefsAsc(keyOf(arr[i]), oldKey) === 0) {
7350
+ if (arr[i].user.id === item.user.id) {
7351
+ arr.splice(i, 1);
7352
+ return;
7353
+ }
7354
+ i++;
7355
+ }
7356
+ };
7357
+ var MessageReceiptsTracker = class {
7358
+ constructor({ locateMessage }) {
7359
+ this.byUser = /* @__PURE__ */ new Map();
7360
+ this.readSorted = [];
7361
+ // asc by lastReadRef
7362
+ this.deliveredSorted = [];
7363
+ this.locateMessage = locateMessage;
7364
+ }
7365
+ /** Build initial state from server snapshots (single pass + sort). */
7366
+ ingestInitial(responses) {
7367
+ this.byUser.clear();
7368
+ this.readSorted = [];
7369
+ this.deliveredSorted = [];
7370
+ for (const r of responses) {
7371
+ const lastReadTimestamp = r.last_read ? new Date(r.last_read).getTime() : null;
7372
+ const lastDeliveredTimestamp = r.last_delivered_at ? new Date(r.last_delivered_at).getTime() : null;
7373
+ const lastReadRef = lastReadTimestamp ? this.locateMessage(lastReadTimestamp) ?? MIN_REF : MIN_REF;
7374
+ let lastDeliveredRef = lastDeliveredTimestamp ? this.locateMessage(lastDeliveredTimestamp) ?? MIN_REF : MIN_REF;
7375
+ const isReadAfterDelivered = compareRefsAsc(lastDeliveredRef, lastReadRef) < 0;
7376
+ if (isReadAfterDelivered) lastDeliveredRef = lastReadRef;
7377
+ const userProgress = { user: r.user, lastReadRef, lastDeliveredRef };
7378
+ this.byUser.set(r.user.id, userProgress);
7379
+ this.readSorted.splice(
7380
+ findIndex(this.readSorted, lastReadRef, (up) => up.lastReadRef),
7381
+ 0,
7382
+ userProgress
7383
+ );
7384
+ this.deliveredSorted.splice(
7385
+ findIndex(this.deliveredSorted, lastDeliveredRef, (up) => up.lastDeliveredRef),
7386
+ 0,
7387
+ userProgress
7388
+ );
7389
+ }
7390
+ }
7391
+ /** message.delivered — user device confirmed delivery up to and including messageId. */
7392
+ onMessageDelivered({
7393
+ user,
7394
+ deliveredAt,
7395
+ lastDeliveredMessageId
7396
+ }) {
7397
+ const timestampMs = new Date(deliveredAt).getTime();
7398
+ const msgRef = lastDeliveredMessageId ? { timestampMs, msgId: lastDeliveredMessageId } : this.locateMessage(new Date(deliveredAt).getTime());
7399
+ if (!msgRef) return;
7400
+ const userProgress = this.ensureUser(user);
7401
+ const newDelivered = compareRefsAsc(msgRef, userProgress.lastReadRef) < 0 ? userProgress.lastReadRef : msgRef;
7402
+ if (compareRefsAsc(newDelivered, userProgress.lastDeliveredRef) <= 0) return;
7403
+ removeByOldKey(
7404
+ this.deliveredSorted,
7405
+ userProgress,
7406
+ userProgress.lastDeliveredRef,
7407
+ (x) => x.lastDeliveredRef
7408
+ );
7409
+ userProgress.lastDeliveredRef = newDelivered;
7410
+ insertByKey(this.deliveredSorted, userProgress, (x) => x.lastDeliveredRef);
7411
+ }
7412
+ /** message.read — user read up to and including messageId. */
7413
+ onMessageRead({
7414
+ user,
7415
+ readAt,
7416
+ lastReadMessageId
7417
+ }) {
7418
+ const timestampMs = new Date(readAt).getTime();
7419
+ const msgRef = lastReadMessageId ? { timestampMs, msgId: lastReadMessageId } : this.locateMessage(timestampMs);
7420
+ if (!msgRef) return;
7421
+ const userProgress = this.ensureUser(user);
7422
+ if (compareRefsAsc(msgRef, userProgress.lastReadRef) <= 0) return;
7423
+ removeByOldKey(
7424
+ this.readSorted,
7425
+ userProgress,
7426
+ userProgress.lastReadRef,
7427
+ (x) => x.lastReadRef
7428
+ );
7429
+ userProgress.lastReadRef = msgRef;
7430
+ insertByKey(this.readSorted, userProgress, (x) => x.lastReadRef);
7431
+ if (compareRefsAsc(userProgress.lastDeliveredRef, userProgress.lastReadRef) < 0) {
7432
+ removeByOldKey(
7433
+ this.deliveredSorted,
7434
+ userProgress,
7435
+ userProgress.lastDeliveredRef,
7436
+ (x) => x.lastDeliveredRef
7437
+ );
7438
+ userProgress.lastDeliveredRef = userProgress.lastReadRef;
7439
+ insertByKey(this.deliveredSorted, userProgress, (x) => x.lastDeliveredRef);
7440
+ }
7441
+ }
7442
+ /** notification.mark_unread — user marked messages unread starting at `first_unread_message_id`.
7443
+ * Sets lastReadRef to the event’s last_read_* values. Delivery never moves backward.
7444
+ * The event is sent only to the user that triggered the action (own user), so we will never adjust read ref
7445
+ * for other users - we will not see changes in the UI for other users. However, this implementation does not
7446
+ * take into consideration this fact and is ready to handle the mark-unread event for any user.
7447
+ */
7448
+ onNotificationMarkUnread({
7449
+ user,
7450
+ lastReadAt,
7451
+ lastReadMessageId
7452
+ }) {
7453
+ const userProgress = this.ensureUser(user);
7454
+ const newReadRef = lastReadAt ? { timestampMs: new Date(lastReadAt).getTime(), msgId: lastReadMessageId ?? "" } : { ...MIN_REF };
7455
+ if (compareRefsAsc(newReadRef, userProgress.lastReadRef) === 0 && newReadRef.msgId === userProgress.lastReadRef.msgId) {
7456
+ return;
7457
+ }
7458
+ removeByOldKey(
7459
+ this.readSorted,
7460
+ userProgress,
7461
+ userProgress.lastReadRef,
7462
+ (x) => x.lastReadRef
7463
+ );
7464
+ userProgress.lastReadRef = newReadRef;
7465
+ insertByKey(this.readSorted, userProgress, (x) => x.lastReadRef);
7466
+ if (compareRefsAsc(userProgress.lastDeliveredRef, userProgress.lastReadRef) < 0) {
7467
+ removeByOldKey(
7468
+ this.deliveredSorted,
7469
+ userProgress,
7470
+ userProgress.lastDeliveredRef,
7471
+ (x) => x.lastDeliveredRef
7472
+ );
7473
+ userProgress.lastDeliveredRef = userProgress.lastReadRef;
7474
+ insertByKey(this.deliveredSorted, userProgress, (x) => x.lastDeliveredRef);
7475
+ }
7476
+ }
7477
+ /** All users who READ this message. */
7478
+ readersForMessage(msgRef) {
7479
+ const index = findIndex(this.readSorted, msgRef, ({ lastReadRef }) => lastReadRef);
7480
+ return this.readSorted.slice(index).map((x) => x.user);
7481
+ }
7482
+ /** All users who have it DELIVERED (includes readers). */
7483
+ deliveredForMessage(msgRef) {
7484
+ const pos = findIndex(
7485
+ this.deliveredSorted,
7486
+ msgRef,
7487
+ ({ lastDeliveredRef }) => lastDeliveredRef
7488
+ );
7489
+ return this.deliveredSorted.slice(pos).map((x) => x.user);
7490
+ }
7491
+ /** Users who delivered but have NOT read. */
7492
+ deliveredNotReadForMessage(msgRef) {
7493
+ const pos = findIndex(
7494
+ this.deliveredSorted,
7495
+ msgRef,
7496
+ ({ lastDeliveredRef }) => lastDeliveredRef
7497
+ );
7498
+ const usersDeliveredNotRead = [];
7499
+ for (let i = pos; i < this.deliveredSorted.length; i++) {
7500
+ const userProgress = this.deliveredSorted[i];
7501
+ if (compareRefsAsc(userProgress.lastReadRef, msgRef) < 0)
7502
+ usersDeliveredNotRead.push(userProgress.user);
7503
+ }
7504
+ return usersDeliveredNotRead;
7505
+ }
7506
+ /** Users for whom `msgRef` is their *last read* (exact match). */
7507
+ usersWhoseLastReadIs(msgRef) {
7508
+ if (!msgRef.msgId) return [];
7509
+ const start = findIndex(this.readSorted, msgRef, (x) => x.lastReadRef);
7510
+ const end = findUpperIndex(this.readSorted, msgRef, (x) => x.lastReadRef);
7511
+ const users = [];
7512
+ for (let i = start; i < end; i++) {
7513
+ const up = this.readSorted[i];
7514
+ if (up.lastReadRef.msgId === msgRef.msgId) users.push(up.user);
7515
+ }
7516
+ return users;
7517
+ }
7518
+ /** Users for whom `msgRef` is their *last delivered* (exact match). */
7519
+ usersWhoseLastDeliveredIs(msgRef) {
7520
+ if (!msgRef.msgId) return [];
7521
+ const start = findIndex(this.deliveredSorted, msgRef, (x) => x.lastDeliveredRef);
7522
+ const end = findUpperIndex(this.deliveredSorted, msgRef, (x) => x.lastDeliveredRef);
7523
+ const users = [];
7524
+ for (let i = start; i < end; i++) {
7525
+ const up = this.deliveredSorted[i];
7526
+ if (up.lastDeliveredRef.msgId === msgRef.msgId) users.push(up.user);
7527
+ }
7528
+ return users;
7529
+ }
7530
+ // ---- queries: per-user status ----
7531
+ hasUserRead(msgRef, userId) {
7532
+ const up = this.byUser.get(userId);
7533
+ return !!up && compareRefsAsc(up.lastReadRef, msgRef) >= 0;
7534
+ }
7535
+ hasUserDelivered(msgRef, userId) {
7536
+ const up = this.byUser.get(userId);
7537
+ return !!up && compareRefsAsc(up.lastDeliveredRef, msgRef) >= 0;
7538
+ }
7539
+ getUserProgress(userId) {
7540
+ const userProgress = this.byUser.get(userId);
7541
+ if (!userProgress) return null;
7542
+ return userProgress;
7543
+ }
7544
+ groupUsersByLastReadMessage() {
7545
+ return Array.from(this.byUser.values()).reduce(
7546
+ (acc, userProgress) => {
7547
+ const msgId = userProgress.lastReadRef.msgId;
7548
+ if (!msgId) return acc;
7549
+ if (!acc[msgId]) acc[msgId] = [];
7550
+ acc[msgId].push(userProgress.user);
7551
+ return acc;
7552
+ },
7553
+ {}
7554
+ );
7555
+ }
7556
+ groupUsersByLastDeliveredMessage() {
7557
+ return Array.from(this.byUser.values()).reduce(
7558
+ (acc, userProgress) => {
7559
+ const msgId = userProgress.lastDeliveredRef.msgId;
7560
+ if (!msgId) return acc;
7561
+ if (!acc[msgId]) acc[msgId] = [];
7562
+ acc[msgId].push(userProgress.user);
7563
+ return acc;
7564
+ },
7565
+ {}
7566
+ );
7567
+ }
7568
+ ensureUser(user) {
7569
+ let up = this.byUser.get(user.id);
7570
+ if (!up) {
7571
+ up = { user, lastReadRef: MIN_REF, lastDeliveredRef: MIN_REF };
7572
+ this.byUser.set(user.id, up);
7573
+ insertByKey(this.readSorted, up, (x) => x.lastReadRef);
7574
+ insertByKey(this.deliveredSorted, up, (x) => x.lastDeliveredRef);
7575
+ }
7576
+ return up;
7577
+ }
7578
+ };
7579
+
7048
7580
  // src/channel.ts
7049
7581
  var Channel = class {
7050
7582
  /**
@@ -7126,6 +7658,12 @@ var Channel = class {
7126
7658
  client: this._client,
7127
7659
  compositionContext: this
7128
7660
  });
7661
+ this.messageReceiptsTracker = new MessageReceiptsTracker({
7662
+ locateMessage: (timestampMs) => {
7663
+ const msg = this.state.findMessageByTimestamp(timestampMs);
7664
+ return msg && { timestampMs, msgId: msg.id };
7665
+ }
7666
+ });
7129
7667
  }
7130
7668
  /**
7131
7669
  * getClient - Get the chat client for this channel. If client.disconnect() was called, this function will error
@@ -7935,15 +8473,24 @@ var Channel = class {
7935
8473
  return messageSlice[0];
7936
8474
  }
7937
8475
  /**
7938
- * markRead - Send the mark read event for this user, only works if the `read_events` setting is enabled
8476
+ * 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.
7939
8477
  *
7940
8478
  * @param {MarkReadOptions} data
7941
8479
  * @return {Promise<EventAPIResponse | null>} Description
7942
8480
  */
7943
8481
  async markRead(data = {}) {
8482
+ return await this.getClient().messageDeliveryReporter.markRead(this, data);
8483
+ }
8484
+ /**
8485
+ * markReadRequest - Send the mark read event for this user, only works if the `read_events` setting is enabled
8486
+ *
8487
+ * @param {MarkReadOptions} data
8488
+ * @return {Promise<EventAPIResponse | null>} Description
8489
+ */
8490
+ async markAsReadRequest(data = {}) {
7944
8491
  this._checkInitialized();
7945
8492
  if (!this.getConfig()?.read_events && !this.getClient()._isUsingServerAuth()) {
7946
- return Promise.resolve(null);
8493
+ return null;
7947
8494
  }
7948
8495
  return await this.getClient().post(this._channelURL() + "/read", {
7949
8496
  ...data
@@ -8251,6 +8798,7 @@ var Channel = class {
8251
8798
  }),
8252
8799
  { method: "upsertChannels" }
8253
8800
  );
8801
+ this.getClient().syncDeliveredCandidates([this]);
8254
8802
  return state;
8255
8803
  }
8256
8804
  /**
@@ -8522,17 +9070,44 @@ var Channel = class {
8522
9070
  break;
8523
9071
  case "message.read":
8524
9072
  if (event.user?.id && event.created_at) {
9073
+ const previousReadState = channelState.read[event.user.id];
8525
9074
  channelState.read[event.user.id] = {
9075
+ // in case we already have delivery information
9076
+ ...previousReadState,
8526
9077
  last_read: new Date(event.created_at),
8527
9078
  last_read_message_id: event.last_read_message_id,
8528
9079
  user: event.user,
8529
9080
  unread_messages: 0
8530
9081
  };
8531
- if (event.user?.id === this.getClient().user?.id) {
9082
+ this.messageReceiptsTracker.onMessageRead({
9083
+ user: event.user,
9084
+ readAt: event.created_at,
9085
+ lastReadMessageId: event.last_read_message_id
9086
+ });
9087
+ const client = this.getClient();
9088
+ const isOwnEvent = event.user?.id === client.user?.id;
9089
+ if (isOwnEvent) {
8532
9090
  channelState.unreadCount = 0;
9091
+ client.syncDeliveredCandidates([this]);
8533
9092
  }
8534
9093
  }
8535
9094
  break;
9095
+ case "message.delivered":
9096
+ if (event.user?.id && event.created_at) {
9097
+ const previousReadState = channelState.read[event.user.id];
9098
+ channelState.read[event.user.id] = {
9099
+ ...previousReadState,
9100
+ last_delivered_at: event.last_delivered_at ? new Date(event.last_delivered_at) : void 0,
9101
+ last_delivered_message_id: event.last_delivered_message_id,
9102
+ user: event.user
9103
+ };
9104
+ this.messageReceiptsTracker.onMessageDelivered({
9105
+ user: event.user,
9106
+ deliveredAt: event.created_at,
9107
+ lastDeliveredMessageId: event.last_delivered_message_id
9108
+ });
9109
+ }
9110
+ break;
8536
9111
  case "user.watching.start":
8537
9112
  case "user.updated":
8538
9113
  if (event.user?.id) {
@@ -8566,7 +9141,8 @@ var Channel = class {
8566
9141
  break;
8567
9142
  case "message.new":
8568
9143
  if (event.message) {
8569
- const ownMessage = event.user?.id === this.getClient().user?.id;
9144
+ const client = this.getClient();
9145
+ const ownMessage = event.user?.id === client.user?.id;
8570
9146
  const isThreadMessage = event.message.parent_id && !event.message.show_in_channel;
8571
9147
  if (this.state.isUpToDate || isThreadMessage) {
8572
9148
  channelState.addMessageSorted(event.message, ownMessage);
@@ -8582,7 +9158,9 @@ var Channel = class {
8582
9158
  channelState.read[event.user.id] = {
8583
9159
  last_read: new Date(event.created_at),
8584
9160
  user: event.user,
8585
- unread_messages: 0
9161
+ unread_messages: 0,
9162
+ last_delivered_at: new Date(event.created_at),
9163
+ last_delivered_message_id: event.message.id
8586
9164
  };
8587
9165
  } else {
8588
9166
  channelState.read[userId].unread_messages += 1;
@@ -8592,6 +9170,7 @@ var Channel = class {
8592
9170
  if (this._countMessageAsUnread(event.message)) {
8593
9171
  channelState.unreadCount = channelState.unreadCount + 1;
8594
9172
  }
9173
+ client.syncDeliveredCandidates([this]);
8595
9174
  }
8596
9175
  break;
8597
9176
  case "message.updated":
@@ -8674,9 +9253,12 @@ var Channel = class {
8674
9253
  break;
8675
9254
  case "notification.mark_unread": {
8676
9255
  const ownMessage = event.user?.id === this.getClient().user?.id;
8677
- if (!(ownMessage && event.user)) break;
9256
+ if (!ownMessage || !event.user) break;
8678
9257
  const unreadCount = event.unread_messages ?? 0;
9258
+ const currentState = channelState.read[event.user.id];
8679
9259
  channelState.read[event.user.id] = {
9260
+ // keep the message delivery info
9261
+ ...currentState,
8680
9262
  first_unread_message_id: event.first_unread_message_id,
8681
9263
  last_read: new Date(event.last_read_at),
8682
9264
  last_read_message_id: event.last_read_message_id,
@@ -8684,6 +9266,11 @@ var Channel = class {
8684
9266
  unread_messages: unreadCount
8685
9267
  };
8686
9268
  channelState.unreadCount = unreadCount;
9269
+ this.messageReceiptsTracker.onNotificationMarkUnread({
9270
+ user: event.user,
9271
+ lastReadAt: event.last_read_at,
9272
+ lastReadMessageId: event.last_read_message_id
9273
+ });
8687
9274
  break;
8688
9275
  }
8689
9276
  case "channel.updated":
@@ -8835,6 +9422,7 @@ var Channel = class {
8835
9422
  this.state.unreadCount = this.state.read[read.user.id].unread_messages;
8836
9423
  }
8837
9424
  }
9425
+ this.messageReceiptsTracker.ingestInitial(state.read);
8838
9426
  }
8839
9427
  return {
8840
9428
  messageSet
@@ -12385,6 +12973,7 @@ var StreamChat = class _StreamChat {
12385
12973
  this.threads = new ThreadManager({ client: this });
12386
12974
  this.polls = new PollManager({ client: this });
12387
12975
  this.reminders = new ReminderManager({ client: this });
12976
+ this.messageDeliveryReporter = new MessageDeliveryReporter({ client: this });
12388
12977
  }
12389
12978
  static getInstance(key, secretOrOptions, options) {
12390
12979
  if (!_StreamChat._instance) {
@@ -13055,6 +13644,7 @@ var StreamChat = class _StreamChat {
13055
13644
  c.messageComposer.initStateFromChannelResponse(channelState);
13056
13645
  channels.push(c);
13057
13646
  }
13647
+ this.syncDeliveredCandidates(channels);
13058
13648
  return channels;
13059
13649
  }
13060
13650
  /**
@@ -13819,10 +14409,26 @@ var StreamChat = class _StreamChat {
13819
14409
  }
13820
14410
  );
13821
14411
  }
13822
- async deleteMessage(messageID, hardDelete) {
14412
+ /**
14413
+ * deleteMessage - Delete a message
14414
+ *
14415
+ * @param {string} messageID The id of the message to delete
14416
+ * @param {boolean | DeleteMessageOptions | undefined} [optionsOrHardDelete]
14417
+ * @return {Promise<APIResponse & { message: MessageResponse }>} The API response
14418
+ */
14419
+ // fixme: remove the signature with optionsOrHardDelete boolean with the next major release
14420
+ async deleteMessage(messageID, optionsOrHardDelete) {
14421
+ let options = {};
14422
+ if (typeof optionsOrHardDelete === "boolean") {
14423
+ options = optionsOrHardDelete ? { hardDelete: true } : {};
14424
+ } else if (optionsOrHardDelete?.deleteForMe) {
14425
+ options = { deleteForMe: true };
14426
+ } else if (optionsOrHardDelete?.hardDelete) {
14427
+ options = { hardDelete: true };
14428
+ }
13823
14429
  try {
13824
14430
  if (this.offlineDb) {
13825
- if (hardDelete) {
14431
+ if (options.hardDelete) {
13826
14432
  await this.offlineDb.hardDeleteMessage({ id: messageID });
13827
14433
  } else {
13828
14434
  await this.offlineDb.softDeleteMessage({ id: messageID });
@@ -13831,7 +14437,7 @@ var StreamChat = class _StreamChat {
13831
14437
  {
13832
14438
  task: {
13833
14439
  messageId: messageID,
13834
- payload: [messageID, hardDelete],
14440
+ payload: [messageID, options],
13835
14441
  type: "delete-message"
13836
14442
  }
13837
14443
  }
@@ -13843,17 +14449,27 @@ var StreamChat = class _StreamChat {
13843
14449
  error
13844
14450
  });
13845
14451
  }
13846
- return this._deleteMessage(messageID, hardDelete);
14452
+ return this._deleteMessage(messageID, options);
13847
14453
  }
13848
- async _deleteMessage(messageID, hardDelete) {
14454
+ // fixme: remove the signature with optionsOrHardDelete boolean with the next major release
14455
+ async _deleteMessage(messageID, optionsOrHardDelete) {
14456
+ const { deleteForMe, hardDelete } = typeof optionsOrHardDelete === "boolean" ? { hardDelete: optionsOrHardDelete } : optionsOrHardDelete ?? {};
13849
14457
  let params = {};
13850
14458
  if (hardDelete) {
13851
14459
  params = { hard: true };
13852
14460
  }
13853
- return await this.delete(
14461
+ if (deleteForMe) {
14462
+ params = { ...params, delete_for_me: true };
14463
+ }
14464
+ const result = await this.delete(
13854
14465
  this.baseURL + `/messages/${encodeURIComponent(messageID)}`,
13855
14466
  params
13856
14467
  );
14468
+ if (deleteForMe) {
14469
+ result.message.deleted_for_me = true;
14470
+ result.message.type = "deleted";
14471
+ }
14472
+ return result;
13857
14473
  }
13858
14474
  /**
13859
14475
  * undeleteMessage - Undelete a message
@@ -13989,7 +14605,7 @@ var StreamChat = class _StreamChat {
13989
14605
  if (this.userAgent) {
13990
14606
  return this.userAgent;
13991
14607
  }
13992
- const version = "9.21.0";
14608
+ const version = "9.22.0";
13993
14609
  const clientBundle = "node-cjs";
13994
14610
  let userAgentString = "";
13995
14611
  if (this.sdkIdentifier) {
@@ -15138,19 +15754,21 @@ var StreamChat = class _StreamChat {
15138
15754
  return this.delete(`${this.baseURL}/uploads/image`, { url });
15139
15755
  }
15140
15756
  /**
15141
- * Send the mark delivered event for this user, only works if the `delivery_receipts` setting is enabled
15757
+ * Send the mark delivered event for this user
15142
15758
  *
15143
15759
  * @param {MarkDeliveredOptions} data
15144
15760
  * @return {Promise<EventAPIResponse | void>} Description
15145
15761
  */
15146
15762
  async markChannelsDelivered(data) {
15147
- const deliveryReceiptsEnabled = this.user?.privacy_settings?.delivery_receipts?.enabled;
15148
- if (!deliveryReceiptsEnabled) return;
15763
+ if (!data?.latest_delivered_messages?.length) return;
15149
15764
  return await this.post(
15150
15765
  this.baseURL + "/channels/delivered",
15151
15766
  data ?? {}
15152
15767
  );
15153
15768
  }
15769
+ syncDeliveredCandidates(collections) {
15770
+ this.messageDeliveryReporter.syncDeliveredCandidates(collections);
15771
+ }
15154
15772
  };
15155
15773
 
15156
15774
  // src/events.ts
@@ -15187,6 +15805,7 @@ var EVENT_MAP = {
15187
15805
  "notification.mark_unread": true,
15188
15806
  "notification.message_new": true,
15189
15807
  "notification.mutes_updated": true,
15808
+ "notification.reminder_due": true,
15190
15809
  "notification.removed_from_channel": true,
15191
15810
  "notification.thread_message_new": true,
15192
15811
  "poll.closed": true,
@@ -15197,6 +15816,9 @@ var EVENT_MAP = {
15197
15816
  "reaction.deleted": true,
15198
15817
  "reaction.new": true,
15199
15818
  "reaction.updated": true,
15819
+ "reminder.created": true,
15820
+ "reminder.deleted": true,
15821
+ "reminder.updated": true,
15200
15822
  "thread.updated": true,
15201
15823
  "typing.start": true,
15202
15824
  "typing.stop": true,
@@ -15221,12 +15843,7 @@ var EVENT_MAP = {
15221
15843
  "transport.changed": true,
15222
15844
  "capabilities.changed": true,
15223
15845
  "live_location_sharing.started": true,
15224
- "live_location_sharing.stopped": true,
15225
- // Reminder events
15226
- "reminder.created": true,
15227
- "reminder.updated": true,
15228
- "reminder.deleted": true,
15229
- "notification.reminder_due": true
15846
+ "live_location_sharing.stopped": true
15230
15847
  };
15231
15848
 
15232
15849
  // src/permissions.ts
@@ -16384,7 +17001,9 @@ var FixedSizeQueueCache = class {
16384
17001
  MergedStateStore,
16385
17002
  MessageComposer,
16386
17003
  MessageComposerMiddlewareExecutor,
17004
+ MessageDeliveryReporter,
16387
17005
  MessageDraftComposerMiddlewareExecutor,
17006
+ MessageReceiptsTracker,
16388
17007
  MessageSearchSource,
16389
17008
  MiddlewareExecutor,
16390
17009
  MinPriority,