Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/stream_chat/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
## Upcoming

✅ Added

- Added support for `MessageReminder` feature, which allows users to bookmark or set reminders
for specific messages in a channel.

## 9.11.0

✅ Added
Expand Down
107 changes: 107 additions & 0 deletions packages/stream_chat/lib/src/client/channel.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1292,6 +1292,44 @@ class Channel {
);
}

/// Create a reminder for the given [messageId].
///
/// Optionally, provide a [remindAt] date to set when the reminder should
/// be triggered. If not provided, the reminder will be created as a
/// bookmark type instead.
Future<CreateReminderResponse> createReminder(
String messageId, {
DateTime? remindAt,
}) {
_checkInitialized();
return _client.createReminder(
messageId,
remindAt: remindAt,
);
}

/// Update an existing reminder with the given [reminderId].
///
/// Optionally, provide a [remindAt] date to set when the reminder should
/// be triggered. If not provided, the reminder will be updated as a
/// bookmark type instead.
Future<UpdateReminderResponse> updateReminder(
String messageId, {
DateTime? remindAt,
}) {
_checkInitialized();
return _client.updateReminder(
messageId,
remindAt: remindAt,
);
}

/// Remove the reminder for the given [messageId].
Future<EmptyResponse> deleteReminder(String messageId) {
_checkInitialized();
return _client.deleteReminder(messageId);
}

/// Send a reaction to this channel.
///
/// Set [enforceUnique] to true to remove the existing user reaction.
Expand Down Expand Up @@ -2093,6 +2131,16 @@ class ChannelClientState {

_listenUserStopWatching();

/* Start of reminder events */

_listenReminderCreated();

_listenReminderUpdated();

_listenReminderDeleted();

/* End of reminder events */

_startCleaningStaleTypingEvents();

_startCleaningStalePinnedMessages();
Expand Down Expand Up @@ -2578,6 +2626,65 @@ class ChannelClientState {
);
}

void _listenReminderCreated() {
_subscriptions.add(
_channel.on(EventType.reminderCreated).listen((event) {
final reminder = event.reminder;
if (reminder == null) return;

updateReminder(reminder);
}),
);
}

void _listenReminderUpdated() {
_subscriptions.add(
_channel.on(EventType.reminderUpdated).listen((event) {
final reminder = event.reminder;
if (reminder == null) return;

updateReminder(reminder);
}),
);
}

void _listenReminderDeleted() {
_subscriptions.add(
_channel.on(EventType.reminderDeleted).listen((event) {
final reminder = event.reminder;
if (reminder == null) return;

deleteReminder(reminder);
}),
);
}

/// Updates the [reminder] of the message if it exists.
void updateReminder(MessageReminder reminder) {
final messageId = reminder.messageId;
// TODO: Improve once we have support for parentId in reminders.
for (final message in [...messages, ...threads.values.flattened]) {
if (message.id == messageId) {
return updateMessage(
message.copyWith(reminder: reminder),
);
}
}
}

/// Deletes the [reminder] of the message if it exists.
void deleteReminder(MessageReminder reminder) {
final messageId = reminder.messageId;
// TODO: Improve once we have support for parentId in reminders.
for (final message in [...messages, ...threads.values.flattened]) {
if (message.id == messageId) {
return updateMessage(
message.copyWith(reminder: null),
);
}
}
}

