stream-chat 9.50.3 → 10.0.0-rc.2

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 (242) hide show
  1. package/dist/cjs/index.browser.js +19798 -14472
  2. package/dist/cjs/index.browser.js.map +4 -4
  3. package/dist/cjs/index.node.js +19817 -14482
  4. package/dist/cjs/index.node.js.map +4 -4
  5. package/dist/esm/index.mjs +19795 -14469
  6. package/dist/esm/index.mjs.map +4 -4
  7. package/dist/types/ChannelManager.d.ts +221 -0
  8. package/dist/types/EventHandlerPipeline.d.ts +104 -0
  9. package/dist/types/api-client.d.ts +26 -0
  10. package/dist/types/campaign.d.ts +0 -44
  11. package/dist/types/channel.d.ts +454 -479
  12. package/dist/types/channel_batch_updater.d.ts +0 -93
  13. package/dist/types/channel_state.d.ts +53 -201
  14. package/dist/types/client.d.ts +451 -1920
  15. package/dist/types/client_state.d.ts +4 -4
  16. package/dist/types/configuration/InstanceConfigurationService.d.ts +26 -0
  17. package/dist/types/configuration/index.d.ts +1 -0
  18. package/dist/types/configuration/types.d.ts +53 -0
  19. package/dist/types/connection.d.ts +49 -41
  20. package/dist/types/connection_fallback.d.ts +6 -4
  21. package/dist/types/constants.d.ts +0 -4
  22. package/dist/types/custom_types.d.ts +2 -0
  23. package/dist/types/entityStore/EntityStore.d.ts +111 -0
  24. package/dist/types/entityStore/StoreBackedItemIndex.d.ts +60 -0
  25. package/dist/types/entityStore/applyReactionLocally.d.ts +26 -0
  26. package/dist/types/entityStore/index.d.ts +2 -0
  27. package/dist/types/errors.d.ts +10 -2
  28. package/dist/types/gen/chat/ChannelApi.d.ts +45 -0
  29. package/dist/types/gen/chat/ChatApi.d.ts +334 -0
  30. package/dist/types/gen/model-decoders/decoders.d.ts +3 -0
  31. package/dist/types/gen/model-decoders/event-decoder-mapping.d.ts +6 -0
  32. package/dist/types/gen/models/index.d.ts +8836 -0
  33. package/dist/types/gen/moderation/ModerationApi.d.ts +50 -0
  34. package/dist/types/gen-imports.d.ts +3 -0
  35. package/dist/types/index.d.ts +8 -7
  36. package/dist/types/insights.d.ts +9 -8
  37. package/dist/types/logger.d.ts +9 -0
  38. package/dist/types/messageComposer/LocationComposer.d.ts +7 -4
  39. package/dist/types/messageComposer/attachmentIdentity.d.ts +2 -2
  40. package/dist/types/messageComposer/attachmentManager.d.ts +8 -4
  41. package/dist/types/messageComposer/configuration/types.d.ts +11 -11
  42. package/dist/types/messageComposer/linkPreviewsManager.d.ts +25 -13
  43. package/dist/types/messageComposer/messageComposer.d.ts +10 -10
  44. package/dist/types/messageComposer/middleware/messageComposer/types.d.ts +3 -3
  45. package/dist/types/messageComposer/middleware/pollComposer/types.d.ts +4 -7
  46. package/dist/types/messageComposer/middleware/textComposer/commandUtils.d.ts +3 -3
  47. package/dist/types/messageComposer/middleware/textComposer/commands.d.ts +7 -7
  48. package/dist/types/messageComposer/middleware/textComposer/mentions.d.ts +218 -4
  49. package/dist/types/messageComposer/middleware/textComposer/types.d.ts +9 -4
  50. package/dist/types/messageComposer/pollComposer.d.ts +5 -4
  51. package/dist/types/messageComposer/textComposer.d.ts +8 -3
  52. package/dist/types/messageComposer/types.d.ts +6 -28
  53. package/dist/types/messageDelivery/MessageDeliveryReporter.d.ts +40 -25
  54. package/dist/types/messageDelivery/MessageReceiptsTracker.d.ts +53 -12
  55. package/dist/types/messageOperations/MessageOperationStatePolicy.d.ts +19 -0
  56. package/dist/types/messageOperations/MessageOperations.d.ts +17 -0
  57. package/dist/types/messageOperations/index.d.ts +3 -0
  58. package/dist/types/messageOperations/types.d.ts +49 -0
  59. package/dist/types/moderation.d.ts +21 -203
  60. package/dist/types/notifications/types.d.ts +8 -7
  61. package/dist/types/offline-support/offline_support_api.d.ts +111 -87
  62. package/dist/types/offline-support/offline_sync_manager.d.ts +0 -7
  63. package/dist/types/offline-support/types.d.ts +19 -23
  64. package/dist/types/offline-support/util.d.ts +4 -2
  65. package/dist/types/pagination/FilterBuilder.d.ts +1 -1
  66. package/dist/types/pagination/ItemIndex.d.ts +25 -0
  67. package/dist/types/pagination/cursorDerivation/createdAtAroundPaginationFlags.d.ts +9 -0
  68. package/dist/types/pagination/cursorDerivation/idAroundPaginationFlags.d.ts +6 -0
  69. package/dist/types/pagination/cursorDerivation/index.d.ts +1 -0
  70. package/dist/types/pagination/cursorDerivation/linearPaginationFlags.d.ts +6 -0
  71. package/dist/types/pagination/filterCompiler.d.ts +7 -0
  72. package/dist/types/pagination/index.d.ts +1 -3
  73. package/dist/types/pagination/paginators/BasePaginator.d.ts +848 -0
  74. package/dist/types/pagination/paginators/ChannelPaginator.d.ts +212 -0
  75. package/dist/types/pagination/paginators/MessageIntervalPaginator.d.ts +282 -0
  76. package/dist/types/pagination/paginators/MessagePaginator.d.ts +208 -0
  77. package/dist/types/pagination/paginators/PinnedMessagePaginator.d.ts +39 -0
  78. package/dist/types/pagination/paginators/ReminderPaginator.d.ts +19 -0
  79. package/dist/types/pagination/paginators/UserGroupPaginator.d.ts +24 -0
  80. package/dist/types/pagination/paginators/index.d.ts +7 -0
  81. package/dist/types/pagination/paginators/stateThrottling.d.ts +2 -0
  82. package/dist/types/pagination/sortCompiler.d.ts +49 -0
  83. package/dist/types/pagination/types.normalization.d.ts +11 -0
  84. package/dist/types/pagination/utility.normalization.d.ts +18 -0
  85. package/dist/types/pagination/utility.queryChannel.d.ts +26 -0
  86. package/dist/types/pagination/utility.search.d.ts +13 -0
  87. package/dist/types/permissions.d.ts +1 -1
  88. package/dist/types/poll.d.ts +29 -32
  89. package/dist/types/poll_manager.d.ts +2 -2
  90. package/dist/types/reminders/Reminder.d.ts +2 -5
  91. package/dist/types/reminders/ReminderManager.d.ts +2 -9
  92. package/dist/types/search/MessageSearchSource.d.ts +4 -4
  93. package/dist/types/search/UserSearchSource.d.ts +1 -1
  94. package/dist/types/search/types.d.ts +3 -3
  95. package/dist/types/segment.d.ts +0 -34
  96. package/dist/types/signing.d.ts +105 -65
  97. package/dist/types/store.d.ts +1 -0
  98. package/dist/types/thread.d.ts +70 -34
  99. package/dist/types/thread_manager.d.ts +3 -3
  100. package/dist/types/token_manager.d.ts +15 -10
  101. package/dist/types/types.d.ts +197 -3404
  102. package/dist/types/utils/FixedSizeQueueCache.d.ts +12 -6
  103. package/dist/types/utils/WithSubscriptions.d.ts +2 -1
  104. package/dist/types/utils/concurrency.d.ts +4 -4
  105. package/dist/types/utils/mergeWith/mergeWith.d.ts +3 -3
  106. package/dist/types/utils/mergeWith/mergeWithCore.d.ts +8 -3
  107. package/dist/types/utils/retryable.d.ts +34 -0
  108. package/dist/types/utils/throttling/throttle.d.ts +36 -0
  109. package/dist/types/utils.d.ts +100 -194
  110. package/package.json +7 -3
  111. package/src/ChannelManager.ts +724 -0
  112. package/src/CooldownTimer.ts +2 -2
  113. package/src/EventHandlerPipeline.ts +228 -0
  114. package/src/LiveLocationManager.ts +12 -11
  115. package/src/api-client.ts +275 -0
  116. package/src/campaign.ts +1 -77
  117. package/src/channel.ts +1344 -1282
  118. package/src/channel_batch_updater.ts +1 -211
  119. package/src/channel_state.ts +144 -1013
  120. package/src/client.ts +936 -3995
  121. package/src/client_state.ts +4 -4
  122. package/src/configuration/InstanceConfigurationService.ts +73 -0
  123. package/src/configuration/index.ts +1 -0
  124. package/src/configuration/types.ts +81 -0
  125. package/src/connection.ts +145 -94
  126. package/src/connection_fallback.ts +29 -27
  127. package/src/constants.ts +1 -5
  128. package/src/custom_types.ts +4 -1
  129. package/src/entityStore/EntityStore.ts +208 -0
  130. package/src/entityStore/StoreBackedItemIndex.ts +115 -0
  131. package/src/entityStore/applyReactionLocally.ts +113 -0
  132. package/src/entityStore/index.ts +2 -0
  133. package/src/errors.ts +14 -2
  134. package/src/gen/chat/ChannelApi.ts +275 -0
  135. package/src/gen/chat/ChatApi.ts +2554 -0
  136. package/src/gen/model-decoders/decoders.ts +2680 -0
  137. package/src/gen/model-decoders/event-decoder-mapping.ts +198 -0
  138. package/src/gen/models/index.ts +12673 -0
  139. package/src/gen/moderation/ModerationApi.ts +607 -0
  140. package/src/gen-imports.ts +3 -0
  141. package/src/index.ts +11 -12
  142. package/src/insights.ts +6 -5
  143. package/src/logger.ts +28 -0
  144. package/src/messageComposer/LocationComposer.ts +9 -5
  145. package/src/messageComposer/MessageComposerEffectHandlers.ts +1 -0
  146. package/src/messageComposer/attachmentIdentity.ts +16 -9
  147. package/src/messageComposer/attachmentManager.ts +13 -8
  148. package/src/messageComposer/configuration/types.ts +11 -11
  149. package/src/messageComposer/linkPreviewsManager.ts +9 -10
  150. package/src/messageComposer/messageComposer.ts +57 -53
  151. package/src/messageComposer/middleware/messageComposer/attachments.ts +1 -2
  152. package/src/messageComposer/middleware/messageComposer/cleanData.ts +1 -1
  153. package/src/messageComposer/middleware/messageComposer/compositionValidation.ts +2 -2
  154. package/src/messageComposer/middleware/messageComposer/messageComposerState.ts +2 -2
  155. package/src/messageComposer/middleware/messageComposer/sharedLocation.ts +4 -3
  156. package/src/messageComposer/middleware/messageComposer/textComposer.ts +2 -2
  157. package/src/messageComposer/middleware/messageComposer/types.ts +3 -4
  158. package/src/messageComposer/middleware/messageComposer/userDataInjection.ts +9 -5
  159. package/src/messageComposer/middleware/pollComposer/state.ts +0 -2
  160. package/src/messageComposer/middleware/pollComposer/types.ts +4 -7
  161. package/src/messageComposer/middleware/textComposer/TextComposerMiddlewareExecutor.ts +10 -1
  162. package/src/messageComposer/middleware/textComposer/commandEffects.ts +4 -4
  163. package/src/messageComposer/middleware/textComposer/commandUtils.ts +3 -6
  164. package/src/messageComposer/middleware/textComposer/commands.ts +3 -3
  165. package/src/messageComposer/middleware/textComposer/mentionUtils.ts +1 -1
  166. package/src/messageComposer/middleware/textComposer/mentions.ts +31 -15
  167. package/src/messageComposer/middleware/textComposer/types.ts +9 -4
  168. package/src/messageComposer/pollComposer.ts +6 -8
  169. package/src/messageComposer/textComposer.ts +27 -2
  170. package/src/messageComposer/types.ts +6 -28
  171. package/src/messageDelivery/MessageDeliveryReporter.ts +101 -45
  172. package/src/messageDelivery/MessageReceiptsTracker.ts +294 -21
  173. package/src/messageOperations/MessageOperationStatePolicy.ts +77 -0
  174. package/src/messageOperations/MessageOperations.ts +212 -0
  175. package/src/messageOperations/index.ts +10 -0
  176. package/src/messageOperations/types.ts +64 -0
  177. package/src/moderation.ts +38 -431
  178. package/src/notifications/types.ts +8 -7
  179. package/src/offline-support/offline_support_api.ts +200 -133
  180. package/src/offline-support/offline_sync_manager.ts +23 -44
  181. package/src/offline-support/types.ts +23 -26
  182. package/src/offline-support/util.ts +5 -4
  183. package/src/pagination/FilterBuilder.ts +4 -1
  184. package/src/pagination/ItemIndex.ts +25 -0
  185. package/src/pagination/cursorDerivation/createdAtAroundPaginationFlags.ts +73 -0
  186. package/src/pagination/cursorDerivation/idAroundPaginationFlags.ts +53 -0
  187. package/src/pagination/cursorDerivation/index.ts +1 -0
  188. package/src/pagination/cursorDerivation/linearPaginationFlags.ts +86 -0
  189. package/src/pagination/filterCompiler.ts +196 -0
  190. package/src/pagination/index.ts +1 -3
  191. package/src/pagination/paginators/BasePaginator.ts +2859 -0
  192. package/src/pagination/paginators/ChannelPaginator.ts +729 -0
  193. package/src/pagination/paginators/MessageIntervalPaginator.ts +1116 -0
  194. package/src/pagination/paginators/MessagePaginator.ts +508 -0
  195. package/src/pagination/paginators/PinnedMessagePaginator.ts +108 -0
  196. package/src/pagination/paginators/ReminderPaginator.ts +107 -0
  197. package/src/pagination/paginators/UserGroupPaginator.ts +127 -0
  198. package/src/pagination/paginators/index.ts +7 -0
  199. package/src/pagination/paginators/stateThrottling.ts +31 -0
  200. package/src/pagination/sortCompiler.ts +193 -0
  201. package/src/pagination/types.normalization.ts +14 -0
  202. package/src/pagination/utility.normalization.ts +106 -0
  203. package/src/pagination/utility.queryChannel.ts +84 -0
  204. package/src/pagination/utility.search.ts +80 -0
  205. package/src/permissions.ts +7 -8
  206. package/src/poll.ts +101 -110
  207. package/src/poll_manager.ts +14 -8
  208. package/src/reminders/Reminder.ts +2 -5
  209. package/src/reminders/ReminderManager.ts +19 -21
  210. package/src/search/BaseSearchSource.ts +0 -3
  211. package/src/search/ChannelMemberSearchSource.ts +7 -1
  212. package/src/search/ChannelSearchSource.ts +10 -3
  213. package/src/search/MessageSearchSource.ts +29 -30
  214. package/src/search/UserSearchSource.ts +12 -8
  215. package/src/search/types.ts +3 -4
  216. package/src/segment.ts +1 -95
  217. package/src/signing.ts +107 -67
  218. package/src/store.ts +2 -1
  219. package/src/thread.ts +562 -238
  220. package/src/thread_manager.ts +37 -16
  221. package/src/token_manager.ts +21 -10
  222. package/src/types.ts +364 -4533
  223. package/src/uploadManager.ts +8 -0
  224. package/src/utils/FixedSizeQueueCache.ts +12 -6
  225. package/src/utils/WithSubscriptions.ts +2 -1
  226. package/src/utils/concurrency.ts +4 -4
  227. package/src/utils/mergeWith/mergeWith.ts +3 -3
  228. package/src/utils/mergeWith/mergeWithCore.ts +140 -164
  229. package/src/utils/mergeWith/mergeWithDiff.ts +3 -3
  230. package/src/utils/retryable.ts +117 -0
  231. package/src/utils/throttling/throttle.ts +130 -0
  232. package/src/utils.ts +208 -754
  233. package/dist/types/channel_manager.d.ts +0 -122
  234. package/dist/types/events.d.ts +0 -72
  235. package/dist/types/pagination/BasePaginator.d.ts +0 -69
  236. package/dist/types/pagination/ReminderPaginator.d.ts +0 -16
  237. package/dist/types/pagination/UserGroupPaginator.d.ts +0 -21
  238. package/src/channel_manager.ts +0 -830
  239. package/src/events.ts +0 -75
  240. package/src/pagination/BasePaginator.ts +0 -184
  241. package/src/pagination/ReminderPaginator.ts +0 -56
  242. package/src/pagination/UserGroupPaginator.ts +0 -93
