stream-chat 10.0.0-rc.10 → 10.0.0-rc.11

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.
Files changed (27) hide show
  1. package/dist/cjs/index.browser.js +127 -78
  2. package/dist/cjs/index.browser.js.map +2 -2
  3. package/dist/cjs/index.node.js +127 -78
  4. package/dist/cjs/index.node.js.map +2 -2
  5. package/dist/esm/index.mjs +127 -78
  6. package/dist/esm/index.mjs.map +2 -2
  7. package/dist/types/ConnectionRecoveryManager.d.ts +1 -1
  8. package/dist/types/messageOperations/MessageOperations.d.ts +7 -0
  9. package/dist/types/notifications/NotificationManager.d.ts +10 -0
  10. package/dist/types/offline-support/offline_support_api.d.ts +20 -4
  11. package/dist/types/offline-support/offline_sync_manager.d.ts +2 -2
  12. package/dist/types/offline-support/types.d.ts +19 -2
  13. package/dist/types/pagination/paginators/ChannelPaginator.d.ts +12 -0
  14. package/dist/types/search/SearchController.d.ts +0 -9
  15. package/package.json +1 -1
  16. package/src/ConnectionRecoveryManager.ts +1 -1
  17. package/src/messageComposer/attachmentManager.ts +14 -4
  18. package/src/messageComposer/middleware/attachmentManager/postUpload/uploadErrorHandler.ts +1 -1
  19. package/src/messageComposer/middleware/attachmentManager/preUpload/blockedUploadNotification.ts +1 -1
  20. package/src/messageOperations/MessageOperationStatePolicy.ts +9 -5
  21. package/src/messageOperations/MessageOperations.ts +21 -0
  22. package/src/notifications/NotificationManager.ts +15 -0
  23. package/src/offline-support/offline_support_api.ts +39 -73
  24. package/src/offline-support/offline_sync_manager.ts +3 -3
  25. package/src/offline-support/types.ts +20 -1
  26. package/src/pagination/paginators/ChannelPaginator.ts +46 -18
  27. package/src/search/SearchController.ts +0 -13
@@ -4125,6 +4125,19 @@ var NotificationManager = class {
4125
4125
  }, duration);
4126
4126
  this.timeouts.set(id, timeout);
4127
4127
  }
