stream-chat 9.39.0 → 9.41.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/dist/cjs/index.browser.js +236 -33
- package/dist/cjs/index.browser.js.map +3 -3
- package/dist/cjs/index.node.js +236 -33
- package/dist/cjs/index.node.js.map +3 -3
- package/dist/esm/index.mjs +236 -33
- package/dist/esm/index.mjs.map +3 -3
- package/dist/types/channel.d.ts +23 -2
- package/dist/types/client.d.ts +8 -3
- package/dist/types/messageComposer/attachmentManager.d.ts +3 -3
- package/dist/types/messageComposer/configuration/types.d.ts +11 -1
- package/dist/types/messageComposer/types.d.ts +2 -0
- package/dist/types/offline-support/offline_support_api.d.ts +22 -0
- package/dist/types/offline-support/types.d.ts +14 -0
- package/dist/types/offline-support/util.d.ts +11 -0
- package/package.json +1 -1
- package/src/channel.ts +25 -0
- package/src/client.ts +55 -2
- package/src/messageComposer/attachmentManager.ts +83 -16
- package/src/messageComposer/configuration/configuration.ts +1 -0
- package/src/messageComposer/configuration/types.ts +12 -0
- package/src/messageComposer/types.ts +2 -0
- package/src/offline-support/offline_support_api.ts +124 -3
- package/src/offline-support/types.ts +18 -0
- package/src/offline-support/util.ts +32 -0
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
APIErrorResponse,
|
|
3
|
+
ChannelResponse,
|
|
4
|
+
Event,
|
|
5
|
+
LocalMessage,
|
|
6
|
+
Message,
|
|
7
|
+
MessageResponse,
|
|
8
|
+
} from '../types';
|
|
2
9
|
|
|
3
10
|
import type {
|
|
4
11
|
OfflineDBApi,
|
|
@@ -11,7 +18,8 @@ import type { StreamChat } from '../client';
|
|
|
11
18
|
import type { AxiosError } from 'axios';
|
|
12
19
|
import { OfflineDBSyncManager } from './offline_sync_manager';
|
|
13
20
|
import { StateStore } from '../store';
|
|
14
|
-
import { runDetached } from '../utils';
|
|
21
|
+
import { localMessageToNewMessagePayload, runDetached } from '../utils';
|
|
22
|
+
import { isMessageUpdateReplayable } from './util';
|
|
15
23
|
|
|
16
24
|
/**
|
|
17
25
|
* Abstract base class for an offline database implementation used with StreamChat.
|
|
@@ -310,6 +318,16 @@ export abstract class AbstractOfflineDB implements OfflineDBApi {
|
|
|
310
318
|
*/
|
|
311
319
|
abstract addPendingTask: OfflineDBApi['addPendingTask'];
|
|
312
320
|
|
|
321
|
+
/**
|
|
322
|
+
* @abstract
|
|
323
|
+
* Updates a pending task in the DB, given its ID.
|
|
324
|
+
* Will return the prepared queries for delayed execution (even if they are
|
|
325
|
+
* already executed).
|
|
326
|
+
* @param {DBUpdatePendingTaskType} options
|
|
327
|
+
* @returns {Promise<ExecuteBatchDBQueriesType>}
|
|
328
|
+
*/
|
|
329
|
+
abstract updatePendingTask: OfflineDBApi['updatePendingTask'];
|
|
330
|
+
|
|
313
331
|
/**
|
|
314
332
|
* @abstract
|
|
315
333
|
* Deletes a pending task from the DB, given its ID.
|
|
@@ -1076,7 +1094,7 @@ export abstract class AbstractOfflineDB implements OfflineDBApi {
|
|
|
1076
1094
|
return await attemptTaskExecution();
|
|
1077
1095
|
} catch (e) {
|
|
1078
1096
|
if (!this.shouldSkipQueueingTask(e as AxiosError<APIErrorResponse>)) {
|
|
1079
|
-
await this.
|
|
1097
|
+
await this.handleAddPendingTask({ task });
|
|
1080
1098
|
}
|
|
1081
1099
|
throw e;
|
|
1082
1100
|
}
|
|
@@ -1092,13 +1110,112 @@ export abstract class AbstractOfflineDB implements OfflineDBApi {
|
|
|
1092
1110
|
private shouldSkipQueueingTask = (error: AxiosError<APIErrorResponse>) =>
|
|
1093
1111
|
error?.response?.data?.code === 4 || error?.response?.data?.code === 17;
|
|
1094
1112
|
|
|
1113
|
+
private mergeFailedMessageUpdateIntoPendingSendMessage = ({
|
|
1114
|
+
editedMessage,
|
|
1115
|
+
pendingMessage,
|
|
1116
|
+
}: {
|
|
1117
|
+
editedMessage: LocalMessage | Partial<MessageResponse>;
|
|
1118
|
+
pendingMessage: Message;
|
|
1119
|
+
}) => {
|
|
1120
|
+
const normalizedEditedMessageSource = {
|
|
1121
|
+
...editedMessage,
|
|
1122
|
+
} as LocalMessage & { message_text_updated_at?: string };
|
|
1123
|
+
|
|
1124
|
+
if (editedMessage.status === 'failed') {
|
|
1125
|
+
delete normalizedEditedMessageSource.message_text_updated_at;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
const normalizedEditedMessage = localMessageToNewMessagePayload(
|
|
1129
|
+
normalizedEditedMessageSource,
|
|
1130
|
+
);
|
|
1131
|
+
const pendingMessageStatus = (pendingMessage as { status?: string }).status;
|
|
1132
|
+
|
|
1133
|
+
return {
|
|
1134
|
+
...pendingMessage,
|
|
1135
|
+
...normalizedEditedMessage,
|
|
1136
|
+
...(typeof pendingMessageStatus !== 'undefined'
|
|
1137
|
+
? { status: pendingMessageStatus }
|
|
1138
|
+
: {}),
|
|
1139
|
+
} as Message;
|
|
1140
|
+
};
|
|
1141
|
+
|
|
1142
|
+
private isPendingSendMessageTask = (
|
|
1143
|
+
task: PendingTask,
|
|
1144
|
+
): task is Extract<PendingTask, { type: 'send-message' }> =>
|
|
1145
|
+
task.type === 'send-message';
|
|
1146
|
+
|
|
1147
|
+
private handleOfflineFailedUpdateMessagePendingTask = async (
|
|
1148
|
+
task: Extract<PendingTask, { type: 'update-message' }>,
|
|
1149
|
+
) => {
|
|
1150
|
+
const [message] = task.payload;
|
|
1151
|
+
if (!message.id) {
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
const pendingTasks = await this.getPendingTasks({ messageId: message.id });
|
|
1156
|
+
const pendingSendMessageTask = pendingTasks.find(this.isPendingSendMessageTask);
|
|
1157
|
+
|
|
1158
|
+
if (!pendingSendMessageTask) {
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
const updatedPendingSendMessage = this.mergeFailedMessageUpdateIntoPendingSendMessage(
|
|
1163
|
+
{
|
|
1164
|
+
editedMessage: message,
|
|
1165
|
+
pendingMessage: pendingSendMessageTask.payload[0],
|
|
1166
|
+
},
|
|
1167
|
+
);
|
|
1168
|
+
|
|
1169
|
+
const updatedPendingTask: Extract<PendingTask, { type: 'send-message' }> = {
|
|
1170
|
+
...pendingSendMessageTask,
|
|
1171
|
+
payload: [updatedPendingSendMessage, pendingSendMessageTask.payload[1]],
|
|
1172
|
+
};
|
|
1173
|
+
|
|
1174
|
+
if (pendingSendMessageTask.id) {
|
|
1175
|
+
await this.updatePendingTask({
|
|
1176
|
+
id: pendingSendMessageTask.id,
|
|
1177
|
+
task: updatedPendingTask,
|
|
1178
|
+
});
|
|
1179
|
+
return;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
await this.addPendingTask({
|
|
1183
|
+
...updatedPendingTask,
|
|
1184
|
+
id: undefined,
|
|
1185
|
+
});
|
|
1186
|
+
};
|
|
1187
|
+
|
|
1188
|
+
/**
|
|
1189
|
+
* Central ingress for persisting pending tasks. It either stores the task as-is
|
|
1190
|
+
* or rewrites an existing pending `send-message` task for offline edits of failed messages.
|
|
1191
|
+
*/
|
|
1192
|
+
public handleAddPendingTask = async ({ task }: { task: PendingTask }) => {
|
|
1193
|
+
if (task.type === 'update-message' && !isMessageUpdateReplayable(task.payload[0])) {
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
if (
|
|
1198
|
+
task.type === 'update-message' &&
|
|
1199
|
+
!this.client.wsConnection?.isHealthy &&
|
|
1200
|
+
task.payload[0].status === 'failed'
|
|
1201
|
+
) {
|
|
1202
|
+
await this.handleOfflineFailedUpdateMessagePendingTask(task);
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
await this.addPendingTask(task);
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1095
1209
|
/**
|
|
1096
1210
|
* Executes a task from the list of supported pending tasks. Currently supported pending tasks
|
|
1097
1211
|
* are:
|
|
1212
|
+
* - Updating a message
|
|
1098
1213
|
* - Deleting a message
|
|
1099
1214
|
* - Sending a reaction
|
|
1100
1215
|
* - Removing a reaction
|
|
1101
1216
|
* - Sending a message
|
|
1217
|
+
* - Creating a draft
|
|
1218
|
+
* - Deleting a draft
|
|
1102
1219
|
* It will throw if we try to execute a pending task that is not supported.
|
|
1103
1220
|
* @param task - The task we want to execute
|
|
1104
1221
|
* @param isPendingTask - a control value telling us if it's an actual pending task being executed
|
|
@@ -1108,6 +1225,10 @@ export abstract class AbstractOfflineDB implements OfflineDBApi {
|
|
|
1108
1225
|
{ task }: { task: PendingTask },
|
|
1109
1226
|
isPendingTask = false,
|
|
1110
1227
|
) => {
|
|
1228
|
+
if (task.type === 'update-message') {
|
|
1229
|
+
return await this.client._updateMessage(...task.payload);
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1111
1232
|
if (task.type === 'delete-message') {
|
|
1112
1233
|
return await this.client._deleteMessage(...task.payload);
|
|
1113
1234
|
}
|
|
@@ -227,6 +227,16 @@ export type DBDeletePendingTaskType = {
|
|
|
227
227
|
id: number;
|
|
228
228
|
};
|
|
229
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Update a pending task by ID.
|
|
232
|
+
*/
|
|
233
|
+
export type DBUpdatePendingTaskType = {
|
|
234
|
+
/** ID of the pending task. */
|
|
235
|
+
id: number;
|
|
236
|
+
/** The next task payload to persist. */
|
|
237
|
+
task: PendingTask;
|
|
238
|
+
};
|
|
239
|
+
|
|
230
240
|
/**
|
|
231
241
|
* Options to delete a reaction from a message.
|
|
232
242
|
*/
|
|
@@ -372,6 +382,9 @@ export interface OfflineDBApi {
|
|
|
372
382
|
addPendingTask: (task: PendingTask) => Promise<() => Promise<void>>;
|
|
373
383
|
getPendingTasks: (conditions?: DBGetPendingTasksType) => Promise<PendingTask[]>;
|
|
374
384
|
deleteDraft: (options: DBDeleteDraftType) => Promise<ExecuteBatchDBQueriesType>;
|
|
385
|
+
updatePendingTask: (
|
|
386
|
+
options: DBUpdatePendingTaskType,
|
|
387
|
+
) => Promise<ExecuteBatchDBQueriesType>;
|
|
375
388
|
deletePendingTask: (
|
|
376
389
|
options: DBDeletePendingTaskType,
|
|
377
390
|
) => Promise<ExecuteBatchDBQueriesType>;
|
|
@@ -397,6 +410,7 @@ export type OfflineDBState = {
|
|
|
397
410
|
};
|
|
398
411
|
|
|
399
412
|
export type PendingTaskTypes = {
|
|
413
|
+
updateMessage: 'update-message';
|
|
400
414
|
deleteMessage: 'delete-message';
|
|
401
415
|
deleteReaction: 'delete-reaction';
|
|
402
416
|
sendReaction: 'send-reaction';
|
|
@@ -417,6 +431,10 @@ export type PendingTask = {
|
|
|
417
431
|
payload: Parameters<Channel['sendReaction']>;
|
|
418
432
|
type: PendingTaskTypes['sendReaction'];
|
|
419
433
|
}
|
|
434
|
+
| {
|
|
435
|
+
payload: Parameters<StreamChat['updateMessage']>;
|
|
436
|
+
type: PendingTaskTypes['updateMessage'];
|
|
437
|
+
}
|
|
420
438
|
| {
|
|
421
439
|
payload: Parameters<StreamChat['deleteMessage']>;
|
|
422
440
|
type: PendingTaskTypes['deleteMessage'];
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Attachment, LocalMessage, MessageResponse } from '../types';
|
|
2
|
+
|
|
3
|
+
export const isLocalUrl = (value: string | undefined) =>
|
|
4
|
+
!!value && !value.startsWith('http');
|
|
5
|
+
|
|
6
|
+
export const isAttachmentReplayable = (attachment: Attachment) => {
|
|
7
|
+
if (!attachment || typeof attachment !== 'object') {
|
|
8
|
+
return true;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return !isLocalUrl(attachment.asset_url) && !isLocalUrl(attachment.image_url);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const isMessageUpdateReplayable = (
|
|
15
|
+
message: LocalMessage | Partial<MessageResponse>,
|
|
16
|
+
) => !message.attachments?.some((attachment) => !isAttachmentReplayable(attachment));
|
|
17
|
+
|
|
18
|
+
export const getPendingTaskChannelData = (cid?: string) => {
|
|
19
|
+
if (!cid) {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const separatorIndex = cid.indexOf(':');
|
|
24
|
+
if (separatorIndex <= 0 || separatorIndex === cid.length - 1) {
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
channelId: cid.slice(separatorIndex + 1),
|
|
30
|
+
channelType: cid.slice(0, separatorIndex),
|
|
31
|
+
};
|
|
32
|
+
};
|