@sidebandchat/client 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/COPYRIGHT +8 -0
  2. package/LICENSE +674 -0
  3. package/README.md +121 -2
  4. package/dist/activity/activity-sharing-controller.d.ts +131 -0
  5. package/dist/activity/activity-sharing-controller.d.ts.map +1 -0
  6. package/dist/activity/activity-sharing-controller.js +351 -0
  7. package/dist/activity/activity-sharing-controller.js.map +1 -0
  8. package/dist/activity/index.d.ts +2 -0
  9. package/dist/activity/index.d.ts.map +1 -0
  10. package/dist/activity/index.js +2 -0
  11. package/dist/activity/index.js.map +1 -0
  12. package/dist/attachments/attachment-client.d.ts +36 -0
  13. package/dist/attachments/attachment-client.d.ts.map +1 -0
  14. package/dist/attachments/attachment-client.js +98 -0
  15. package/dist/attachments/attachment-client.js.map +1 -0
  16. package/dist/attachments/draft-attachment.d.ts +17 -0
  17. package/dist/attachments/draft-attachment.d.ts.map +1 -0
  18. package/dist/attachments/draft-attachment.js +2 -0
  19. package/dist/attachments/draft-attachment.js.map +1 -0
  20. package/dist/auth/identity-client.d.ts +64 -0
  21. package/dist/auth/identity-client.d.ts.map +1 -0
  22. package/dist/auth/identity-client.js +65 -0
  23. package/dist/auth/identity-client.js.map +1 -0
  24. package/dist/auth/index.d.ts +2 -0
  25. package/dist/auth/index.d.ts.map +1 -0
  26. package/dist/auth/index.js +2 -0
  27. package/dist/auth/index.js.map +1 -0
  28. package/dist/badges/badge-verifier.d.ts +11 -0
  29. package/dist/badges/badge-verifier.d.ts.map +1 -0
  30. package/dist/badges/badge-verifier.js +128 -0
  31. package/dist/badges/badge-verifier.js.map +1 -0
  32. package/dist/calls/calls-client.d.ts +16 -0
  33. package/dist/calls/calls-client.d.ts.map +1 -0
  34. package/dist/calls/calls-client.js +55 -0
  35. package/dist/calls/calls-client.js.map +1 -0
  36. package/dist/calls/index.d.ts +3 -0
  37. package/dist/calls/index.d.ts.map +1 -0
  38. package/dist/calls/index.js +2 -0
  39. package/dist/calls/index.js.map +1 -0
  40. package/dist/calls/media-controls.d.ts +38 -0
  41. package/dist/calls/media-controls.d.ts.map +1 -0
  42. package/dist/calls/media-controls.js +134 -0
  43. package/dist/calls/media-controls.js.map +1 -0
  44. package/dist/calls/media-presentation-controller.d.ts +39 -0
  45. package/dist/calls/media-presentation-controller.d.ts.map +1 -0
  46. package/dist/calls/media-presentation-controller.js +106 -0
  47. package/dist/calls/media-presentation-controller.js.map +1 -0
  48. package/dist/calls/media-types.d.ts +37 -0
  49. package/dist/calls/media-types.d.ts.map +1 -0
  50. package/dist/calls/media-types.js +2 -0
  51. package/dist/calls/media-types.js.map +1 -0
  52. package/dist/calls/media.d.ts +39 -0
  53. package/dist/calls/media.d.ts.map +1 -0
  54. package/dist/calls/media.js +501 -0
  55. package/dist/calls/media.js.map +1 -0
  56. package/dist/client/ephemeral-state.d.ts +52 -0
  57. package/dist/client/ephemeral-state.d.ts.map +1 -0
  58. package/dist/client/ephemeral-state.js +371 -0
  59. package/dist/client/ephemeral-state.js.map +1 -0
  60. package/dist/client/index.d.ts +7 -0
  61. package/dist/client/index.d.ts.map +1 -0
  62. package/dist/client/index.js +4 -0
  63. package/dist/client/index.js.map +1 -0
  64. package/dist/client/live-event-controller.d.ts +14 -0
  65. package/dist/client/live-event-controller.d.ts.map +1 -0
  66. package/dist/client/live-event-controller.js +28 -0
  67. package/dist/client/live-event-controller.js.map +1 -0
  68. package/dist/client/platform-adapters.d.ts +122 -0
  69. package/dist/client/platform-adapters.d.ts.map +1 -0
  70. package/dist/client/platform-adapters.js +2 -0
  71. package/dist/client/platform-adapters.js.map +1 -0
  72. package/dist/client/session-runtime.d.ts +115 -0
  73. package/dist/client/session-runtime.d.ts.map +1 -0
  74. package/dist/client/session-runtime.js +607 -0
  75. package/dist/client/session-runtime.js.map +1 -0
  76. package/dist/client/sideband-client.d.ts +141 -0
  77. package/dist/client/sideband-client.d.ts.map +1 -0
  78. package/dist/client/sideband-client.js +575 -0
  79. package/dist/client/sideband-client.js.map +1 -0
  80. package/dist/client/snapshot-client.d.ts +9 -0
  81. package/dist/client/snapshot-client.d.ts.map +1 -0
  82. package/dist/client/snapshot-client.js +18 -0
  83. package/dist/client/snapshot-client.js.map +1 -0
  84. package/dist/client/state.d.ts +112 -0
  85. package/dist/client/state.d.ts.map +1 -0
  86. package/dist/client/state.js +992 -0
  87. package/dist/client/state.js.map +1 -0
  88. package/dist/communities/community-client.d.ts +40 -0
  89. package/dist/communities/community-client.d.ts.map +1 -0
  90. package/dist/communities/community-client.js +166 -0
  91. package/dist/communities/community-client.js.map +1 -0
  92. package/dist/conversations/index.d.ts +2 -0
  93. package/dist/conversations/index.d.ts.map +1 -0
  94. package/dist/conversations/index.js +2 -0
  95. package/dist/conversations/index.js.map +1 -0
  96. package/dist/conversations/mentions.d.ts +18 -0
  97. package/dist/conversations/mentions.d.ts.map +1 -0
  98. package/dist/conversations/mentions.js +108 -0
  99. package/dist/conversations/mentions.js.map +1 -0
  100. package/dist/conversations/messaging-client.d.ts +56 -0
  101. package/dist/conversations/messaging-client.d.ts.map +1 -0
  102. package/dist/conversations/messaging-client.js +333 -0
  103. package/dist/conversations/messaging-client.js.map +1 -0
  104. package/dist/conversations/replies-controller.d.ts +50 -0
  105. package/dist/conversations/replies-controller.d.ts.map +1 -0
  106. package/dist/conversations/replies-controller.js +206 -0
  107. package/dist/conversations/replies-controller.js.map +1 -0
  108. package/dist/devices/device-client.d.ts +11 -0
  109. package/dist/devices/device-client.d.ts.map +1 -0
  110. package/dist/devices/device-client.js +27 -0
  111. package/dist/devices/device-client.js.map +1 -0
  112. package/dist/domain.d.ts +528 -0
  113. package/dist/domain.d.ts.map +1 -0
  114. package/dist/domain.js +13 -0
  115. package/dist/domain.js.map +1 -0
  116. package/dist/entity-id.d.ts +32 -0
  117. package/dist/entity-id.d.ts.map +1 -0
  118. package/dist/entity-id.js +17 -0
  119. package/dist/entity-id.js.map +1 -0
  120. package/dist/index.d.ts +11 -0
  121. package/dist/index.d.ts.map +1 -0
  122. package/dist/index.js +10 -0
  123. package/dist/index.js.map +1 -0
  124. package/dist/instance/index.d.ts +5 -0
  125. package/dist/instance/index.d.ts.map +1 -0
  126. package/dist/instance/index.js +3 -0
  127. package/dist/instance/index.js.map +1 -0
  128. package/dist/instance/instance-policy.d.ts +13 -0
  129. package/dist/instance/instance-policy.d.ts.map +1 -0
  130. package/dist/instance/instance-policy.js +2 -0
  131. package/dist/instance/instance-policy.js.map +1 -0
  132. package/dist/instance-access.d.ts +66 -0
  133. package/dist/instance-access.d.ts.map +1 -0
  134. package/dist/instance-access.js +84 -0
  135. package/dist/instance-access.js.map +1 -0
  136. package/dist/instance-capabilities.d.ts +12 -0
  137. package/dist/instance-capabilities.d.ts.map +1 -0
  138. package/dist/instance-capabilities.js +2 -0
  139. package/dist/instance-capabilities.js.map +1 -0
  140. package/dist/instance-endpoint.d.ts +32 -0
  141. package/dist/instance-endpoint.d.ts.map +1 -0
  142. package/dist/instance-endpoint.js +147 -0
  143. package/dist/instance-endpoint.js.map +1 -0
  144. package/dist/model/index.d.ts +3 -0
  145. package/dist/model/index.d.ts.map +1 -0
  146. package/dist/model/index.js +3 -0
  147. package/dist/model/index.js.map +1 -0
  148. package/dist/model/selectors.d.ts +22 -0
  149. package/dist/model/selectors.d.ts.map +1 -0
  150. package/dist/model/selectors.js +73 -0
  151. package/dist/model/selectors.js.map +1 -0
  152. package/dist/private-conversation/file-hash.d.ts +2 -0
  153. package/dist/private-conversation/file-hash.d.ts.map +1 -0
  154. package/dist/private-conversation/file-hash.js +13 -0
  155. package/dist/private-conversation/file-hash.js.map +1 -0
  156. package/dist/private-conversation/private-conversation-crypto.d.ts +48 -0
  157. package/dist/private-conversation/private-conversation-crypto.d.ts.map +1 -0
  158. package/dist/private-conversation/private-conversation-crypto.js +169 -0
  159. package/dist/private-conversation/private-conversation-crypto.js.map +1 -0
  160. package/dist/private-conversation/private-conversations-client.d.ts +23 -0
  161. package/dist/private-conversation/private-conversations-client.d.ts.map +1 -0
  162. package/dist/private-conversation/private-conversations-client.js +77 -0
  163. package/dist/private-conversation/private-conversations-client.js.map +1 -0
  164. package/dist/private-conversation/private-transfer-limits.d.ts +9 -0
  165. package/dist/private-conversation/private-transfer-limits.d.ts.map +1 -0
  166. package/dist/private-conversation/private-transfer-limits.js +9 -0
  167. package/dist/private-conversation/private-transfer-limits.js.map +1 -0
  168. package/dist/private-conversation/private-transfer-protocol.d.ts +53 -0
  169. package/dist/private-conversation/private-transfer-protocol.d.ts.map +1 -0
  170. package/dist/private-conversation/private-transfer-protocol.js +184 -0
  171. package/dist/private-conversation/private-transfer-protocol.js.map +1 -0
  172. package/dist/private-conversation/private-transfer-transport.d.ts +22 -0
  173. package/dist/private-conversation/private-transfer-transport.d.ts.map +1 -0
  174. package/dist/private-conversation/private-transfer-transport.js +94 -0
  175. package/dist/private-conversation/private-transfer-transport.js.map +1 -0
  176. package/dist/realtime/realtime-client.d.ts +31 -0
  177. package/dist/realtime/realtime-client.d.ts.map +1 -0
  178. package/dist/realtime/realtime-client.js +103 -0
  179. package/dist/realtime/realtime-client.js.map +1 -0
  180. package/dist/sbp/codec.d.ts +33 -0
  181. package/dist/sbp/codec.d.ts.map +1 -0
  182. package/dist/sbp/codec.js +93 -0
  183. package/dist/sbp/codec.js.map +1 -0
  184. package/dist/sbp/command-transport.d.ts +9 -0
  185. package/dist/sbp/command-transport.d.ts.map +1 -0
  186. package/dist/sbp/command-transport.js +2 -0
  187. package/dist/sbp/command-transport.js.map +1 -0
  188. package/dist/sbp/frame.d.ts +34 -0
  189. package/dist/sbp/frame.d.ts.map +1 -0
  190. package/dist/sbp/frame.js +95 -0
  191. package/dist/sbp/frame.js.map +1 -0
  192. package/dist/sbp/records.d.ts +32 -0
  193. package/dist/sbp/records.d.ts.map +1 -0
  194. package/dist/sbp/records.js +527 -0
  195. package/dist/sbp/records.js.map +1 -0
  196. package/dist/sbp/session.d.ts +110 -0
  197. package/dist/sbp/session.d.ts.map +1 -0
  198. package/dist/sbp/session.js +349 -0
  199. package/dist/sbp/session.js.map +1 -0
  200. package/dist/sbp/value.d.ts +13 -0
  201. package/dist/sbp/value.d.ts.map +1 -0
  202. package/dist/sbp/value.js +58 -0
  203. package/dist/sbp/value.js.map +1 -0
  204. package/dist/social/social-client.d.ts +38 -0
  205. package/dist/social/social-client.d.ts.map +1 -0
  206. package/dist/social/social-client.js +110 -0
  207. package/dist/social/social-client.js.map +1 -0
  208. package/dist/transports/media-server-identity.d.ts +8 -0
  209. package/dist/transports/media-server-identity.d.ts.map +1 -0
  210. package/dist/transports/media-server-identity.js +22 -0
  211. package/dist/transports/media-server-identity.js.map +1 -0
  212. package/dist/workspace/index.d.ts +2 -0
  213. package/dist/workspace/index.d.ts.map +1 -0
  214. package/dist/workspace/index.js +2 -0
  215. package/dist/workspace/index.js.map +1 -0
  216. package/dist/workspace/workspace-controller.d.ts +39 -0
  217. package/dist/workspace/workspace-controller.d.ts.map +1 -0
  218. package/dist/workspace/workspace-controller.js +188 -0
  219. package/dist/workspace/workspace-controller.js.map +1 -0
  220. package/dist/workspace/workspace-selectors.d.ts +49 -0
  221. package/dist/workspace/workspace-selectors.d.ts.map +1 -0
  222. package/dist/workspace/workspace-selectors.js +110 -0
  223. package/dist/workspace/workspace-selectors.js.map +1 -0
  224. package/package.json +88 -4