package/src/utils.ts CHANGED
@@ -1,23 +1,12 @@
1
1
  import FormData from 'form-data';
2
2
  import type {
3
- AscDesc,
4
- ChannelFilters,
5
- ChannelQueryOptions,
6
- ChannelSort,
7
- ChannelSortBase,
8
3
  LocalMessage,
9
- LocalMessageBase,
10
- Logger,
11
- Message,
12
- MessagePaginationOptions,
4
+ MessageRequest,
13
5
  MessageResponse,
14
- MessageResponseBase,
15
- MessageSet,
16
6
  OwnUserBase,
17
7
  OwnUserResponse,
18
- PromoteChannelParams,
19
- QueryChannelAPIResponse,
20
8
  ReactionGroupResponse,
9
+ ReactionResponse,
21
10
  UpdatedMessage,
22
11
  UserResponse,
23
12
  } from './types';
@@ -25,18 +14,22 @@ import type { StreamChat } from './client';
25
14
  import type { Channel } from './channel';
26
15
  import type { AxiosRequestConfig } from 'axios';
27
16
  import { LOCAL_MESSAGE_FIELDS, RESERVED_UPDATED_MESSAGE_FIELDS } from './constants';
17
+ import { chatLoggerSystem } from './logger';
18
+
19
+ const logger = chatLoggerSystem.getLogger('utils');
28
20
 