4128
+ /**
4129
+ * Starts the notification's auto-dismiss countdown unless one is already running.
4130
+ *
4131
+ * The counterpart to {@link startTimeout}, which always restarts. Use this wherever the caller
4132
+ * cannot know whether the countdown has been started already -- a UI component that remounts and
4133
+ * has lost the record, or a second view displaying the same notification. Restarting there is
4134
+ * invisible and open-ended: each call hands the notification another full lifetime, so it can
4135
+ * outlive its duration indefinitely while looking like it is counting down.
4136
+ */
4137
+ ensureTimeout(id, durationOverride) {
4138
+ if (this.timeouts.has(id)) return;
4139
+ this.startTimeout(id, durationOverride);
4140
+ }
4128
4141
  remove(id) {
4129
4142
  this.clearTimeout(id);
4130
4143
  this.store.partialNext({
@@ -5921,7 +5934,7 @@ var createUploadErrorHandlerMiddleware = (composer) => ({
5921
5934
  message: "Error uploading attachment",
5922
5935
  origin: {
5923
5936
  emitter: "AttachmentManager",
5924
- context: { attachment }
5937
+ context: { attachment, composer }
5925
5938
  },
5926
5939
  options: {
5927
5940
  type: CORE_NOTIFICATION_TYPE.attachmentUploadFailed,
@@ -5989,7 +6002,7 @@ var createBlockedAttachmentUploadNotificationMiddleware = (composer) => ({
5989
6002
  message: `The attachment upload was blocked`,
5990
6003
  origin: {
5991
6004
  emitter: "AttachmentManager",
5992
- context: { blockedAttachment: attachment }
6005
+ context: { blockedAttachment: attachment, composer }
5993
6006
  },
5994
6007
  options: {
5995
6008
  type: CORE_NOTIFICATION_TYPE.attachmentUploadBlocked,
@@ -6195,7 +6208,10 @@ var _AttachmentManager = class _AttachmentManager {
6195
6208
  if (!attachment.localMetadata?.file) {
6196
6209
  this.client.notifications.addError({
6197
6210
  message: "File is required for upload attachment",
6198
- origin: { emitter: "AttachmentManager", context: { attachment } },
6211
+ origin: {
6212
+ emitter: "AttachmentManager",
6213
+ context: { attachment, composer: this.composer }
6214
+ },
6199
6215
  options: { type: CORE_NOTIFICATION_TYPE.attachmentFileMissing }
6200
6216
  });
6201
6217
  return;
@@ -6203,7 +6219,10 @@ var _AttachmentManager = class _AttachmentManager {
6203
6219
  if (!attachment.localMetadata.id) {
6204
6220
  this.client.notifications.addError({
6205
6221
  message: "Local upload attachment missing local id",
6206
- origin: { emitter: "AttachmentManager", context: { attachment } },
6222
+ origin: {
6223
+ emitter: "AttachmentManager",
6224
+ context: { attachment, composer: this.composer }
6225
+ },
6207
6226
  options: { type: CORE_NOTIFICATION_TYPE.attachmentIdMissing }
6208
6227
  });
6209
6228
  return;
@@ -6270,7 +6289,11 @@ var _AttachmentManager = class _AttachmentManager {
6270
6289
  message: `The attachment upload was blocked`,
6271
6290
  origin: {
6272
6291
  emitter: "AttachmentManager",
6273
- context: { attachment, blockedAttachment: localAttachment }
6292
+ context: {
6293
+ attachment,
6294
+ blockedAttachment: localAttachment,
6295
+ composer: this.composer
6296
+ }
6274
6297
  },
6275
6298
  options: {
6276
6299
  type: CORE_NOTIFICATION_TYPE.attachmentUploadBlocked,
@@ -6298,7 +6321,7 @@ var _AttachmentManager = class _AttachmentManager {
6298
6321
  message: "Error uploading attachment",
6299
6322
  origin: {
6300
6323
  emitter: "AttachmentManager",
6301
- context: { attachment, failedAttachment }
6324
+ context: { attachment, failedAttachment, composer: this.composer }
6302
6325
  },
6303
6326
  options: {
6304
6327
  type: CORE_NOTIFICATION_TYPE.attachmentUploadFailed,
@@ -7605,6 +7628,13 @@ var ChannelPaginator = class extends BasePaginator {
7605
7628
  retryCount: DEFAULT_QUERY_CHANNELS_RETRY_COUNT,
7606
7629
  ...paginatorOptions
7607
7630
  });
7631
+ /**
7632
+ * True while the loaded items came ONLY from the offline cache - seeded by `setItems`, never
7633
+ * confirmed by a query. Such a state has no pagination position worth continuing from, so the seed
7634
+ * records `offset = <cache size>` and a query shape, which is what makes the next query look like a
7635
+ * continuation rather than a first page. See `executeQuery`.
7636
+ */
7637
+ this._itemsLoadedFromOfflineDB = false;
7608
7638
  /**
7609
7639
  * Filters sent to the server with the next query (and used as the offline-db query key). Deliberately
7610
7640
  * built from the locally configured filters only — feeding the backend-resolved filter back in would
@@ -7629,12 +7659,15 @@ var ChannelPaginator = class extends BasePaginator {
7629
7659
  return void 0;
7630
7660
  const { stateOptions: _, ...request } = queryShape;
7631
7661
  try {
7632
- const channelsFromDB = await this.client.offlineDb.getChannelsForQuery({
7662
+ const cachedQuery = await this.client.offlineDb.getChannelsForQuery({
7633
7663
  userId: this.client.user.id,
7634
7664
  options: request
7635
7665
  });
7636
- if (channelsFromDB) {
7637
- return this.client.hydrateActiveChannels(channelsFromDB, {
7666
+ if (cachedQuery) {
7667
+ if (cachedQuery.predefinedFilter) {
7668
+ this.applyPredefinedFilterResponse(cachedQuery.predefinedFilter);
7669
+ }
7670
+ return this.client.hydrateActiveChannels(cachedQuery.channels, {
7638
7671
  offlineMode: true,
7639
7672
  skipInitialization: []
7640
7673
  // passing empty array will clear out the existing messages from channel state, this removes the possibility of duplicate messages
@@ -7658,6 +7691,7 @@ var ChannelPaginator = class extends BasePaginator {
7658
7691
  });
7659
7692
  };
7660
7693
  this.query = async () => {
7694
+ this._itemsLoadedFromOfflineDB = false;
7661
7695
  if (!this._nextQueryShape) {
7662
7696
  this._nextQueryShape = this.getNextQueryShape();
7663
7697
  }
@@ -7676,7 +7710,7 @@ var ChannelPaginator = class extends BasePaginator {
7676
7710
  return { items };
7677
7711
  };
7678
7712
  this.filterQueryResults = (items) => items;
7679
- const definedSort = sort ?? DEFAULT_BACKEND_SORT2;
7713
+ const definedSort = sort?.length ? sort : DEFAULT_BACKEND_SORT2;
7680
7714
  this.client = client;
7681
7715
  this._id = id ?? `channel-paginator-${generateUUIDv4()}`;
7682
7716
  this._sort = definedSort;
@@ -7732,7 +7766,7 @@ var ChannelPaginator = class extends BasePaginator {
7732
7766
  return this._staticFilters;
7733
7767
  }
7734
7768
  get sort() {
7735
- return this._sort ?? DEFAULT_BACKEND_SORT2;
7769
+ return this._sort?.length ? this._sort : DEFAULT_BACKEND_SORT2;
7736
7770
  }
7737
7771
  /**
7738
7772
  * What the backend reported it actually filtered and sorted by for the last first-page query —
@@ -7856,6 +7890,11 @@ var ChannelPaginator = class extends BasePaginator {
7856
7890
  * `predefined_filter` / `filter_values` / `sort_values` appear, without which two predefined-filter
7857
7891
  * lists cannot be told apart. The duplication goes away once `convertFilterSortToQuery` derives the
7858
7892
  * key from `options` (a change in that implementation plus a schema-version bump to flush stale rows).
7893
+ *
7894
+ * The resolved `predefinedFilter` rides along with the order it produced: a cache-seeded list has no
7895
+ * response to learn it from, and without it `effectiveFilters` / `effectiveSort` fall back to the
7896
+ * local filters and the default sort - i.e. it would match and order by a different rule than the
7897
+ * one the cached order came from.
7859
7898
  */
7860
7899
  cacheCidsForQuery({
7861
7900
  cids,
@@ -7866,6 +7905,7 @@ var ChannelPaginator = class extends BasePaginator {
7866
7905
  cids,
7867
7906
  filters: request.filter_conditions,
7868
7907
  options: request,
7908
+ predefinedFilter: this._predefinedFilter,
7869
7909
  sort: request.sort
7870
7910
  }),
7871
7911
  { method: "upsertCidsForQuery" }
@@ -7895,24 +7935,32 @@ var ChannelPaginator = class extends BasePaginator {
7895
7935
  */
7896
7936
  async executeQuery(params = {}) {
7897
7937
  const { offlineDb } = this.client;
7898
- const queryShape = params.queryShape ?? this.getNextQueryShape();
7899
- const shouldDeferUntilSynced = !!offlineDb?.getChannelsForQuery && !!this.client.user?.id && !offlineDb.syncManager.syncStatus && this.isFirstPageQuery({ queryShape, reset: params.reset });
7900
- if (!shouldDeferUntilSynced) return await super.executeQuery(params);
7938
+ const effectiveParams = this._itemsLoadedFromOfflineDB && params.reset !== "yes" ? { ...params, keepPreviousItems: true, reset: "yes" } : params;
7939
+ const queryShape = effectiveParams.queryShape ?? this.getNextQueryShape();
7940
+ const shouldDeferUntilSynced = !!offlineDb?.getChannelsForQuery && !!this.client.user?.id && !offlineDb.syncManager.isSynced && // Items seeded from the cache are not an already loaded list, so paginating them has to defer too.
7941
+ (this._itemsLoadedFromOfflineDB || this.isFirstPageQuery({ queryShape, reset: effectiveParams.reset }));
7942
+ if (!shouldDeferUntilSynced) return await super.executeQuery(effectiveParams);
7901
7943
  if (!this.isInitialized) {
7902
7944
  const cachedChannels = await this.preloadFirstPageFromOfflineDb({
7903
- ...params,
7945
+ ...effectiveParams,
7904
7946
  queryShape
7905
7947
  });
7906
7948
  if (cachedChannels?.length) {
7907
7949
  this.setItems({ valueOrFactory: cachedChannels, isFirstPage: true });
7950
+ this._itemsLoadedFromOfflineDB = true;
7908
7951
  }
7909
7952
  this.state.partialNext({ isLoading: false });
7910
7953
  }
7911
- if (offlineDb.syncManager.syncStatus) {
7912
- return await super.executeQuery({ ...params, keepPreviousItems: true });
7954
+ const refreshParams = {
7955
+ ...effectiveParams,
7956
+ keepPreviousItems: true,
7957
+ ...this._itemsLoadedFromOfflineDB ? { reset: "yes" } : {}
7958
+ };
7959
+ if (offlineDb.syncManager.isSynced) {
7960
+ return await super.executeQuery(refreshParams);
7913
7961
  }
7914
7962
  offlineDb.syncManager.scheduleSyncStatusChangeCallback(this.id, async () => {
7915
- await this.executeQuery({ ...params, keepPreviousItems: true });
7963
+ await this.executeQuery(refreshParams);
7916
7964
  });
7917
7965
  }
7918
7966
  setItems(params) {
@@ -8671,7 +8719,6 @@ var SearchController = class {
8671
8719
  searchQuery: "",
8672
8720
  sources: sources ?? []
8673
8721
  });
8674
- this._internalState = new import_state_store12.StateStore({});
8675
8722
  this.configController = new ConfigController({
8676
8723
  defaults: DEFAULT_SEARCH_CONTROLLER_CONFIG,
8677
8724
  constructorOptions: config
@@ -11500,9 +11547,10 @@ var MessageOperationStatePolicy = class {
11500
11547
  }
11501
11548
  return;
11502
11549
  }
11503
- const queued = kind !== "send" && kind !== "retry" && await this.ctx.isQueued(messageId, [
11504
- kind === "delete" ? "delete-message" : "update-message"
11505
- ]);
11550
+ const queued = kind !== "send" && kind !== "retry" && await this.ctx.isQueued(
11551
+ messageId,
11552
+ kind === "delete" ? ["delete-message"] : ["update-message", "send-message"]
11553
+ );
11506
11554
  if (kind === "delete") {
11507
11555
  if (!queued) this.revertDelete({ messageId, optimistic, options });
11508
11556
  return;
@@ -11630,6 +11678,23 @@ var MessageOperations = class {
11630
11678
  clearCachedFailedSend(messageId) {
11631
11679
  this.failedSendCache.delete(messageId);
11632
11680
  }
11681
+ /**
11682
+ * Folds an edit into the payload cached for this message's failed send, so `retry` resends what the
11683
+ * message contains now rather than what it contained when the send failed.
11684
+ *
11685
+ * This exists so a retry while offline for example is also persisted, rather than just disappearing.
11686
+ */
11687
+ rewriteCachedFailedSend(localMessage) {
11688
+ const cached = this.getCachedFailedSend(localMessage.id);
11689
+ if (!cached) return;
11690
+ this.failedSendCache.set(localMessage.id, {
11691
+ ...cached,
11692
+ message: {
11693
+ ...cached.message,
11694
+ ...localMessageToNewMessagePayload(localMessage)
11695
+ }
11696
+ });
11697
+ }
11633
11698
  /**
11634
11699
  * The shared lifecycle: apply the optimistic state, fire the request, then reconcile or record the
11635
11700
  * failure. `kind` is threaded through because the three operations want materially different state
@@ -11710,6 +11775,7 @@ var MessageOperations = class {
11710
11775
  }
11711
11776
  }
11712
11777
  async update(params, requestFn) {
11778
+ this.rewriteCachedFailedSend(params.localMessage);
11713
11779
  const handlers = this.ctx.handlers();
11714
11780
  let updateOptions;
11715
11781
  if (params.options) {
@@ -23200,7 +23266,7 @@ var StreamChat = class _StreamChat extends ChatApi {
23200
23266
  return this.userAgent;
23201
23267
  }
23202
23268
  if (!this.cachedUserAgent) {
23203
- const version = "10.0.0-rc.10";
23269
+ const version = "10.0.0-rc.11";
23204
23270
  const { name: sdkName, version: sdkVersion } = this.sdkIdentifier ?? {};
23205
23271
  const { name: appName, version: appVersion } = this.appIdentifier ?? {};
23206
23272
  const { os, model: deviceModel } = this.deviceIdentifier ?? {};
@@ -24875,7 +24941,7 @@ var OfflineDBSyncManager = class {
24875
24941
  offlineDb,
24876
24942
  syncMaxEventCount
24877
24943
  }) {
24878
- this.syncStatus = false;
24944
+ this.isSynced = false;
24879
24945
  this.connectionChangedListener = null;
24880
24946
  this.syncStatusListeners = [];
24881
24947
  this.scheduledSyncStatusCallbacks = /* @__PURE__ */ new Map();
@@ -24940,7 +25006,7 @@ var OfflineDBSyncManager = class {
24940
25006
  * @param status - The new sync status (`true` or `false`).
24941
25007
  */
24942
25008
  this.invokeSyncStatusListeners = async (status) => {
24943
- this.syncStatus = status;
25009
+ this.isSynced = status;
24944
25010
  this.syncStatusListeners.forEach((l) => {
24945
25011
  try {
24946
25012
  l(status);
@@ -25033,7 +25099,7 @@ var OfflineDBSyncManager = class {
25033
25099
  * Each step is isolated so a failure in one does not prevent the other, and
25034
25100
  * neither can escape to the callers (init + the connection.changed handler).
25035
25101
  * This guarantees the subsequent invokeSyncStatusListeners(true) always runs,
25036
- * so syncStatus recovers to true and gated channel queries are unblocked.
25102
+ * so isSynced recovers to true and gated channel queries are unblocked.
25037
25103
  * Failed syncs degrade to "possibly stale data until the next query" rather
25038
25104
  * than freezing all future queries. See issue #1816.
25039
25105
  */
@@ -25786,69 +25852,52 @@ var AbstractOfflineDB = class {
25786
25852
  * @param error - The error thrown while executing the failed task.
25787
25853
  */
25788
25854
  this.shouldSkipQueueingTask = (error) => !isEphemeral(error);
25789
- this.mergeFailedMessageUpdateIntoPendingSendMessage = ({
25790
- editedMessage,
25791
- pendingMessage
25792
- }) => {
25793
- const normalizedEditedMessage = localMessageToNewMessagePayload(
25794
- editedMessage
25795
- );
25796
- const pendingMessageStatus = pendingMessage.status;
25797
- return {
25798
- ...pendingMessage,
25799
- ...normalizedEditedMessage,
25800
- ...typeof pendingMessageStatus !== "undefined" ? { status: pendingMessageStatus } : {}
25801
- };
25802
- };
25803
25855
  this.isPendingSendMessageTask = (task) => task.type === "send-message";
25804
- this.handleOfflineFailedUpdateMessagePendingTask = async (task) => {
25856
+ /**
25857
+ * Folds an edit of a message the server has never seen into that message's queued `send-message`
25858
+ * task, so a replay sends the edited text once instead of sending the original and then editing it.
25859
+ * The send task keeps its row, and with it its place in the queue, which is what keeps the edited
25860
+ * message ordered against everything queued after it.
25861
+ *
25862
+ * Keyed on a queued `send-message` task existing for this id - the only reliable statement of "the
25863
+ * server has never seen this message". NOT on the edited message's status: both payloads are built
25864
+ * by `localMessageToNewMessagePayload`, which strips `status` along with every other client-only
25865
+ * field, so nothing arriving here carries one.
25866
+ *
25867
+ * That same normalization is why the two merge by a plain spread: both are already `MessageRequest`s,
25868
+ * and normalizing again would map an already-flattened `mentioned_users` back over itself as
25869
+ * `[undefined]`.
25870
+ *
25871
+ * With nothing to fold into, the edit is queued on its own and replays as an ordinary update.
25872
+ */
25873
+ this.handleUpdateMessagePendingTask = async (task) => {
25805
25874
  const [{ id, message }] = task.payload;
25806
- if (!id) {
25807
- return;
25808
- }
25809
25875
  const pendingTasks = await this.getPendingTasks({ messageId: id });
25810
- const pendingSendMessageTask = pendingTasks.find(this.isPendingSendMessageTask);
25811
- if (!pendingSendMessageTask) {
25876
+ const sendTask = pendingTasks.find(this.isPendingSendMessageTask);
25877
+ if (sendTask?.id === void 0) {
25878
+ if (!isMessageUpdateReplayable(message)) return;
25879
+ await this.addPendingTask(task);
25812
25880
  return;
25813
25881
  }
25814
- const updatedPendingSendMessage = this.mergeFailedMessageUpdateIntoPendingSendMessage(
25815
- {
25816
- // TODO: this is not good, we have too many message types, should probably only have two (request, response)
25817
- editedMessage: message,
25818
- pendingMessage: pendingSendMessageTask.payload[0].message
25882
+ const [sendRequest, ...sendRequestOptions] = sendTask.payload;
25883
+ await this.updatePendingTask({
25884
+ id: sendTask.id,
25885
+ task: {
25886
+ ...sendTask,
25887
+ payload: [
25888
+ { ...sendRequest, message: { ...sendRequest.message, ...message } },
25889
+ ...sendRequestOptions
25890
+ ]
25819
25891
  }
25820
- );
25821
- const updatedPendingTask = {
25822
- ...pendingSendMessageTask,
25823
- payload: [
25824
- {
25825
- ...pendingSendMessageTask.payload[0],
25826
- message: updatedPendingSendMessage
25827
- }
25828
- ]
25829
- };
25830
- if (pendingSendMessageTask.id) {
25831
- await this.updatePendingTask({
25832
- id: pendingSendMessageTask.id,
25833
- task: updatedPendingTask
25834
- });
25835
- return;
25836
- }
25837
- await this.addPendingTask({
25838
- ...updatedPendingTask,
25839
- id: void 0
25840
25892
  });
25841
25893
  };
25842
25894
  /**
25843
- * Central ingress for persisting pending tasks. It either stores the task as-is
25844
- * or rewrites an existing pending `send-message` task for offline edits of failed messages.
25895
+ * Central ingress for persisting pending tasks. It either stores the task as-is or folds an edit of
25896
+ * a message that has not been sent yet into that message's queued `send-message` task.
25845
25897
  */
25846
25898
  this.handleAddPendingTask = async ({ task }) => {
25847
- if (task.type === "update-message" && !isMessageUpdateReplayable(task.payload[0].message ?? {})) {
25848
- return;
25849
- }
25850
- if (task.type === "update-message" && !this.client.wsConnection?.isHealthy && task.payload[0].message?.status === "failed") {
25851
- await this.handleOfflineFailedUpdateMessagePendingTask(task);
25899
+ if (task.type === "update-message") {
25900
+ await this.handleUpdateMessagePendingTask(task);
25852
25901
  return;
25853
25902
  }
25854
25903
  await this.addPendingTask(task);