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.
@@ -1,4 +1,11 @@
1
- import type { APIErrorResponse, ChannelResponse, Event } from '../types';
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.addPendingTask(task);
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
+ };