29
21
  /**
30
- * logChatPromiseExecution - utility function for logging the execution of a promise..
31
- * use this when you want to run the promise and handle errors by logging a warning
32
- *
33
- * @param {Promise<T>} promise The promise you want to run and log
34
- * @param {string} name A descriptive name of what the promise does for log output
22
+ * Logs the execution of a promise. Use this when you want to run the promise and handle errors by
23
+ * logging a warning.
35
24
  *
25
+ * @param promise - The promise you want to run and log.
26
+ * @param name - A descriptive name of what the promise does for log output.
36
27
  */
37
28
  export function logChatPromiseExecution<T>(promise: Promise<T>, name: string) {
38
29
  promise.then().catch((error) => {
39
- console.warn(`failed to do ${name}, ran into error: `, error);
30
+ logger
31
+ .withExtraTags('logChatPromiseExecution')
32
+ .error(`Failed to execute "${name}".`, { error });
40
33
  });
41
34
  }
42
35
 
@@ -153,29 +146,11 @@ export function addFileToFormData(
153
146
 
154
147
  return data;
155
148
  }
156
- export function normalizeQuerySort<T extends Record<string, AscDesc | undefined>>(
157
- sort: T | T[],
158
- ) {
159
- const sortFields: Array<{ direction: AscDesc; field: keyof T }> = [];
160
- const sortArr = Array.isArray(sort) ? sort : [sort];
161
- for (const item of sortArr) {
162
- const entries = Object.entries(item) as [keyof T, AscDesc][];
163
- if (entries.length > 1) {
164
- console.warn(
165
- "client._buildSort() - multiple fields in a single sort object detected. Object's field order is not guaranteed",
166
- );
167
- }
168
- for (const [field, direction] of entries) {
169
- sortFields.push({ field, direction });
170
- }
171
- }
172
- return sortFields;
173
- }
174
149
 
175
150
  /**
176
151
  * retryInterval - A retry interval which increases acc to number of failures
177
152
  *
178
- * @return {number} Duration to wait in milliseconds
153
+ * @returns Duration to wait in milliseconds
179
154
  */
