Skip to content

Commit 50a2427

Browse files
authored
feat(llc, core): message reminders (#2269)
1 parent 2b8c1f0 commit 50a2427

35 files changed

+3662
-329
lines changed

packages/stream_chat/CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## Upcoming
2+
3+
✅ Added
4+
5+
- Added support for `MessageReminder` feature, which allows users to bookmark or set reminders
6+
for specific messages in a channel.
7+
18
## 9.11.0
29

310
✅ Added

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

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1292,6 +1292,44 @@ class Channel {
12921292
);
12931293
}
12941294

1295+
/// Create a reminder for the given [messageId].
1296+
///
1297+
/// Optionally, provide a [remindAt] date to set when the reminder should
1298+
/// be triggered. If not provided, the reminder will be created as a
1299+
/// bookmark type instead.
1300+
Future<CreateReminderResponse> createReminder(
1301+
String messageId, {
1302+
DateTime? remindAt,
1303+
}) {
1304+
_checkInitialized();
1305+
return _client.createReminder(
1306+
messageId,
1307+
remindAt: remindAt,
1308+
);
1309+
}
1310+
1311+
/// Update an existing reminder with the given [reminderId].
1312+
///
1313+
/// Optionally, provide a [remindAt] date to set when the reminder should
1314+
/// be triggered. If not provided, the reminder will be updated as a
1315+
/// bookmark type instead.
1316+
Future<UpdateReminderResponse> updateReminder(
1317+
String messageId, {
1318+
DateTime? remindAt,
1319+
}) {
1320+
_checkInitialized();
1321+
return _client.updateReminder(
1322+
messageId,
1323+
remindAt: remindAt,
1324+
);
1325+
}
1326+
1327+
/// Remove the reminder for the given [messageId].
1328+
Future<EmptyResponse> deleteReminder(String messageId) {
1329+
_checkInitialized();
1330+
return _client.deleteReminder(messageId);
1331+
}
1332+
12951333
/// Send a reaction to this channel.
12961334
///
12971335
/// Set [enforceUnique] to true to remove the existing user reaction.
@@ -2093,6 +2131,16 @@ class ChannelClientState {
20932131

20942132
_listenUserStopWatching();
20952133

2134+
/* Start of reminder events */
2135+
2136+
_listenReminderCreated();
2137+
2138+
_listenReminderUpdated();
2139+
2140+
_listenReminderDeleted();
2141+
2142+
/* End of reminder events */
2143+
20962144
_startCleaningStaleTypingEvents();
20972145

20982146
_startCleaningStalePinnedMessages();
@@ -2578,6 +2626,65 @@ class ChannelClientState {
25782626
);
25792627
}
25802628

