Skip to content

Commit d8f8f66

Browse files
authored
feat(llc): add support for draft messages (#2200)
1 parent e027192 commit d8f8f66

36 files changed

+2665
-103
lines changed

packages/stream_chat/CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
## Upcoming
22

3+
✅ Added
4+
5+
- Added support for 'DraftMessage' feature, which allows users to save draft messages in channels.
6+
Several methods have been added to the `Client` and `Channel` class to manage draft messages:
7+
- `channel.createDraft`: Saves a draft message for a specific channel.
8+
- `channel.getDraft`: Retrieves a draft message for a specific channel.
9+
- `channel.deleteDraft`: Deletes a draft message for a specific channel.
10+
- `client.queryDrafts`: Queries draft messages created by the current user.
11+
312
🔄 Changed
413

514
- Improved read event handling in the `Channel` class to properly update unread state information.

packages/stream_chat/lib/src/client/channel.dart

Lines changed: 127 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
// ignore_for_file: avoid_redundant_argument_values
2+
13
import 'dart:async';
24
import 'dart:math';
35

46
import 'package:collection/collection.dart';
57
import 'package:rxdart/rxdart.dart';
68
import 'package:stream_chat/src/client/retry_queue.dart';
7-
import 'package:stream_chat/src/core/models/banned_user.dart';
89
import 'package:stream_chat/src/core/util/utils.dart';
910
import 'package:stream_chat/stream_chat.dart';
1011
import 'package:synchronized/synchronized.dart';
@@ -1017,6 +1018,35 @@ class Channel {
10171018
},
10181019
);
10191020

1021+
/// Creates or updates a new [draft] for this channel.
1022+
Future<CreateDraftResponse> createDraft(
1023+
DraftMessage draft,
1024+
) {
1025+
_checkInitialized();
1026+
return _client.createDraft(draft, id!, type);
1027+
}
1028+
1029+
/// Retrieves the draft for this channel.
1030+
///
1031+
/// Optionally, provide a [parentId] to get the draft for a specific thread.
1032+
Future<GetDraftResponse> getDraft({
1033+
String? parentId,
1034+
}) {
1035+
_checkInitialized();
1036+
return _client.getDraft(id!, type, parentId: parentId);
1037+
}
1038+
1039+
/// Deletes the draft for this channel.
1040+
///
1041+
/// Optionally, provide a [parentId] to delete the draft for a specific
1042+
/// thread.
1043+
Future<EmptyResponse> deleteDraft({
1044+
String? parentId,
1045+
}) {
1046+
_checkInitialized();
1047+
return _client.deleteDraft(id!, type, parentId: parentId);
1048+
}
1049+
10201050
/// Send a file to this channel.
10211051
Future<SendFileResponse> sendFile(
10221052
AttachmentFile file, {
@@ -2010,6 +2040,14 @@ class ChannelClientState {
20102040

20112041
_listenMessageUpdated();
20122042

2043+
/* Start of draft events */
2044+
2045+
_listenDraftUpdated();
2046+
2047+
_listenDraftDeleted();
2048+
2049+
/* End of draft events */
2050+
20132051
_listenReactions();
20142052

20152053
_listenReactionDeleted();
@@ -2515,6 +2553,48 @@ class ChannelClientState {
25152553
}));
25162554
}
25172555

2556+
void _listenDraftUpdated() {
2557+
_subscriptions.add(
2558+
_channel.on(EventType.draftUpdated).listen((event) {
2559+
final draft = event.draft;
2560+
if (draft == null) return;
2561+
2562+
if (draft.parentId case final parentId?) {
2563+
for (final message in messages) {
2564+
if (message.id == parentId) {
2565+
return updateMessage(message.copyWith(draft: draft));
2566+
}
2567+
}
2568+
}
2569+
2570+
updateChannelState(channelState.copyWith(draft: draft));
2571+
}),
2572+
);
2573+
}
2574+
2575+
void _listenDraftDeleted() {
2576+
_subscriptions.add(
2577+
_channel.on(EventType.draftDeleted).listen((event) {
2578+
final draft = event.draft;
2579+
if (draft == null) return;
2580+
2581+
if (draft.parentId case final parentId?) {
2582+
for (final message in messages) {
2583+
if (message.id == parentId) {
2584+
return updateMessage(
2585+
message.copyWith(draft: null),
2586+
);
2587+
}
2588+
}
2589+
}
2590+
2591+
updateChannelState(
2592+
channelState.copyWith(draft: null),
2593+
);
2594+
}),
2595+
);
2596+
}
2597+
25182598
void _listenReactionDeleted() {
25192599
_subscriptions.add(_channel.on(EventType.reactionDeleted).listen((event) {
25202600
final oldMessage =
@@ -2699,8 +2779,8 @@ class ChannelClientState {
26992779
}
27002780

27012781
// If the message is part of a thread, update thread information.
2702-
if (message.parentId != null) {
2703-
updateThreadInfo(message.parentId!, [message]);
2782+
if (message.parentId case final parentId?) {
2783+
updateThreadInfo(parentId, [message]);
27042784
}
27052785
}
27062786

@@ -2920,6 +3000,14 @@ class ChannelClientState {
29203000
(watchers, users) => [...?watchers?.map((e) => users[e.id] ?? e)],
29213001
).distinct(const ListEquality().equals);
29223002

3003+
/// Channel draft.
3004+
Draft? get draft => _channelState.draft;
3005+
3006+
/// Channel draft as a stream.
3007+
Stream<Draft?> get draftStream {
3008+
return channelStateStream.map((cs) => cs.draft).distinct();
3009+
}
3010+
29233011
/// Channel member for the current user.
29243012
Member? get currentUserMember => members.firstWhereOrNull(
29253013
(m) => m.user?.id == _channel.client.state.currentUser?.id,
@@ -3019,24 +3107,6 @@ class ChannelClientState {
30193107
return count;
30203108
}
30213109

3022-
/// Update threads with updated information about messages.
3023-
void updateThreadInfo(String parentId, List<Message> messages) {
3024-
final newThreads = Map<String, List<Message>>.from(threads);
3025-
3026-
if (newThreads.containsKey(parentId)) {
3027-
newThreads[parentId] = [
3028-
...messages,
3029-
...newThreads[parentId]!.where(
3030-
(newMessage) => !messages.any((m) => m.id == newMessage.id),
3031-
),
3032-
].sorted(_sortByCreatedAt);
3033-
} else {
3034-
newThreads[parentId] = messages;
3035-
}
3036-
3037-
_threads = newThreads;
3038-
}
3039-
30403110
/// Delete all channel messages.
30413111
void truncate() {
30423112
_channelState = _channelState.copyWith(
@@ -3087,6 +3157,7 @@ class ChannelClientState {
30873157
members: newMembers,
30883158
membership: updatedState.membership,
30893159
read: newReads,
3160+
draft: updatedState.draft,
30903161
pinnedMessages: updatedState.pinnedMessages,
30913162
);
30923163
}
@@ -3112,15 +3183,11 @@ class ChannelClientState {
31123183
}
31133184

31143185
/// The channel threads related to this channel.
3115-
Map<String, List<Message>> get threads =>
3116-
_threadsController.value.map(MapEntry.new);
3186+
Map<String, List<Message>> get threads => {..._threadsController.value};
31173187

31183188
/// The channel threads related to this channel as a stream.
3119-
Stream<Map<String, List<Message>>> get threadsStream =>
3120-
_threadsController.stream;
3121-
final BehaviorSubject<Map<String, List<Message>>> _threadsController =
3122-
BehaviorSubject.seeded({});
3123-
3189+
Stream<Map<String, List<Message>>> get threadsStream => _threadsController;
3190+
final _threadsController = BehaviorSubject.seeded(<String, List<Message>>{});
31243191
set _threads(Map<String, List<Message>> threads) {
31253192
_threadsController.safeAdd(threads);
31263193
_channel.client.chatPersistenceClient?.updateChannelThreads(
@@ -3129,6 +3196,38 @@ class ChannelClientState {
31293196
);
31303197
}
31313198

3199+
/// Update threads with updated information about messages.
3200+
void updateThreadInfo(String parentId, List<Message> messages) {
3201+
final newThreads = {...threads}..update(
3202+
parentId,
3203+
(original) => <Message>[
3204+
...original.merge(
3205+
messages,
3206+
key: (message) => message.id,
3207+
update: (original, updated) => updated.syncWith(original),
3208+
),
3209+
].sorted(_sortByCreatedAt),
3210+
ifAbsent: () => messages.sorted(_sortByCreatedAt),
3211+
);
3212+
3213+
_threads = newThreads;
3214+
}
3215+
3216+
Draft? _getThreadDraft(String parentId, List<Message>? messages) {
3217+
return messages?.firstWhereOrNull((it) => it.id == parentId)?.draft;
3218+
}
3219+
3220+
/// Draft for a specific thread identified by [parentId].
3221+
Draft? threadDraft(String parentId) => _getThreadDraft(parentId, messages);
3222+
3223+
/// Stream of draft for a specific thread identified by [parentId].
3224+
///
3225+
/// This stream emits a new value whenever the draft associated with the
3226+
/// specified thread is updated or removed.
3227+
Stream<Draft?> threadDraftStream(String parentId) => channelStateStream
3228+
.map((cs) => _getThreadDraft(parentId, cs.messages))
3229+
.distinct();
3230+
31323231
/// Channel related typing users stream.
31333232
Stream<Map<User, Event>> get typingEventsStream =>
31343233
_typingEventsController.stream;

packages/stream_chat/lib/src/client/client.dart

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import 'package:stream_chat/src/core/http/token_manager.dart';
2020
import 'package:stream_chat/src/core/models/attachment_file.dart';
2121
import 'package:stream_chat/src/core/models/banned_user.dart';
2222
import 'package:stream_chat/src/core/models/channel_state.dart';
23+
import 'package:stream_chat/src/core/models/draft.dart';
24+
import 'package:stream_chat/src/core/models/draft_message.dart';
2325
import 'package:stream_chat/src/core/models/event.dart';
2426
import 'package:stream_chat/src/core/models/filter.dart';
2527
import 'package:stream_chat/src/core/models/member.dart';
@@ -1719,6 +1721,57 @@ class StreamChatClient {
17191721
language,
17201722
);
17211723

1724+
/// Creates a draft for the given [channelId] of type [channelType].
1725+
Future<CreateDraftResponse> createDraft(
1726+
DraftMessage draft,
1727+
String channelId,
1728+
String channelType,
1729+
) =>
1730+
_chatApi.message.createDraft(
1731+
channelId,
1732+
channelType,
1733+
draft,
1734+
);
1735+
1736+
/// Retrieves a draft for the given [channelId] of type [channelType].
1737+
///
1738+
/// Optionally, pass [parentId] to get the draft for a thread.
1739+
Future<GetDraftResponse> getDraft(
1740+
String channelId,
1741+
String channelType, {
1742+
String? parentId,
1743+
}) =>
1744+
_chatApi.message.getDraft(
1745+
channelId,
1746+
channelType,
1747+
parentId: parentId,
1748+
);
1749+
1750+
/// Deletes a draft for the given [channelId] of type [channelType].
1751+
///
1752+
/// Optionally, pass [parentId] to delete the draft for a thread.
1753+
Future<EmptyResponse> deleteDraft(
1754+
String channelId,
1755+
String channelType, {
1756+
String? parentId,
1757+
}) =>
1758+
_chatApi.message.deleteDraft(
1759+
channelId,
1760+
channelType,
1761+
parentId: parentId,
1762+
);
1763+
1764+
/// Queries drafts for the current user.
1765+
Future<QueryDraftsResponse> queryDrafts({
1766+
Filter? filter,
1767+
SortOrder<Draft>? sort,
1768+
PaginationParams? pagination,
1769+
}) =>
1770+
_chatApi.message.queryDrafts(
1771+
sort: sort,
1772+
pagination: pagination,
1773+
);
1774+
17221775
/// Enables slow mode
17231776
Future<PartialUpdateChannelResponse> enableSlowdown(
17241777
String channelId,

0 commit comments

Comments
 (0)