Skip to content

Commit e6e3dbb

Browse files
committed
feat(persistence): add support for DraftMessages
1 parent 239ad76 commit e6e3dbb

20 files changed

+5396
-2719
lines changed

packages/stream_chat/lib/src/db/chat_persistence_client.dart

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import 'package:stream_chat/src/core/api/sort_order.dart';
44
import 'package:stream_chat/src/core/models/attachment_file.dart';
55
import 'package:stream_chat/src/core/models/channel_model.dart';
66
import 'package:stream_chat/src/core/models/channel_state.dart';
7+
import 'package:stream_chat/src/core/models/draft.dart';
78
import 'package:stream_chat/src/core/models/event.dart';
89
import 'package:stream_chat/src/core/models/filter.dart';
910
import 'package:stream_chat/src/core/models/member.dart';
@@ -78,32 +79,40 @@ abstract class ChatPersistenceClient {
7879
PaginationParams? messagePagination,
7980
});
8081

82+
/// Get stored [Draft] message by providing channel [cid].
83+
Future<Draft?> getDraftMessageByCid(String cid);
84+
85+
/// Get stored [Draft] message by providing parent message [id].
86+
Future<Draft?> getDraftMessageByParentId(String parentId);
87+
8188
/// Get [ChannelState] data by providing channel [cid]
8289
Future<ChannelState> getChannelStateByCid(
8390
String cid, {
8491
PaginationParams? messagePagination,
8592
PaginationParams? pinnedMessagePagination,
8693
}) async {
87-
final data = await Future.wait([
94+
final (members, reads, channel, messages, pinnedMessages, draft) = await (
8895
getMembersByCid(cid),
8996
getReadsByCid(cid),
9097
getChannelByCid(cid),
9198
getMessagesByCid(cid, messagePagination: messagePagination),
9299
getPinnedMessagesByCid(cid, messagePagination: pinnedMessagePagination),
93-
]);
100+
getDraftMessageByCid(cid),
101+
).wait;
94102

95-
final members = data[0] as List<Member>?;
96-
final membership = userId == null
97-
? null
98-
: members?.firstWhereOrNull((it) => it.userId == userId);
103+
final membership = switch (userId) {
104+
final userId? => members?.firstWhereOrNull((it) => it.userId == userId),
105+
_ => null,
106+
};
99107

100108
return ChannelState(
101109
members: members,
102110
membership: membership,
103-
read: data[1] as List<Read>?,
104-
channel: data[2] as ChannelModel?,
105-
messages: data[3] as List<Message>?,
106-
pinnedMessages: data[4] as List<Message>?,
111+
read: reads,
112+
channel: channel,
113+
messages: messages,
114+
pinnedMessages: pinnedMessages,
115+
draft: draft,
107116
);
108117
}
109118