void _listenReactionDeleted() {
_subscriptions.add(_channel.on(EventType.reactionDeleted).listen((event) {
final oldMessage =
Expand Down
56 changes: 48 additions & 8 deletions packages/stream_chat/lib/src/client/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import 'package:stream_chat/src/core/models/filter.dart';
import 'package:stream_chat/src/core/models/member.dart';
import 'package:stream_chat/src/core/models/message.dart';
import 'package:stream_chat/src/core/models/message_reminder.dart';
import 'package:stream_chat/src/core/models/own_user.dart';
import 'package:stream_chat/src/core/models/poll.dart';
import 'package:stream_chat/src/core/models/poll_option.dart';
Expand Down Expand Up @@ -1914,14 +1915,6 @@
required String channelId,
required String channelType,
}) {
final currentUser = state.currentUser;
if (currentUser == null) {
throw const StreamChatError(
'User is not set on client, '
'use `connectUser` or `connectAnonymousUser` instead',
);
}

return partialMemberUpdate(
channelId: channelId,
channelType: channelType,
Expand Down Expand Up @@ -1962,6 +1955,53 @@
);
}

/// Queries reminders for the current user.
///
/// Optionally, pass [filter], [sort] and [pagination] to filter, sort and
/// paginate the reminders.
Future<QueryRemindersResponse> queryReminders({

Check warning on line 1962 in packages/stream_chat/lib/src/client/client.dart

View check run for this annotation

Codecov / codecov/patch

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

Added line #L1962 was not covered by tests
Filter? filter,
SortOrder<MessageReminder>? sort,
PaginationParams pagination = const PaginationParams(),
}) {
return _chatApi.reminders.queryReminders(

Check warning on line 1967 in packages/stream_chat/lib/src/client/client.dart

View check run for this annotation

Codecov / codecov/patch

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

Added line #L1967 was not covered by tests
filter: filter,
sort: sort,
pagination: pagination,
);
}

/// Creates a reminder for the given [messageId].
///
/// Optionally, pass [remindAt] to set the reminder time.
Future<CreateReminderResponse> createReminder(

Check warning on line 1977 in packages/stream_chat/lib/src/client/client.dart

View check run for this annotation

Codecov / codecov/patch

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

Added line #L1977 was not covered by tests
String messageId, {
DateTime? remindAt,
}) {
return _chatApi.reminders.createReminder(

Check warning on line 1981 in packages/stream_chat/lib/src/client/client.dart

View check run for this annotation

Codecov / codecov/patch

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

Added line #L1981 was not covered by tests
messageId,
remindAt: remindAt,
);
}

/// Updates a reminder for the given [messageId].
///
/// Optionally, pass [remindAt] to set the new reminder time.
Future<UpdateReminderResponse> updateReminder(

Check warning on line 1990 in packages/stream_chat/lib/src/client/client.dart

View check run for this annotation

Codecov / codecov/patch

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

Added line #L1990 was not covered by tests
String messageId, {
DateTime? remindAt,
}) {
return _chatApi.reminders.updateReminder(

Check warning on line 1994 in packages/stream_chat/lib/src/client/client.dart

View check run for this annotation

Codecov / codecov/patch

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

Added line #L1994 was not covered by tests
messageId,
remindAt: remindAt,
);
}

/// Deletes a reminder for the given [messageId].
Future<EmptyResponse> deleteReminder(String messageId) {
return _chatApi.reminders.deleteReminder(messageId);

Check warning on line 2002 in packages/stream_chat/lib/src/client/client.dart

View check run for this annotation

Codecov / codecov/patch

packages/stream_chat/lib/src/client/client.dart#L2001-L2002

Added lines #L2001 - L2002 were not covered by tests
}

/// Closes the [_ws] connection and resets the [state]
/// If [flushChatPersistence] is true the client deletes all offline
/// user's data.
Expand Down
91 changes: 91 additions & 0 deletions packages/stream_chat/lib/src/core/api/reminders_api.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import 'dart:convert';

import 'package:stream_chat/src/core/api/requests.dart';
import 'package:stream_chat/src/core/api/responses.dart';
import 'package:stream_chat/src/core/api/sort_order.dart';
import 'package:stream_chat/src/core/http/stream_http_client.dart';
import 'package:stream_chat/src/core/models/filter.dart';
import 'package:stream_chat/src/core/models/message_reminder.dart';

/// Defines the api dedicated to message reminders operations
class RemindersApi {
/// Initialize a new reminders api
const RemindersApi(this._client);

final StreamHttpClient _client;

/// Retrieves the list of reminders for the current user.
///
/// Optionally, you can filter and sort the reminders using the [filter] and
/// [sort] parameters respectively. You can also paginate the results using
/// [pagination].
///
/// Returns a [QueryRemindersResponse] containing the list of reminders.
Future<QueryRemindersResponse> queryReminders({
Filter? filter,
SortOrder<MessageReminder>? sort,
PaginationParams? pagination,
}) async {
final response = await _client.post(
'/reminders/query',
data: jsonEncode({
if (filter != null) 'filter': filter,
if (sort != null) 'sort': sort,
if (pagination != null) ...pagination.toJson(),
}),
);

return QueryRemindersResponse.fromJson(response.data);
}

/// Creates a new reminder for the specified [messageId].
///
/// You can specify the time to remind using the [remindAt] parameter.
///
/// Returns a [CreateReminderResponse] containing the created reminder.
Future<CreateReminderResponse> createReminder(
String messageId, {
DateTime? remindAt,
}) async {
final response = await _client.post(
'/messages/$messageId/reminders',
data: jsonEncode({
if (remindAt != null) 'remind_at': remindAt.toUtc().toIso8601String(),
}),
);

return CreateReminderResponse.fromJson(response.data);
}

/// Updates an existing reminder for the specified [messageId].
///
/// You can change the reminder time using the [remindAt] parameter.
///
/// Returns an [UpdateReminderResponse] containing the updated reminder.
Future<UpdateReminderResponse> updateReminder(
String messageId, {
DateTime? remindAt,
}) async {
final response = await _client.patch(
'/messages/$messageId/reminders',
data: jsonEncode({
if (remindAt != null) 'remind_at': remindAt.toUtc().toIso8601String(),
}),
);

return UpdateReminderResponse.fromJson(response.data);
}

/// Deletes a reminder for the specified [messageId].
///
/// Returns an [EmptyResponse] indicating the deletion was successful.
Future<EmptyResponse> deleteReminder(
String messageId,
) async {
final response = await _client.delete(
'/messages/$messageId/reminders',
);

return EmptyResponse.fromJson(response.data);
}
}
38 changes: 38 additions & 0 deletions packages/stream_chat/lib/src/core/api/responses.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:stream_chat/src/core/models/draft.dart';
import 'package:stream_chat/src/core/models/event.dart';
import 'package:stream_chat/src/core/models/member.dart';
import 'package:stream_chat/src/core/models/message.dart';
import 'package:stream_chat/src/core/models/message_reminder.dart';
import 'package:stream_chat/src/core/models/poll.dart';
import 'package:stream_chat/src/core/models/poll_option.dart';
import 'package:stream_chat/src/core/models/poll_vote.dart';
Expand Down Expand Up @@ -764,3 +765,40 @@ class QueryDraftsResponse extends _BaseResponse {
static QueryDraftsResponse fromJson(Map<String, dynamic> json) =>
_$QueryDraftsResponseFromJson(json);
}

/// Base Model response for draft based api calls.
class MessageReminderResponse extends _BaseResponse {
/// Draft returned by the api call
late MessageReminder reminder;
}

/// Model response for [StreamChatClient.createReminder] api call
@JsonSerializable(createToJson: false)
class CreateReminderResponse extends MessageReminderResponse {
/// Create a new instance from a json
static CreateReminderResponse fromJson(Map<String, dynamic> json) =>
_$CreateReminderResponseFromJson(json);
}

/// Model response for [StreamChatClient.updateReminder] api call
@JsonSerializable(createToJson: false)
class UpdateReminderResponse extends MessageReminderResponse {
/// Create a new instance from a json
static UpdateReminderResponse fromJson(Map<String, dynamic> json) =>
_$UpdateReminderResponseFromJson(json);
}

/// Model response for [StreamChatClient.queryReminders] api call
@JsonSerializable(createToJson: false)
class QueryRemindersResponse extends _BaseResponse {
/// List of reminders returned by the query
@JsonKey(defaultValue: [])
late List<MessageReminder> reminders;

/// The next page token
late String? next;

/// Create a new instance from a json
static QueryRemindersResponse fromJson(Map<String, dynamic> json) =>
_$QueryRemindersResponseFromJson(json);
}
24 changes: 24 additions & 0 deletions packages/stream_chat/lib/src/core/api/responses.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading