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
@@ -4,20 +4,11 @@ import type {
4
4
  Event,
5
5
  LocalMessage,
6
6
  MessageResponse,
7
- MessageResponseBase,
8
- MessageSet,
9
- MessageSetType,
10
7
  PendingMessageResponse,
11
- ReactionResponse,
12
8
  UserResponse,
13
9
  } from './types';
14
- import {
15
- deleteUserMessages as _deleteUserMessages,
16
- addToMessageList,
17
- formatMessage,
18
- isBlockedMessage,
19
- } from './utils';
20
- import { DEFAULT_MESSAGE_SET_PAGINATION } from './constants';
10
+ import { formatMessage } from './utils';
11
+ import { StateStore } from './store';
21
12
 
22
13
  type ChannelReadStatus = Record<
23
14
  string,
@@ -32,790 +23,222 @@ type ChannelReadStatus = Record<
32
23
  }
33
24
  >;
34
25
 
35
- const messageSetBounds = (
36
- a: LocalMessage[] | MessageResponse[],
37
- b: LocalMessage[] | MessageResponse[],
38
- ) => ({
39
- newestMessageA: new Date(a[0]?.created_at ?? 0),
40
- oldestMessageA: new Date(a.slice(-1)[0]?.created_at ?? 0),
41
- newestMessageB: new Date(b[0]?.created_at ?? 0),
42
- oldestMessageB: new Date(b.slice(-1)[0]?.created_at ?? 0),
43
- });
26
+ export type WatcherState = {
27
+ watcherCount: number;
28
+ watchers: Record<string, UserResponse>;
29
+ };
44
30
 
45
- const aContainsOrEqualsB = (a: LocalMessage[], b: LocalMessage[]) => {
46
- const { newestMessageA, newestMessageB, oldestMessageA, oldestMessageB } =
47
- messageSetBounds(a, b);
48
- return newestMessageA >= newestMessageB && oldestMessageB >= oldestMessageA;
31
+ export type TypingUsersState = {
32
+ typing: Record<string, Event>;
49
33
  };
50
34
 
51
- const aOverlapsB = (a: LocalMessage[], b: LocalMessage[]) => {
52
- const { newestMessageA, newestMessageB, oldestMessageA, oldestMessageB } =
53
- messageSetBounds(a, b);
54
- return (
55
- oldestMessageA < oldestMessageB &&
56
- oldestMessageB < newestMessageA &&
57
- newestMessageA < newestMessageB
58
- );
35
+ export type ReadState = {
36
+ read: ChannelReadStatus;
59
37
  };
60
38
 
61
- const messageSetsOverlapByTimestamp = (a: LocalMessage[], b: LocalMessage[]) =>
62
- aContainsOrEqualsB(a, b) ||
63
- aContainsOrEqualsB(b, a) ||
64
- aOverlapsB(a, b) ||
65
- aOverlapsB(b, a);
39
+ export type MutedUsersState = {
40
+ mutedUsers: Array<UserResponse>;
41
+ };
42
+
43
+ export type MembersState = {
44
+ members: Record<string, ChannelMemberResponse>;
45
+ memberCount: number;
46
+ };
47
+
48
+ export type OwnCapabilitiesState = {
49
+ ownCapabilities: string[];
50
+ };
66
51
 
67
52
  /**
68
53
  * ChannelState - A container class for the channel state.
69
54
  */
70
55
  export class ChannelState {
71
56
  _channel: Channel;
72
- watcher_count: number;
73
- typing: Record<string, Event>;
74
- read: ChannelReadStatus;
75
- pinnedMessages: Array<ReturnType<ChannelState['formatMessage']>>;
57
+ readonly watcherStore: StateStore<WatcherState>;
58
+ readonly typingStore: StateStore<TypingUsersState>;
59
+ readonly readStore: StateStore<ReadState>;
60
+ readonly membersStore: StateStore<MembersState>;
61
+ readonly ownCapabilitiesStore: StateStore<OwnCapabilitiesState>;
62
+ // todo: is this actually used somewhere?
63
+ readonly mutedUsersStore: StateStore<MutedUsersState>;
76
64
  pending_messages: Array<PendingMessageResponse>;
77
- threads: Record<string, Array<ReturnType<ChannelState['formatMessage']>>>;
78
- mutedUsers: Array<UserResponse>;
79
- watchers: Record<string, UserResponse>;
80
- members: Record<string, ChannelMemberResponse>;
81
65
  unreadCount: number;
82
66
  membership: ChannelMemberResponse;
83
- last_message_at: Date | null;
84
- /**
85
- * Flag which indicates if channel state contain latest/recent messages or no.
86
- * This flag should be managed by UI sdks using a setter - setIsUpToDate.
87
- * When false, any new message (received by websocket event - message.new) will not
88
- * be pushed on to message list.
89
- */
90
- isUpToDate: boolean;
91
- /**
92
- * Disjoint lists of messages
93
- * Users can jump in the message list (with searching) and this can result in disjoint lists of messages
94
- * The state manages these lists and merges them when lists overlap
95
- * The messages array contains the currently active set
96
- */
97
- messageSets: MessageSet[] = [];
98
67
 
99
68
  constructor(channel: Channel) {
100
69
  this._channel = channel;
101
- this.watcher_count = 0;
102
- this.typing = {};
103
- this.read = {};
104
- this.initMessages();
105
- this.pinnedMessages = [];
106
- this.pending_messages = [];
107
- this.threads = {};
70
+ this.watcherStore = new StateStore<WatcherState>({
71
+ watcherCount: 0,
72
+ watchers: {},
73
+ });
74
+ this.typingStore = new StateStore<TypingUsersState>({
75
+ typing: {},
76
+ });
77
+ this.readStore = new StateStore<ReadState>({ read: {} });
108
78
  // a list of users to hide messages from
109
- this.mutedUsers = [];
110
- this.watchers = {};
111
- this.members = {};
112
- this.membership = {};
79
+ this.mutedUsersStore = new StateStore<MutedUsersState>({ mutedUsers: [] });
80
+ this.membersStore = new StateStore<MembersState>({ members: {}, memberCount: 0 });
81
+ this.ownCapabilitiesStore = new StateStore<OwnCapabilitiesState>({
82
+ ownCapabilities: [],
83
+ });
84
+ this.syncMemberCountFromChannelData(channel?.data);
85
+ this.syncOwnCapabilitiesFromChannelData(channel?.data);
86
+ this.pending_messages = [];
87
+ this.membership = {} as ChannelMemberResponse;
113
88
  this.unreadCount = 0;
114
- /**
115
- * Flag which indicates if channel state contain latest/recent messages or no.
116
- * This flag should be managed by UI sdks using a setter - setIsUpToDate.
117
- * When false, any new message (received by websocket event - message.new) will not
118
- * be pushed on to message list.
119
- */
120
- this.isUpToDate = true;
121
- this.last_message_at =
122
- channel?.state?.last_message_at != null
123
- ? new Date(channel.state.last_message_at)
124
- : null;
125
89
  }
126
90
 
127
- get messages() {
128
- return this.messageSets.find((s) => s.isCurrent)?.messages || [];
91
+ get members() {
92
+ return this.membersStore.getLatestValue().members;
129
93
  }
130
94
 
131
- set messages(messages: Array<ReturnType<ChannelState['formatMessage']>>) {
132
- const index = this.messageSets.findIndex((s) => s.isCurrent);
133
- this.messageSets[index].messages = messages;
95
+ set members(members: Record<string, ChannelMemberResponse>) {
96
+ this.membersStore.partialNext({ members });
134
97
  }
135
98
 
136
- /**
137
- * The list of latest messages
138
- * The messages array not always contains the latest messages (for example if a user searched for an earlier message, that is in a different message set)
139
- */
140
- get latestMessages() {
141
- return this.messageSets.find((s) => s.isLatest)?.messages || [];
99
+ get member_count() {
100
+ return this.membersStore.getLatestValue().memberCount;
142
101
  }
143
102
 
144
- set latestMessages(messages: Array<ReturnType<ChannelState['formatMessage']>>) {
145
- const index = this.messageSets.findIndex((s) => s.isLatest);
146
- this.messageSets[index].messages = messages;
103
+ set member_count(memberCount: number) {
104
+ this.membersStore.partialNext({ memberCount });
147
105
  }
148
106
 
149
- get messagePagination() {
150
- return (
151
- this.messageSets.find((s) => s.isCurrent)?.pagination ||
152
- DEFAULT_MESSAGE_SET_PAGINATION
153
- );
107
+ get read() {
108
+ return this.readStore.getLatestValue().read;
154
109
  }
155
110
 
156
- pruneOldest(maxMessages: number) {
157
- const currentIndex = this.messageSets.findIndex((s) => s.isCurrent);
158
- if (this.messageSets[currentIndex].isLatest) {
159
- const newMessages = this.messageSets[currentIndex].messages;
160
- this.messageSets[currentIndex].messages = newMessages.slice(-maxMessages);
161
- this.messageSets[currentIndex].pagination.hasPrev = true;
162
- }
111
+ set read(read: ChannelReadStatus) {
112
+ this.readStore.next({ read });
163
113
  }
164
114
 
165
- /**
166
- * addMessageSorted - Add a message to the state
167
- *
168
- * @param {MessageResponse} newMessage A new message
169
- * @param {boolean} timestampChanged Whether updating a message with changed created_at value.
170
- * @param {boolean} addIfDoesNotExist Add message if it is not in the list, used to prevent out of order updated messages from being added.
171
- * @param {MessageSetType} messageSetToAddToIfDoesNotExist Which message set to add to if message is not in the list (only used if addIfDoesNotExist is true)
172
- */
173
- addMessageSorted(
174
- newMessage: MessageResponse | LocalMessage,
175
- timestampChanged = false,
176
- addIfDoesNotExist = true,
177
- messageSetToAddToIfDoesNotExist: MessageSetType = 'latest',
178
- ) {
179
- return this.addMessagesSorted(
180
- [newMessage],
181
- timestampChanged,
182
- false,
183
- addIfDoesNotExist,
184
- messageSetToAddToIfDoesNotExist,
115
+ get typing() {
116
+ return (
117
+ this._channel?.messageComposer?.textComposer.typing ??
118
+ this.typingStore.getLatestValue().typing
185
119
  );
186
120
  }
187
121
 
188
- /**
189
- * Takes the message object, parses the dates, sets `__html`
190
- * and sets the status to `received` if missing; returns a new message object.
191
- *
192
- * @param {MessageResponse} message `MessageResponse` object
193
- */
194
- formatMessage = (message: MessageResponse | MessageResponseBase | LocalMessage) =>
195
- formatMessage(message);
196
-
197
- /**
198
- * addMessagesSorted - Add the list of messages to state and resorts the messages
199
- *
200
- * @param {Array<MessageResponse>} newMessages A list of messages
201
- * @param {boolean} timestampChanged Whether updating messages with changed created_at value.
202
- * @param {boolean} initializing Whether channel is being initialized.
203
- * @param {boolean} addIfDoesNotExist Add message if it is not in the list, used to prevent out of order updated messages from being added.
204
- * @param {MessageSetType} messageSetToAddToIfDoesNotExist Which message set to add to if messages are not in the list (only used if addIfDoesNotExist is true)
205
- *
206
- */
207
- addMessagesSorted(
208
- newMessages: (MessageResponse | LocalMessage)[],
209
- timestampChanged = false,
210
- initializing = false,
211
- addIfDoesNotExist = true,
212
- messageSetToAddToIfDoesNotExist: MessageSetType = 'current',
213
- ) {
214
- const { messagesToAdd, targetMessageSetIndex } = this.findTargetMessageSet(
215
- newMessages,
216
- addIfDoesNotExist,
217
- messageSetToAddToIfDoesNotExist,
218
- );
219
-
220
- const filteredMessageIds: string[] = [];
221
-
222
- for (let i = 0; i < messagesToAdd.length; i += 1) {
223
- const isFromShadowBannedUser = messagesToAdd[i].shadowed;
224
- if (isFromShadowBannedUser && addIfDoesNotExist) {
225
- filteredMessageIds.push(messagesToAdd[i].id);
226
- continue;
227
- }
228
- // If message is already formatted we can skip the tasks below
229
- // This will be true for messages that are already present at the state -> this happens when we perform merging of message sets
230
- // This will be also true for message previews used by some SDKs
231
- const isMessageFormatted = messagesToAdd[i].created_at instanceof Date;
232
- let message: ReturnType<ChannelState['formatMessage']>;
233
- if (isMessageFormatted) {
234
- message = messagesToAdd[i] as ReturnType<ChannelState['formatMessage']>;
235
- } else {
236
- message = this.formatMessage(messagesToAdd[i]);
237
-
238
- if (message.user && this._channel?.cid) {
239
- /**
240
- * Store the reference to user for this channel, so that when we have to
241
- * handle updates to user, we can use the reference map, to determine which
242
- * channels need to be updated with updated user object.
243
- */
244
- this._channel
245
- .getClient()
246
- .state.updateUserReference(message.user, this._channel.cid);
247
- }
248
-
249
- if (
250
- initializing &&
251
- message.id &&
252
- this.threads[message.id] &&
253
- !this._channel.getClient().preventThreadCleanup
254
- ) {
255
- // If we are initializing the state of channel (e.g., in case of connection recovery),
256
- // then in that case we remove thread related to this message from threads object.
257
- // This way we can ensure that we don't have any stale data in thread object
258
- // and consumer can refetch the replies.
259
- delete this.threads[message.id];
260
- }
261
-
262
- const shouldSkipLastMessageAtUpdate =
263
- this._channel.getConfig()?.skip_last_msg_update_for_system_msgs &&
264
- message.type === 'system';
265
-
266
- if (
267
- !shouldSkipLastMessageAtUpdate &&
268
- (!this.last_message_at ||
269
- message.created_at.getTime() > this.last_message_at.getTime())
270
- ) {
271
- this.last_message_at = new Date(message.created_at.getTime());
272
- }
273
- }
274
-
275
- // update or append the messages...
276
- const parentID = message.parent_id;
277
-
278
- // add to the given message set
279
- if ((!parentID || message.show_in_channel) && targetMessageSetIndex !== -1) {
280
- this.messageSets[targetMessageSetIndex].messages = this._addToMessageList(
281
- this.messageSets[targetMessageSetIndex].messages,
282
- message,
283
- timestampChanged,
284
- 'created_at',
285
- addIfDoesNotExist,
286
- );
287
- }
288
-
289
- /**
290
- * Add message to thread if applicable and the message
291
- * was added when querying for replies, or the thread already exits.
292
- * This is to prevent the thread state from getting out of sync if
293
- * a thread message is shown in channel but older than the newest thread
294
- * message. This situation can result in a thread state where a random
295
- * message is "oldest" message, and newer messages are therefore not loaded.
296
- * This can also occur if an old thread message is updated.
297
- */
298
- if (parentID && !initializing) {
299
- const thread = this.threads[parentID] || [];
300
- this.threads[parentID] = this._addToMessageList(
301
- thread,
302
- message,
303
- timestampChanged,
304
- 'created_at',
305
- addIfDoesNotExist,
306
- );
307
- }
308
- }
309
-
310
- return {
311
- messageSet: this.messageSets[targetMessageSetIndex],
312
- filteredMessageIds,
313
- };
314
- }
122
+ set typing(typing: Record<string, Event>) {
123
+ this.typingStore.next({ typing });
315
124
 
316
- /**
317
- * addPinnedMessages - adds messages in pinnedMessages property
318
- *
319
- * @param {Array<MessageResponse>} pinnedMessages A list of pinned messages
320
- *
321
- */
322
- addPinnedMessages(pinnedMessages: MessageResponse[]) {
323
- for (let i = 0; i < pinnedMessages.length; i += 1) {
324
- this.addPinnedMessage(pinnedMessages[i]);
125
+ if (this._channel?.messageComposer) {
126
+ this._channel.messageComposer.textComposer.setTyping(typing);
325
127
  }
326
128
  }
327
129
 
328
- /**
329
- * addPinnedMessage - adds message in pinnedMessages
330
- *
331
- * @param {MessageResponse} pinnedMessage message to update
332
- *
333
- */
334
- addPinnedMessage(pinnedMessage: MessageResponse) {
335
- this.pinnedMessages = this._addToMessageList(
336
- this.pinnedMessages,
337
- this.formatMessage(pinnedMessage),
338
- false,
339
- 'pinned_at',
340
- );
341
- }
342
-
343
- /**
344
- * removePinnedMessage - removes pinned message from pinnedMessages
345
- *
346
- * @param {MessageResponse} message message to remove
347
- *
348
- */
349
- removePinnedMessage(message: MessageResponse) {
350
- const { result } = this.removeMessageFromArray(this.pinnedMessages, message);
351
- this.pinnedMessages = result;
352
- }
353
-
354
- addReaction(
355
- reaction: ReactionResponse,
356
- message?: MessageResponse,
357
- enforce_unique?: boolean,
130
+ syncMemberCountFromChannelData(
131
+ data: Channel['data'],
132
+ fallbackData: Channel['data'] = this._channel?.data,
358
133
  ) {
359
- const messageWithReaction = message;
360
- let messageFromState: LocalMessage | undefined;
361
- if (!messageWithReaction) {
362
- messageFromState = this.findMessage(reaction.message_id);
363
- }
134
+ const fallbackMemberCount =
135
+ typeof fallbackData?.member_count === 'number'
136
+ ? fallbackData.member_count
137
+ : this.membersStore.getLatestValue().memberCount;
364
138
 
365
- if (!messageWithReaction && !messageFromState) {
139
+ if (!data || typeof data !== 'object') {
140
+ this.membersStore.partialNext({ memberCount: fallbackMemberCount ?? 0 });
366
141
  return;
367
142
  }
368
143
 
369
- const messageToUpdate = messageWithReaction ?? messageFromState;
370
- const updateData = {
371
- id: messageToUpdate?.id,
372
- parent_id: messageToUpdate?.parent_id,
373
- pinned: messageToUpdate?.pinned,
374
- show_in_channel: messageToUpdate?.show_in_channel,
375
- };
376
-
377
- this._updateMessage(updateData, (msg) => {
378
- if (messageWithReaction) {
379
- const updatedMessage = { ...messageWithReaction };
380
- // This part will remove own_reactions from what is essentially
381
- // a copy of event.message; we do not want to return that as someone
382
- // else reaction would remove our own_reactions needlessly. This
383
- // only happens when we are not the sender of the reaction. We need
384
- // the variable itself so that the event can be properly enriched
385
- // later on.
386
- messageWithReaction.own_reactions = this._addOwnReactionToMessage(
387
- msg.own_reactions,
388
- reaction,
389
- enforce_unique,
390
- );
391
- // Whenever we are the ones sending the reaction, the helper enriches
392
- // own_reactions as normal so we can use that, otherwise we fallback
393
- // to whatever state we had.
394
- updatedMessage.own_reactions =
395
- this._channel.getClient().userID === reaction.user_id
396
- ? messageWithReaction.own_reactions
397
- : msg.own_reactions;
398
- return this.formatMessage(updatedMessage);
399
- }
144
+ const dataDescriptor = Object.getOwnPropertyDescriptor(data, 'member_count');
145
+ let memberCount =
146
+ typeof data.member_count === 'number'
147
+ ? data.member_count
148
+ : typeof fallbackMemberCount === 'number'
149
+ ? fallbackMemberCount
150
+ : undefined;
400
151
 
401
- if (messageFromState) {
402
- return this._addReactionToState(messageFromState, reaction, enforce_unique);
403
- }
152
+ this.membersStore.partialNext({ memberCount: memberCount ?? 0 });
404
153
 
405
- return msg;
154
+ Object.defineProperty(data, 'member_count', {
155
+ configurable: true,
156
+ enumerable: dataDescriptor?.enumerable ?? false,
157
+ get: () => memberCount,
158
+ set: (nextMemberCount: number | undefined) => {
159
+ memberCount = typeof nextMemberCount === 'number' ? nextMemberCount : undefined;
160
+ this.membersStore.partialNext({ memberCount: memberCount ?? 0 });
161
+ },
406
162
  });
407
- return messageWithReaction ?? messageFromState;
408
- }
409
-
410
- _addReactionToState(
411
- messageFromState: LocalMessage,
412
- reaction: ReactionResponse,
413
- enforce_unique?: boolean,
414
- ) {
415
- if (!messageFromState.reaction_groups) {
416
- messageFromState.reaction_groups = {};
417
- }
418
-
419
- // 1. Firstly, get rid of all of our own reactions from the reaction_groups
420
- // if enforce_unique is enabled.
421
- if (enforce_unique) {
422
- for (const ownReaction of messageFromState.own_reactions ?? []) {
423
- const oldOwnReactionTypeData = messageFromState.reaction_groups[ownReaction.type];
424
- messageFromState.reaction_groups[ownReaction.type] = {
425
- ...oldOwnReactionTypeData,
426
- count: oldOwnReactionTypeData.count - 1,
427
- sum_scores: oldOwnReactionTypeData.sum_scores - (ownReaction.score ?? 1),
428
- };
429
- // If there are no reactions left in this group, simply remove it.
430
- if (messageFromState.reaction_groups[ownReaction.type].count < 1) {
431
- delete messageFromState.reaction_groups[ownReaction.type];
432
- }
433
- }
434
- }
435
-
436
- const newReactionGroups = messageFromState.reaction_groups;
437
- const oldReactionTypeData = newReactionGroups[reaction.type];
438
- const score = reaction.score ?? 1;
439
-
440
- // 2. Next, update the reaction_groups with the new reaction.
441
- messageFromState.reaction_groups[reaction.type] = oldReactionTypeData
442
- ? {
443
- ...oldReactionTypeData,
444
- count: oldReactionTypeData.count + 1,
445
- sum_scores: oldReactionTypeData.sum_scores + score,
446
- last_reaction_at: reaction.created_at,
447
- }
448
- : {
449
- count: 1,
450
- first_reaction_at: reaction.created_at,
451
- last_reaction_at: reaction.created_at,
452
- sum_scores: score,
453
- };
454
-
455
- // 3. Update the own_reactions with the new reaction.
456
- messageFromState.own_reactions = this._addOwnReactionToMessage(
457
- messageFromState.own_reactions,
458
- reaction,
459
- enforce_unique,
460
- );
461
-
462
- // 4. Finally, update the latest_reactions with the new reaction,
463
- // while respecting enforce_unique.
464
- const userId = this._channel.getClient().userID;
465
- messageFromState.latest_reactions = enforce_unique
466
- ? [
467
- ...(messageFromState.latest_reactions || []).filter(
468
- (r) => r.user_id !== userId,
469
- ),
470
- reaction,
471
- ]
472
- : [...(messageFromState.latest_reactions || []), reaction];
473
-
474
- return messageFromState;
475
163
  }
476
164
 
477
- _addOwnReactionToMessage(
478
- ownReactions: ReactionResponse[] | null | undefined,
479
- reaction: ReactionResponse,
480
- enforce_unique?: boolean,
165
+ syncOwnCapabilitiesFromChannelData(
166
+ data: Channel['data'],
167
+ fallbackData: Channel['data'] = this._channel?.data,
481
168
  ) {
482
- if (enforce_unique) {
483
- ownReactions = [];
484
- } else {
485
- ownReactions = this._removeOwnReactionFromMessage(ownReactions, reaction);
486
- }
487
-
488
- ownReactions = ownReactions || [];
489
- if (this._channel.getClient().userID === reaction.user_id) {
490
- ownReactions.push(reaction);
491
- }
492
-
493
- return ownReactions;
494
- }
495
-
496
- _removeOwnReactionFromMessage(
497
- ownReactions: ReactionResponse[] | null | undefined,
498
- reaction: ReactionResponse,
499
- ) {
500
- if (ownReactions) {
501
- return ownReactions.filter(
502
- (item) => item.user_id !== reaction.user_id || item.type !== reaction.type,
503
- );
504
- }
505
- return ownReactions;
506
- }
507
-
508
- removeReaction(reaction: ReactionResponse, message?: MessageResponse) {
509
- const messageWithRemovedReaction = message;
510
- let messageFromState: LocalMessage | undefined;
511
- if (!messageWithRemovedReaction) {
512
- messageFromState = this.findMessage(reaction.message_id);
513
- }
514
-
515
- if (!messageWithRemovedReaction && !messageFromState) {
169
+ if (!data || typeof data !== 'object') {
170
+ this.ownCapabilitiesStore.next({ ownCapabilities: [] });
516
171
  return;
517
172
  }
518
173
 
519
- const messageToUpdate = messageWithRemovedReaction ?? messageFromState;
520
- const updateData = {
521
- id: messageToUpdate?.id,
522
- parent_id: messageToUpdate?.parent_id,
523
- pinned: messageToUpdate?.pinned,
524
- show_in_channel: messageToUpdate?.show_in_channel,
525
- };
526
- this._updateMessage(updateData, (msg) => {
527
- if (messageWithRemovedReaction) {
528
- messageWithRemovedReaction.own_reactions = this._removeOwnReactionFromMessage(
529
- msg.own_reactions,
530
- reaction,
531
- );
532
- return this.formatMessage(messageWithRemovedReaction);
533
- }
534
-
535
- if (messageFromState) {
536
- return this._removeReactionFromState(messageFromState, reaction);
537
- }
538
-
539
- return msg;
174
+ let ownCapabilities: string[] | undefined = Array.isArray(data.own_capabilities)
175
+ ? [...data.own_capabilities]
176
+ : Array.isArray(fallbackData?.own_capabilities)
177
+ ? [...fallbackData.own_capabilities]
178
+ : undefined;
179
+
180
+ this.ownCapabilitiesStore.next({ ownCapabilities: ownCapabilities ?? [] });
181
+
182
+ // Keep the reactive getter/setter so backward-compatible assignments still sync to
183
+ // the store, but return `undefined` until capabilities are actually known. Forcing
184
+ // `[]` on an unloaded channel would make read-events–gated logic (e.g. unread
185
+ // counting, regression #1732) treat "not yet loaded" as "explicitly no capabilities".
186
+ Object.defineProperty(data, 'own_capabilities', {
187
+ configurable: true,
188
+ enumerable: true,
189
+ get: () => ownCapabilities,
190
+ set: (nextOwnCapabilities: string[] | undefined) => {
191
+ ownCapabilities = Array.isArray(nextOwnCapabilities)
192
+ ? [...nextOwnCapabilities]
193
+ : undefined;
194
+ this.ownCapabilitiesStore.next({ ownCapabilities: ownCapabilities ?? [] });
195
+ },
540
196
  });
541
- return messageWithRemovedReaction;
542
197
  }
543
198
 
544
- _removeReactionFromState(messageFromState: LocalMessage, reaction: ReactionResponse) {
545
- const reactionToRemove = messageFromState.own_reactions?.find(
546
- (r) => r.type === reaction.type,
547
- );
548
- if (reactionToRemove && messageFromState.reaction_groups?.[reactionToRemove.type]) {
549
- const newReactionGroup = messageFromState.reaction_groups[reactionToRemove.type];
550
- messageFromState.reaction_groups[reactionToRemove.type] = {
551
- ...newReactionGroup,
552
- count: newReactionGroup.count - 1,
553
- sum_scores: newReactionGroup.sum_scores - (reactionToRemove.score ?? 1),
554
- };
555
- // If there are no reactions left in this group, simply remove it.
556
- if (messageFromState.reaction_groups[reactionToRemove.type].count < 1) {
557
- delete messageFromState.reaction_groups[reactionToRemove.type];
558
- }
559
- }
560
- messageFromState.own_reactions = messageFromState.own_reactions?.filter(
561
- (r) => r.type !== reaction.type,
562
- );
563
- const userId = this._channel.getClient().userID;
564
- messageFromState.latest_reactions = messageFromState.latest_reactions?.filter(
565
- (r) => !(r.user_id === userId && r.type === reaction.type),
566
- );
567
- return messageFromState;
199
+ setTypingEvent(userID: string, event: Event) {
200
+ this.typing = { ...this.typing, [userID]: event };
568
201
  }
569
202
 
570
- _updateQuotedMessageReferences({
571
- message,
572
- remove,
573
- }: {
574
- message: MessageResponse;
575
- remove?: boolean;
576
- }) {
577
- const parseMessage = (m: ReturnType<ChannelState['formatMessage']>) =>
578
- ({
579
- ...m,
580
- created_at: m.created_at.toISOString(),
581
- pinned_at: m.pinned_at?.toISOString(),
582
- updated_at: m.updated_at?.toISOString(),
583
- }) as unknown as MessageResponse;
203
+ removeTypingEvent(userID: string) {
204
+ if (!this.typing[userID]) return;
584
205
 
585
- const update = (messages: LocalMessage[]) => {
586
- const updatedMessages = messages.reduce<MessageResponse[]>((acc, msg) => {
587
- if (msg.quoted_message_id === message.id) {
588
- acc.push({
589
- ...parseMessage(msg),
590
- quoted_message: remove ? { ...message, attachments: [] } : message,
591
- });
592
- }
593
- return acc;
594
- }, []);
595
- this.addMessagesSorted(updatedMessages, true);
596
- };
597
-
598
- if (!message.parent_id) {
599
- this.messageSets.forEach((set) => update(set.messages));
600
- } else if (message.parent_id && this.threads[message.parent_id]) {
601
- // prevent going through all the threads even though it is possible to quote a message from another thread
602
- update(this.threads[message.parent_id]);
603
- }
206
+ const typing = { ...this.typing };
207
+ delete typing[userID];
208
+ this.typing = typing;
604
209
  }
605
210
 
606
- removeQuotedMessageReferences(message: MessageResponse) {
607
- this._updateQuotedMessageReferences({ message, remove: true });
211
+ get mutedUsers() {
212
+ return this.mutedUsersStore.getLatestValue().mutedUsers;
608
213
  }
609
214
 
610
- /**
611
- * Updates all instances of given message in channel state
612
- * @param message
613
- * @param updateFunc
614
- */
615
- _updateMessage(
616
- message: {
617
- id?: string;
618
- parent_id?: string;
619
- pinned?: boolean;
620
- show_in_channel?: boolean;
621
- },
622
- updateFunc: (
623
- msg: ReturnType<ChannelState['formatMessage']>,
624
- ) => ReturnType<ChannelState['formatMessage']>,
625
- ) {
626
- const { parent_id, show_in_channel, pinned } = message;
627
-
628
- if (parent_id && this.threads[parent_id]) {
629
- const thread = this.threads[parent_id];
630
- const msgIndex = thread.findIndex((msg) => msg.id === message.id);
631
- if (msgIndex !== -1) {
632
- thread[msgIndex] = updateFunc(thread[msgIndex]);
633
- this.threads[parent_id] = thread;
634
- }
635
- }
636
-
637
- if ((!show_in_channel && !parent_id) || show_in_channel) {
638
- const messageSetIndex = this.findMessageSetIndex(message);
639
- if (messageSetIndex !== -1) {
640
- const msgIndex = this.messageSets[messageSetIndex].messages.findIndex(
641
- (msg) => msg.id === message.id,
642
- );
643
- if (msgIndex !== -1) {
644
- const upMsg = updateFunc(this.messageSets[messageSetIndex].messages[msgIndex]);
645
- this.messageSets[messageSetIndex].messages[msgIndex] = upMsg;
646
- }
647
- }
648
- }
649
-
650
- if (pinned) {
651
- const msgIndex = this.pinnedMessages.findIndex((msg) => msg.id === message.id);
652
- if (msgIndex !== -1) {
653
- this.pinnedMessages[msgIndex] = updateFunc(this.pinnedMessages[msgIndex]);
654
- }
655
- }
215
+ set mutedUsers(mutedUsers: Array<UserResponse>) {
216
+ this.mutedUsersStore.next({ mutedUsers });
656
217
  }
657
218
 
658
- /**
659
- * Setter for isUpToDate.
660
- *
661
- * @param isUpToDate Flag which indicates if channel state contain latest/recent messages or no.
662
- * This flag should be managed by UI sdks using a setter - setIsUpToDate.
663
- * When false, any new message (received by websocket event - message.new) will not
664
- * be pushed on to message list.
665
- */
666
- setIsUpToDate = (isUpToDate: boolean) => {
667
- this.isUpToDate = isUpToDate;
668
- };
669
-
670
- /**
671
- * _addToMessageList - Adds a message to a list of messages, tries to update first, appends if message isn't found
672
- *
673
- * @param {Array<ReturnType<ChannelState['formatMessage']>>} messages A list of messages
674
- * @param message
675
- * @param {boolean} timestampChanged Whether updating a message with changed created_at value.
676
- * @param {string} sortBy field name to use to sort the messages by
677
- * @param {boolean} addIfDoesNotExist Add message if it is not in the list, used to prevent out of order updated messages from being added.
678
- */
679
- _addToMessageList(
680
- messages: Array<ReturnType<ChannelState['formatMessage']>>,
681
- message: ReturnType<ChannelState['formatMessage']>,
682
- timestampChanged = false,
683
- sortBy: 'pinned_at' | 'created_at' = 'created_at',
684
- addIfDoesNotExist = true,
685
- ) {
686
- return addToMessageList(
687
- messages,
688
- message,
689
- timestampChanged,
690
- sortBy,
691
- addIfDoesNotExist,
692
- );
219
+ get watchers() {
220
+ return this.watcherStore.getLatestValue().watchers;
693
221
  }
694
222
 
695
- /**
696
- * removeMessage - Description
697
- *
698
- * @param {{ id: string; parent_id?: string }} messageToRemove Object of the message to remove. Needs to have at id specified.
699
- *
700
- * @return {boolean} Returns if the message was removed
701
- */
702
- removeMessage(messageToRemove: {
703
- id: string;
704
- messageSetIndex?: number;
705
- parent_id?: string;
706
- }) {
707
- let isRemoved = false;
708
- if (messageToRemove.parent_id && this.threads[messageToRemove.parent_id]) {
709
- const { removed, result: threadMessages } = this.removeMessageFromArray(
710
- this.threads[messageToRemove.parent_id],
711
- messageToRemove,
712
- );
713
-
714
- this.threads[messageToRemove.parent_id] = threadMessages;
715
- isRemoved = removed;
716
- } else {
717
- const messageSetIndex =
718
- messageToRemove.messageSetIndex ?? this.findMessageSetIndex(messageToRemove);
719
- if (messageSetIndex !== -1) {
720
- const { removed, result: messages } = this.removeMessageFromArray(
721
- this.messageSets[messageSetIndex].messages,
722
- messageToRemove,
723
- );
724
- this.messageSets[messageSetIndex].messages = messages;
725
- isRemoved = removed;
726
- }
727
- }
728
-
729
- return isRemoved;
223
+ set watchers(watchers: Record<string, UserResponse>) {
224
+ this.watcherStore.partialNext({ watchers });
730
225
  }
731
226
 
732
- removeMessageFromArray = (
733
- msgArray: Array<ReturnType<ChannelState['formatMessage']>>,
734
- msg: { id: string; parent_id?: string },
735
- ) => {
736
- const result = msgArray.filter(
737
- (message) => !(!!message.id && !!msg.id && message.id === msg.id),
738
- );
739
-
740
- return { removed: result.length < msgArray.length, result };
741
- };
742
-
743
- /**
744
- * Updates the message.user property with updated user object, for messages.
745
- *
746
- * @param {UserResponse} user
747
- */
748
- updateUserMessages = (user: UserResponse) => {
749
- const _updateUserMessages = (
750
- messages: Array<ReturnType<ChannelState['formatMessage']>>,
751
- user: UserResponse,
752
- ) => {
753
- for (let i = 0; i < messages.length; i++) {
754
- const m = messages[i];
755
- if (m.user?.id === user.id) {
756
- messages[i] = { ...m, user };
757
- }
758
- }
759
- };
760
-
761
- this.messageSets.forEach((set) => _updateUserMessages(set.messages, user));
762
-
763
- for (const parentId in this.threads) {
764
- _updateUserMessages(this.threads[parentId], user);
765
- }
766
-
767
- _updateUserMessages(this.pinnedMessages, user);
768
- };
769
-
770
- /**
771
- * Marks the messages as deleted, from deleted user.
772
- *
773
- * @param {UserResponse} user
774
- * @param {boolean} hardDelete
775
- */
776
- deleteUserMessages = (
777
- user: UserResponse,
778
- hardDelete = false,
779
- deletedAt?: LocalMessage['deleted_at'],
780
- ) => {
781
- this.messageSets.forEach(({ messages }) =>
782
- _deleteUserMessages({ messages, user, hardDelete, deletedAt: deletedAt ?? null }),
783
- );
784
-
785
- for (const parentId in this.threads) {
786
- _deleteUserMessages({
787
- messages: this.threads[parentId],
788
- user,
789
- hardDelete,
790
- deletedAt: deletedAt ?? null,
791
- });
792
- }
227
+ get watcher_count() {
228
+ return this.watcherStore.getLatestValue().watcherCount;
229
+ }
793
230
 
794
- _deleteUserMessages({
795
- messages: this.pinnedMessages,
796
- user,
797
- hardDelete,
798
- deletedAt: deletedAt ?? null,
799
- });
800
- };
231
+ set watcher_count(watcherCount: number) {
232
+ this.watcherStore.partialNext({ watcherCount });
233
+ }
801
234
 
802
235
  /**
803
- * filterErrorMessages - Removes error messages from the channel state.
236
+ * Takes the message object, adds SDK-specific fields (status, error),
237
+ * and returns a new message object.
804
238
  *
239
+ * @param message - `MessageResponse` object
805
240
  */
806
- filterErrorMessages() {
807
- const filteredMessages = this.latestMessages.filter(
808
- (message) => message.type !== 'error',
809
- );
810
-
811
- const blockedMessages = this.latestMessages.filter(isBlockedMessage);
812
- // We need to hard delete the blocked messages from the offline database.
813
- for (const message of blockedMessages) {
814
- this._channel.getClient().offlineDb?.hardDeleteMessage({ id: message.id });
815
- }
816
-
817
- this.latestMessages = filteredMessages;
818
- }
241
+ formatMessage = (message: MessageResponse | LocalMessage) => formatMessage(message);
819
242
 
820
243
  /**
821
244
  * clean - Remove stale data such as users that stayed in typing state for more than 5 seconds
@@ -829,7 +252,7 @@ export class ChannelState {
829
252
  ? new Date(lastEvent.received_at)
830
253
  : lastEvent.received_at || new Date();
831
254
  if (now.getTime() - receivedAt.getTime() > 7000) {
832
- delete this.typing[userID];
255
+ this.removeTypingEvent(userID);
833
256
  this._channel.getClient().dispatchEvent({
834
257
  cid: this._channel.cid,
835
258
  type: 'typing.stop',
@@ -838,296 +261,4 @@ export class ChannelState {
838
261
  }
839
262
  }
840
263
  }
841
-
842
- clearMessages() {
843
- this.initMessages();
844
- this.pinnedMessages = [];
845
- }
846
-
847
- initMessages() {
848
- this.messageSets = [
849
- {
850
- messages: [],
851
- isLatest: true,
852
- isCurrent: true,
853
- pagination: { ...DEFAULT_MESSAGE_SET_PAGINATION },
854
- },
855
- ];
856
- }
857
-
858
- /**
859
- * loadMessageIntoState - Loads a given message (and messages around it) into the state
860
- *
861
- * @param {string} messageId The id of the message, or 'latest' to indicate switching to the latest messages
862
- * @param {string} parentMessageId The id of the parent message, if we want load a thread reply
863
- * @param {number} limit The page size if the message has to be queried from the server
864
- */
865
- async loadMessageIntoState(
866
- messageId: string | 'latest',
867
- parentMessageId?: string,
868
- limit = 25,
869
- ) {
870
- let messageSetIndex: number;
871
- let switchedToMessageSet = false;
872
- let loadedMessageThread = false;
873
- const messageIdToFind = parentMessageId || messageId;
874
- if (messageId === 'latest') {
875
- if (this.messages === this.latestMessages) {
876
- return;
877
- }
878
- messageSetIndex = this.messageSets.findIndex((s) => s.isLatest);
879
- } else {
880
- messageSetIndex = this.findMessageSetIndex({ id: messageIdToFind });
881
- }
882
- if (messageSetIndex !== -1) {
883
- this.switchToMessageSet(messageSetIndex);
884
- switchedToMessageSet = true;
885
- }
886
- loadedMessageThread =
887
- !parentMessageId ||
888
- !!this.threads[parentMessageId]?.find((m) => m.id === messageId);
889
- if (switchedToMessageSet && loadedMessageThread) {
890
- return;
891
- }
892
- if (!switchedToMessageSet) {
893
- await this._channel.query(
894
- { messages: { id_around: messageIdToFind, limit } },
895
- 'new',
896
- );
897
- }
898
- if (!loadedMessageThread && parentMessageId) {
899
- await this._channel.getReplies(parentMessageId, { id_around: messageId, limit });
900
- }
901
- messageSetIndex = this.findMessageSetIndex({ id: messageIdToFind });
902
- if (messageSetIndex !== -1) {
903
- this.switchToMessageSet(messageSetIndex);
904
- }
905
- }
906
-
907
- /**
908
- * findMessage - Finds a message inside the state
909
- *
910
- * @param {string} messageId The id of the message
911
- * @param {string} parentMessageId The id of the parent message, if we want load a thread reply
912
- *
913
- * @return {ReturnType<ChannelState['formatMessage']>} Returns the message, or undefined if the message wasn't found
914
- */
915
- findMessage(messageId: string, parentMessageId?: string) {
916
- if (parentMessageId) {
917
- const messages = this.threads[parentMessageId];
918
- if (!messages) {
919
- return undefined;
920
- }
921
- return messages.find((m) => m.id === messageId);
922
- }
923
-
924
- const messageSetIndex = this.findMessageSetIndex({ id: messageId });
925
- if (messageSetIndex === -1) {
926
- return undefined;
927
- }
928
- return this.messageSets[messageSetIndex].messages.find((m) => m.id === messageId);
929
- }
930
-
931
- findMessageByTimestamp(
932
- timestampMs: number,
933
- parentMessageId?: string,
934
- exactTsMatch: boolean = false,
935
- ): LocalMessage | null {
936
- if (
937
- (parentMessageId && !this.threads[parentMessageId]) ||
938
- this.messageSets.length === 0
939
- )
940
- return null;
941
- const setIndex = this.findMessageSetByOldestTimestamp(timestampMs);
942
- const targetMsgSet = this.messageSets[setIndex]?.messages;
943
- if (!targetMsgSet?.length) return null;
944
- const firstMsgTimestamp = targetMsgSet[0].created_at.getTime();
945
- const lastMsgTimestamp = targetMsgSet.slice(-1)[0].created_at.getTime();
946
- const isOutOfBound =
947
- timestampMs < firstMsgTimestamp || lastMsgTimestamp < timestampMs;
948
- if (isOutOfBound && exactTsMatch) return null;
949
-
950
- let msgIndex = 0,
951
- hi = targetMsgSet.length - 1;
952
- while (msgIndex < hi) {
953
- const mid = (msgIndex + hi) >>> 1;
954
- if (timestampMs <= targetMsgSet[mid].created_at.getTime()) hi = mid;
955
- else msgIndex = mid + 1;
956
- }
957
-
958
- const foundMessage = targetMsgSet[msgIndex];
959
- return !exactTsMatch
960
- ? foundMessage
961
- : foundMessage.created_at.getTime() === timestampMs
962
- ? foundMessage
963
- : null;
964
- }
965
-
966
- private switchToMessageSet(index: number) {
967
- const currentMessages = this.messageSets.find((s) => s.isCurrent);
968
- if (!currentMessages) {
969
- return;
970
- }
971
- currentMessages.isCurrent = false;
972
- this.messageSets[index].isCurrent = true;
973
- }
974
-
975
- private areMessageSetsOverlap(
976
- messages1: Array<{ id: string }>,
977
- messages2: Array<{ id: string }>,
978
- ) {
979
- return messages1.some((m1) => messages2.find((m2) => m1.id === m2.id));
980
- }
981
-
982
- private findMessageSetIndex(message: { id?: string }) {
983
- return this.messageSets.findIndex(
984
- (set) => !!set.messages.find((m) => m.id === message.id),
985
- );
986
- }
987
-
988
- /**
989
- * Identifies the set index into which a message set would pertain if its first item's creation date corresponded to oldestTimestampMs.
990
- * @param oldestTimestampMs
991
- */
992
- private findMessageSetByOldestTimestamp = (oldestTimestampMs: number): number => {
993
- let lo = 0,
994
- hi = this.messageSets.length;
995
- while (lo < hi) {
996
- const mid = (lo + hi) >>> 1;
997
- const msgSet = this.messageSets[mid];
998
- // should not happen
999
- if (msgSet.messages.length === 0) return -1;
1000
-
1001
- const oldestMessageTimestampInSet = msgSet.messages[0].created_at.getTime();
1002
- if (oldestMessageTimestampInSet <= oldestTimestampMs) hi = mid;
1003
- else lo = mid + 1;
1004
- }
1005
- return lo;
1006
- };
1007
-
1008
- private findTargetMessageSet(
1009
- newMessages: (MessageResponse | LocalMessage)[],
1010
- addIfDoesNotExist = true,
1011
- messageSetToAddToIfDoesNotExist: MessageSetType = 'current',
1012
- ) {
1013
- let messagesToAdd: (MessageResponse | LocalMessage)[] = newMessages;
1014
- let targetMessageSetIndex!: number;
1015
- if (newMessages.length === 0)
1016
- return { targetMessageSetIndex: 0, messagesToAdd: newMessages };
1017
- if (addIfDoesNotExist) {
1018
- const overlappingMessageSetIndicesByMsgIds = this.messageSets
1019
- .map((_, i) => i)
1020
- .filter((i) =>
1021
- this.areMessageSetsOverlap(this.messageSets[i].messages, newMessages),
1022
- );
1023
- const overlappingMessageSetIndicesByTimestamp = this.messageSets
1024
- .map((_, i) => i)
1025
- .filter((i) =>
1026
- messageSetsOverlapByTimestamp(
1027
- this.messageSets[i].messages,
1028
- newMessages.map(formatMessage),
1029
- ),
1030
- );
1031
- switch (messageSetToAddToIfDoesNotExist) {
1032
- case 'new':
1033
- if (overlappingMessageSetIndicesByMsgIds.length > 0) {
1034
- targetMessageSetIndex = overlappingMessageSetIndicesByMsgIds[0];
1035
- } else if (overlappingMessageSetIndicesByTimestamp.length > 0) {
1036
- targetMessageSetIndex = overlappingMessageSetIndicesByTimestamp[0];
1037
- // No new message set is created if newMessages only contains thread replies
1038
- } else if (newMessages.some((m) => !m.parent_id)) {
1039
- // find the index to insert the set
1040
- const setIngestIndex = this.findMessageSetByOldestTimestamp(
1041
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1042
- new Date(newMessages[0].created_at!).getTime(),
1043
- );
1044
- if (setIngestIndex === -1) {
1045
- this.messageSets.push({
1046
- messages: [],
1047
- isCurrent: false,
1048
- isLatest: false,
1049
- pagination: { ...DEFAULT_MESSAGE_SET_PAGINATION },
1050
- });
1051
- targetMessageSetIndex = this.messageSets.length - 1;
1052
- } else {
1053
- const isLatest = setIngestIndex === 0;
1054
- this.messageSets.splice(setIngestIndex, 0, {
1055
- messages: [],
1056
- isCurrent: false,
1057
- isLatest,
1058
- pagination: { ...DEFAULT_MESSAGE_SET_PAGINATION }, // fixme: it is problematic decide about pagination without having data
1059
- });
1060
- if (isLatest) {
1061
- this.messageSets.slice(1).forEach((set) => {
1062
- set.isLatest = false;
1063
- });
1064
- }
1065
- targetMessageSetIndex = setIngestIndex;
1066
- }
1067
- }
1068
- break;
1069
- case 'current':
1070
- // determine if there is another set to which it would match taken into consideration the timestamp
1071
- if (overlappingMessageSetIndicesByTimestamp.length > 0) {
1072
- targetMessageSetIndex = overlappingMessageSetIndicesByTimestamp[0];
1073
- } else {
1074
- targetMessageSetIndex = this.messageSets.findIndex((s) => s.isCurrent);
1075
- }
1076
- break;
1077
- case 'latest':
1078
- // determine if there is another set to which it would match taken into consideration the timestamp
1079
- if (overlappingMessageSetIndicesByTimestamp.length > 0) {
1080
- targetMessageSetIndex = overlappingMessageSetIndicesByTimestamp[0];
1081
- } else {
1082
- targetMessageSetIndex = this.messageSets.findIndex((s) => s.isLatest);
1083
- }
1084
- break;
1085
- default:
1086
- targetMessageSetIndex = -1;
1087
- }
1088
- // when merging the target set will be the first one from the overlapping message sets
1089
- const mergeTargetMessageSetIndex = overlappingMessageSetIndicesByMsgIds.splice(
1090
- 0,
1091
- 1,
1092
- )[0];
1093
- const mergeSourceMessageSetIndices = [...overlappingMessageSetIndicesByMsgIds];
1094
- if (
1095
- mergeTargetMessageSetIndex !== undefined &&
1096
- mergeTargetMessageSetIndex !== targetMessageSetIndex
1097
- ) {
1098
- mergeSourceMessageSetIndices.push(targetMessageSetIndex);
1099
- }
1100
- // merge message sets
1101
- if (mergeSourceMessageSetIndices.length > 0) {
1102
- const target = this.messageSets[mergeTargetMessageSetIndex];
1103
- const sources = this.messageSets.filter(
1104
- (_, i) => mergeSourceMessageSetIndices.indexOf(i) !== -1,
1105
- );
1106
- sources.forEach((messageSet) => {
1107
- target.isLatest = target.isLatest || messageSet.isLatest;
1108
- target.isCurrent = target.isCurrent || messageSet.isCurrent;
1109
- target.pagination.hasPrev =
1110
- messageSet.messages[0].created_at < target.messages[0].created_at
1111
- ? messageSet.pagination.hasPrev
1112
- : target.pagination.hasPrev;
1113
- target.pagination.hasNext =
1114
- target.messages.slice(-1)[0].created_at <
1115
- messageSet.messages.slice(-1)[0].created_at
1116
- ? messageSet.pagination.hasNext
1117
- : target.pagination.hasNext;
1118
- messagesToAdd = [...messagesToAdd, ...messageSet.messages];
1119
- });
1120
- sources.forEach((s) => this.messageSets.splice(this.messageSets.indexOf(s), 1));
1121
- const overlappingMessageSetIndex = this.messageSets.findIndex((s) =>
1122
- this.areMessageSetsOverlap(s.messages, newMessages),
1123
- );
1124
- targetMessageSetIndex = overlappingMessageSetIndex;
1125
- }
1126
- } else {
1127
- // assumes that all new messages belong to the same set
1128
- targetMessageSetIndex = this.findMessageSetIndex(newMessages[0]);
1129
- }
1130
-
1131
- return { targetMessageSetIndex, messagesToAdd };
1132
- }
1133
264
  }