Skip to content

Commit 4a168ef

Browse files
authored
feat: handle offline support for drafts (#1559)
## CLA - [x] I have signed the [Stream CLA](https://docs.google.com/forms/d/e/1FAIpQLScFKsKkAJI7mhCr7K9rEIOpqIDThrWxuvxnwUq2XkHyG154vQ/viewform) (required). - [x] Code changes are tested ## Description of the changes, What, Why and How? The goal of the PR is to handle offline support for draft messages and also allow composing a message when the composition is not empty when the offline DB is enabled. Thanks to @isekovanic for the help with the offline support explanation here. ## Changelog -
1 parent 8e225d5 commit 4a168ef

File tree

6 files changed

+424
-1
lines changed

6 files changed

+424
-1
lines changed

src/messageComposer/messageComposer.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,10 @@ export class MessageComposer extends WithSubscriptions {
280280
}
281281

282282
get hasSendableData() {
283+
// If the offline mode is enabled, we allow sending a message if the composition is not empty.
284+
if (this.client.offlineDb) {
285+
return !this.compositionIsEmpty;
286+
}
283287
return !!(
284288
(!this.attachmentManager.uploadsInProgressCount &&
285289
(!this.textComposer.textIsEmpty ||
@@ -669,6 +673,48 @@ export class MessageComposer extends WithSubscriptions {
669673
await this.channel.deleteDraft({ parent_id: this.threadId ?? undefined });
670674
};
671675

676+
getDraft = async () => {
677+
if (this.editedMessage || !this.config.drafts.enabled || !this.client.userID) return;
678+
679+
const draftFromOfflineDB = await this.client.offlineDb?.getDraft({
680+
cid: this.channel.cid,
681+
userId: this.client.userID,
682+
parent_id: this.threadId ?? undefined,
683+
});
684+
685+
if (draftFromOfflineDB) {
686+
this.initState({ composition: draftFromOfflineDB });
687+
}
688+
689+
try {
690+
const response = await this.channel.getDraft({
691+
parent_id: this.threadId ?? undefined,
692+
});
693+
694+
const { draft } = response;
695+
696+
if (!draft) return;
697+
698+
this.client.offlineDb?.executeQuerySafely(
699+
(db) =>
700+
db.upsertDraft({
701+
draft,
702+
}),
703+
{ method: 'upsertDraft' },
704+
);
705+
706+
this.initState({ composition: draft });
707+
} catch (error) {
708+
this.client.notifications.add({
709+
message: 'Failed to get the draft',
710+
origin: {
711+
emitter: 'MessageComposer',
712+
context: { composer: this },
713+
},
714+
});
715+
}
716+
};
717+
672718
createPoll = async () => {
673719
const composition = await this.pollComposer.compose();
674720
if (!composition || !composition.data.id) return;

src/offline-support/offline_support_api.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,35 @@ export abstract class AbstractOfflineDB implements OfflineDBApi {
194194
*/
195195
abstract updateMessage: OfflineDBApi['updateMessage'];
196196

197+
/**
198+
* @abstract
199+
* Fetches the provided draft from the DB. Should return as close to
200+
* the server side DraftResponse as possible.
201+
* @param {DBGetDraftType} options
202+
* @returns {Promise<DraftResponse | null>}
203+
*/
204+
abstract getDraft: OfflineDBApi['getDraft'];
205+
/**
206+
* @abstract
207+
* Upserts a draft in the DB.
208+
* Will write to the draft table upserting the draft.
209+
* Will return the prepared queries for delayed execution (even if they are
210+
* already executed).
211+
* @param {DBUpsertDraftType} options
212+
* @returns {Promise<ExecuteBatchDBQueriesType>}
213+
*/
214+
abstract upsertDraft: OfflineDBApi['upsertDraft'];
215+
/**
216+
* @abstract
217+
* Deletes a draft from the DB.
218+
* Will write to the draft table removing the draft.
219+
* Will return the prepared queries for delayed execution (even if they are
220+
* already executed).
221+
* @param {DBDeleteDraftType} options
222+
* @returns {Promise<ExecuteBatchDBQueriesType>}
223+
*/
224+
abstract deleteDraft: OfflineDBApi['deleteDraft'];
225+
197226
/**
198227
* @abstract
199228
* Fetches the provided channels from the DB and aggregates all data associated
@@ -896,6 +925,44 @@ export abstract class AbstractOfflineDB implements OfflineDBApi {
896925
);
897926
};
898927

928+
/**
929+
* A utility handler for all draft events:
930+
* - draft.updated -> updateDraft
931+
* - draft.deleted -> deleteDraft
932+
* @param event - the WS event we are trying to process
933+
* @param execute - whether to immediately execute the operation.
934+
*/
935+
handleDraftEvent = async ({
936+
event,
937+
execute = true,
938+
}: {
939+
event: Event;
940+
execute?: boolean;
941+
}) => {
942+
const { cid, draft, type } = event;
943+
944+
if (!draft) return [];
945+
946+
if (type === 'draft.updated') {
947+
return await this.upsertDraft({
948+
draft,
949+
execute,
950+
});
951+
}
952+
953+
if (type === 'draft.deleted') {
954+
if (!cid) return [];
955+
956+
return await this.deleteDraft({
957+
cid,
958+
parent_id: draft.parent_id,
959+
execute,
960+
});
961+
}
962+
963+
return [];
964+
};
965+
899966
/**
900967
* A generic event handler that decides which DB API to invoke based on
901968
* event.type for all events we are currently handling. It is used to both
@@ -944,6 +1011,10 @@ export abstract class AbstractOfflineDB implements OfflineDBApi {
9441011
return await this.handleChannelVisibilityEvent({ event, execute });
9451012
}
9461013

1014+
if (type === 'draft.updated' || type === 'draft.deleted') {
1015+
return await this.handleDraftEvent({ event, execute });
1016+
}
1017+
9471018
// Note: It is a bit counter-intuitive that we do not touch the messages in the
9481019
// offline DB when receiving notification.message_new, however we do this
9491020
// because we anyway cannot get the messages for a channel until we run

src/offline-support/types.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
ChannelMemberResponse,
66
ChannelResponse,
77
ChannelSort,
8+
DraftResponse,
89
LocalMessage,
910
MessageResponse,
1011
PollResponse,
@@ -300,6 +301,31 @@ export type DBChannelExistsType = {
300301
cid: string;
301302
};
302303

304+
export type DBUpsertDraftType = {
305+
/** Draft message to upsert. */
306+
draft: DraftResponse;
307+
/** Whether to immediately execute the operation. */
308+
execute?: boolean;
309+
};
310+
311+
export type DBGetDraftType = {
312+
/** Channel ID for which to get the draft. */
313+
cid: string;
314+
/** ID of the user requesting the draft. */
315+
userId: string;
316+
/** Optional parent ID for the parent message in thread, if applicable. */
317+
parent_id?: string;
318+
};
319+
320+
export type DBDeleteDraftType = {
321+
/** Channel ID for which to delete the draft. */
322+
cid: string;
323+
/** Optional parent ID for the parent message in thread, if applicable. */
324+
parent_id?: string;
325+
/** Whether to immediately execute the operation. */
326+
execute?: boolean;
327+
};
328+
303329
/**
304330
* Represents a list of batch SQL queries to be executed.
305331
*/
@@ -317,6 +343,7 @@ export interface OfflineDBApi {
317343
upsertAppSettings: (
318344
options: DBUpsertAppSettingsType,
319345
) => Promise<ExecuteBatchDBQueriesType>;
346+
upsertDraft: (options: DBUpsertDraftType) => Promise<ExecuteBatchDBQueriesType>;
320347
upsertPoll: (options: DBUpsertPollType) => Promise<ExecuteBatchDBQueriesType>;
321348
upsertChannelData: (
322349
options: DBUpsertChannelDataType,
@@ -326,6 +353,7 @@ export interface OfflineDBApi {
326353
upsertMembers: (options: DBUpsertMembersType) => Promise<ExecuteBatchDBQueriesType>;
327354
updateReaction: (options: DBUpdateReactionType) => Promise<ExecuteBatchDBQueriesType>;
328355
updateMessage: (options: DBUpdateMessageType) => Promise<ExecuteBatchDBQueriesType>;
356+
getDraft: (options: DBGetDraftType) => Promise<DraftResponse | null>;
329357
getChannels: (
330358
options: DBGetChannelsType,
331359
) => Promise<Omit<ChannelAPIResponse, 'duration'>[] | null>;
@@ -341,6 +369,7 @@ export interface OfflineDBApi {
341369
executeSqlBatch: (queries: ExecuteBatchDBQueriesType) => Promise<unknown>;
342370
addPendingTask: (task: PendingTask) => Promise<() => Promise<void>>;
343371
getPendingTasks: (conditions?: DBGetPendingTasksType) => Promise<PendingTask[]>;
372+
deleteDraft: (options: DBDeleteDraftType) => Promise<ExecuteBatchDBQueriesType>;
344373
deletePendingTask: (
345374
options: DBDeletePendingTaskType,
346375
) => Promise<ExecuteBatchDBQueriesType>;

0 commit comments

Comments
 (0)