@ermis-network/ermis-chat-sdk 1.0.9 → 2.0.1
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.
- package/README.md +330 -0
- package/bin/init-call.js +9 -0
- package/dist/encryption/index.browser.cjs +13045 -0
- package/dist/encryption/index.browser.cjs.map +1 -0
- package/dist/encryption/index.browser.mjs +12959 -0
- package/dist/encryption/index.browser.mjs.map +1 -0
- package/dist/encryption/index.cjs +13045 -0
- package/dist/encryption/index.cjs.map +1 -0
- package/dist/encryption/index.d.mts +3 -0
- package/dist/encryption/index.d.ts +3 -0
- package/dist/encryption/index.mjs +12959 -0
- package/dist/encryption/index.mjs.map +1 -0
- package/dist/index-CcvHIY5q.d.mts +4988 -0
- package/dist/index-CcvHIY5q.d.ts +4988 -0
- package/dist/index.browser.cjs +20399 -6823
- package/dist/index.browser.cjs.map +1 -1
- package/dist/index.browser.full-bundle.min.js +20 -18
- package/dist/index.browser.full-bundle.min.js.map +1 -1
- package/dist/index.browser.mjs +20315 -6790
- package/dist/index.browser.mjs.map +1 -1
- package/dist/index.cjs +20400 -6824
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +167 -1356
- package/dist/index.d.ts +167 -1356
- package/dist/index.mjs +20312 -6787
- package/dist/index.mjs.map +1 -1
- package/dist/wasm_worker.worker.mjs +1600 -0
- package/dist/wasm_worker.worker.mjs.map +1 -0
- package/package.json +22 -7
- package/public/e2ee-media-stream-worker.js +627 -0
- package/public/ermis_call_node_wasm_bg.wasm +0 -0
- package/public/openmls_wasm_bg.wasm +0 -0
- package/src/attachment_utils.ts +0 -148
- package/src/auth.ts +0 -352
- package/src/channel.ts +0 -1806
- package/src/channel_state.ts +0 -607
- package/src/client.ts +0 -1617
- package/src/client_state.ts +0 -55
- package/src/connection.ts +0 -587
- package/src/ermis_call_node.ts +0 -978
- package/src/errors.ts +0 -60
- package/src/events.ts +0 -46
- package/src/hevc_decoder_config.ts +0 -305
- package/src/index.ts +0 -16
- package/src/media_stream_receiver.ts +0 -525
- package/src/media_stream_sender.ts +0 -400
- package/src/shims/empty.ts +0 -1
- package/src/signal_message.ts +0 -146
- package/src/system_message.ts +0 -117
- package/src/token_manager.ts +0 -48
- package/src/types.ts +0 -581
- package/src/utils.ts +0 -534
- package/src/wasm/ermis_call_node_wasm.d.ts +0 -154
- package/src/wasm/ermis_call_node_wasm.js +0 -1498
package/src/channel_state.ts
DELETED
|
@@ -1,607 +0,0 @@
|
|
|
1
|
-
import { Channel } from './channel';
|
|
2
|
-
import {
|
|
3
|
-
ChannelMemberResponse,
|
|
4
|
-
ChannelMembership,
|
|
5
|
-
FormatMessageResponse,
|
|
6
|
-
Event,
|
|
7
|
-
ExtendableGenerics,
|
|
8
|
-
DefaultGenerics,
|
|
9
|
-
MessageSetType,
|
|
10
|
-
MessageResponse,
|
|
11
|
-
ReactionResponse,
|
|
12
|
-
UserResponse,
|
|
13
|
-
} from './types';
|
|
14
|
-
import { addToMessageList } from './utils';
|
|
15
|
-
|
|
16
|
-
type ChannelReadStatus<ErmisChatGenerics extends ExtendableGenerics = DefaultGenerics> = Record<
|
|
17
|
-
string,
|
|
18
|
-
{
|
|
19
|
-
last_read: Date;
|
|
20
|
-
unread_messages: number;
|
|
21
|
-
user: UserResponse<ErmisChatGenerics>;
|
|
22
|
-
last_read_message_id?: string;
|
|
23
|
-
last_send?: string;
|
|
24
|
-
}
|
|
25
|
-
>;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* ChannelState - A container class for the channel state.
|
|
29
|
-
* This class synchronously binds to a `Channel` and holds the single source of truth for
|
|
30
|
-
* messages, read status, watchers, and typing indicators locally on the client.
|
|
31
|
-
*/
|
|
32
|
-
export class ChannelState<ErmisChatGenerics extends ExtendableGenerics = DefaultGenerics> {
|
|
33
|
-
_channel: Channel<ErmisChatGenerics>;
|
|
34
|
-
/** The current count of users actively watching (having an open WebSocket) this channel. */
|
|
35
|
-
watcher_count: number;
|
|
36
|
-
/** A dictionary of active typing events gracefully keyed by the user's ID. */
|
|
37
|
-
typing: Record<string, Event<ErmisChatGenerics>>;
|
|
38
|
-
/** A dictionary of read states mapped per user's ID detailing the last viewed message. */
|
|
39
|
-
read: ChannelReadStatus<ErmisChatGenerics>;
|
|
40
|
-
/** The locally cached array of pinned messages across the channel. */
|
|
41
|
-
pinnedMessages: Array<ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>>;
|
|
42
|
-
/** A directory of users actively watching the channel, keyed by User ID. */
|
|
43
|
-
watchers: Record<string, UserResponse<ErmisChatGenerics>>;
|
|
44
|
-
/** A comprehensive directory mapping user IDs to their member status in the channel. */
|
|
45
|
-
members: Record<string, ChannelMemberResponse<ErmisChatGenerics>>;
|
|
46
|
-
/** The count of messages not yet read by the currently authenticated user. */
|
|
47
|
-
unreadCount: number;
|
|
48
|
-
/** Information detailing the authenticated user's own membership relation to this channel. */
|
|
49
|
-
membership: ChannelMembership<ErmisChatGenerics>;
|
|
50
|
-
/** Timestamp indicating when the very last message was created in this chat. */
|
|
51
|
-
last_message_at: Date | null;
|
|
52
|
-
/** Designates if the local channel state is entirely synchronized with the backend history. */
|
|
53
|
-
isUpToDate: boolean;
|
|
54
|
-
messageSets: {
|
|
55
|
-
isCurrent: boolean;
|
|
56
|
-
isLatest: boolean;
|
|
57
|
-
messages: Array<ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>>;
|
|
58
|
-
}[] = [];
|
|
59
|
-
topics?: Channel<ErmisChatGenerics>[] = [];
|
|
60
|
-
constructor(channel: Channel<ErmisChatGenerics>) {
|
|
61
|
-
this._channel = channel;
|
|
62
|
-
this.watcher_count = 0;
|
|
63
|
-
this.typing = {};
|
|
64
|
-
this.read = {};
|
|
65
|
-
this.initMessages();
|
|
66
|
-
this.pinnedMessages = [];
|
|
67
|
-
this.watchers = {};
|
|
68
|
-
this.members = {};
|
|
69
|
-
this.membership = {};
|
|
70
|
-
this.unreadCount = 0;
|
|
71
|
-
this.isUpToDate = true;
|
|
72
|
-
this.last_message_at = channel?.state?.last_message_at != null ? new Date(channel.state.last_message_at) : null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
get messages() {
|
|
76
|
-
return this.messageSets.find((s) => s.isCurrent)?.messages || [];
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
set messages(messages: Array<ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>>) {
|
|
80
|
-
const index = this.messageSets.findIndex((s) => s.isCurrent);
|
|
81
|
-
this.messageSets[index].messages = messages;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
get latestMessages() {
|
|
85
|
-
return this.messageSets.find((s) => s.isLatest)?.messages || [];
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
set latestMessages(messages: Array<ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>>) {
|
|
89
|
-
const index = this.messageSets.findIndex((s) => s.isLatest);
|
|
90
|
-
this.messageSets[index].messages = messages;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Pushes a new message directly into the sorted array of the local tracking state.
|
|
95
|
-
* Useful to achieve optimistic UI updates locally.
|
|
96
|
-
*
|
|
97
|
-
* @param newMessage - The message context payload to insert.
|
|
98
|
-
* @param timestampChanged - Specifies if the underlying `created_at` timestamp mutated.
|
|
99
|
-
* @param addIfDoesNotExist - Append it strictly if its ID doesn't already exist.
|
|
100
|
-
* @param messageSetToAddToIfDoesNotExist - Specifies which message set scope to manipulate.
|
|
101
|
-
*/
|
|
102
|
-
addMessageSorted(
|
|
103
|
-
newMessage: MessageResponse<ErmisChatGenerics>,
|
|
104
|
-
timestampChanged = false,
|
|
105
|
-
addIfDoesNotExist = true,
|
|
106
|
-
messageSetToAddToIfDoesNotExist: MessageSetType = 'latest',
|
|
107
|
-
) {
|
|
108
|
-
return this.addMessagesSorted(
|
|
109
|
-
[newMessage],
|
|
110
|
-
timestampChanged,
|
|
111
|
-
false,
|
|
112
|
-
addIfDoesNotExist,
|
|
113
|
-
messageSetToAddToIfDoesNotExist,
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
formatMessage(message: MessageResponse<ErmisChatGenerics>): FormatMessageResponse<ErmisChatGenerics> {
|
|
118
|
-
return {
|
|
119
|
-
...message,
|
|
120
|
-
/**
|
|
121
|
-
* @deprecated please use `html`
|
|
122
|
-
*/
|
|
123
|
-
__html: message.html,
|
|
124
|
-
// parse the date..
|
|
125
|
-
pinned_at: message.pinned_at ? new Date(message.pinned_at) : null,
|
|
126
|
-
created_at: message.created_at ? new Date(message.created_at) : new Date(),
|
|
127
|
-
updated_at: message.updated_at ? new Date(message.updated_at) : null,
|
|
128
|
-
status: message.status || 'received',
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
addMessagesSorted(
|
|
133
|
-
newMessages: MessageResponse<ErmisChatGenerics>[],
|
|
134
|
-
timestampChanged = false,
|
|
135
|
-
initializing = false,
|
|
136
|
-
addIfDoesNotExist = true,
|
|
137
|
-
messageSetToAddToIfDoesNotExist: MessageSetType = 'current',
|
|
138
|
-
) {
|
|
139
|
-
const { messagesToAdd, targetMessageSetIndex } = this.findTargetMessageSet(
|
|
140
|
-
newMessages,
|
|
141
|
-
addIfDoesNotExist,
|
|
142
|
-
messageSetToAddToIfDoesNotExist,
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
for (let i = 0; i < messagesToAdd.length; i += 1) {
|
|
146
|
-
// If message is already formatted we can skip the tasks below
|
|
147
|
-
// This will be true for messages that are already present at the state -> this happens when we perform merging of message sets
|
|
148
|
-
// This will be also true for message previews used by some SDKs
|
|
149
|
-
const isMessageFormatted = messagesToAdd[i].created_at instanceof Date;
|
|
150
|
-
let message: ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>;
|
|
151
|
-
if (isMessageFormatted) {
|
|
152
|
-
message = messagesToAdd[i] as ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>;
|
|
153
|
-
} else {
|
|
154
|
-
message = this.formatMessage(messagesToAdd[i] as MessageResponse<ErmisChatGenerics>);
|
|
155
|
-
|
|
156
|
-
if (message.user && this._channel?.cid) {
|
|
157
|
-
/**
|
|
158
|
-
* Store the reference to user for this channel, so that when we have to
|
|
159
|
-
* handle updates to user, we can use the reference map, to determine which
|
|
160
|
-
* channels need to be updated with updated user object.
|
|
161
|
-
*/
|
|
162
|
-
this._channel.getClient().state.updateUserReference(message.user, this._channel.cid);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (!this.last_message_at) {
|
|
166
|
-
this.last_message_at = new Date(message.created_at.getTime());
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (message.created_at.getTime() > this.last_message_at.getTime()) {
|
|
170
|
-
this.last_message_at = new Date(message.created_at.getTime());
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Cross-reference with pinnedMessages to ensure `pinned` and `pinned_at` are accurate
|
|
175
|
-
if (!message.pinned) {
|
|
176
|
-
const pinnedMatch = this.pinnedMessages.find((pm) => pm.id === message.id);
|
|
177
|
-
if (pinnedMatch) {
|
|
178
|
-
message.pinned = true;
|
|
179
|
-
message.pinned_at = pinnedMatch.pinned_at || new Date();
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
// update or append the messages...
|
|
184
|
-
const parentID = message.parent_id;
|
|
185
|
-
|
|
186
|
-
// add to the given message set
|
|
187
|
-
if (!parentID && targetMessageSetIndex !== -1) {
|
|
188
|
-
this.messageSets[targetMessageSetIndex].messages = this._addToMessageList(
|
|
189
|
-
this.messageSets[targetMessageSetIndex].messages,
|
|
190
|
-
message,
|
|
191
|
-
timestampChanged,
|
|
192
|
-
'created_at',
|
|
193
|
-
addIfDoesNotExist,
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return {
|
|
199
|
-
messageSet: this.messageSets[targetMessageSetIndex],
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
addPinnedMessages(pinnedMessages: MessageResponse<ErmisChatGenerics>[]) {
|
|
204
|
-
for (let i = 0; i < pinnedMessages.length; i += 1) {
|
|
205
|
-
this.addPinnedMessage(pinnedMessages[i]);
|
|
206
|
-
}
|
|
207
|
-
// Sort by pinned_at descending (newest pin first)
|
|
208
|
-
this.pinnedMessages.sort((a, b) => {
|
|
209
|
-
const timeA = a.pinned_at ? new Date(a.pinned_at).getTime() : 0;
|
|
210
|
-
const timeB = b.pinned_at ? new Date(b.pinned_at).getTime() : 0;
|
|
211
|
-
return timeB - timeA;
|
|
212
|
-
});
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
addPinnedMessage(pinnedMessage: MessageResponse<ErmisChatGenerics>) {
|
|
216
|
-
const formatted = this.formatMessage(pinnedMessage);
|
|
217
|
-
// Remove existing entry if present (to avoid duplicates)
|
|
218
|
-
this.pinnedMessages = this.pinnedMessages.filter((msg) => msg.id !== formatted.id);
|
|
219
|
-
// Add to the beginning of the list (newest pin first)
|
|
220
|
-
this.pinnedMessages = [formatted, ...this.pinnedMessages];
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
removePinnedMessage(message: MessageResponse<ErmisChatGenerics>) {
|
|
224
|
-
const { result } = this.removeMessageFromArray(this.pinnedMessages, message);
|
|
225
|
-
this.pinnedMessages = result;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
addReaction(
|
|
229
|
-
reaction: ReactionResponse<ErmisChatGenerics>,
|
|
230
|
-
message?: MessageResponse<ErmisChatGenerics>,
|
|
231
|
-
enforce_unique?: boolean,
|
|
232
|
-
) {
|
|
233
|
-
if (!message) return;
|
|
234
|
-
const messageWithReaction = message;
|
|
235
|
-
this._updateMessage(message, (msg) => {
|
|
236
|
-
messageWithReaction.own_reactions = this._addOwnReactionToMessage(msg.own_reactions, reaction, enforce_unique);
|
|
237
|
-
return this.formatMessage(messageWithReaction);
|
|
238
|
-
});
|
|
239
|
-
return messageWithReaction;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
_addOwnReactionToMessage(
|
|
243
|
-
ownReactions: ReactionResponse<ErmisChatGenerics>[] | null | undefined,
|
|
244
|
-
reaction: ReactionResponse<ErmisChatGenerics>,
|
|
245
|
-
enforce_unique?: boolean,
|
|
246
|
-
) {
|
|
247
|
-
if (enforce_unique) {
|
|
248
|
-
ownReactions = [];
|
|
249
|
-
} else {
|
|
250
|
-
ownReactions = this._removeOwnReactionFromMessage(ownReactions, reaction);
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
ownReactions = ownReactions || [];
|
|
254
|
-
if (this._channel.getClient().userID === reaction.user_id) {
|
|
255
|
-
ownReactions.push(reaction);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return ownReactions;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
_removeOwnReactionFromMessage(
|
|
262
|
-
ownReactions: ReactionResponse<ErmisChatGenerics>[] | null | undefined,
|
|
263
|
-
reaction: ReactionResponse<ErmisChatGenerics>,
|
|
264
|
-
) {
|
|
265
|
-
if (ownReactions) {
|
|
266
|
-
return ownReactions.filter((item) => item.user_id !== reaction.user_id || item.type !== reaction.type);
|
|
267
|
-
}
|
|
268
|
-
return ownReactions;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
removeReaction(reaction: ReactionResponse<ErmisChatGenerics>, message?: MessageResponse<ErmisChatGenerics>) {
|
|
272
|
-
if (!message) return;
|
|
273
|
-
const messageWithReaction = message;
|
|
274
|
-
this._updateMessage(message, (msg) => {
|
|
275
|
-
messageWithReaction.own_reactions = this._removeOwnReactionFromMessage(msg.own_reactions, reaction);
|
|
276
|
-
return this.formatMessage(messageWithReaction);
|
|
277
|
-
});
|
|
278
|
-
return messageWithReaction;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
removeQuotedMessageReferences(message: MessageResponse<ErmisChatGenerics>) {
|
|
282
|
-
const parseMessage = (m: ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>) =>
|
|
283
|
-
({
|
|
284
|
-
...m,
|
|
285
|
-
created_at: m.created_at.toISOString(),
|
|
286
|
-
pinned_at: m.pinned_at?.toISOString(),
|
|
287
|
-
updated_at: m.updated_at?.toISOString(),
|
|
288
|
-
} as unknown as MessageResponse<ErmisChatGenerics>);
|
|
289
|
-
|
|
290
|
-
this.messageSets.forEach((set) => {
|
|
291
|
-
const updatedMessages = set.messages
|
|
292
|
-
.filter((msg) => msg.quoted_message_id === message.id)
|
|
293
|
-
.map(parseMessage)
|
|
294
|
-
.map((msg) => ({ ...msg, quoted_message: { ...message, attachments: [] } }));
|
|
295
|
-
|
|
296
|
-
this.addMessagesSorted(updatedMessages, true);
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
_updateMessage(
|
|
301
|
-
message: {
|
|
302
|
-
id?: string;
|
|
303
|
-
parent_id?: string;
|
|
304
|
-
pinned?: boolean;
|
|
305
|
-
},
|
|
306
|
-
updateFunc: (
|
|
307
|
-
msg: ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>,
|
|
308
|
-
) => ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>,
|
|
309
|
-
) {
|
|
310
|
-
const { parent_id, pinned } = message;
|
|
311
|
-
|
|
312
|
-
if (!parent_id) {
|
|
313
|
-
const messageSetIndex = this.findMessageSetIndex(message);
|
|
314
|
-
if (messageSetIndex !== -1) {
|
|
315
|
-
const msgIndex = this.messageSets[messageSetIndex].messages.findIndex((msg) => msg.id === message.id);
|
|
316
|
-
if (msgIndex !== -1) {
|
|
317
|
-
this.messageSets[messageSetIndex].messages[msgIndex] = updateFunc(
|
|
318
|
-
this.messageSets[messageSetIndex].messages[msgIndex],
|
|
319
|
-
);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
if (pinned) {
|
|
325
|
-
const msgIndex = this.pinnedMessages.findIndex((msg) => msg.id === message.id);
|
|
326
|
-
if (msgIndex !== -1) {
|
|
327
|
-
this.pinnedMessages[msgIndex] = updateFunc(this.pinnedMessages[msgIndex]);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
setIsUpToDate = (isUpToDate: boolean) => {
|
|
333
|
-
this.isUpToDate = isUpToDate;
|
|
334
|
-
};
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Update the status of a message by ID (used for optimistic UI).
|
|
338
|
-
*/
|
|
339
|
-
updateMessageStatus(messageId: string, status: string) {
|
|
340
|
-
this._updateMessage({ id: messageId }, (msg) => ({
|
|
341
|
-
...msg,
|
|
342
|
-
status,
|
|
343
|
-
}));
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
_addToMessageList(
|
|
347
|
-
messages: Array<ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>>,
|
|
348
|
-
message: ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>,
|
|
349
|
-
timestampChanged = false,
|
|
350
|
-
sortBy: 'pinned_at' | 'created_at' = 'created_at',
|
|
351
|
-
addIfDoesNotExist = true,
|
|
352
|
-
) {
|
|
353
|
-
return addToMessageList(messages, message, timestampChanged, sortBy, addIfDoesNotExist);
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
removeMessage(messageToRemove: { id: string; messageSetIndex?: number; parent_id?: string }) {
|
|
357
|
-
let isRemoved = false;
|
|
358
|
-
const messageSetIndex = messageToRemove.messageSetIndex ?? this.findMessageSetIndex(messageToRemove);
|
|
359
|
-
if (messageSetIndex !== -1) {
|
|
360
|
-
const { removed, result: messages } = this.removeMessageFromArray(
|
|
361
|
-
this.messageSets[messageSetIndex].messages,
|
|
362
|
-
messageToRemove,
|
|
363
|
-
);
|
|
364
|
-
this.messageSets[messageSetIndex].messages = messages;
|
|
365
|
-
isRemoved = removed;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
return isRemoved;
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
removeMessageFromArray = (
|
|
372
|
-
msgArray: Array<ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>>,
|
|
373
|
-
msg: { id: string; parent_id?: string },
|
|
374
|
-
) => {
|
|
375
|
-
const result = msgArray.filter((message) => !(!!message.id && !!msg.id && message.id === msg.id));
|
|
376
|
-
|
|
377
|
-
return { removed: result.length < msgArray.length, result };
|
|
378
|
-
};
|
|
379
|
-
|
|
380
|
-
/**
|
|
381
|
-
* Refreshes internal user references cascading across all presently active messages.
|
|
382
|
-
* Invoked instantly whenever an underlying user's profile metadata updates (e.g. name or avatar changes).
|
|
383
|
-
*
|
|
384
|
-
* @param user - The newly formatted and populated User details object.
|
|
385
|
-
*/
|
|
386
|
-
updateUserMessages = (user: UserResponse<ErmisChatGenerics>) => {
|
|
387
|
-
const _updateUserMessages = (
|
|
388
|
-
messages: Array<ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>>,
|
|
389
|
-
user: UserResponse<ErmisChatGenerics>,
|
|
390
|
-
) => {
|
|
391
|
-
for (let i = 0; i < messages.length; i++) {
|
|
392
|
-
const m = messages[i];
|
|
393
|
-
const latestReactions = m?.latest_reactions || [];
|
|
394
|
-
if (m.user?.id === user.id) {
|
|
395
|
-
messages[i] = {
|
|
396
|
-
...m,
|
|
397
|
-
user: m.user?.id === user.id ? user : m.user,
|
|
398
|
-
};
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
if (latestReactions && latestReactions.some((r) => r.user?.id === user.id)) {
|
|
402
|
-
messages[i] = {
|
|
403
|
-
...m,
|
|
404
|
-
latest_reactions: latestReactions.map((r) => (r.user?.id === user.id ? { ...r, user } : r)),
|
|
405
|
-
};
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
};
|
|
409
|
-
|
|
410
|
-
this.messageSets.forEach((set) => _updateUserMessages(set.messages, user));
|
|
411
|
-
|
|
412
|
-
_updateUserMessages(this.pinnedMessages, user);
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
deleteUserMessages = (user: UserResponse<ErmisChatGenerics>, hardDelete = false) => {
|
|
416
|
-
const _deleteUserMessages = (
|
|
417
|
-
messages: Array<ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>>,
|
|
418
|
-
user: UserResponse<ErmisChatGenerics>,
|
|
419
|
-
hardDelete = false,
|
|
420
|
-
) => {
|
|
421
|
-
for (let i = 0; i < messages.length; i++) {
|
|
422
|
-
const m = messages[i];
|
|
423
|
-
if (m.user?.id !== user.id) {
|
|
424
|
-
continue;
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
if (hardDelete) {
|
|
428
|
-
/**
|
|
429
|
-
* In case of hard delete, we need to strip down all text, html,
|
|
430
|
-
* attachments and all the custom properties on message
|
|
431
|
-
*/
|
|
432
|
-
messages[i] = {
|
|
433
|
-
cid: m.cid,
|
|
434
|
-
created_at: m.created_at,
|
|
435
|
-
deleted_at: new Date().toISOString(),
|
|
436
|
-
id: m.id,
|
|
437
|
-
latest_reactions: [],
|
|
438
|
-
mentioned_users: [],
|
|
439
|
-
own_reactions: [],
|
|
440
|
-
parent_id: m.parent_id,
|
|
441
|
-
reply_count: m.reply_count,
|
|
442
|
-
status: m.status,
|
|
443
|
-
type: 'deleted',
|
|
444
|
-
updated_at: m.updated_at,
|
|
445
|
-
user: m.user,
|
|
446
|
-
} as unknown as ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>;
|
|
447
|
-
} else {
|
|
448
|
-
messages[i] = {
|
|
449
|
-
...m,
|
|
450
|
-
type: 'deleted',
|
|
451
|
-
deleted_at: new Date().toISOString(),
|
|
452
|
-
};
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
};
|
|
456
|
-
|
|
457
|
-
this.messageSets.forEach((set) => _deleteUserMessages(set.messages, user, hardDelete));
|
|
458
|
-
|
|
459
|
-
_deleteUserMessages(this.pinnedMessages, user, hardDelete);
|
|
460
|
-
};
|
|
461
|
-
|
|
462
|
-
filterErrorMessages() {
|
|
463
|
-
const filteredMessages = this.latestMessages.filter((message) => message.type !== 'error');
|
|
464
|
-
|
|
465
|
-
this.latestMessages = filteredMessages;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
clean() {
|
|
469
|
-
const now = new Date();
|
|
470
|
-
// prevent old users from showing up as typing
|
|
471
|
-
for (const [userID, lastEvent] of Object.entries(this.typing)) {
|
|
472
|
-
const receivedAt =
|
|
473
|
-
typeof lastEvent.received_at === 'string'
|
|
474
|
-
? new Date(lastEvent.received_at)
|
|
475
|
-
: lastEvent.received_at || new Date();
|
|
476
|
-
if (now.getTime() - receivedAt.getTime() > 7000) {
|
|
477
|
-
delete this.typing[userID];
|
|
478
|
-
this._channel.getClient().dispatchEvent({
|
|
479
|
-
cid: this._channel.cid,
|
|
480
|
-
type: 'typing.stop',
|
|
481
|
-
user: { id: userID },
|
|
482
|
-
} as Event<ErmisChatGenerics>);
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
clearMessages() {
|
|
488
|
-
this.initMessages();
|
|
489
|
-
this.pinnedMessages = [];
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
initMessages() {
|
|
493
|
-
this.messageSets = [{ messages: [], isLatest: true, isCurrent: true }];
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
async loadMessageIntoState(messageId: string | 'latest', parentMessageId?: string, limit = 25) {
|
|
497
|
-
let messageSetIndex: number;
|
|
498
|
-
let switchedToMessageSet = false;
|
|
499
|
-
const messageIdToFind = parentMessageId || messageId;
|
|
500
|
-
if (messageId === 'latest') {
|
|
501
|
-
if (this.messages === this.latestMessages) {
|
|
502
|
-
return;
|
|
503
|
-
}
|
|
504
|
-
messageSetIndex = this.messageSets.findIndex((s) => s.isLatest);
|
|
505
|
-
} else {
|
|
506
|
-
messageSetIndex = this.findMessageSetIndex({ id: messageIdToFind });
|
|
507
|
-
}
|
|
508
|
-
if (messageSetIndex !== -1) {
|
|
509
|
-
this.switchToMessageSet(messageSetIndex);
|
|
510
|
-
switchedToMessageSet = true;
|
|
511
|
-
}
|
|
512
|
-
if (!switchedToMessageSet) {
|
|
513
|
-
await this._channel.query({ messages: { id_around: messageIdToFind, limit } }, 'new');
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
messageSetIndex = this.findMessageSetIndex({ id: messageIdToFind });
|
|
517
|
-
if (messageSetIndex !== -1) {
|
|
518
|
-
this.switchToMessageSet(messageSetIndex);
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
findMessage(messageId: string, parentMessageId?: string) {
|
|
523
|
-
const messageSetIndex = this.findMessageSetIndex({ id: messageId });
|
|
524
|
-
if (messageSetIndex === -1) {
|
|
525
|
-
return undefined;
|
|
526
|
-
}
|
|
527
|
-
return this.messageSets[messageSetIndex].messages.find((m) => m.id === messageId);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
private switchToMessageSet(index: number) {
|
|
531
|
-
const currentMessages = this.messageSets.find((s) => s.isCurrent);
|
|
532
|
-
if (!currentMessages) {
|
|
533
|
-
return;
|
|
534
|
-
}
|
|
535
|
-
currentMessages.isCurrent = false;
|
|
536
|
-
this.messageSets[index].isCurrent = true;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
private areMessageSetsOverlap(messages1: Array<{ id: string }>, messages2: Array<{ id: string }>) {
|
|
540
|
-
return messages1.some((m1) => messages2.find((m2) => m1.id === m2.id));
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
private findMessageSetIndex(message: { id?: string }) {
|
|
544
|
-
return this.messageSets.findIndex((set) => !!set.messages.find((m) => m.id === message.id));
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
private findTargetMessageSet(
|
|
548
|
-
newMessages: MessageResponse<ErmisChatGenerics>[],
|
|
549
|
-
addIfDoesNotExist = true,
|
|
550
|
-
messageSetToAddToIfDoesNotExist: MessageSetType = 'current',
|
|
551
|
-
) {
|
|
552
|
-
let messagesToAdd: (
|
|
553
|
-
| MessageResponse<ErmisChatGenerics>
|
|
554
|
-
| ReturnType<ChannelState<ErmisChatGenerics>['formatMessage']>
|
|
555
|
-
)[] = newMessages;
|
|
556
|
-
let targetMessageSetIndex!: number;
|
|
557
|
-
if (addIfDoesNotExist) {
|
|
558
|
-
const overlappingMessageSetIndices = this.messageSets
|
|
559
|
-
.map((_, i) => i)
|
|
560
|
-
.filter((i) => this.areMessageSetsOverlap(this.messageSets[i].messages, newMessages));
|
|
561
|
-
switch (messageSetToAddToIfDoesNotExist) {
|
|
562
|
-
case 'new':
|
|
563
|
-
if (overlappingMessageSetIndices.length > 0) {
|
|
564
|
-
targetMessageSetIndex = overlappingMessageSetIndices[0];
|
|
565
|
-
} else if (newMessages.some((m) => !m.parent_id)) {
|
|
566
|
-
this.messageSets.push({ messages: [], isCurrent: false, isLatest: false });
|
|
567
|
-
targetMessageSetIndex = this.messageSets.length - 1;
|
|
568
|
-
}
|
|
569
|
-
break;
|
|
570
|
-
case 'current':
|
|
571
|
-
targetMessageSetIndex = this.messageSets.findIndex((s) => s.isCurrent);
|
|
572
|
-
break;
|
|
573
|
-
case 'latest':
|
|
574
|
-
targetMessageSetIndex = this.messageSets.findIndex((s) => s.isLatest);
|
|
575
|
-
break;
|
|
576
|
-
default:
|
|
577
|
-
targetMessageSetIndex = -1;
|
|
578
|
-
}
|
|
579
|
-
// when merging the target set will be the first one from the overlapping message sets
|
|
580
|
-
const mergeTargetMessageSetIndex = overlappingMessageSetIndices.splice(0, 1)[0];
|
|
581
|
-
const mergeSourceMessageSetIndices = [...overlappingMessageSetIndices];
|
|
582
|
-
if (mergeTargetMessageSetIndex !== undefined && mergeTargetMessageSetIndex !== targetMessageSetIndex) {
|
|
583
|
-
mergeSourceMessageSetIndices.push(targetMessageSetIndex);
|
|
584
|
-
}
|
|
585
|
-
// merge message sets
|
|
586
|
-
if (mergeSourceMessageSetIndices.length > 0) {
|
|
587
|
-
const target = this.messageSets[mergeTargetMessageSetIndex];
|
|
588
|
-
const sources = this.messageSets.filter((_, i) => mergeSourceMessageSetIndices.indexOf(i) !== -1);
|
|
589
|
-
sources.forEach((messageSet) => {
|
|
590
|
-
target.isLatest = target.isLatest || messageSet.isLatest;
|
|
591
|
-
target.isCurrent = target.isCurrent || messageSet.isCurrent;
|
|
592
|
-
messagesToAdd = [...messagesToAdd, ...messageSet.messages];
|
|
593
|
-
});
|
|
594
|
-
sources.forEach((s) => this.messageSets.splice(this.messageSets.indexOf(s), 1));
|
|
595
|
-
const overlappingMessageSetIndex = this.messageSets.findIndex((s) =>
|
|
596
|
-
this.areMessageSetsOverlap(s.messages, newMessages),
|
|
597
|
-
);
|
|
598
|
-
targetMessageSetIndex = overlappingMessageSetIndex;
|
|
599
|
-
}
|
|
600
|
-
} else {
|
|
601
|
-
// assumes that all new messages belong to the same set
|
|
602
|
-
targetMessageSetIndex = this.findMessageSetIndex(newMessages[0]);
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
return { targetMessageSetIndex, messagesToAdd };
|
|
606
|
-
}
|
|
607
|
-
}
|