2629+
void _listenReminderCreated() {
2630+
_subscriptions.add(
2631+
_channel.on(EventType.reminderCreated).listen((event) {
2632+
final reminder = event.reminder;
2633+
if (reminder == null) return;
2634+
2635+
updateReminder(reminder);
2636+
}),
2637+
);
2638+
}
2639+
2640+
void _listenReminderUpdated() {
2641+
_subscriptions.add(
2642+
_channel.on(EventType.reminderUpdated).listen((event) {
2643+
final reminder = event.reminder;
2644+
if (reminder == null) return;
2645+
2646+
updateReminder(reminder);
2647+
}),
2648+
);
2649+
}
2650+
2651+
void _listenReminderDeleted() {
2652+
_subscriptions.add(
2653+
_channel.on(EventType.reminderDeleted).listen((event) {
2654+
final reminder = event.reminder;
2655+
if (reminder == null) return;
2656+
2657+
deleteReminder(reminder);
2658+
}),
2659+
);
2660+
}
2661+
2662+
/// Updates the [reminder] of the message if it exists.
2663+
void updateReminder(MessageReminder reminder) {
2664+
final messageId = reminder.messageId;
2665+
// TODO: Improve once we have support for parentId in reminders.
2666+
for (final message in [...messages, ...threads.values.flattened]) {
2667+
if (message.id == messageId) {
2668+
return updateMessage(
2669+
message.copyWith(reminder: reminder),
2670+
);
2671+
}
2672+
}
2673+
}
2674+
2675+
/// Deletes the [reminder] of the message if it exists.
2676+
void deleteReminder(MessageReminder reminder) {
2677+
final messageId = reminder.messageId;
2678+
// TODO: Improve once we have support for parentId in reminders.
2679+
for (final message in [...messages, ...threads.values.flattened]) {
2680+
if (message.id == messageId) {
2681+
return updateMessage(
2682+
message.copyWith(reminder: null),
2683+
);
2684+
}
2685+
}
2686+
}
2687+
25812688
void _listenReactionDeleted() {
25822689
_subscriptions.add(_channel.on(EventType.reactionDeleted).listen((event) {
25832690
final oldMessage =

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

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import 'package:stream_chat/src/core/models/event.dart';
2626
import 'package:stream_chat/src/core/models/filter.dart';
2727
import 'package:stream_chat/src/core/models/member.dart';
2828
import 'package:stream_chat/src/core/models/message.dart';
29+
import 'package:stream_chat/src/core/models/message_reminder.dart';
2930
import 'package:stream_chat/src/core/models/own_user.dart';
3031
import 'package:stream_chat/src/core/models/poll.dart';
3132
import 'package:stream_chat/src/core/models/poll_option.dart';
@@ -1914,14 +1915,6 @@ class StreamChatClient {
19141915
required String channelId,
19151916
required String channelType,
19161917
}) {
1917-
final currentUser = state.currentUser;
1918-
if (currentUser == null) {
1919-
throw const StreamChatError(
1920-
'User is not set on client, '
1921-
'use `connectUser` or `connectAnonymousUser` instead',
1922-
);
1923-
}
1924-
19251918
return partialMemberUpdate(
19261919
channelId: channelId,
19271920
channelType: channelType,
@@ -1962,6 +1955,53 @@ class StreamChatClient {
19621955
);
19631956
}
19641957

1958+
/// Queries reminders for the current user.
1959+
///
1960+
/// Optionally, pass [filter], [sort] and [pagination] to filter, sort and
1961+
/// paginate the reminders.
1962+
Future<QueryRemindersResponse> queryReminders({
1963+
Filter? filter,
1964+
SortOrder<MessageReminder>? sort,
1965+
PaginationParams pagination = const PaginationParams(),
1966+
}) {
1967+
return _chatApi.reminders.queryReminders(
1968+
filter: filter,
1969+
sort: sort,
1970+
pagination: pagination,
1971+
);
1972+
}
1973+
1974+
/// Creates a reminder for the given [messageId].
1975+
///
1976+
/// Optionally, pass [remindAt] to set the reminder time.
1977+
Future<CreateReminderResponse> createReminder(
1978+
String messageId, {
1979+
DateTime? remindAt,
1980+
}) {
1981+
return _chatApi.reminders.createReminder(
1982+
messageId,
1983+
remindAt: remindAt,
1984+
);
1985+
}
1986+
1987+
/// Updates a reminder for the given [messageId].
1988+
///
1989+
/// Optionally, pass [remindAt] to set the new reminder time.
1990+
Future<UpdateReminderResponse> updateReminder(
1991+
String messageId, {
1992+
DateTime? remindAt,
1993+
}) {
1994+
return _chatApi.reminders.updateReminder(
1995+
messageId,
1996+
remindAt: remindAt,
1997+
);
1998+
}
1999+
2000+
/// Deletes a reminder for the given [messageId].
2001+
Future<EmptyResponse> deleteReminder(String messageId) {
2002+
return _chatApi.reminders.deleteReminder(messageId);
2003+
}
2004+
19652005
/// Closes the [_ws] connection and resets the [state]
19662006
/// If [flushChatPersistence] is true the client deletes all offline
19672007
/// user's data.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import 'dart:convert';
2+
3+
import 'package:stream_chat/src/core/api/requests.dart';
4+
import 'package:stream_chat/src/core/api/responses.dart';
5+
import 'package:stream_chat/src/core/api/sort_order.dart';
6+
import 'package:stream_chat/src/core/http/stream_http_client.dart';
7+
import 'package:stream_chat/src/core/models/filter.dart';
8+
import 'package:stream_chat/src/core/models/message_reminder.dart';
9+
10+
/// Defines the api dedicated to message reminders operations
11+
class RemindersApi {
12+
/// Initialize a new reminders api
13+
const RemindersApi(this._client);
14+
15+
final StreamHttpClient _client;
16+
17+
/// Retrieves the list of reminders for the current user.
18+
///
19+
/// Optionally, you can filter and sort the reminders using the [filter] and
20+
/// [sort] parameters respectively. You can also paginate the results using
21+
/// [pagination].
22+
///
23+
/// Returns a [QueryRemindersResponse] containing the list of reminders.
24+
Future<QueryRemindersResponse> queryReminders({
25+
Filter? filter,
26+
SortOrder<MessageReminder>? sort,
27+
PaginationParams? pagination,
28+
}) async {
29+
final response = await _client.post(
30+
'/reminders/query',
31+
data: jsonEncode({
32+
if (filter != null) 'filter': filter,
33+
if (sort != null) 'sort': sort,
34+
if (pagination != null) ...pagination.toJson(),
35+
}),
36+
);
37+
38+
return QueryRemindersResponse.fromJson(response.data);
39+
}
40+
41+
/// Creates a new reminder for the specified [messageId].
42+
///
43+
/// You can specify the time to remind using the [remindAt] parameter.
44+
///
45+
/// Returns a [CreateReminderResponse] containing the created reminder.
46+
Future<CreateReminderResponse> createReminder(
47+
String messageId, {
48+
DateTime? remindAt,
49+
}) async {
50+
final response = await _client.post(
51+
'/messages/$messageId/reminders',
52+
data: jsonEncode({
53+
if (remindAt != null) 'remind_at': remindAt.toUtc().toIso8601String(),
54+
}),
55+
);
56+
57+
return CreateReminderResponse.fromJson(response.data);
58+
}
59+
60+
/// Updates an existing reminder for the specified [messageId].
61+
///
62+
/// You can change the reminder time using the [remindAt] parameter.
63+
///
64+
/// Returns an [UpdateReminderResponse] containing the updated reminder.
65+
Future<UpdateReminderResponse> updateReminder(
66+
String messageId, {
67+
DateTime? remindAt,
68+
}) async {
69+
final response = await _client.patch(
70+
'/messages/$messageId/reminders',
71+
data: jsonEncode({
72+
if (remindAt != null) 'remind_at': remindAt.toUtc().toIso8601String(),
73+
}),
74+
);
75+
76+
return UpdateReminderResponse.fromJson(response.data);
77+
}
78+
79+
/// Deletes a reminder for the specified [messageId].
80+
///
81+
/// Returns an [EmptyResponse] indicating the deletion was successful.
82+
Future<EmptyResponse> deleteReminder(
83+
String messageId,
84+
) async {
85+
final response = await _client.delete(
86+
'/messages/$messageId/reminders',
87+
);
88+
89+
return EmptyResponse.fromJson(response.data);
90+
}
91+
}

packages/stream_chat/lib/src/core/api/responses.dart

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import 'package:stream_chat/src/core/models/draft.dart';
1111
import 'package:stream_chat/src/core/models/event.dart';
1212
import 'package:stream_chat/src/core/models/member.dart';
1313
import 'package:stream_chat/src/core/models/message.dart';
14+
import 'package:stream_chat/src/core/models/message_reminder.dart';
1415
import 'package:stream_chat/src/core/models/poll.dart';
1516
import 'package:stream_chat/src/core/models/poll_option.dart';
1617
import 'package:stream_chat/src/core/models/poll_vote.dart';
@@ -764,3 +765,40 @@ class QueryDraftsResponse extends _BaseResponse {
764765
static QueryDraftsResponse fromJson(Map<String, dynamic> json) =>
765766
_$QueryDraftsResponseFromJson(json);
766767
}
768+
769+
/// Base Model response for draft based api calls.
770+
class MessageReminderResponse extends _BaseResponse {
771+
/// Draft returned by the api call
772+
late MessageReminder reminder;
773+
}
774+
775+
/// Model response for [StreamChatClient.createReminder] api call
776+
@JsonSerializable(createToJson: false)
777+
class CreateReminderResponse extends MessageReminderResponse {
778+
/// Create a new instance from a json
779+
static CreateReminderResponse fromJson(Map<String, dynamic> json) =>
780+
_$CreateReminderResponseFromJson(json);
781+
}
782+
783+
/// Model response for [StreamChatClient.updateReminder] api call
784+
@JsonSerializable(createToJson: false)
785+
class UpdateReminderResponse extends MessageReminderResponse {
786+
/// Create a new instance from a json
787+
static UpdateReminderResponse fromJson(Map<String, dynamic> json) =>
788+
_$UpdateReminderResponseFromJson(json);
789+
}
790+
791+
/// Model response for [StreamChatClient.queryReminders] api call
792+
@JsonSerializable(createToJson: false)
793+
class QueryRemindersResponse extends _BaseResponse {
794+
/// List of reminders returned by the query
795+
@JsonKey(defaultValue: [])
796+
late List<MessageReminder> reminders;
797+
798+
/// The next page token
799+
late String? next;
800+
801+
/// Create a new instance from a json
802+
static QueryRemindersResponse fromJson(Map<String, dynamic> json) =>
803+
_$QueryRemindersResponseFromJson(json);
804+
}

packages/stream_chat/lib/src/core/api/responses.g.dart

Lines changed: 24 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)