@@ -0,0 +1,992 @@
1
+ import { decodeBlock, decodeCommunity, decodeCommunityChannel, decodeCommunityRole, decodeConversation, decodeDevice, decodeFriendship, decodeInboxEntry, decodeMessage, decodeNotificationSettings, decodeReactionSummaries, decodeRealtimeSettings, decodeUserSummary, } from "../sbp/records.js";
2
+ import { decodeEntityRef } from "../sbp/codec.js";
3
+ import { arrayValue, booleanValue, mapValue, orderedValue, required, timestampValue, unsignedValue, } from "../sbp/value.js";
4
+ export function initialAccountState() {
5
+ return {
6
+ loaded: false,
7
+ users: new Map(),
8
+ friendships: new Map(),
9
+ conversations: new Map(),
10
+ communities: new Map(),
11
+ messages: new Map(),
12
+ pendingMessages: new Map(),
13
+ reachedBeginning: new Map(),
14
+ inbox: new Map(),
15
+ blocks: new Map(),
16
+ notificationSettings: [],
17
+ devices: new Map(),
18
+ conversationEventIds: new Map(),
19
+ };
20
+ }
21
+ export class AccountStateController {
22
+ snapshots;
23
+ messageCache;
24
+ onPersistenceError;
25
+ verifyProfile;
26
+ onNotification;
27
+ resolveProfiles;
28
+ static remoteNotificationBatchMs = 100;
29
+ current = initialAccountState();
30
+ listeners = new Set();
31
+ deferredNotifications = 0;
32
+ notificationPending = false;
33
+ notificationTimer;
34
+ hydratedConversations = new Set();
35
+ profileLoads = new Map();
36
+ profileBatch;
37
+ persistenceQueue = Promise.resolve();
38
+ optimisticRollbacks = new Map();
39
+ constructor(snapshots, messageCache, onPersistenceError = () => undefined, verifyProfile = async (profile) => profile, onNotification = () => undefined, resolveProfiles = async () => []) {
40
+ this.snapshots = snapshots;
41
+ this.messageCache = messageCache;
42
+ this.onPersistenceError = onPersistenceError;
43
+ this.verifyProfile = verifyProfile;
44
+ this.onNotification = onNotification;
45
+ this.resolveProfiles = resolveProfiles;
46
+ }
47
+ get state() {
48
+ return this.current;
49
+ }
50
+ subscribe = (listener) => {
51
+ this.listeners.add(listener);
52
+ return () => this.listeners.delete(listener);
53
+ };
54
+ async loadAccount() {
55
+ const snapshot = await this.snapshots.account();
56
+ this.replaceAccount(snapshot);
57
+ return snapshot;
58
+ }
59
+ loadInitialAccount(snapshot) {
60
+ this.hydratedConversations.clear();
61
+ this.replaceAccount(snapshot);
62
+ this.set({
63
+ ...this.current,
64
+ accountEventId: undefined,
65
+ conversationEventIds: new Map(),
66
+ });
67
+ return snapshot;
68
+ }
69
+ conversationNeedsRefresh(conversationId) {
70
+ return !this.hydratedConversations.has(conversationId);
71
+ }
72
+ async loadConversation(conversationId) {
73
+ if (!this.hydratedConversations.has(conversationId)) {
74
+ this.hydratedConversations.add(conversationId);
75
+ const cached = await this.loadCachedConversation(conversationId);
76
+ if (cached)
77
+ this.mergeCachedConversation(conversationId, cached);
78
+ }
79
+ const snapshot = await this.snapshots.conversation(conversationId);
80
+ const messages = new Map(this.current.messages);
81
+ const conversationMessages = new Map(messages.get(conversationId) ?? []);
82
+ const snapshotMessages = snapshot.messages.map((message) => withDirection(message, this.current.ownUserId));
83
+ snapshotMessages.forEach((message) => conversationMessages.set(message.id, message));
84
+ messages.set(conversationId, conversationMessages);
85
+ const conversations = new Map(this.current.conversations);
86
+ conversations.set(conversationId, snapshot.conversation);
87
+ const reachedBeginning = new Map(this.current.reachedBeginning);
88
+ const complete = snapshot.reachedBeginning ||
89
+ this.current.reachedBeginning.get(conversationId) === true;
90
+ reachedBeginning.set(conversationId, complete);
91
+ const conversationEventIds = new Map(this.current.conversationEventIds);
92
+ const users = mergeProfiles(this.current.users, snapshot.profiles);
93
+ this.set({
94
+ ...this.current,
95
+ users,
96
+ messages,
97
+ conversations,
98
+ reachedBeginning,
99
+ conversationEventIds,
100
+ });
101
+ await this.persistMessages(conversationId, snapshotMessages, {
102
+ reachedBeginning: complete,
103
+ });
104
+ return snapshot;
105
+ }
106
+ async mergeHistory(page) {
107
+ const messages = new Map(this.current.messages);
108
+ const conversationMessages = new Map(messages.get(page.conversationId) ?? []);
109
+ const pageMessages = page.messages.map((message) => withDirection(message, this.current.ownUserId));
110
+ pageMessages.forEach((message) => conversationMessages.set(message.id, message));
111
+ messages.set(page.conversationId, conversationMessages);
112
+ const reachedBeginning = new Map(this.current.reachedBeginning);
113
+ reachedBeginning.set(page.conversationId, page.reachedBeginning ||
114
+ this.current.reachedBeginning.get(page.conversationId) === true);
115
+ const users = mergeProfiles(this.current.users, page.profiles);
116
+ this.set({ ...this.current, users, messages, reachedBeginning });
117
+ await this.persistMessages(page.conversationId, pageMessages, {
118
+ reachedBeginning: reachedBeginning.get(page.conversationId),
119
+ });
120
+ }
121
+ async applyEvent(frame) {
122
+ this.deferredNotifications += 1;
123
+ try {
124
+ return this.applyEventNow(frame);
125
+ }
126
+ finally {
127
+ this.deferredNotifications -= 1;
128
+ if (this.deferredNotifications === 0 && this.notificationPending)
129
+ this.scheduleNotification();
130
+ }
131
+ }
132
+ applyEventNow(frame) {
133
+ if (frame.stream.kind === "account") {
134
+ if (this.current.accountEventId !== undefined &&
135
+ frame.eventId <= this.current.accountEventId)
136
+ return false;
137
+ this.applyAccountEvent(frame.opcode, frame.payload);
138
+ this.set({ ...this.current, accountEventId: frame.eventId });
139
+ return true;
140
+ }
141
+ const conversationId = frame.stream.conversationId;
142
+ const previous = this.current.conversationEventIds.get(conversationId);
143
+ if (previous !== undefined && frame.eventId <= previous)
144
+ return false;
145
+ const cacheMutation = this.applyConversationEvent(conversationId, frame.opcode, frame.payload, frame.timestamp);
146
+ const conversationEventIds = new Map(this.current.conversationEventIds);
147
+ conversationEventIds.set(conversationId, frame.eventId);
148
+ this.set({ ...this.current, conversationEventIds });
149
+ if (cacheMutation?.message)
150
+ this.enqueuePersistence(() => this.persistMessages(conversationId, [cacheMutation.message], {}));
151
+ else if (cacheMutation?.remove)
152
+ this.enqueuePersistence(() => this.removeCachedMessage(conversationId, cacheMutation.remove));
153
+ return true;
154
+ }
155
+ async ensureUserProfiles(userIds) {
156
+ const requested = [...new Set(userIds)].filter((userId) => !this.current.users.has(userId));
157
+ if (!requested.length)
158
+ return;
159
+ requested
160
+ .filter((userId) => !this.profileLoads.has(userId))
161
+ .forEach((userId) => this.queueProfileLoad(userId));
162
+ await Promise.all(requested.map((userId) => this.profileLoads.get(userId)).filter(Boolean));
163
+ }
164
+ reset() {
165
+ if (this.notificationTimer)
166
+ clearTimeout(this.notificationTimer);
167
+ this.notificationTimer = undefined;
168
+ this.notificationPending = false;
169
+ this.deferredNotifications = 0;
170
+ this.hydratedConversations.clear();
171
+ this.profileLoads.clear();
172
+ if (this.profileBatch)
173
+ clearTimeout(this.profileBatch.timer);
174
+ this.profileBatch = undefined;
175
+ this.persistenceQueue = Promise.resolve();
176
+ this.set(initialAccountState());
177
+ }
178
+ upsertConversation(conversation) {
179
+ this.upsert("conversations", conversation, (item) => item.id);
180
+ }
181
+ upsertCommunity(community) {
182
+ this.upsert("communities", community, (item) => item.id);
183
+ }
184
+ mergeCommunityMembers(page) {
185
+ const community = this.current.communities.get(page.communityId);
186
+ if (!community)
187
+ return;
188
+ const existing = page.online
189
+ ? (community.onlineMembers ?? [])
190
+ : community.members;
191
+ const members = new Map(existing.map((member) => [member.userId, member]));
192
+ page.members.forEach((member) => members.set(member.userId, member));
193
+ const communities = new Map(this.current.communities);
194
+ communities.set(page.communityId, page.online
195
+ ? {
196
+ ...community,
197
+ onlineMembers: [...members.values()],
198
+ onlineMembersComplete: page.complete,
199
+ onlineMembersNextAfter: page.nextAfter,
200
+ onlineMemberCount: page.total,
201
+ }
202
+ : {
203
+ ...community,
204
+ members: [...members.values()],
205
+ membersComplete: page.complete,
206
+ membersNextAfter: page.nextAfter,
207
+ });
208
+ const users = new Map(this.current.users);
209
+ page.profiles.forEach((profile) => users.set(profile.id, profile));
210
+ this.set({ ...this.current, communities, users });
211
+ }
212
+ mergeChannelMembers(page, replace = false) {
213
+ if (!page.channelId)
214
+ return;
215
+ const communities = new Map(this.current.communities);
216
+ for (const [communityId, community] of communities) {
217
+ const channelIndex = community.channels.findIndex((channel) => channel.id === page.channelId);
218
+ if (channelIndex < 0)
219
+ continue;
220
+ const channel = community.channels[channelIndex];
221
+ const existing = page.online
222
+ ? (channel.onlineMembers ?? [])
223
+ : (channel.members ?? []);
224
+ const members = new Map((replace ? [] : existing).map((member) => [member.userId, member]));
225
+ const pageProfiles = new Map(page.profiles.map((profile) => [profile.id, profile]));
226
+ page.members.forEach((member) => members.set(member.userId, {
227
+ ...member,
228
+ profile: pageProfiles.get(member.userId),
229
+ }));
230
+ const updatedChannel = page.online
231
+ ? {
232
+ ...channel,
233
+ onlineMembers: [...members.values()],
234
+ onlineMembersComplete: page.complete,
235
+ onlineMembersNextCursor: page.nextCursor,
236
+ onlineMemberCount: page.total,
237
+ }
238
+ : {
239
+ ...channel,
240
+ members: [...members.values()],
241
+ memberCount: page.total,
242
+ membersComplete: page.complete,
243
+ membersNextCursor: page.nextCursor,
244
+ };
245
+ const channels = [...community.channels];
246
+ channels[channelIndex] = updatedChannel;
247
+ communities.set(communityId, { ...community, channels });
248
+ break;
249
+ }
250
+ const users = new Map(this.current.users);
251
+ page.profiles.forEach((profile) => users.set(profile.id, profile));
252
+ this.set({ ...this.current, communities, users });
253
+ }
254
+ updateChannelOnlinePresence(channelId, userId, online) {
255
+ this.updateChannelOnlinePresences(channelId, new Map([[userId, online]]));
256
+ }
257
+ updateChannelOnlinePresences(channelId, changes, onlineCount) {
258
+ const communities = new Map(this.current.communities);
259
+ for (const [communityId, community] of communities) {
260
+ const channelIndex = community.channels.findIndex((channel) => channel.id === channelId);
261
+ if (channelIndex < 0)
262
+ continue;
263
+ const channel = community.channels[channelIndex];
264
+ if (!channel.onlineMembers)
265
+ return;
266
+ const onlineMembers = new Map(channel.onlineMembers.map((member) => [member.userId, member]));
267
+ const members = new Map((channel.members ?? []).map((member) => [member.userId, member]));
268
+ changes.forEach((online, userId) => {
269
+ if (!online)
270
+ onlineMembers.delete(userId);
271
+ else if (!onlineMembers.has(userId) && members.has(userId))
272
+ onlineMembers.set(userId, members.get(userId));
273
+ });
274
+ const channels = [...community.channels];
275
+ channels[channelIndex] = {
276
+ ...channel,
277
+ onlineMembers: [...onlineMembers.values()],
278
+ onlineMemberCount: onlineCount ?? channel.onlineMemberCount,
279
+ onlineMembersComplete: onlineCount === undefined
280
+ ? channel.onlineMembersComplete
281
+ : onlineMembers.size >= onlineCount,
282
+ };
283
+ communities.set(communityId, { ...community, channels });
284
+ this.set({ ...this.current, communities });
285
+ return;
286
+ }
287
+ }
288
+ resetCommunityOnlineMembers(communityId) {
289
+ const community = this.current.communities.get(communityId);
290
+ if (!community)
291
+ return;
292
+ const communities = new Map(this.current.communities);
293
+ communities.set(communityId, {
294
+ ...community,
295
+ onlineMembers: [],
296
+ onlineMembersComplete: false,
297
+ onlineMembersNextAfter: undefined,
298
+ onlineMemberCount: undefined,
299
+ });
300
+ this.set({ ...this.current, communities });
301
+ }
302
+ replaceCommunityOnlineMembers(page) {
303
+ if (!page.online)
304
+ return;
305
+ const communityId = page.communityId;
306
+ const community = this.current.communities.get(communityId);
307
+ if (!community)
308
+ return;
309
+ const communities = new Map(this.current.communities);
310
+ communities.set(communityId, {
311
+ ...community,
312
+ onlineMembers: page.members,
313
+ onlineMembersComplete: page.complete,
314
+ onlineMembersNextAfter: page.nextAfter,
315
+ onlineMemberCount: page.total,
316
+ });
317
+ const users = new Map(this.current.users);
318
+ page.profiles.forEach((profile) => users.set(profile.id, profile));
319
+ this.set({ ...this.current, communities, users });
320
+ }
321
+ updateCommunityOnlinePresence(communityId, userId, online) {
322
+ const community = this.current.communities.get(communityId);
323
+ if (!community?.onlineMembers)
324
+ return;
325
+ const existing = community.onlineMembers.find((member) => member.userId === userId);
326
+ const count = community.onlineMemberCount ?? community.onlineMembers.length;
327
+ if ((online && existing) || (!online && !existing && count === 0))
328
+ return;
329
+ const onlineMembers = online
330
+ ? [
331
+ ...community.onlineMembers,
332
+ community.members.find((member) => member.userId === userId) ?? {
333
+ userId,
334
+ joinedAt: new Date(0).toISOString(),
335
+ roleIds: [],
336
+ permissions: [],
337
+ revision: 0,
338
+ },
339
+ ]
340
+ : community.onlineMembers.filter((member) => member.userId !== userId);
341
+ const communities = new Map(this.current.communities);
342
+ communities.set(communityId, {
343
+ ...community,
344
+ onlineMembers,
345
+ onlineMemberCount: Math.max(0, count + (online ? 1 : -1)),
346
+ });
347
+ this.set({ ...this.current, communities });
348
+ }
349
+ addPendingMessage(requestId, message) {
350
+ const pendingMessages = new Map(this.current.pendingMessages);
351
+ pendingMessages.set(requestId, message);
352
+ this.set({ ...this.current, pendingMessages });
353
+ }
354
+ failPendingMessage(requestId, reason) {
355
+ const message = this.current.pendingMessages.get(requestId);
356
+ if (!message)
357
+ return;
358
+ const pendingMessages = new Map(this.current.pendingMessages);
359
+ pendingMessages.set(requestId, {
360
+ ...message,
361
+ pending: {
362
+ requestId,
363
+ state: "failed",
364
+ error: reason instanceof Error ? reason.message : "Message failed",
365
+ },
366
+ });
367
+ this.set({ ...this.current, pendingMessages });
368
+ }
369
+ markPendingMessageSending(requestId) {
370
+ const message = this.current.pendingMessages.get(requestId);
371
+ if (!message)
372
+ return;
373
+ const pendingMessages = new Map(this.current.pendingMessages);
374
+ pendingMessages.set(requestId, {
375
+ ...message,
376
+ pending: { requestId, state: "sending" },
377
+ });
378
+ this.set({ ...this.current, pendingMessages });
379
+ }
380
+ async confirmPendingMessage(requestId, message) {
381
+ const pendingMessages = new Map(this.current.pendingMessages);
382
+ pendingMessages.delete(requestId);
383
+ const messages = new Map(this.current.messages);
384
+ const conversationMessages = new Map(messages.get(message.conversationId) ?? []);
385
+ const confirmed = withDirection(message, this.current.ownUserId);
386
+ const transitioning = {
387
+ ...confirmed,
388
+ confirmationTransition: { requestId },
389
+ };
390
+ conversationMessages.set(transitioning.id, transitioning);
391
+ messages.set(transitioning.conversationId, conversationMessages);
392
+ this.set({ ...this.current, pendingMessages, messages });
393
+ await this.persistMessages(confirmed.conversationId, [confirmed], {});
394
+ globalThis.setTimeout(() => this.finishConfirmationTransition(confirmed.conversationId, confirmed.id, requestId), 65);
395
+ }
396
+ finishConfirmationTransition(conversationId, messageId, requestId) {
397
+ const conversationMessages = this.current.messages.get(conversationId);
398
+ const current = conversationMessages?.get(messageId);
399
+ if (current?.confirmationTransition?.requestId !== requestId)
400
+ return;
401
+ const messages = new Map(this.current.messages);
402
+ const nextConversationMessages = new Map(conversationMessages);
403
+ const confirmed = { ...current };
404
+ delete confirmed.confirmationTransition;
405
+ nextConversationMessages.set(messageId, confirmed);
406
+ messages.set(conversationId, nextConversationMessages);
407
+ this.set({ ...this.current, messages });
408
+ }
409
+ discardPendingMessage(requestId) {
410
+ if (!this.current.pendingMessages.has(requestId))
411
+ return;
412
+ const pendingMessages = new Map(this.current.pendingMessages);
413
+ pendingMessages.delete(requestId);
414
+ this.set({ ...this.current, pendingMessages });
415
+ }
416
+ beginOptimisticMutation(requestId, conversationId, messageId, kind, update) {
417
+ const original = this.current.messages.get(conversationId)?.get(messageId);
418
+ if (!original)
419
+ return;
420
+ this.optimisticRollbacks.set(requestId, {
421
+ conversationId,
422
+ messageId,
423
+ message: original,
424
+ kind,
425
+ });
426
+ const messages = new Map(this.current.messages);
427
+ const conversationMessages = new Map(messages.get(conversationId) ?? []);
428
+ if (kind === "delete")
429
+ conversationMessages.delete(messageId);
430
+ else if (update)
431
+ conversationMessages.set(messageId, {
432
+ ...update(original),
433
+ pendingMutation: { requestId, kind },
434
+ });
435
+ messages.set(conversationId, conversationMessages);
436
+ this.set({ ...this.current, messages });
437
+ }
438
+ completeOptimisticMutation(requestId, revision, update) {
439
+ const rollback = this.optimisticRollbacks.get(requestId);
440
+ if (!rollback)
441
+ return;
442
+ this.optimisticRollbacks.delete(requestId);
443
+ if (rollback.kind === "delete")
444
+ return;
445
+ const current = this.current.messages
446
+ .get(rollback.conversationId)
447
+ ?.get(rollback.messageId);
448
+ if (current?.pendingMutation?.requestId !== requestId)
449
+ return;
450
+ const messages = new Map(this.current.messages);
451
+ const conversationMessages = new Map(messages.get(rollback.conversationId) ?? []);
452
+ const confirmed = { ...(update ? update(current) : current) };
453
+ delete confirmed.pendingMutation;
454
+ conversationMessages.set(rollback.messageId, {
455
+ ...confirmed,
456
+ revision: revision ?? confirmed.revision,
457
+ });
458
+ messages.set(rollback.conversationId, conversationMessages);
459
+ this.set({ ...this.current, messages });
460
+ }
461
+ rollbackOptimisticMutation(requestId) {
462
+ const rollback = this.optimisticRollbacks.get(requestId);
463
+ if (!rollback)
464
+ return;
465
+ this.optimisticRollbacks.delete(requestId);
466
+ const current = this.current.messages
467
+ .get(rollback.conversationId)
468
+ ?.get(rollback.messageId);
469
+ if (rollback.kind !== "delete" &&
470
+ current?.pendingMutation?.requestId !== requestId)
471
+ return;
472
+ const messages = new Map(this.current.messages);
473
+ const conversationMessages = new Map(messages.get(rollback.conversationId) ?? []);
474
+ conversationMessages.set(rollback.messageId, rollback.message);
475
+ messages.set(rollback.conversationId, conversationMessages);
476
+ this.set({ ...this.current, messages });
477
+ }
478
+ replaceAccount(snapshot) {
479
+ const users = new Map([snapshot.ownUser, ...snapshot.users].map((user) => [user.id, user]));
480
+ this.set({
481
+ ...this.current,
482
+ loaded: true,
483
+ ownUserId: snapshot.ownUser.id,
484
+ ownProfile: snapshot.ownUser,
485
+ users,
486
+ friendships: keyed(snapshot.friendships, (item) => item.friendshipId),
487
+ conversations: keyed(snapshot.conversations, (item) => item.id),
488
+ communities: keyed(snapshot.communities, (item) => item.id),
489
+ inbox: keyed(snapshot.inbox, (item) => item.id),
490
+ blocks: keyed(snapshot.blocks, (item) => item.id),
491
+ notificationSettings: snapshot.notificationSettings,
492
+ devices: keyed(snapshot.devices, (item) => item.id),
493
+ realtimeSettings: snapshot.realtimeSettings,
494
+ accountEventId: undefined,
495
+ });
496
+ if (snapshot.ownUser.badgeAssertion)
497
+ this.verifyAndUpsertProfile(snapshot.ownUser);
498
+ }
499
+ mergeCachedConversation(conversationId, cached) {
500
+ const messages = new Map(this.current.messages);
501
+ const conversationMessages = new Map(cached.messages.map((message) => {
502
+ const directed = withDirection(message, this.current.ownUserId);
503
+ return [directed.id, directed];
504
+ }));
505
+ for (const [id, message] of messages.get(conversationId) ?? [])
506
+ conversationMessages.set(id, message);
507
+ messages.set(conversationId, conversationMessages);
508
+ const reachedBeginning = new Map(this.current.reachedBeginning);
509
+ reachedBeginning.set(conversationId, cached.reachedBeginning);
510
+ this.set({
511
+ ...this.current,
512
+ messages,
513
+ reachedBeginning,
514
+ });
515
+ }
516
+ async loadCachedConversation(conversationId) {
517
+ try {
518
+ return await this.messageCache?.load(conversationId);
519
+ }
520
+ catch (reason) {
521
+ this.onPersistenceError(reason);
522
+ return undefined;
523
+ }
524
+ }
525
+ async persistMessages(conversationId, messages, metadata) {
526
+ try {
527
+ await this.messageCache?.put(conversationId, messages, metadata);
528
+ }
529
+ catch (reason) {
530
+ this.onPersistenceError(reason);
531
+ }
532
+ }
533
+ async removeCachedMessage(conversationId, messageId) {
534
+ try {
535
+ await this.messageCache?.remove(conversationId, messageId);
536
+ }
537
+ catch (reason) {
538
+ this.onPersistenceError(reason);
539
+ }
540
+ }
541
+ enqueuePersistence(operation) {
542
+ this.persistenceQueue = this.persistenceQueue
543
+ .then(operation)
544
+ .catch((reason) => {
545
+ this.onPersistenceError(reason);
546
+ });
547
+ }
548
+ queueProfileLoad(userId) {
549
+ if (!this.profileBatch) {
550
+ let resolve;
551
+ let reject;
552
+ const promise = new Promise((batchResolve, batchReject) => {
553
+ resolve = batchResolve;
554
+ reject = batchReject;
555
+ });
556
+ const createdBatch = {
557
+ ids: new Set(),
558
+ promise,
559
+ resolve,
560
+ reject,
561
+ timer: undefined,
562
+ };
563
+ createdBatch.timer = setTimeout(() => void this.flushProfileBatch(createdBatch), 25);
564
+ this.profileBatch = createdBatch;
565
+ }
566
+ const batch = this.profileBatch;
567
+ batch.ids.add(userId);
568
+ this.profileLoads.set(userId, batch.promise);
569
+ }
570
+ async flushProfileBatch(batch) {
571
+ if (this.profileBatch === batch)
572
+ this.profileBatch = undefined;
573
+ const ids = [...batch.ids];
574
+ try {
575
+ const profiles = [];
576
+ for (let offset = 0; offset < ids.length; offset += 100) {
577
+ profiles.push(...(await this.resolveProfiles(ids.slice(offset, offset + 100))));
578
+ }
579
+ const users = mergeProfiles(this.current.users, profiles);
580
+ this.set({ ...this.current, users });
581
+ batch.resolve();
582
+ }
583
+ catch (reason) {
584
+ batch.reject(reason);
585
+ }
586
+ finally {
587
+ ids.forEach((userId) => {
588
+ if (this.profileLoads.get(userId) === batch.promise)
589
+ this.profileLoads.delete(userId);
590
+ });
591
+ }
592
+ }
593
+ applyAccountEvent(opcode, payload) {
594
+ switch (opcode) {
595
+ case 2:
596
+ this.upsert("devices", decodeDevice(payload), (item) => item.id);
597
+ return;
598
+ case 3:
599
+ this.remove("devices", decodeEntityRef(required(mapValue(payload), 0), "device"));
600
+ return;
601
+ case 20:
602
+ this.upsertUserSummaryNow(decodeUserSummary(payload));
603
+ return;
604
+ case 24:
605
+ this.upsert("friendships", decodeFriendship(payload), (item) => item.friendshipId);
606
+ return;
607
+ case 30:
608
+ {
609
+ const entry = decodeInboxEntry(payload);
610
+ this.upsert("inbox", entry, (item) => item.id);
611
+ this.onNotification(entry);
612
+ }
613
+ return;
614
+ case 31: {
615
+ const map = mapValue(payload);
616
+ const id = orderedValue(required(map, 0));
617
+ const current = this.current.inbox.get(id);
618
+ if (current)
619
+ this.upsert("inbox", { ...current, read: booleanValue(required(map, 1)) }, (item) => item.id);
620
+ return;
621
+ }
622
+ case 32:
623
+ this.upsert("blocks", decodeBlock(payload), (item) => item.id);
624
+ return;
625
+ case 33:
626
+ this.remove("blocks", decodeEntityRef(required(mapValue(payload), 0), "block"));
627
+ return;
628
+ case 34: {
629
+ const setting = decodeNotificationSettings(payload);
630
+ this.set({
631
+ ...this.current,
632
+ notificationSettings: [
633
+ ...this.current.notificationSettings.filter((candidate) => notificationScopeKey(candidate) !==
634
+ notificationScopeKey(setting)),
635
+ setting,
636
+ ],
637
+ });
638
+ return;
639
+ }
640
+ case 104: {
641
+ const map = mapValue(payload);
642
+ const conversationId = decodeEntityRef(required(map, 0), "conversation");
643
+ const userId = decodeEntityRef(required(map, 1), "user");
644
+ if (userId !== this.current.ownUserId)
645
+ return;
646
+ const readThroughMessageId = orderedValue(required(map, 2));
647
+ const conversation = this.current.conversations.get(conversationId);
648
+ const conversations = new Map(this.current.conversations);
649
+ if (conversation)
650
+ conversations.set(conversationId, {
651
+ ...conversation,
652
+ readThroughMessageId,
653
+ });
654
+ const inbox = new Map(this.current.inbox);
655
+ for (const [id, entry] of inbox) {
656
+ if (!entry.read &&
657
+ entry.conversationId === conversationId &&
658
+ entry.messageId !== undefined &&
659
+ entry.messageId <= readThroughMessageId)
660
+ inbox.set(id, { ...entry, read: true });
661
+ }
662
+ this.set({ ...this.current, conversations, inbox });
663
+ return;
664
+ }
665
+ case 105:
666
+ this.upsert("conversations", decodeConversation(payload), (item) => item.id);
667
+ return;
668
+ case 106:
669
+ this.remove("conversations", decodeEntityRef(required(mapValue(payload), 0), "conversation"));
670
+ return;
671
+ case 200:
672
+ this.upsert("communities", decodeCommunity(payload), (item) => item.id);
673
+ return;
674
+ case 201:
675
+ case 202:
676
+ this.applyCommunityMembership(opcode, payload);
677
+ return;
678
+ case 210:
679
+ this.applyChannelUpsert(decodeCommunityChannel(payload));
680
+ return;
681
+ case 211:
682
+ this.applyChannelRemoval(payload);
683
+ return;
684
+ case 220:
685
+ this.applyRoleUpsert(decodeCommunityRole(payload));
686
+ return;
687
+ case 221:
688
+ this.applyMemberRoles(payload);
689
+ return;
690
+ case 222:
691
+ this.applyRoleRemoval(payload);
692
+ return;
693
+ case 500:
694
+ this.set({
695
+ ...this.current,
696
+ realtimeSettings: decodeRealtimeSettings(payload),
697
+ });
698
+ return;
699
+ default:
700
+ return;
701
+ }
702
+ }
703
+ verifyAndUpsertProfile(profile) {
704
+ if (!profile.badgeAssertion) {
705
+ if (profile.id === this.current.ownUserId)
706
+ this.current = { ...this.current, ownProfile: profile };
707
+ this.upsert("users", profile, (item) => item.id);
708
+ return;
709
+ }
710
+ void this.verifyProfile(profile)
711
+ .then((verified) => {
712
+ if (verified.id === this.current.ownUserId)
713
+ this.current = { ...this.current, ownProfile: verified };
714
+ this.upsert("users", verified, (item) => item.id);
715
+ })
716
+ .catch(this.onPersistenceError);
717
+ }
718
+ setOwnProfile(profile) {
719
+ this.verifyAndUpsertProfile(profile);
720
+ }
721
+ upsertUserSummaryNow(summary) {
722
+ const current = this.current.users.get(summary.id);
723
+ if (current && current.revision > summary.revision)
724
+ return;
725
+ this.upsert("users", current ? { ...current, ...summary } : summary, (item) => item.id);
726
+ }
727
+ applyConversationEvent(conversationId, opcode, payload, timestamp) {
728
+ const messages = new Map(this.current.messages);
729
+ const conversationMessages = new Map(messages.get(conversationId) ?? []);
730
+ if (opcode === 100 || opcode === 101) {
731
+ const message = withDirection(decodeMessage(payload), this.current.ownUserId);
732
+ if (message.conversationId !== conversationId)
733
+ throw new TypeError("Message event belongs to a different conversation");
734
+ this.clearOptimisticRollbacks(conversationId, message.id);
735
+ conversationMessages.set(message.id, message);
736
+ messages.set(conversationId, conversationMessages);
737
+ const users = new Map(this.current.users);
738
+ for (const summary of [message.author, message.replyPreview?.author]) {
739
+ if (!summary)
740
+ continue;
741
+ const current = users.get(summary.id);
742
+ if (!current || current.revision <= summary.revision) {
743
+ users.set(summary.id, current ? { ...current, ...summary } : summary);
744
+ }
745
+ }
746
+ const conversation = this.current.conversations.get(conversationId);
747
+ let inbox = this.current.inbox;
748
+ let notification;
749
+ if (opcode === 100 &&
750
+ conversation?.kind === "community-channel" &&
751
+ message.authorId !== undefined &&
752
+ message.authorId !== this.current.ownUserId) {
753
+ const previous = [...inbox.values()].find((entry) => entry.kind === "unread" &&
754
+ !entry.read &&
755
+ entry.conversationId === conversationId);
756
+ if (previous?.messageId === undefined ||
757
+ previous.messageId < message.id) {
758
+ const updatedInbox = new Map(inbox);
759
+ if (previous)
760
+ updatedInbox.delete(previous.id);
761
+ notification = {
762
+ id: message.id,
763
+ kind: "unread",
764
+ conversationId,
765
+ messageId: message.id,
766
+ senderId: message.authorId,
767
+ preview: message.body.slice(0, 240),
768
+ createdAt: message.timestamp,
769
+ read: false,
770
+ unreadCount: (previous?.unreadCount ?? 0) + 1,
771
+ };
772
+ updatedInbox.set(notification.id, notification);
773
+ inbox = updatedInbox;
774
+ }
775
+ }
776
+ this.set({ ...this.current, users, messages, inbox });
777
+ if (notification)
778
+ this.onNotification(notification);
779
+ return { message };
780
+ }
781
+ else if (opcode === 102) {
782
+ const map = mapValue(payload);
783
+ if (decodeEntityRef(required(map, 0), "conversation") !== conversationId)
784
+ throw new TypeError("Deletion event belongs to a different conversation");
785
+ const messageId = orderedValue(required(map, 1));
786
+ this.clearOptimisticRollbacks(conversationId, messageId);
787
+ const message = conversationMessages.get(messageId);
788
+ if (message) {
789
+ const updated = {
790
+ ...message,
791
+ body: "",
792
+ deletedAt: timestamp.toISOString(),
793
+ revision: unsignedValue(required(map, 2)),
794
+ };
795
+ conversationMessages.set(messageId, {
796
+ ...updated,
797
+ });
798
+ messages.set(conversationId, conversationMessages);
799
+ this.set({ ...this.current, messages });
800
+ return { message: updated };
801
+ }
802
+ return { remove: messageId };
803
+ }
804
+ else if (opcode === 103) {
805
+ const map = mapValue(payload);
806
+ if (decodeEntityRef(required(map, 0), "conversation") !== conversationId)
807
+ throw new TypeError("Reaction event belongs to a different conversation");
808
+ const messageId = orderedValue(required(map, 1));
809
+ this.clearOptimisticRollbacks(conversationId, messageId);
810
+ const senderId = decodeEntityRef(required(map, 2), "user");
811
+ const message = conversationMessages.get(messageId);
812
+ if (message) {
813
+ const ownEmojis = senderId === this.current.ownUserId
814
+ ? arrayValue(required(map, 3)).map(String)
815
+ : (message.reactions ?? [])
816
+ .filter((reaction) => reaction.own)
817
+ .map((reaction) => reaction.emoji);
818
+ const own = new Set(ownEmojis);
819
+ const reactions = decodeReactionSummaries(required(map, 6)).map((reaction) => ({
820
+ ...reaction,
821
+ own: own.has(reaction.emoji),
822
+ }));
823
+ conversationMessages.set(messageId, {
824
+ ...message,
825
+ reactions,
826
+ revision: unsignedValue(required(map, 5)),
827
+ });
828
+ const updated = conversationMessages.get(messageId);
829
+ messages.set(conversationId, conversationMessages);
830
+ this.set({ ...this.current, messages });
831
+ return { message: updated };
832
+ }
833
+ return { remove: messageId };
834
+ }
835
+ return undefined;
836
+ }
837
+ clearOptimisticRollbacks(conversationId, messageId) {
838
+ for (const [requestId, rollback] of this.optimisticRollbacks) {
839
+ if (rollback.conversationId === conversationId &&
840
+ rollback.messageId === messageId)
841
+ this.optimisticRollbacks.delete(requestId);
842
+ }
843
+ }
844
+ applyCommunityMembership(opcode, payload) {
845
+ const map = mapValue(payload);
846
+ const communityId = decodeEntityRef(required(map, 0), "community");
847
+ const userId = decodeEntityRef(required(map, 1), "user");
848
+ const community = this.current.communities.get(communityId);
849
+ if (!community)
850
+ return;
851
+ const members = community.members.filter((member) => member.userId !== userId);
852
+ if (opcode === 201)
853
+ members.push({
854
+ userId,
855
+ joinedAt: timestampValue(required(map, 2)),
856
+ roleIds: [],
857
+ permissions: [],
858
+ revision: 0,
859
+ });
860
+ this.upsert("communities", { ...community, members }, (item) => item.id);
861
+ }
862
+ applyChannelUpsert(channel) {
863
+ const community = this.current.communities.get(channel.communityId);
864
+ if (!community)
865
+ return;
866
+ const existing = community.channels.find((item) => item.id === channel.id);
867
+ this.upsert("communities", {
868
+ ...community,
869
+ channels: [
870
+ ...community.channels.filter((item) => item.id !== channel.id),
871
+ { ...existing, ...channel },
872
+ ],
873
+ }, (item) => item.id);
874
+ }
875
+ applyChannelRemoval(payload) {
876
+ const map = mapValue(payload);
877
+ const communityId = decodeEntityRef(required(map, 0), "community");
878
+ const channelId = decodeEntityRef(required(map, 1), "channel");
879
+ const community = this.current.communities.get(communityId);
880
+ if (community)
881
+ this.upsert("communities", {
882
+ ...community,
883
+ channels: community.channels.filter((item) => item.id !== channelId),
884
+ }, (item) => item.id);
885
+ }
886
+ applyRoleUpsert(role) {
887
+ const community = this.current.communities.get(role.communityId);
888
+ if (!community)
889
+ return;
890
+ this.upsert("communities", {
891
+ ...community,
892
+ roles: [...community.roles.filter((item) => item.id !== role.id), role],
893
+ }, (item) => item.id);
894
+ }
895
+ applyMemberRoles(payload) {
896
+ const map = mapValue(payload);
897
+ const communityId = decodeEntityRef(required(map, 0), "community");
898
+ const userId = decodeEntityRef(required(map, 1), "user");
899
+ const community = this.current.communities.get(communityId);
900
+ if (!community)
901
+ return;
902
+ const roleIds = arrayValue(required(map, 2)).map((entry) => decodeEntityRef(entry, "role"));
903
+ const revision = unsignedValue(required(map, 3));
904
+ this.upsert("communities", {
905
+ ...community,
906
+ members: community.members.map((member) => member.userId === userId ? { ...member, roleIds, revision } : member),
907
+ }, (item) => item.id);
908
+ }
909
+ applyRoleRemoval(payload) {
910
+ const map = mapValue(payload);
911
+ const communityId = decodeEntityRef(required(map, 0), "community");
912
+ const roleId = decodeEntityRef(required(map, 1), "role");
913
+ const community = this.current.communities.get(communityId);
914
+ if (!community)
915
+ return;
916
+ this.upsert("communities", {
917
+ ...community,
918
+ roles: community.roles.filter((role) => role.id !== roleId),
919
+ members: community.members.map((member) => ({
920
+ ...member,
921
+ roleIds: member.roleIds.filter((id) => id !== roleId),
922
+ })),
923
+ }, (item) => item.id);
924
+ }
925
+ upsert(name, value, key) {
926
+ const values = new Map(this.current[name]);
927
+ values.set(key(value), value);
928
+ this.set({ ...this.current, [name]: values });
929
+ }
930
+ remove(name, key) {
931
+ const values = new Map(this.current[name]);
932
+ values.delete(key);
933
+ this.set({ ...this.current, [name]: values });
934
+ }
935
+ set(state) {
936
+ if (state === this.current)
937
+ return;
938
+ this.current = state;
939
+ if (this.deferredNotifications > 0) {
940
+ this.notificationPending = true;
941
+ return;
942
+ }
943
+ this.notify();
944
+ }
945
+ scheduleNotification() {
946
+ if (this.notificationTimer)
947
+ return;
948
+ this.notificationTimer = setTimeout(() => {
949
+ this.notificationTimer = undefined;
950
+ if (this.notificationPending)
951
+ this.notify();
952
+ }, AccountStateController.remoteNotificationBatchMs);
953
+ }
954
+ notify() {
955
+ if (this.notificationTimer)
956
+ clearTimeout(this.notificationTimer);
957
+ this.notificationTimer = undefined;
958
+ this.notificationPending = false;
959
+ this.listeners.forEach((listener) => listener());
960
+ }
961
+ }
962
+ function keyed(values, key) {
963
+ return new Map(values.map((value) => [key(value), value]));
964
+ }
965
+ function mergeProfiles(current, profiles) {
966
+ if (!profiles.length)
967
+ return current;
968
+ const users = new Map(current);
969
+ profiles.forEach((profile) => {
970
+ const existing = users.get(profile.id);
971
+ if (!existing || existing.revision <= profile.revision)
972
+ users.set(profile.id, profile);
973
+ });
974
+ return users;
975
+ }
976
+ function withDirection(message, ownUserId) {
977
+ return {
978
+ ...message,
979
+ outgoing: ownUserId !== undefined && message.authorId === ownUserId,
980
+ };
981
+ }
982
+ function notificationScopeKey(setting) {
983
+ switch (setting.scope.kind) {
984
+ case "account":
985
+ return "account";
986
+ case "conversation":
987
+ return `conversation:${setting.scope.conversationId}`;
988
+ case "community":
989
+ return `community:${setting.scope.communityId}`;
990
+ }
991
+ }
992
+ //# sourceMappingURL=state.js.map