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