180
155
  export function retryInterval(numberOfFailures: number) {
181
156
  // try to reconnect in 0.25-25 seconds (random to spread out the load from failures)
@@ -270,9 +245,9 @@ export function isOnline() {
270
245
  : undefined;
271
246
 
272
247
  if (!nav) {
273
- console.warn(
274
- 'isOnline failed to access window.navigator and assume browser is online',
275
- );
248
+ logger
249
+ .withExtraTags('isOnline')
250
+ .warn('Could not access window.navigator; assuming the browser is online.');
276
251
  return true;
277
252
  }
278
253
 
@@ -319,102 +294,204 @@ export const axiosParamsSerializer: AxiosRequestConfig['paramsSerializer'] = (pa
319
294
  };
320
295
 
321
296
  /**
322
- * Takes the message object, parses the dates, sets `__html`
323
- * and sets the status to `received` if missing; returns a new LocalMessage object.
297
+ * Takes the message object, adds SDK-specific fields (status, error),
298
+ * and returns a new LocalMessage object.
324
299
  *
325
- * @param {LocalMessage} message `LocalMessage` object
300
+ * @param message - message object
326
301
  */
327
- export function formatMessage(
328
- message: MessageResponse | MessageResponseBase | LocalMessage,
329
- ): LocalMessage {
302
+ export function formatMessage(message: MessageResponse | LocalMessage): LocalMessage {
330
303
  const toLocalMessageBase = (
331
- msg: MessageResponse | MessageResponseBase | LocalMessage | null | undefined,
332
- ): LocalMessageBase | null => {
304
+ msg: MessageResponse | LocalMessage | null | undefined,
305
+ ): LocalMessage | null => {
333
306
  if (!msg) return null;
334
307
  return {
335
308
  ...msg,
336
309
  created_at: msg.created_at ? new Date(msg.created_at) : new Date(),
337
- deleted_at: msg.deleted_at ? new Date(msg.deleted_at) : null,
338
- pinned_at: msg.pinned_at ? new Date(msg.pinned_at) : null,
310
+ deleted_at: msg.deleted_at ? new Date(msg.deleted_at) : undefined,
311
+ pinned_at: msg.pinned_at ? new Date(msg.pinned_at) : undefined,
339
312
  reaction_groups: maybeGetReactionGroupsFallback(
340
313
  msg.reaction_groups,
341
314
  msg.reaction_counts,
342
315
  msg.reaction_scores,
343
316
  ),
344
- status: msg.status || 'received',
317
+ status: (msg as LocalMessage).status || 'received',
345
318
  updated_at: msg.updated_at ? new Date(msg.updated_at) : new Date(),
346
319
  };
347
320
  };
348
321
 
349
322
  return {
350
323
  ...toLocalMessageBase(message),
351
- error: (message as LocalMessage).error ?? null,
352
- quoted_message: toLocalMessageBase((message as MessageResponse).quoted_message),
324
+ error: (message as LocalMessage).error ?? undefined,
325
+ quoted_message:
326
+ toLocalMessageBase((message as MessageResponse).quoted_message) ?? undefined,
353
327
  } as LocalMessage;
354
328
  }
355
329
 
356
330
  /**
357
- * @private
331
+ * Computes the current user's `own_reactions` after applying a single reaction change, off a
332
+ * `current` base. A WS reaction/edit event carries `own_reactions: []` (or stale), so consumers
333
+ * must recompute from the copy they already hold rather than trusting the event — this is the
334
+ * shared core used by both the message paginator (`reflectReaction`) and the thread's parent
335
+ * message (which is not held in any paginator, so it needs the same logic directly).
358
336
  *
359
- * Takes a LocalMessage, parses the dates back to strings,
360
- * and converts the message back to a MessageResponse.
337
+ * - `removed`: drop the current user's reaction of this type.
338
+ * - `enforceUnique`: replace any existing own reaction with the incoming one (used by
339
+ * `reaction.updated`, where a user's reaction supersedes their previous one).
340
+ * - otherwise: add the reaction, de-duped by type.
361
341
  *
362
- * @param {MessageResponse} message `MessageResponse` object
342
+ * A reaction by another user never changes the current user's `own_reactions`.
363
343
  */
364
- export function unformatMessage(message: LocalMessage): MessageResponse {
365
- const toMessageResponseBase = (
366
- msg: LocalMessage | null | undefined,
367
- ): MessageResponseBase | null => {
368
- if (!msg) return null;
369
- const newDateString = new Date().toISOString();
370
- return {
371
- ...msg,
372
- created_at: message.created_at ? message.created_at.toISOString() : newDateString,
373
- deleted_at: message.deleted_at ? message.deleted_at.toISOString() : undefined,
374
- pinned_at: message.pinned_at ? message.pinned_at.toISOString() : undefined,
375
- updated_at: message.updated_at ? message.updated_at.toISOString() : newDateString,
376
- };
344
+ export function computeOwnReactions({
345
+ current,
346
+ enforceUnique = false,
347
+ reaction,
348
+ removed = false,
349
+ userId,
350
+ }: {
351
+ current: ReactionResponse[];
352
+ reaction: ReactionResponse;
353
+ enforceUnique?: boolean;
354
+ removed?: boolean;
355
+ userId?: string;
356
+ }): ReactionResponse[] {
357
+ const withoutType = current.filter(
358
+ (r) => r.user_id !== reaction.user_id || r.type !== reaction.type,
359
+ );
360
+ if (removed) return withoutType;
361
+ if (userId !== reaction.user_id) return withoutType;
362
+ return enforceUnique ? [reaction] : [...withoutType, reaction];
363
+ }
364
+
365
+ /**
366
+ * Returns a copy of `message` with `reaction` folded into its `reaction_groups` /
367
+ * `latest_reactions`. Does not touch `own_reactions` — the reaction entry points
368
+ * ({@link MessageIntervalPaginator.reflectReaction} / `Thread.applyParentReactionLocally`) own that
369
+ * via {@link computeOwnReactions}. Shared so both the paginator and the (paginator-less) thread
370
+ * parent compute counts identically.
371
+ */
372
+ export function messageWithReactionAdded(
373
+ message: LocalMessage,
374
+ reaction: ReactionResponse,
375
+ enforceUnique: boolean,
376
+ ): LocalMessage {
377
+ const score = reaction.score ?? 1;
378
+ const reactionGroups: Record<string, ReactionGroupResponse> = {
379
+ ...(message.reaction_groups ?? {}),
377
380
  };
378
381
 
382
+ // When enforcing uniqueness, first back the current user's existing reactions out of the groups
383
+ if (enforceUnique) {
384
+ for (const ownReaction of message.own_reactions ?? []) {
385
+ const group = reactionGroups[ownReaction.type];
386
+ if (!group) continue;
387
+ const next = {
388
+ ...group,
389
+ count: group.count - 1,
390
+ sum_scores: group.sum_scores - (ownReaction.score ?? 1),
391
+ };
392
+ if (next.count < 1) delete reactionGroups[ownReaction.type];
393
+ else reactionGroups[ownReaction.type] = next;
394
+ }
395
+ }
396
+
397
+ const existingGroup = reactionGroups[reaction.type];
398
+ reactionGroups[reaction.type] = existingGroup
399
+ ? {
400
+ ...existingGroup,
401
+ count: existingGroup.count + 1,
402
+ last_reaction_at: reaction.created_at,
403
+ sum_scores: existingGroup.sum_scores + score,
404
+ }
405
+ : {
406
+ count: 1,
407
+ first_reaction_at: reaction.created_at,
408
+ last_reaction_at: reaction.created_at,
409
+ latest_reactions_by: [],
410
+ sum_scores: score,
411
+ };
412
+
413
+ const latestReactions = enforceUnique
414
+ ? [
415
+ ...(message.latest_reactions ?? []).filter((r) => r.user_id !== reaction.user_id),
416
+ reaction,
417
+ ]
418
+ : [...(message.latest_reactions ?? []), reaction];
419
+
379
420
  return {
380
- ...toMessageResponseBase(message),
381
- quoted_message: toMessageResponseBase((message as LocalMessage).quoted_message),
382
- } as MessageResponse;
421
+ ...message,
422
+ latest_reactions: latestReactions,
423
+ reaction_groups: reactionGroups,
424
+ };
383
425
  }
384
426
 
385
- export const localMessageToNewMessagePayload = (localMessage: LocalMessage): Message => {
386
- /* eslint-disable @typescript-eslint/no-unused-vars */
427
+ /**
428
+ * Returns a copy of `message` with the current user's reaction of `reaction.type` backed out of its
429
+ * `reaction_groups` / `latest_reactions`. Does not touch `own_reactions`.
430
+ */
431
+ export function messageWithReactionRemoved(
432
+ message: LocalMessage,
433
+ reaction: ReactionResponse,
434
+ ): LocalMessage {
435
+ const reactionGroups: Record<string, ReactionGroupResponse> = {
436
+ ...(message.reaction_groups ?? {}),
437
+ };
438
+ const reactionToRemove = message.own_reactions?.find((r) => r.type === reaction.type);
439
+
440
+ if (reactionToRemove && reactionGroups[reactionToRemove.type]) {
441
+ const group = reactionGroups[reactionToRemove.type];
442
+ const next = {
443
+ ...group,
444
+ count: group.count - 1,
445
+ sum_scores: group.sum_scores - (reactionToRemove.score ?? 1),
446
+ };
447
+ if (next.count < 1) delete reactionGroups[reactionToRemove.type];
448
+ else reactionGroups[reactionToRemove.type] = next;
449
+ }
450
+
451
+ return {
452
+ ...message,
453
+ latest_reactions: message.latest_reactions?.filter(
454
+ (r) => !(r.user_id === reaction.user_id && r.type === reaction.type),
455
+ ),
456
+ reaction_groups: reactionGroups,
457
+ };
458
+ }
459
+
460
+ export const localMessageToNewMessagePayload = (
461
+ localMessage: LocalMessage,
462
+ ): MessageRequest => {
387
463
  const {
388
464
  // Remove all timestamp fields and client-specific fields.
389
465
  // Field pinned_at can therefore be earlier than created_at as new message payload can hold it.
390
- created_at,
391
- updated_at,
392
- deleted_at,
466
+ created_at: _created_at,
467
+ updated_at: _updated_at,
468
+ deleted_at: _deleted_at,
393
469
  // Client-specific fields
394
- error,
395
- status,
470
+ error: _error,
471
+ status: _status,
396
472
  // Reaction related fields
397
- latest_reactions,
398
- own_reactions,
399
- reaction_counts,
400
- reaction_scores,
401
- reply_count,
402
- // Message text related fields that shouldn't be in update
403
- command,
404
- html,
405
- i18n,
406
- mentioned_groups,
407
- quoted_message,
473
+ latest_reactions: _latest_reactions,
474
+ own_reactions: _own_reactions,
475
+ reaction_counts: _reaction_counts,
476
+ reaction_scores: _reaction_scores,
477
+ reply_count: _reply_count,
478
+ // MessageRequest text related fields that shouldn't be in update
479
+ command: _command,
480
+ html: _html,
481
+ i18n: _i18n,
482
+ mentioned_groups: _mentioned_groups,
483
+ quoted_message: _quoted_message,
408
484
  mentioned_users,
409
- // Message content related fields
485
+ // MessageRequest content related fields
410
486
  ...messageFields
411
487
  } = localMessage;
412
488
 
489
+ // `messageFields` still carries LocalMessage-only fields (cid, deleted_reply_count, mentioned_*,
490
+ // pinned, shadowed, …) that the stricter OpenAPI `MessageRequest` omits; the server ignores them.
413
491
  return {
414
492
  ...messageFields,
415
- pinned_at: messageFields.pinned_at?.toISOString(),
416
493
  mentioned_users: mentioned_users?.map((user) => user.id),
417
- };
494
+ } as MessageRequest;
418
495
  };
419
496
 
420
497
  export const toUpdatedMessagePayload = (
@@ -445,7 +522,7 @@ export const toDeletedMessage = ({
445
522
  deletedAt,
446
523
  hardDelete = false,
447
524
  }: {
448
- message: LocalMessage | LocalMessageBase;
525
+ message: LocalMessage;
449
526
  deletedAt: LocalMessage['deleted_at'];
450
527
  hardDelete: boolean;
451
528
  }) => {
@@ -453,7 +530,7 @@ export const toDeletedMessage = ({
453
530
  /**
454
531
  * In case of hard delete, we need to strip down all text, html, attachments and all the custom properties on message
455
532
  * The hard-deleted message is kept in the UI until the messages are re-queried
456
- * FIXME: we are returning an object that does not match LocalMessage | LocalMessageBase
533
+ * FIXME: we are returning an object that does not match LocalMessage
457
534
  */
458
535
  return {
459
536
  attachments: [],
@@ -482,39 +559,6 @@ export const toDeletedMessage = ({
482
559
  }
483
560
  };
484
561
 
485
- export const deleteUserMessages = ({
486
- messages,
487
- user,
488
- hardDelete = false,
489
- deletedAt,
490
- }: {
491
- messages: Array<LocalMessage>;
492
- user: UserResponse;
493
- hardDelete: boolean;
494
- deletedAt: LocalMessage['deleted_at'];
495
- }) => {
496
- for (let i = 0; i < messages.length; i++) {
497
- const message = messages[i];
498
- if (message.user?.id === user.id) {
499
- messages[i] =
500
- message.type === 'deleted'
501
- ? message
502
- : (toDeletedMessage({ message, hardDelete, deletedAt }) as LocalMessage);
503
- }
504
-
505
- if (messages[i].quoted_message && message.quoted_message?.user?.id === user.id) {
506
- messages[i].quoted_message =
507
- message.quoted_message.type === 'deleted'
508
- ? message.quoted_message
509
- : (toDeletedMessage({
510
- message: messages[i].quoted_message as LocalMessageBase,
511
- hardDelete,
512
- deletedAt,
513
- }) as LocalMessage);
514
- }
515
- }
516
- };
517
-
518
562
  export const findIndexInSortedArray = <T, L>({
519
563
  needle,
520
564
  sortedArray,
@@ -604,77 +648,11 @@ export const findIndexInSortedArray = <T, L>({
604
648
  return left;
605
649
  };
606
650
 
607
- export function addToMessageList<T extends LocalMessage>(
608
- messages: readonly T[],
609
- newMessage: T,
610
- timestampChanged = false,
611
- sortBy: 'pinned_at' | 'created_at' = 'created_at',
612
- addIfDoesNotExist = true,
613
- ) {
614
- const addMessageToList = addIfDoesNotExist || timestampChanged;
615
- let newMessages = [...messages];
616
-
617
- // if created_at has changed, message should be filtered and re-inserted in correct order
618
- // slow op but usually this only happens for a message inserted to state before actual response with correct timestamp
619
- if (timestampChanged) {
620
- newMessages = newMessages.filter(
621
- (message) => !(message.id && newMessage.id === message.id),
622
- );
623
- }
624
-
625
- // for empty list just concat and return unless it's an update or deletion
626
- if (newMessages.length === 0 && addMessageToList) {
627
- return newMessages.concat(newMessage);
628
- } else if (newMessages.length === 0) {
629
- return newMessages;
630
- }
631
-
632
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
633
- const messageTime = newMessage[sortBy]!.getTime();
634
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
635
- const messageIsNewest = newMessages.at(-1)![sortBy]!.getTime() < messageTime;
636
-
637
- // if message is newer than last item in the list concat and return unless it's an update or deletion
638
- if (messageIsNewest && addMessageToList) {
639
- return newMessages.concat(newMessage);
640
- } else if (messageIsNewest) {
641
- return newMessages;
642
- }
643
-
644
- // find the closest index to push the new message
645
- const insertionIndex = findIndexInSortedArray({
646
- needle: newMessage,
647
- sortedArray: newMessages,
648
- sortDirection: 'ascending',
649
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
650
- selectValueToCompare: (m) => m[sortBy]!.getTime(),
651
- selectKey: (m) => m.id,
652
- });
653
-
654
- // message already exists and not filtered with timestampChanged, update and return
655
- if (
656
- !timestampChanged &&
657
- newMessage.id &&
658
- newMessages[insertionIndex] &&
659
- newMessage.id === newMessages[insertionIndex].id
660
- ) {
661
- newMessages[insertionIndex] = newMessage;
662
- return newMessages;
663
- }
664
-
665
- // do not add updated or deleted messages to the list if they already exist or come with a timestamp change
666
- if (addMessageToList) {
667
- newMessages.splice(insertionIndex, 0, newMessage);
668
- }
669
-
670
- return newMessages;
671
- }
672
-
673
651
  function maybeGetReactionGroupsFallback(
674
652
  groups: { [key: string]: ReactionGroupResponse } | null | undefined,
675
653
  counts: { [key: string]: number } | null | undefined,
676
654
  scores: { [key: string]: number } | null | undefined,
677
- ): { [key: string]: ReactionGroupResponse } | null {
655
+ ): { [key: string]: ReactionGroupResponse } | undefined {
678
656
  if (groups) {
679
657
  return groups;
680
658
  }
@@ -683,19 +661,20 @@ function maybeGetReactionGroupsFallback(
683
661
  const fallback: { [key: string]: ReactionGroupResponse } = {};
684
662
 
685
663
  for (const type of Object.keys(counts)) {
664
+ // Best-effort fallback derived from counts/scores; the richer OpenAPI `ReactionGroupResponse`
665
+ // fields (first/last_reaction_at, latest_reactions_by) are not available here.
686
666
  fallback[type] = {
687
667
  count: counts[type],
688
668
  sum_scores: scores[type],
689
- };
669
+ } as ReactionGroupResponse;
690
670
  }
691
671
 
692
672
  return fallback;
693
673
  }
694
674
 
695
- return null;
675
+ return undefined;
696
676
  }
697
677
 
698
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
699
678
  export interface DebouncedFunc<T extends (...args: any[]) => any> {
700
679
  /**
701
680
  * Call the original function, but applying the debounce rules.
@@ -724,7 +703,7 @@ export interface DebouncedFunc<T extends (...args: any[]) => any> {
724
703
  }
725
704
 
726
705
  // works exactly the same as lodash.debounce
727
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
706
+
728
707
  export const debounce = <T extends (...args: any[]) => any>(
729
708
  fn: T,
730
709
  timeout = 0,
@@ -771,39 +750,14 @@ export const debounce = <T extends (...args: any[]) => any>(
771
750
  return debouncedFn;
772
751
  };
773
752
 
774
- // works exactly the same as lodash.throttle
775
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
776
- export const throttle = <T extends (...args: any[]) => any>(
777
- fn: T,
778
- timeout = 200,
779
- { leading = true, trailing = false }: { leading?: boolean; trailing?: boolean } = {},
780
- ) => {
781
- let runningTimeout: null | NodeJS.Timeout = null;
782
- let storedArgs: Parameters<T> | null = null;
783
-
784
- return (...args: Parameters<T>) => {
785
- if (runningTimeout) {
786
- if (trailing) storedArgs = args;
787
- return;
788
- }
789
-
790
- if (leading) fn(...args);
791
-
792
- const timeoutHandler = () => {
793
- if (storedArgs) {
794
- fn(...storedArgs);
795
- storedArgs = null;
796
- runningTimeout = setTimeout(timeoutHandler, timeout);
797
-
798
- return;
799
- }
800
-
801
- runningTimeout = null;
802
- };
803
-
804
- runningTimeout = setTimeout(timeoutHandler, timeout);
805
- };
806
- };
753
+ // The single throttle implementation lives in ./utils/throttling/throttle; re-exported here so
754
+ // `import { throttle } from './utils'` keeps working (lodash.throttle-style leading/trailing).
755
+ export { throttle } from './utils/throttling/throttle';
756
+ export type {
757
+ Throttled,
758
+ ThrottleOptions,
759
+ ThrottledCallback,
760
+ } from './utils/throttling/throttle';
807
761
 
808
762
  const get = <T>(obj: T, path: string): unknown =>
809
763
  path.split('.').reduce<unknown>((acc, key) => {
@@ -830,342 +784,13 @@ export const uniqBy = <T>(
830
784
  });
831
785
  };
832
786
 
833
- type MessagePaginationUpdatedParams = {
834
- parentSet: MessageSet;
835
- requestedPageSize: number;
836
- returnedPage: MessageResponse[];
837
- filteredReturnedPage: MessageResponse[];
838
- logger?: Logger;
839
- messagePaginationOptions?: MessagePaginationOptions;
840
- };
841
-
842
- export function binarySearchByDateEqualOrNearestGreater(
843
- array: {
844
- created_at?: string;
845
- }[],
846
- targetDate: Date,
847
- ): number {
848
- let left = 0;
849
- let right = array.length - 1;
850
-
851
- while (left <= right) {
852
- const mid = Math.floor((left + right) / 2);
853
- const midCreatedAt = array[mid].created_at;
854
- if (!midCreatedAt) {
855
- left += 1;
856
- continue;
857
- }
858
- const midDate = new Date(midCreatedAt);
859
-
860
- if (midDate.getTime() === targetDate.getTime()) {
861
- return mid;
862
- } else if (midDate.getTime() < targetDate.getTime()) {
863
- left = mid + 1;
864
- } else {
865
- right = mid - 1;
866
- }
867
- }
868
-
869
- return left;
870
- }
871
-
872
- const messagePaginationCreatedAtAround = ({
873
- parentSet,
874
- requestedPageSize,
875
- returnedPage,
876
- filteredReturnedPage,
877
- messagePaginationOptions,
878
- }: MessagePaginationUpdatedParams) => {
879
- const newPagination = { ...parentSet.pagination };
880
- if (!messagePaginationOptions?.created_at_around) return newPagination;
881
- let hasPrev;
882
- let hasNext;
883
- let updateHasPrev;
884
- let updateHasNext;
885
- const createdAtAroundDate = new Date(messagePaginationOptions.created_at_around);
886
- const [firstPageMsg, lastPageMsg] = [returnedPage[0], returnedPage.slice(-1)[0]];
887
-
888
- // expect ASC order (from oldest to newest)
889
- const wholePageHasNewerMessages =
890
- !!firstPageMsg?.created_at && new Date(firstPageMsg.created_at) > createdAtAroundDate;
891
- const wholePageHasOlderMessages =
892
- !!lastPageMsg?.created_at && new Date(lastPageMsg.created_at) < createdAtAroundDate;
893
-
894
- const requestedPageSizeNotMet =
895
- requestedPageSize > parentSet.messages.length &&
896
- requestedPageSize > returnedPage.length;
897
- const noMoreMessages =
898
- (requestedPageSize > parentSet.messages.length ||
899
- parentSet.messages.length >= returnedPage.length) &&
900
- requestedPageSize > returnedPage.length;
901
-
902
- if (wholePageHasNewerMessages) {
903
- hasPrev = false;
904
- updateHasPrev = true;
905
- if (requestedPageSizeNotMet) {
906
- hasNext = false;
907
- updateHasNext = true;
908
- }
909
- } else if (wholePageHasOlderMessages) {
910
- hasNext = false;
911
- updateHasNext = true;
912
- if (requestedPageSizeNotMet) {
913
- hasPrev = false;
914
- updateHasPrev = true;
915
- }
916
- } else if (noMoreMessages) {
917
- hasNext = hasPrev = false;
918
- updateHasPrev = updateHasNext = true;
919
- } else {
920
- const [firstFilteredPageMsg, lastFilteredPageMsg] = [
921
- filteredReturnedPage[0],
922
- filteredReturnedPage.slice(-1)[0],
923
- ];
924
- const [firstPageMsgIsFirstInSet, lastPageMsgIsLastInSet] = [
925
- firstFilteredPageMsg?.id && firstFilteredPageMsg.id === parentSet.messages[0]?.id,
926
- lastFilteredPageMsg?.id &&
927
- lastFilteredPageMsg.id === parentSet.messages.slice(-1)[0]?.id,
928
- ];
929
- updateHasPrev = firstPageMsgIsFirstInSet;
930
- updateHasNext = lastPageMsgIsLastInSet;
931
- const midPointByCount = Math.floor(returnedPage.length / 2);
932
- const midPointByCreationDate = binarySearchByDateEqualOrNearestGreater(
933
- returnedPage,
934
- createdAtAroundDate,
935
- );
936
-
937
- if (midPointByCreationDate !== -1) {
938
- hasPrev = midPointByCount <= midPointByCreationDate;
939
- hasNext = midPointByCount >= midPointByCreationDate;
940
- }
941
- }
942
-
943
- if (updateHasPrev && typeof hasPrev !== 'undefined') newPagination.hasPrev = hasPrev;
944
- if (updateHasNext && typeof hasNext !== 'undefined') newPagination.hasNext = hasNext;
945
-
946
- return newPagination;
947
- };
948
-
949
- const messagePaginationIdAround = ({
950
- parentSet,
951
- requestedPageSize,
952
- returnedPage,
953
- filteredReturnedPage,
954
- messagePaginationOptions,
955
- }: MessagePaginationUpdatedParams) => {
956
- const newPagination = { ...parentSet.pagination };
957
- const { id_around } = messagePaginationOptions || {};
958
- if (!id_around) return newPagination;
959
- let hasPrev;
960
- let hasNext;
961
-
962
- const [firstFilteredPageMsg, lastFilteredPageMsg] = [
963
- filteredReturnedPage[0],
964
- filteredReturnedPage.slice(-1)[0],
965
- ];
966
- const [firstPageMsgIsFirstInSet, lastPageMsgIsLastInSet] = [
967
- firstFilteredPageMsg?.id === parentSet.messages[0]?.id,
968
- lastFilteredPageMsg?.id === parentSet.messages.slice(-1)[0]?.id,
969
- ];
970
- let updateHasPrev = firstPageMsgIsFirstInSet;
971
- let updateHasNext = lastPageMsgIsLastInSet;
972
-
973
- const midPoint = Math.floor(returnedPage.length / 2);
974
- const noMoreMessages =
975
- (requestedPageSize > parentSet.messages.length ||
976
- parentSet.messages.length >= returnedPage.length) &&
977
- requestedPageSize > returnedPage.length;
978
-
979
- if (noMoreMessages) {
980
- hasNext = hasPrev = false;
981
- updateHasPrev = updateHasNext = true;
982
- } else if (!returnedPage[midPoint]) {
983
- return newPagination;
984
- } else if (returnedPage[midPoint].id === id_around) {
985
- hasPrev = hasNext = true;
986
- } else {
987
- let targetMsg;
988
- const halves = [returnedPage.slice(0, midPoint), returnedPage.slice(midPoint)];
989
- hasPrev = hasNext = true;
990
- for (let i = 0; i < halves.length; i++) {
991
- targetMsg = halves[i].find((message) => message.id === id_around);
992
- if (targetMsg && i === 0) {
993
- hasPrev = false;
994
- }
995
- if (targetMsg && i === 1) {
996
- hasNext = false;
997
- }
998
- }
999
- }
1000
-
1001
- if (updateHasPrev && typeof hasPrev !== 'undefined') newPagination.hasPrev = hasPrev;
1002
- if (updateHasNext && typeof hasNext !== 'undefined') newPagination.hasNext = hasNext;
1003
-
1004
- return newPagination;
1005
- };
1006
-
1007
- const messagePaginationLinear = ({
1008
- parentSet,
1009
- requestedPageSize,
1010
- returnedPage,
1011
- filteredReturnedPage,
1012
- messagePaginationOptions,
1013
- }: MessagePaginationUpdatedParams) => {
1014
- const newPagination = { ...parentSet.pagination };
1015
-
1016
- let hasPrev;
1017
- let hasNext;
1018
-
1019
- const [firstFilteredPageMsg, lastFilteredPageMsg] = [
1020
- filteredReturnedPage[0],
1021
- filteredReturnedPage.slice(-1)[0],
1022
- ];
1023
- const [firstPageMsgIsFirstInSet, lastPageMsgIsLastInSet] = [
1024
- firstFilteredPageMsg?.id && firstFilteredPageMsg.id === parentSet.messages[0]?.id,
1025
- lastFilteredPageMsg?.id &&
1026
- lastFilteredPageMsg.id === parentSet.messages.slice(-1)[0]?.id,
1027
- ];
1028
-
1029
- const queriedNextMessages =
1030
- messagePaginationOptions &&
1031
- (messagePaginationOptions.created_at_after_or_equal ||
1032
- messagePaginationOptions.created_at_after ||
1033
- messagePaginationOptions.id_gt ||
1034
- messagePaginationOptions.id_gte);
1035
-
1036
- const queriedPrevMessages =
1037
- typeof messagePaginationOptions === 'undefined'
1038
- ? true
1039
- : messagePaginationOptions.created_at_before_or_equal ||
1040
- messagePaginationOptions.created_at_before ||
1041
- messagePaginationOptions.id_lt ||
1042
- messagePaginationOptions.id_lte ||
1043
- messagePaginationOptions.offset;
1044
-
1045
- const containsUnrecognizedOptionsOnly =
1046
- !queriedNextMessages &&
1047
- !queriedPrevMessages &&
1048
- !messagePaginationOptions?.id_around &&
1049
- !messagePaginationOptions?.created_at_around;
1050
-
1051
- const hasMore = returnedPage.length >= requestedPageSize;
1052
-
1053
- if (typeof queriedPrevMessages !== 'undefined' || containsUnrecognizedOptionsOnly) {
1054
- hasPrev = hasMore;
1055
- }
1056
- if (typeof queriedNextMessages !== 'undefined') {
1057
- hasNext = hasMore;
1058
- }
1059
- const returnedPageIsEmpty = returnedPage.length === 0;
1060
-
1061
- if ((firstPageMsgIsFirstInSet || returnedPageIsEmpty) && typeof hasPrev !== 'undefined')
1062
- newPagination.hasPrev = hasPrev;
1063
- if ((lastPageMsgIsLastInSet || returnedPageIsEmpty) && typeof hasNext !== 'undefined')
1064
- newPagination.hasNext = hasNext;
1065
-
1066
- return newPagination;
1067
- };
1068
-
1069
- export const messageSetPagination = (params: MessagePaginationUpdatedParams) => {
1070
- if (
1071
- params.parentSet.messages.length +
1072
- (params.returnedPage.length - params.filteredReturnedPage.length) <
1073
- params.returnedPage.length
1074
- ) {
1075
- params.logger?.(
1076
- 'error',
1077
- 'Corrupted message set state: parent set size < returned page size',
1078
- );
1079
- return params.parentSet.pagination;
1080
- }
1081
-
1082
- if (params.messagePaginationOptions?.created_at_around) {
1083
- return messagePaginationCreatedAtAround(params);
1084
- } else if (params.messagePaginationOptions?.id_around) {
1085
- return messagePaginationIdAround(params);
1086
- } else {
1087
- return messagePaginationLinear(params);
1088
- }
1089
- };
1090
-
1091
- /**
1092
- * A utility object used to prevent duplicate invocation of channel.watch() to be triggered when
1093
- * 'notification.message_new' and 'notification.added_to_channel' events arrive at the same time.
1094
- */
1095
- const WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL: Record<
1096
- string,
1097
- Promise<QueryChannelAPIResponse> | undefined
1098
- > = {};
1099
-
1100
- type GetChannelParams = {
1101
- client: StreamChat;
1102
- channel?: Channel;
1103
- id?: string;
1104
- members?: string[];
1105
- options?: ChannelQueryOptions;
1106
- type?: string;
1107
- };
1108
- /**
1109
- * Calls channel.watch() if it was not already recently called. Waits for watch promise to resolve even if it was invoked previously.
1110
- * If the channel is not passed as a property, it will get it either by its channel.cid or by its members list and do the same.
1111
- * @param client
1112
- * @param members
1113
- * @param options
1114
- * @param type
1115
- * @param id
1116
- * @param channel
1117
- */
1118
- export const getAndWatchChannel = async ({
1119
- channel,
1120
- client,
1121
- id,
1122
- members,
1123
- options,
1124
- type,
1125
- }: GetChannelParams) => {
1126
- if (!channel && !type) {
1127
- throw new Error('Channel or channel type have to be provided to query a channel.');
1128
- }
1129
-
1130
- // unfortunately typescript is not able to infer that if (!channel && !type) === false, then channel or type has to be truthy
1131
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1132
- const channelToWatch = channel || client.channel(type!, id, { members });
1133
-
1134
- // need to keep as with call to channel.watch the id can be changed from undefined to an actual ID generated server-side
1135
- const originalCid = channelToWatch.id
1136
- ? channelToWatch.cid
1137
- : members && members.length
1138
- ? generateChannelTempCid(channelToWatch.type, members)
1139
- : undefined;
1140
-
1141
- if (!originalCid) {
1142
- throw new Error(
1143
- 'Channel ID or channel members array have to be provided to query a channel.',
1144
- );
1145
- }
1146
-
1147
- const queryPromise = WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[originalCid];
1148
-
1149
- if (queryPromise) {
1150
- await queryPromise;
1151
- } else {
1152
- try {
1153
- WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[originalCid] = channelToWatch.watch(options);
1154
- await WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[originalCid];
1155
- } finally {
1156
- delete WATCH_QUERY_IN_PROGRESS_FOR_CHANNEL[originalCid];
1157
- }
1158
- }
1159
-
1160
- return channelToWatch;
1161
- };
1162
-
1163
787
  /**
1164
788
  * Generates a temporary channel.cid for channels created without ID, as they need to be referenced
1165
789
  * by an identifier until the back-end generates the final ID. The cid is generated by its member IDs
1166
790
  * which are sorted and can be recreated the same every time given the same arguments.
1167
- * @param channelType
1168
- * @param members
791
+ *
792
+ * @param channelType - The channel type.
793
+ * @param members - The member ids used to build the temporary cid.
1169
794
  */
1170
795
  export const generateChannelTempCid = (channelType: string, members: string[]) => {
1171
796
  if (!members) return;
@@ -1174,186 +799,6 @@ export const generateChannelTempCid = (channelType: string, members: string[]) =
1174
799
  return `${channelType}:!members-${membersStr}`;
1175
800
  };
1176
801
 
1177
- /**
1178
- * Checks if a channel is pinned or not. Will return true only if channel.state.membership.pinned_at exists.
1179
- * @param channel
1180
- */
1181
- export const isChannelPinned = (channel: Channel) => {
1182
- if (!channel) return false;
1183
-
1184
- const member = channel.state.membership;
1185
-
1186
- return !!member?.pinned_at;
1187
- };
1188
-
1189
- /**
1190
- * Checks if a channel is archived or not. Will return true only if channel.state.membership.archived_at exists.
1191
- * @param channel
1192
- */
1193
- export const isChannelArchived = (channel: Channel) => {
1194
- if (!channel) return false;
1195
-
1196
- const member = channel.state.membership;
1197
-
1198
- return !!member?.archived_at;
1199
- };
1200
-
1201
- /**
1202
- * A utility that tells us whether we should consider archived channels or not based
1203
- * on filters. Will return true only if filters.archived exists and is a boolean value.
1204
- * @param filters
1205
- */
1206
- export const shouldConsiderArchivedChannels = (filters: ChannelFilters) => {
1207
- if (!filters) return false;
1208
-
1209
- return typeof filters.archived === 'boolean';
1210
- };
1211
-
1212
- /**
1213
- * Extracts the value of the sort parameter at a given index, for a targeted key. Can
1214
- * handle both array and object versions of sort. Will return null if the index/key
1215
- * combination does not exist.
1216
- * @param atIndex - the index at which we'll examine the sort value, if it's an array one
1217
- * @param sort - the sort value - both array and object notations are accepted
1218
- * @param targetKey - the target key which needs to exist for the sort at a certain index
1219
- */
1220
- export const extractSortValue = ({
1221
- atIndex,
1222
- sort,
1223
- targetKey,
1224
- }: {
1225
- atIndex: number;
1226
- targetKey: keyof ChannelSortBase;
1227
- sort?: ChannelSort;
1228
- }) => {
1229
- if (!sort) return null;
1230
- let option: null | ChannelSortBase = null;
1231
-
1232
- if (Array.isArray(sort)) {
1233
- option = sort[atIndex] ?? null;
1234
- } else {
1235
- let index = 0;
1236
- for (const key in sort) {
1237
- if (index !== atIndex) {
1238
- index++;
1239
- continue;
1240
- }
1241
-
1242
- if (key !== targetKey) {
1243
- return null;
1244
- }
1245
-
1246
- option = sort;
1247
-
1248
- break;
1249
- }
1250
- }
1251
-
1252
- return option?.[targetKey] ?? null;
1253
- };
1254
-
1255
- /**
1256
- * Returns true only if `{ pinned_at: -1 }` or `{ pinned_at: 1 }` option is first within the `sort` array.
1257
- */
1258
- export const shouldConsiderPinnedChannels = (sort: ChannelSort) => {
1259
- const value = findPinnedAtSortOrder({ sort });
1260
-
1261
- if (typeof value !== 'number') return false;
1262
-
1263
- return Math.abs(value) === 1;
1264
- };
1265
-
1266
- /**
1267
- * Checks whether the sort value of type object contains a pinned_at value or if
1268
- * an array sort value type has the first value be an object containing pinned_at.
1269
- * @param sort
1270
- */
1271
- export const findPinnedAtSortOrder = ({ sort }: { sort: ChannelSort }) =>
1272
- extractSortValue({
1273
- atIndex: 0,
1274
- sort,
1275
- targetKey: 'pinned_at',
1276
- });
1277
-
1278
- /**
1279
- * Finds the index of the last consecutively pinned channel, starting from the start of the
1280
- * array. Will not consider any pinned channels after the contiguous subsequence at the
1281
- * start of the array.
1282
- * @param channels
1283
- */
1284
- export const findLastPinnedChannelIndex = ({ channels }: { channels: Channel[] }) => {
1285
- let lastPinnedChannelIndex: number | null = null;
1286
-
1287
- for (const channel of channels) {
1288
- if (!isChannelPinned(channel)) break;
1289
-
1290
- if (typeof lastPinnedChannelIndex === 'number') {
1291
- lastPinnedChannelIndex++;
1292
- } else {
1293
- lastPinnedChannelIndex = 0;
1294
- }
1295
- }
1296
-
1297
- return lastPinnedChannelIndex;
1298
- };
1299
-
1300
- /**
1301
- * A utility used to move a channel towards the beginning of a list of channels (promote it to a higher position). It
1302
- * considers pinned channels in the process if needed and makes sure to only update the list reference if the list
1303
- * should actually change. It will try to move the channel as high as it can within the list.
1304
- * @param channels - the list of channels we want to modify
1305
- * @param channelToMove - the channel we want to promote
1306
- * @param channelToMoveIndexWithinChannels - optionally, the index of the channel we want to move if we know it (will skip a manual check)
1307
- * @param sort - the sort value used to check for pinned channels
1308
- */
1309
- export const promoteChannel = ({
1310
- channels,
1311
- channelToMove,
1312
- channelToMoveIndexWithinChannels,
1313
- sort,
1314
- }: PromoteChannelParams) => {
1315
- // get index of channel to move up
1316
- const targetChannelIndex =
1317
- channelToMoveIndexWithinChannels ??
1318
- channels.findIndex((channel) => channel.cid === channelToMove.cid);
1319
-
1320
- const targetChannelExistsWithinList = targetChannelIndex >= 0;
1321
- const targetChannelAlreadyAtTheTop = targetChannelIndex === 0;
1322
-
1323
- // pinned channels should not move within the list based on recent activity, channels which
1324
- // receive messages and are not pinned should move upwards but only under the last pinned channel
1325
- // in the list
1326
- const considerPinnedChannels = shouldConsiderPinnedChannels(sort);
1327
- const isTargetChannelPinned = isChannelPinned(channelToMove);
1328
-
1329
- if (targetChannelAlreadyAtTheTop || (considerPinnedChannels && isTargetChannelPinned)) {
1330
- return channels;
1331
- }
1332
-
1333
- const newChannels = [...channels];
1334
-
1335
- // target channel index is known, remove it from the list
1336
- if (targetChannelExistsWithinList) {
1337
- newChannels.splice(targetChannelIndex, 1);
1338
- }
1339
-
1340
- // as position of pinned channels has to stay unchanged, we need to
1341
- // find last pinned channel in the list to move the target channel after
1342
- let lastPinnedChannelIndex: number | null = null;
1343
- if (considerPinnedChannels) {
1344
- lastPinnedChannelIndex = findLastPinnedChannelIndex({ channels: newChannels });
1345
- }
1346
-
1347
- // re-insert it at the new place (to specific index if pinned channels are considered)
1348
- newChannels.splice(
1349
- typeof lastPinnedChannelIndex === 'number' ? lastPinnedChannelIndex + 1 : 0,
1350
- 0,
1351
- channelToMove,
1352
- );
1353
-
1354
- return newChannels;
1355
- };
1356
-
1357
802
  export const isDate = (value: unknown): value is Date => !!(value as Date).getTime;
1358
803
 
1359
804
  export const isLocalMessage = (message: unknown): message is LocalMessage =>
@@ -1367,9 +812,11 @@ export const runDetached = <T>(
1367
812
  onErrorCallback?: (error: Error) => void | Promise<void>;
1368
813
  },
1369
814
  ) => {
1370
- const { context, onSuccessCallback = () => undefined, onErrorCallback } = options ?? {};
815
+ const { context, onSuccessCallback, onErrorCallback } = options ?? {};
1371
816
  const defaultOnError = (error: Error) => {
1372
- console.log(`An error has occurred in context ${context}: ${error}`);
817
+ logger
818
+ .withExtraTags('runDetached')
819
+ .error(`An error occurred in context "${context}".`, { error });
1373
820
  };
1374
821
  const onError = onErrorCallback ?? defaultOnError;
1375
822
 
@@ -1383,11 +830,18 @@ export const runDetached = <T>(
1383
830
  };
1384
831
 
1385
832
  export const isBlockedMessage = (message: LocalMessage) =>
1386
- message.type === 'error' &&
1387
- (message.moderation_details?.action === 'MESSAGE_RESPONSE_ACTION_REMOVE' ||
1388
- message.moderation?.action === 'remove');
833
+ message.type === 'error' && message.moderation?.action === 'remove';
1389
834
 
1390
835
  export const isBouncedMessage = (message: LocalMessage) =>
1391
- message.type === 'error' &&
1392
- (message?.moderation_details?.action === 'MESSAGE_RESPONSE_ACTION_BOUNCE' ||
1393
- message?.moderation?.action === 'bounce');
836
+ message.type === 'error' && message?.moderation?.action === 'bounce';
837
+
838
+ export const getEnv = (envKey: keyof NodeJS.ProcessEnv) => {
839
+ if (
840
+ typeof process !== 'undefined' &&
841
+ (Object.hasOwn(process, 'env') || 'env' in process)
842
+ ) {
843
+ return process.env[envKey];
844
+ }
845
+
846
+ return undefined;
847
+ };