@@ -157,6 +166,9 @@ abstract class ChatPersistenceClient {
157166
/// Remove a channel by [channelId]
158167
Future<void> deleteChannels(List<String> cids);
159168

169+
/// Removes all the draft messages by draft [messageIds]
170+
Future<void> deleteDraftMessagesByIds(List<String> messageIds);
171+
160172
/// Updates the message data of a particular channel [cid] with
161173
/// the new [messages] data
162174
Future<void> updateMessages(String cid, List<Message> messages) =>
@@ -214,6 +226,9 @@ abstract class ChatPersistenceClient {
214226
/// Updates the poll votes data with the new [pollVotes] data
215227
Future<void> updatePollVotes(List<PollVote> pollVotes);
216228

229+
/// Updates the draft messages data with the new [draftMessages] data
230+
Future<void> updateDraftMessages(List<Draft> draftMessages);
231+
217232
/// Deletes all the reactions by [messageIds]
218233
Future<void> deleteReactionsByMessageId(List<String> messageIds);
219234

@@ -272,6 +287,9 @@ abstract class ChatPersistenceClient {
272287
final pollVotes = <PollVote>[];
273288
final pollVotesToDelete = <String>[];
274289

290+
final drafts = <Draft>[];
291+
final draftsToDelete = <String>[];
292+
275293
for (final state in channelStates) {
276294
final channel = state.channel;
277295
// Continue if channel is not available.
@@ -315,6 +333,14 @@ abstract class ChatPersistenceClient {
315333

316334
pollVotes.addAll(polls.expand(_expandPollVotes));
317335

336+
drafts.addAll([
337+
state.draft,
338+
...?messages?.map((it) => it.draft),
339+
...?pinnedMessages?.map((it) => it.draft),
340+
].nonNulls);
341+
342+
draftsToDelete.addAll(drafts.map((it) => it.message.id));
343+
318344
users.addAll([
319345
channel.createdBy,
320346
...?messages?.map((it) => it.user),
@@ -335,6 +361,7 @@ abstract class ChatPersistenceClient {
335361
deleteReactionsByMessageId(reactionsToDelete),
336362
deletePinnedMessageReactionsByMessageId(pinnedReactionsToDelete),
337363
deletePollVotesByPollIds(pollVotesToDelete),
364+
deleteDraftMessagesByIds(draftsToDelete),
338365
]);
339366

340367
// Updating first as does not depend on any other table.
@@ -357,6 +384,7 @@ abstract class ChatPersistenceClient {
357384
updateReactions(reactions),
358385
updatePinnedMessageReactions(pinnedReactions),
359386
updatePollVotes(pollVotes),
387+
updateDraftMessages(drafts),
360388
]);
361389
}
362390

packages/stream_chat/test/src/db/chat_persistence_client_test.dart

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import 'package:stream_chat/src/core/api/requests.dart';
22
import 'package:stream_chat/src/core/api/sort_order.dart';
33
import 'package:stream_chat/src/core/models/channel_model.dart';
44
import 'package:stream_chat/src/core/models/channel_state.dart';
5+
import 'package:stream_chat/src/core/models/draft.dart';
6+
import 'package:stream_chat/src/core/models/draft_message.dart';
57
import 'package:stream_chat/src/core/models/event.dart';
68
import 'package:stream_chat/src/core/models/filter.dart';
79
import 'package:stream_chat/src/core/models/member.dart';
@@ -55,6 +57,10 @@ class TestPersistenceClient extends ChatPersistenceClient {
5557
@override
5658
Future<void> deletePollVotesByPollIds(List<String> pollIds) => Future.value();
5759

60+
@override
61+
Future<void> deleteDraftMessagesByIds(List<String> messageIds) =>
62+
Future.value();
63+
5864
@override
5965
Future<void> disconnect({bool flush = false}) => throw UnimplementedError();
6066

@@ -98,6 +104,21 @@ class TestPersistenceClient extends ChatPersistenceClient {
98104
@override
99105
Future<List<Read>> getReadsByCid(String cid) async => [];
100106

107+
@override
108+
Future<Draft?> getDraftMessageByCid(String cid) async => Draft(
109+
channelCid: cid,
110+
createdAt: DateTime.now(),
111+
message: DraftMessage(id: 'message-id', text: 'message-text'),
112+
);
113+
114+
@override
115+
Future<Draft?> getDraftMessageByParentId(String parentId) async => Draft(
116+
channelCid: 'test:cid',
117+
createdAt: DateTime.now(),
118+
parentId: parentId,
119+
message: DraftMessage(id: 'message-id', text: 'message-text'),
120+
);
121+
101122
@override
102123
Future<List<Message>> getReplies(String parentId,
103124
{PaginationParams? options}) =>
@@ -152,6 +173,9 @@ class TestPersistenceClient extends ChatPersistenceClient {
152173

153174
@override
154175
Future<void> updatePolls(List<Poll> polls) => Future.value();
176+
177+
@override
178+
Future<void> updateDraftMessages(List<Draft> draftMessages) => Future.value();
155179
}
156180

157181
void main() {
@@ -194,6 +218,20 @@ void main() {
194218
persistenceClient.updatePolls([poll]);
195219
});
196220

221+
test('deleteDraftMessagesByIds', () {
222+
const messageIds = ['message-id'];
223+
persistenceClient.deleteDraftMessagesByIds(messageIds);
224+
});
225+
226+
test('updateDraftMessages', () async {
227+
final draft = Draft(
228+
channelCid: 'test:cid',
229+
createdAt: DateTime.now(),
230+
message: DraftMessage(id: 'message-id', text: 'message-text'),
231+
);
232+
persistenceClient.updateDraftMessages([draft]);
233+
});
234+
197235
test('updateChannelThreads', () async {
198236
const cid = 'test:cid';
199237
final user = User(id: 'test-user-id');

packages/stream_chat_persistence/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
## Upcoming
22

33
- Added `pinnedAt` and `archivedAt` fields on `Member`.
4+
- Added support for DraftMessages.
45

56
## 9.7.0
67

packages/stream_chat_persistence/lib/src/dao/dao.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export 'channel_dao.dart';
22
export 'channel_query_dao.dart';
33
export 'connection_event_dao.dart';
4+
export 'draft_message_dao.dart';
45
export 'member_dao.dart';
56
export 'message_dao.dart';
67
export 'pinned_message_dao.dart';
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import 'package:drift/drift.dart';
2+
import 'package:stream_chat/stream_chat.dart';
3+
import 'package:stream_chat_persistence/src/db/drift_chat_database.dart';
4+
import 'package:stream_chat_persistence/src/entity/entity.dart';
5+
import 'package:stream_chat_persistence/src/mapper/mapper.dart';
6+
7+
part 'draft_message_dao.g.dart';
8+
9+
/// The Data Access Object for operations in [DraftMessages] table.
10+
@DriftAccessor(tables: [DraftMessages, Messages])
11+
class DraftMessageDao extends DatabaseAccessor<DriftChatDatabase>
12+
with _$DraftMessageDaoMixin {
13+
/// Creates a new draft message dao instance
14+
DraftMessageDao(this._db) : super(_db);
15+
16+
final DriftChatDatabase _db;
17+
18+
Future<Draft> _draftFromEntity(DraftMessageEntity entity) async {
19+
// We do not want to fetch the draft message of the parent and quoted
20+
// message because it will create a circular dependency and will
21+
// result in infinite loop.
22+
const fetchDraft = false;
23+
24+
final parentMessage = await switch (entity.parentId) {
25+
final id? => _db.messageDao.getMessageById(id, fetchDraft: fetchDraft),
26+
_ => null,
27+
};
28+
29+
final quotedMessage = await switch (entity.quotedMessageId) {
30+
final id? => _db.messageDao.getMessageById(id, fetchDraft: fetchDraft),
31+
_ => null,
32+
};
33+
34+
final poll = await switch (entity.pollId) {
35+
final id? => _db.pollDao.getPollById(id),
36+
_ => null,
37+
};
38+
39+
return entity.toDraft(
40+
parentMessage: parentMessage,
41+
quotedMessage: quotedMessage,
42+
poll: poll,
43+
);
44+
}
45+
46+
/// Returns the draft message by matching [DraftMessages.id] with [id]
47+
Future<Draft?> getDraftMessageById(String id) async {
48+
final query = select(draftMessages)..where((tbl) => tbl.id.equals(id));
49+
50+
final result = await query.getSingleOrNull();
51+
if (result == null) return null;
52+
53+
return _draftFromEntity(result);
54+
}
55+
56+
/// Returns the draft message by matching [DraftMessages.channelCid].
57+
///
58+
/// Note: This will skip the thread draft messages.
59+
Future<Draft?> getDraftMessageByCid(String cid) async {
60+
final query = select(draftMessages)
61+
..where((tbl) => tbl.channelCid.equals(cid) & tbl.parentId.isNull());
62+
63+
final result = await query.getSingleOrNull();
64+
if (result == null) return null;
65+
66+
return _draftFromEntity(result);
67+
}
68+
69+
/// Returns the draft message by matching [DraftMessages.parentId].
70+
Future<Draft?> getDraftMessageByParentId(String parentId) async {
71+
final query = select(draftMessages)
72+
..where((tbl) => tbl.parentId.equals(parentId));
73+
74+
final result = await query.getSingleOrNull();
75+
if (result == null) return null;
76+
77+
return _draftFromEntity(result);
78+
}
79+
80+
/// Updates the draft message data of a particular channel with
81+
/// the new [messageList] data.
82+
Future<void> updateDraftMessages(List<Draft> draftMessageList) {
83+
return transaction(() async {
84+
for (final draftMessage in draftMessageList) {
85+
final entity = draftMessage.toEntity();
86+
87+
// Find and delete existing drafts with the same channelCid
88+
// and parentId (if any).
89+
final deleteQuery = delete(draftMessages)
90+
..where((tbl) {
91+
var filter = tbl.channelCid.equals(entity.channelCid);
92+
if (entity.parentId case final parentId?) {
93+
filter &= tbl.parentId.equals(parentId);
94+
}
95+
96+
return filter;
97+
});
98+
99+
await deleteQuery.go();
100+
await into(draftMessages).insertOnConflictUpdate(entity);
101+
}
102+
});
103+
}
104+
105+
/// Deletes all the draft messages whose [DraftMessages.id] is present in
106+
/// [messageIds].
107+
Future<void> deleteDraftMessagesByIds(List<String> messageIds) =>
108+
(delete(draftMessages)..where((tbl) => tbl.id.isIn(messageIds))).go();
109+
}

packages/stream_chat_persistence/lib/src/dao/draft_message_dao.g.dart

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)