Skip to content

Commit 75087db

Browse files
committed
feat: add offline support for draft messages
1 parent 13e388d commit 75087db

17 files changed

+442
-4
lines changed

package/src/store/OfflineDB.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,12 @@ export class OfflineDB extends AbstractOfflineDB {
3030

3131
upsertPoll = api.upsertPoll;
3232

33+
upsertDraft = api.upsertDraft;
34+
35+
getDraft = api.getDraft;
36+
37+
deleteDraft = api.deleteDraft;
38+
3339
upsertChannelData = api.upsertChannelData;
3440

3541
upsertReads = api.upsertReads;

package/src/store/SqliteClient.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import type { PreparedBatchQueries, PreparedQueries, Scalar, Table } from './typ
2828
* This way usage @op-engineering/op-sqlite package is scoped to a single class/file.
2929
*/
3030
export class SqliteClient {
31-
static dbVersion = 9;
31+
static dbVersion = 10;
3232

3333
static dbName = DB_NAME;
3434
static dbLocation = DB_LOCATION;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { createDeleteQuery } from '../sqlite-utils/createDeleteQuery';
2+
import { SqliteClient } from '../SqliteClient';
3+
4+
export const deleteDraft = async ({
5+
cid,
6+
parent_id,
7+
execute = true,
8+
}: {
9+
cid: string;
10+
parent_id?: string;
11+
execute?: boolean;
12+
}) => {
13+
const query = createDeleteQuery('draft', {
14+
cid,
15+
parentId: parent_id,
16+
});
17+
18+
SqliteClient.logger?.('info', 'deleteDraft', {
19+
cid,
20+
execute,
21+
});
22+
23+
if (execute) {
24+
await SqliteClient.executeSql.apply(null, query);
25+
}
26+
27+
return [query];
28+
};

package/src/store/apis/getChannelMessages.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ export const getChannelMessages = async ({
3333
}
3434
messageIdVsReactions[reaction.messageId].push(reaction);
3535
});
36+
37+
// Populate the polls.
3638
const messageIdsVsPolls: Record<string, TableRow<'poll'>> = {};
3739
const pollsById: Record<string, TableRow<'poll'>> = {};
3840
const messagesWithPolls = messageRows.filter((message) => !!message.poll_id);

package/src/store/apis/getChannels.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { ChannelAPIResponse } from 'stream-chat';
22

33
import { getChannelMessages } from './getChannelMessages';
4+
import { getDraftForChannels } from './getDraftsForChannels';
45
import { getMembers } from './getMembers';
56
import { getReads } from './getReads';
67
import { selectChannels } from './queries/selectChannels';
@@ -26,8 +27,9 @@ export const getChannels = async ({
2627
}): Promise<Omit<ChannelAPIResponse, 'duration'>[]> => {
2728
SqliteClient.logger?.('info', 'getChannels', { channelIds, currentUserId });
2829

29-
const [channels, cidVsMembers, cidVsReads, cidVsMessages] = await Promise.all([
30+
const [channels, cidVsDraft, cidVsMembers, cidVsReads, cidVsMessages] = await Promise.all([
3031
selectChannels({ channelIds }),
32+
getDraftForChannels({ channelIds, currentUserId }),
3133
getMembers({ channelIds }),
3234
getReads({ channelIds }),
3335
getChannelMessages({
@@ -39,6 +41,7 @@ export const getChannels = async ({
3941
// Enrich the channels with state
4042
return channels.map((c) => ({
4143
...mapStorableToChannel(c),
44+
draft: cidVsDraft[c.cid],
4245
members: cidVsMembers[c.cid] || [],
4346
membership: (cidVsMembers[c.cid] || []).find((member) => member.user_id === currentUserId),
4447
messages: cidVsMessages[c.cid] || [],

package/src/store/apis/getDraft.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { selectMessageForId } from './queries/selectMessageById';
2+
3+
import { mapStorableToDraft } from '../mappers/mapStorableToDraft';
4+
import { createSelectQuery } from '../sqlite-utils/createSelectQuery';
5+
import { SqliteClient } from '../SqliteClient';
6+
import { TableRow } from '../types';
7+
8+
export const getDraft = async ({
9+
cid,
10+
currentUserId,
11+
parent_id,
12+
}: {
13+
cid: string;
14+
currentUserId: string;
15+
parent_id?: string;
16+
}) => {
17+
SqliteClient.logger?.('info', 'getDraft', { cid, parent_id });
18+
19+
const query = createSelectQuery('draft', ['*'], { cid, parentId: parent_id });
20+
21+
const rows = await SqliteClient.executeSql.apply(null, query);
22+
23+
if (!rows.length) return null;
24+
25+
const draftRow = rows[0];
26+
27+
const draftMessageQuery = createSelectQuery('draftMessage', ['*'], {
28+
id: draftRow.draftMessageId,
29+
});
30+
const draftMessageRows = await SqliteClient.executeSql.apply(null, draftMessageQuery);
31+
32+
const channelQuery = createSelectQuery('channels', ['*'], { cid });
33+
const channelRows = await SqliteClient.executeSql.apply(null, channelQuery);
34+
35+
const quotedMessageRows = await selectMessageForId(draftRow.quotedMessageId);
36+
37+
const polls = (await SqliteClient.executeSql.apply(
38+
null,
39+
createSelectQuery('poll', ['*'], {
40+
id: quotedMessageRows?.poll_id,
41+
}),
42+
)) as unknown as TableRow<'poll'>[];
43+
44+
return mapStorableToDraft({
45+
channelRow: channelRows[0] as unknown as TableRow<'channels'>,
46+
currentUserId,
47+
draftMessageRow: draftMessageRows[0] as unknown as TableRow<'draftMessage'>,
48+
draftRow: draftRow[0] as unknown as TableRow<'draft'>,
49+
pollRow: polls[0],
50+
quotedMessageRow: quotedMessageRows,
51+
});
52+
};
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { DraftResponse } from 'stream-chat';
2+
3+
import { selectMessageForId } from './queries/selectMessageById';
4+
5+
import { mapStorableToDraft } from '../mappers/mapStorableToDraft';
6+
import { createSelectQuery } from '../sqlite-utils/createSelectQuery';
7+
import { SqliteClient } from '../SqliteClient';
8+
import { TableRow } from '../types';
9+
10+
export const getDraftForChannels = async ({
11+
channelIds,
12+
currentUserId,
13+
}: {
14+
channelIds: string[];
15+
currentUserId: string;
16+
}) => {
17+
SqliteClient.logger?.('info', 'getDraftsForChannel', { channelIds });
18+
19+
const query = createSelectQuery('draft', ['*'], { cid: channelIds });
20+
21+
const rows = await SqliteClient.executeSql.apply(null, query);
22+
23+
const cidVsDrafts: Record<string, DraftResponse> = {};
24+
25+
for (const row of rows) {
26+
const draftMessageQuery = createSelectQuery('draftMessage', ['*'], {
27+
id: row.draftMessageId,
28+
});
29+
const draftMessageRows = await SqliteClient.executeSql.apply(null, draftMessageQuery);
30+
31+
const channelQuery = createSelectQuery('channels', ['*'], { cid: row.cid });
32+
const channelRows = await SqliteClient.executeSql.apply(null, channelQuery);
33+
34+
const quotedMessageRow = await selectMessageForId(row.quotedMessageId);
35+
36+
const polls = (await SqliteClient.executeSql.apply(
37+
null,
38+
createSelectQuery('poll', ['*'], {
39+
id: quotedMessageRow?.poll_id,
40+
}),
41+
)) as unknown as TableRow<'poll'>[];
42+
43+
cidVsDrafts[row.cid] = mapStorableToDraft({
44+
channelRow: channelRows[0] as unknown as TableRow<'channels'>,
45+
currentUserId,
46+
draftMessageRow: draftMessageRows[0] as unknown as TableRow<'draftMessage'>,
47+
draftRow: row as unknown as TableRow<'draft'>,
48+
pollRow: polls[0],
49+
quotedMessageRow,
50+
});
51+
}
52+
return cidVsDrafts;
53+
};

package/src/store/apis/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,6 @@ export * from './getPendingTasks';
3131
export * from './softDeleteMessage';
3232
export * from './channelExists';
3333
export * from './dropPendingTasks';
34+
export * from './getDraft';
35+
export * from './upsertDraft';
36+
export * from './deleteDraft';
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { tables } from '../../schema';
2+
import { SqliteClient } from '../../SqliteClient';
3+
import type { TableRowJoinedUser } from '../../types';
4+
5+
export const selectMessageForId = async (
6+
msgId?: string,
7+
): Promise<TableRowJoinedUser<'messages'> | undefined> => {
8+
if (!msgId) {
9+
return undefined;
10+
}
11+
12+
const messagesColumnNames = Object.keys(tables.messages.columns)
13+
.map((name) => `'${name}', a.${name}`)
14+
.join(', ');
15+
const userColumnNames = Object.keys(tables.users.columns)
16+
.map((name) => `'${name}', b.${name}`)
17+
.join(', ');
18+
19+
SqliteClient.logger?.('info', 'selectMessagesForId', {
20+
msgId,
21+
});
22+
23+
const result = await SqliteClient.executeSql(
24+
`SELECT
25+
json_object(
26+
'user', json_object(
27+
${userColumnNames}
28+
),
29+
${messagesColumnNames}
30+
) as value
31+
FROM (
32+
SELECT
33+
*
34+
FROM messages
35+
WHERE id = ?
36+
) a
37+
LEFT JOIN
38+
users b
39+
ON b.id = a.userId`,
40+
[msgId],
41+
);
42+
43+
return result[0] ? JSON.parse(result[0].value) : undefined;
44+
};

package/src/store/apis/upsertChannels.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type { ChannelAPIResponse, ChannelMemberResponse } from 'stream-chat';
22

3+
import { upsertDraft } from './upsertDraft';
34
import { upsertMembers } from './upsertMembers';
45

56
import { upsertMessages } from './upsertMessages';
@@ -31,13 +32,18 @@ export const upsertChannels = async ({
3132
for (const channel of channels) {
3233
queries.push(createUpsertQuery('channels', mapChannelDataToStorable(channel.channel)));
3334

34-
const { members, membership, messages, read } = channel;
35+
const { draft, members, membership, messages, read } = channel;
3536
if (
3637
membership &&
3738
!members.includes((m: ChannelMemberResponse) => m.user?.id === membership.user?.id)
3839
) {
3940
members.push({ ...membership, user_id: membership.user?.id });
4041
}
42+
43+
if (draft) {
44+
queries = queries.concat(await upsertDraft({ draft }));
45+
}
46+
4147
queries = queries.concat(
4248
await upsertMembers({
4349
cid: channel.channel.cid,

0 commit comments

Comments
 (0)