From a4b7f4feba1c74a2a015a6ff66b47dc37fd5825b Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 25 Sep 2025 07:48:19 +0200 Subject: [PATCH 01/16] feat(llc): add delete message for me This commit introduces the ability to delete a message only for the current user. The following changes are included: - Added `MessageDeleteScope` to represent the scope of deletion for a message. - Added `deleteMessageForMe` method to `StreamChatClient` and `Channel` to delete a message only for the current user. - Updated `Message` model to include `deletedOnlyForMe` field. - Updated `Event` model to include `deletedForMe` field. - Updated `Member` model to include `deletedMessages` field. - Updated `MessageState` to include `deletingForMe`, `deletedForMe`, and `deletingForMeFailed` states. - Updated `MessageApi` to include `delete_for_me` parameter in `deleteMessage` method. - Updated sample app to include "Delete Message for Me" option in message actions. --- .../stream_chat/lib/src/client/channel.dart | 142 ++++++++++---- .../stream_chat/lib/src/client/client.dart | 24 ++- .../lib/src/core/api/message_api.dart | 16 +- .../lib/src/core/models/event.dart | 7 + .../lib/src/core/models/event.g.dart | 2 + .../lib/src/core/models/member.dart | 13 +- .../lib/src/core/models/member.g.dart | 5 + .../lib/src/core/models/message.dart | 22 ++- .../lib/src/core/models/message.g.dart | 1 + .../src/core/models/message_delete_scope.dart | 61 ++++++ .../models/message_delete_scope.freezed.dart | 166 ++++++++++++++++ .../core/models/message_delete_scope.g.dart | 27 +++ .../lib/src/core/models/message_state.dart | 177 ++++++++++++++---- .../core/models/message_state.freezed.dart | 105 +++++++---- .../lib/src/core/models/message_state.g.dart | 18 +- packages/stream_chat/lib/stream_chat.dart | 1 + .../stream_chat/test/fixtures/member.json | 5 + .../test/src/client/channel_test.dart | 136 ++++++++++++-- .../test/src/client/client_test.dart | 55 ++++++ .../test/src/core/models/member_test.dart | 1 + .../src/core/models/message_state_test.dart | 156 ++++++++++++--- sample_app/lib/pages/channel_page.dart | 57 ++++-- 22 files changed, 1002 insertions(+), 195 deletions(-) create mode 100644 packages/stream_chat/lib/src/core/models/message_delete_scope.dart create mode 100644 packages/stream_chat/lib/src/core/models/message_delete_scope.freezed.dart create mode 100644 packages/stream_chat/lib/src/core/models/message_delete_scope.g.dart diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index d71761fd82..2683585537 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -920,31 +920,49 @@ class Channel { final _deleteMessageLock = Lock(); - /// Deletes the [message] from the channel. - Future deleteMessage( + /// Deletes the [message] for everyone. + /// + /// If [hard] is true, the message is permanently deleted from the server + /// and cannot be recovered. In this case, any attachments associated with the + /// message are also deleted from the server. + Future deleteMessage(Message message, {bool hard = false}) { + final deletionScope = MessageDeleteScope.deleteForAll(hard: hard); + + return _deleteMessage(message, scope: deletionScope); + } + + /// Deletes the [message] only for the current user. + /// + /// Note: This does not delete the message for other channel members and + /// they can still see the message. + Future deleteMessageForMe(Message message) { + const deletionScope = MessageDeleteScope.deleteForMe(); + + return _deleteMessage(message, scope: deletionScope); + } + + // Deletes the [message] from the channel. + // + // The [scope] defines whether to delete the message for everyone or just + // for the current user. + // + // If the message is a local message (not yet sent to the server) or a bounced + // error message, it is deleted locally without making an API call. + // + // If the message is deleted for everyone and [scope.hard] is true, the + // message is permanently deleted from the server and cannot be recovered. + // In this case, any attachments associated with the message are also deleted + // from the server. + Future _deleteMessage( Message message, { - bool hard = false, + required MessageDeleteScope scope, }) async { _checkInitialized(); // Directly deleting the local messages and bounced error messages as they // are not available on the server. if (message.remoteCreatedAt == null || message.isBouncedWithError) { - state?.deleteMessage( - message.copyWith( - type: MessageType.deleted, - localDeletedAt: DateTime.now(), - state: MessageState.deleted(hard: hard), - ), - hardDelete: hard, - ); - - // Removing the attachments upload completer to stop the `sendMessage` - // waiting for attachments to complete. - _messageAttachmentsUploadCompleter - .remove(message.id) - ?.completeError(const StreamChatError('Message deleted')); - + _deleteLocalMessage(message); // Returning empty response to mark the api call as success. return EmptyResponse(); } @@ -953,44 +971,39 @@ class Channel { message = message.copyWith( type: MessageType.deleted, deletedAt: DateTime.now(), - state: MessageState.deleting(hard: hard), + deletedOnlyForMe: scope is DeleteForMe, + state: MessageState.deleting(scope: scope), ); - state?.deleteMessage(message, hardDelete: hard); + state?.deleteMessage(message, hardDelete: scope.hard); try { // Wait for the previous delete call to finish. Otherwise, the order of // messages will not be maintained. final response = await _deleteMessageLock.synchronized( - () => _client.deleteMessage(message.id, hard: hard), + () => switch (scope) { + DeleteForMe() => _client.deleteMessageForMe(message.id), + DeleteForAll() => _client.deleteMessage(message.id, hard: scope.hard), + }, ); final deletedMessage = message.copyWith( - state: MessageState.deleted(hard: hard), + deletedOnlyForMe: scope is DeleteForMe, + state: MessageState.deleted(scope: scope), ); - state?.deleteMessage(deletedMessage, hardDelete: hard); - - if (hard) { - deletedMessage.attachments.forEach((attachment) { - if (attachment.uploadState.isSuccess) { - if (attachment.type == AttachmentType.image) { - deleteImage(attachment.imageUrl!); - } else if (attachment.type == AttachmentType.file) { - deleteFile(attachment.assetUrl!); - } - } - }); - } + state?.deleteMessage(deletedMessage, hardDelete: scope.hard); + // If hard delete, also delete the attachments from the server. + if (scope.hard) _deleteMessageAttachments(deletedMessage); return response; } catch (e) { final failedMessage = message.copyWith( // Update the message state to failed. - state: MessageState.deletingFailed(hard: hard), + state: MessageState.deletingFailed(scope: scope), ); - state?.deleteMessage(failedMessage, hardDelete: hard); + state?.deleteMessage(failedMessage, hardDelete: scope.hard); // If the error is retriable, add it to the retry queue. if (e is StreamChatNetworkError && e.isRetriable) { state?._retryQueue.add([failedMessage]); @@ -1000,6 +1013,43 @@ class Channel { } } + // Deletes a local [message] that is not yet sent to the server. + // + // This is typically called when a user wants to delete a message that they + // have composed but not yet sent, or if a message failed to send and the user + // wants to remove it from their local view. + void _deleteLocalMessage(Message message) { + state?.deleteMessage( + hardDelete: true, // Local messages are always hard deleted. + message.copyWith( + type: MessageType.deleted, + localDeletedAt: DateTime.now(), + state: MessageState.hardDeleted, + ), + ); + + // Removing the attachments upload completer to stop the `sendMessage` + // waiting for attachments to complete. + final completer = _messageAttachmentsUploadCompleter.remove(message.id); + completer?.completeError(const StreamChatError('Message deleted')); + } + + // Deletes all the attachments associated with the given [message] + // from the server. This is typically called when a message is hard deleted. + Future _deleteMessageAttachments(Message message) async { + final attachments = message.attachments; + final deleteFutures = attachments.map((it) async { + if (it.imageUrl case final url?) return deleteImage(url); + if (it.assetUrl case final url?) return deleteFile(url); + }); + + try { + await Future.wait(deleteFutures); + } catch (e, stk) { + _client.logger.warning('Error deleting message attachments', e, stk); + } + } + /// Retries operations on a message based on its failed state. /// /// This method examines the message's state and performs the appropriate @@ -1009,9 +1059,12 @@ class Channel { /// - For [MessageState.partialUpdatingFailed], it attempts to partially /// update the message with the same 'set' and 'unset' parameters that were /// used in the original request. - /// - For [MessageState.deletingFailed], it attempts to delete the message. - /// with the same 'hard' parameter that was used in the original request + /// - For [MessageState.deletingFailed], it attempts to delete the message + /// again, using the same scope (for me or for all) as the original request. /// - For messages with [isBouncedWithError], it attempts to send the message. + /// + /// Throws a [StateError] if the message is not in a failed state or + /// bounced with an error. Future retryMessage(Message message) async { assert( message.state.isFailed || message.isBouncedWithError, @@ -1038,7 +1091,10 @@ class Channel { skipEnrichUrl: skipEnrichUrl, ); }, - deletingFailed: (hard) => deleteMessage(message, hard: hard), + deletingFailed: (scope) => switch (scope) { + DeleteForMe() => deleteMessageForMe(message), + DeleteForAll(hard: final hard) => deleteMessage(message, hard: hard), + }, ), orElse: () { // Check if the message is bounced with error. @@ -2996,9 +3052,13 @@ class ChannelClientState { void _listenMessageDeleted() { _subscriptions.add(_channel.on(EventType.messageDeleted).listen((event) { - final message = event.message!; final hardDelete = event.hardDelete ?? false; + final message = event.message!.copyWith( + // TODO: Remove once deletedForMe is properly enriched on the backend. + deletedOnlyForMe: event.deletedForMe, + ); + return deleteMessage(message, hardDelete: hardDelete); })); } diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index 4a03630c8e..0c8472eaad 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -1744,21 +1744,29 @@ class StreamChatClient { skipEnrichUrl: skipEnrichUrl, ); - /// Deletes the given message + /// Deletes the given message. + /// + /// If [hard] is true, the message is permanently deleted. Future deleteMessage( String messageId, { bool hard = false, - }) async { - final response = await _chatApi.message.deleteMessage( + }) { + return _chatApi.message.deleteMessage( messageId, hard: hard, ); + } - if (hard) { - await chatPersistenceClient?.deleteMessageById(messageId); - } - - return response; + /// Deletes the given message for the current user only. + /// + /// Note: This does not delete the message for other users in the channel. + Future deleteMessageForMe( + String messageId, + ) { + return _chatApi.message.deleteMessage( + messageId, + deleteForMe: true, + ); } /// Get a message by [messageId] diff --git a/packages/stream_chat/lib/src/core/api/message_api.dart b/packages/stream_chat/lib/src/core/api/message_api.dart index 19a363bb3b..46e0979c83 100644 --- a/packages/stream_chat/lib/src/core/api/message_api.dart +++ b/packages/stream_chat/lib/src/core/api/message_api.dart @@ -175,14 +175,20 @@ class MessageApi { Future deleteMessage( String messageId, { bool? hard, + bool? deleteForMe, }) async { + if (hard == true && deleteForMe == true) { + throw ArgumentError( + 'Both hard and deleteForMe cannot be set at the same time.' + ); + } + final response = await _client.delete( '/messages/$messageId', - queryParameters: hard != null - ? { - 'hard': hard, - } - : null, + queryParameters: { + if (hard != null) 'hard': hard, + if (deleteForMe != null) 'delete_for_me': deleteForMe, + }, ); return EmptyResponse.fromJson(response.data); } diff --git a/packages/stream_chat/lib/src/core/models/event.dart b/packages/stream_chat/lib/src/core/models/event.dart index 75e0211b45..8c96bebfe8 100644 --- a/packages/stream_chat/lib/src/core/models/event.dart +++ b/packages/stream_chat/lib/src/core/models/event.dart @@ -30,6 +30,7 @@ class Event { this.channelLastMessageAt, this.parentId, this.hardDelete, + this.deletedForMe, this.aiState, this.aiMessage, this.messageId, @@ -122,6 +123,9 @@ class Event { /// This is true if the message has been hard deleted final bool? hardDelete; + /// Whether the message was deleted only for the current user. + final bool? deletedForMe; + /// The current state of the AI assistant. @JsonKey(unknownEnumValue: AITypingState.idle) final AITypingState? aiState; @@ -189,6 +193,7 @@ class Event { 'channel_last_message_at', 'parent_id', 'hard_delete', + 'deleted_for_me', 'is_local', 'ai_state', 'ai_message', @@ -233,6 +238,7 @@ class Event { bool? online, String? parentId, bool? hardDelete, + bool? deletedForMe, AITypingState? aiState, String? aiMessage, String? messageId, @@ -270,6 +276,7 @@ class Event { channelLastMessageAt: channelLastMessageAt ?? this.channelLastMessageAt, parentId: parentId ?? this.parentId, hardDelete: hardDelete ?? this.hardDelete, + deletedForMe: deletedForMe ?? this.deletedForMe, aiState: aiState ?? this.aiState, aiMessage: aiMessage ?? this.aiMessage, messageId: messageId ?? this.messageId, diff --git a/packages/stream_chat/lib/src/core/models/event.g.dart b/packages/stream_chat/lib/src/core/models/event.g.dart index 1623dc1438..dc9681a9c2 100644 --- a/packages/stream_chat/lib/src/core/models/event.g.dart +++ b/packages/stream_chat/lib/src/core/models/event.g.dart @@ -48,6 +48,7 @@ Event _$EventFromJson(Map json) => Event( : DateTime.parse(json['channel_last_message_at'] as String), parentId: json['parent_id'] as String?, hardDelete: json['hard_delete'] as bool?, + deletedForMe: json['deleted_for_me'] as bool?, aiState: $enumDecodeNullable(_$AITypingStateEnumMap, json['ai_state'], unknownValue: AITypingState.idle), aiMessage: json['ai_message'] as String?, @@ -105,6 +106,7 @@ Map _$EventToJson(Event instance) => { if (instance.parentId case final value?) 'parent_id': value, 'is_local': instance.isLocal, if (instance.hardDelete case final value?) 'hard_delete': value, + if (instance.deletedForMe case final value?) 'deleted_for_me': value, if (_$AITypingStateEnumMap[instance.aiState] case final value?) 'ai_state': value, if (instance.aiMessage case final value?) 'ai_message': value, diff --git a/packages/stream_chat/lib/src/core/models/member.dart b/packages/stream_chat/lib/src/core/models/member.dart index 66b51a287a..452412f7c1 100644 --- a/packages/stream_chat/lib/src/core/models/member.dart +++ b/packages/stream_chat/lib/src/core/models/member.dart @@ -26,6 +26,7 @@ class Member extends Equatable implements ComparableFieldProvider { this.shadowBanned = false, this.pinnedAt, this.archivedAt, + this.deletedMessages = const [], this.extraData = const {}, }) : userId = userId ?? user?.id, createdAt = createdAt ?? DateTime.now(), @@ -53,7 +54,8 @@ class Member extends Equatable implements ComparableFieldProvider { 'created_at', 'updated_at', 'pinned_at', - 'archived_at' + 'archived_at', + 'deleted_messages', ]; /// The interested user @@ -98,6 +100,12 @@ class Member extends Equatable implements ComparableFieldProvider { /// The last date of update final DateTime updatedAt; + /// List of message ids deleted by this member only for himself. + /// + /// These messages are not visible to this member anymore, but are still + /// visible to other channel members. + final List deletedMessages; + /// Map of custom member extraData. final Map extraData; @@ -118,6 +126,7 @@ class Member extends Equatable implements ComparableFieldProvider { bool? banned, DateTime? banExpires, bool? shadowBanned, + List? deletedMessages, Map? extraData, }) => Member( @@ -135,6 +144,7 @@ class Member extends Equatable implements ComparableFieldProvider { archivedAt: archivedAt ?? this.archivedAt, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, + deletedMessages: deletedMessages ?? this.deletedMessages, extraData: extraData ?? this.extraData, ); @@ -159,6 +169,7 @@ class Member extends Equatable implements ComparableFieldProvider { archivedAt, createdAt, updatedAt, + deletedMessages, extraData, ]; diff --git a/packages/stream_chat/lib/src/core/models/member.g.dart b/packages/stream_chat/lib/src/core/models/member.g.dart index 0cd677e72a..abdaf29d65 100644 --- a/packages/stream_chat/lib/src/core/models/member.g.dart +++ b/packages/stream_chat/lib/src/core/models/member.g.dart @@ -37,6 +37,10 @@ Member _$MemberFromJson(Map json) => Member( archivedAt: json['archived_at'] == null ? null : DateTime.parse(json['archived_at'] as String), + deletedMessages: (json['deleted_messages'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [], extraData: json['extra_data'] as Map? ?? const {}, ); @@ -55,5 +59,6 @@ Map _$MemberToJson(Member instance) => { 'archived_at': instance.archivedAt?.toIso8601String(), 'created_at': instance.createdAt.toIso8601String(), 'updated_at': instance.updatedAt.toIso8601String(), + 'deleted_messages': instance.deletedMessages, 'extra_data': instance.extraData, }; diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index 150224031c..f8a320d249 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -51,6 +51,7 @@ class Message extends Equatable implements ComparableFieldProvider { this.localUpdatedAt, DateTime? deletedAt, this.localDeletedAt, + this.deletedOnlyForMe = false, this.messageTextUpdatedAt, this.user, this.pinned = false, @@ -82,14 +83,22 @@ class Message extends Equatable implements ComparableFieldProvider { Serializer.moveToExtraDataFromRoot(json, topLevelFields), ); + // TODO: Remove this once type are properly enriched on the backend. + var type = message.type; + if (message.deletedOnlyForMe) { + type = MessageType.deleted; + } + var state = MessageState.sent; - if (message.deletedAt != null) { + if (message.deletedOnlyForMe) { + state = MessageState.deletedForMe; + } else if (message.deletedAt != null) { state = MessageState.softDeleted; } else if (message.updatedAt.isAfter(message.createdAt)) { state = MessageState.updated; } - return message.copyWith(state: state); + return message.copyWith(type: type, state: state); } /// The message ID. This is either created by Stream or set client side when @@ -311,6 +320,10 @@ class Message extends Equatable implements ComparableFieldProvider { @JsonKey(includeIfNull: false) final Location? sharedLocation; + /// Whether the message was deleted only for the current user. + @JsonKey(name: 'deleted_for_me', defaultValue: false, includeToJson: false) + final bool deletedOnlyForMe; + /// Message custom extraData. final Map extraData; @@ -360,6 +373,7 @@ class Message extends Equatable implements ComparableFieldProvider { 'draft', 'reminder', 'shared_location', + 'deleted_for_me', ]; /// Serialize to json. @@ -419,6 +433,7 @@ class Message extends Equatable implements ComparableFieldProvider { Object? draft = _nullConst, Object? reminder = _nullConst, Location? sharedLocation, + bool? deletedOnlyForMe, }) { assert(() { if (pinExpires is! DateTime && @@ -497,6 +512,7 @@ class Message extends Equatable implements ComparableFieldProvider { reminder: reminder == _nullConst ? this.reminder : reminder as MessageReminder?, sharedLocation: sharedLocation ?? this.sharedLocation, + deletedOnlyForMe: deletedOnlyForMe ?? this.deletedOnlyForMe, ); } @@ -543,6 +559,7 @@ class Message extends Equatable implements ComparableFieldProvider { draft: other.draft, reminder: other.reminder, sharedLocation: other.sharedLocation, + deletedOnlyForMe: other.deletedOnlyForMe, ); } @@ -609,6 +626,7 @@ class Message extends Equatable implements ComparableFieldProvider { draft, reminder, sharedLocation, + deletedOnlyForMe, ]; @override diff --git a/packages/stream_chat/lib/src/core/models/message.g.dart b/packages/stream_chat/lib/src/core/models/message.g.dart index 3add1df41b..e549a16d87 100644 --- a/packages/stream_chat/lib/src/core/models/message.g.dart +++ b/packages/stream_chat/lib/src/core/models/message.g.dart @@ -54,6 +54,7 @@ Message _$MessageFromJson(Map json) => Message( deletedAt: json['deleted_at'] == null ? null : DateTime.parse(json['deleted_at'] as String), + deletedOnlyForMe: json['deleted_for_me'] as bool? ?? false, messageTextUpdatedAt: json['message_text_updated_at'] == null ? null : DateTime.parse(json['message_text_updated_at'] as String), diff --git a/packages/stream_chat/lib/src/core/models/message_delete_scope.dart b/packages/stream_chat/lib/src/core/models/message_delete_scope.dart new file mode 100644 index 0000000000..027ce9ce4c --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/message_delete_scope.dart @@ -0,0 +1,61 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'message_delete_scope.freezed.dart'; +part 'message_delete_scope.g.dart'; + +/// Represents the scope of deletion for a message. +/// +/// - [forMe]: The message is deleted only for the current user. +/// - [forAll]: The message is deleted for all users. The [hard] +/// parameter indicates whether the deletion is permanent (hard) or soft. +@freezed +sealed class MessageDeleteScope with _$MessageDeleteScope { + /// The message is deleted only for the current user. + /// + /// Note: This does not permanently delete the message, it will remain + /// visible to other channel members. + const factory MessageDeleteScope.deleteForMe() = DeleteForMe; + + /// The message is deleted for all users. + /// + /// If [hard] is true, the message is permanently deleted and cannot be + /// recovered. If false, the message is soft deleted and may be recoverable + /// by channel members with the appropriate permissions. + /// + /// Defaults to soft deletion (hard = false). + const factory MessageDeleteScope.deleteForAll({ + @Default(false) bool hard, + }) = DeleteForAll; + + /// Creates a MessageDeletionScope from a JSON map. + factory MessageDeleteScope.fromJson(Map json) => + _$MessageDeleteScopeFromJson(json); + + // region Predefined Scopes + + /// The message is soft deleted for all users. + /// + /// This is equivalent to `MessageDeleteScope.deleteForAll(hard: false)`. + static const softDeleteForAll = MessageDeleteScope.deleteForAll(); + + /// The message is permanently (hard) deleted for all users. + /// + /// This is equivalent to `MessageDeleteScope.deleteForAll(hard: true)`. + static const hardDeleteForAll = MessageDeleteScope.deleteForAll(hard: true); + + // endregion +} + +/// Extension methods for [MessageDeleteScope] to provide additional +/// functionality. +extension MessageDeleteScopeX on MessageDeleteScope { + /// Indicates whether the deletion is permanent (hard) or soft. + /// + /// For [DeleteForMe], this is always false. + bool get hard { + return switch (this) { + DeleteForMe() => false, + DeleteForAll(hard: final hard) => hard, + }; + } +} diff --git a/packages/stream_chat/lib/src/core/models/message_delete_scope.freezed.dart b/packages/stream_chat/lib/src/core/models/message_delete_scope.freezed.dart new file mode 100644 index 0000000000..84da7fb10b --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/message_delete_scope.freezed.dart @@ -0,0 +1,166 @@ +// dart format width=80 +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'message_delete_scope.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +// dart format off +T _$identity(T value) => value; +MessageDeleteScope _$MessageDeleteScopeFromJson(Map json) { + switch (json['runtimeType']) { + case 'deleteForMe': + return DeleteForMe.fromJson(json); + case 'deleteForAll': + return DeleteForAll.fromJson(json); + + default: + throw CheckedFromJsonException(json, 'runtimeType', 'MessageDeleteScope', + 'Invalid union type "${json['runtimeType']}"!'); + } +} + +/// @nodoc +mixin _$MessageDeleteScope { + /// Serializes this MessageDeleteScope to a JSON map. + Map toJson(); + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is MessageDeleteScope); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => runtimeType.hashCode; + + @override + String toString() { + return 'MessageDeleteScope()'; + } +} + +/// @nodoc +class $MessageDeleteScopeCopyWith<$Res> { + $MessageDeleteScopeCopyWith( + MessageDeleteScope _, $Res Function(MessageDeleteScope) __); +} + +/// @nodoc +@JsonSerializable() +class DeleteForMe implements MessageDeleteScope { + const DeleteForMe({final String? $type}) : $type = $type ?? 'deleteForMe'; + factory DeleteForMe.fromJson(Map json) => + _$DeleteForMeFromJson(json); + + @JsonKey(name: 'runtimeType') + final String $type; + + @override + Map toJson() { + return _$DeleteForMeToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && other is DeleteForMe); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => runtimeType.hashCode; + + @override + String toString() { + return 'MessageDeleteScope.deleteForMe()'; + } +} + +/// @nodoc +@JsonSerializable() +class DeleteForAll implements MessageDeleteScope { + const DeleteForAll({this.hard = false, final String? $type}) + : $type = $type ?? 'deleteForAll'; + factory DeleteForAll.fromJson(Map json) => + _$DeleteForAllFromJson(json); + + @JsonKey() + final bool hard; + + @JsonKey(name: 'runtimeType') + final String $type; + + /// Create a copy of MessageDeleteScope + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $DeleteForAllCopyWith get copyWith => + _$DeleteForAllCopyWithImpl(this, _$identity); + + @override + Map toJson() { + return _$DeleteForAllToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is DeleteForAll && + (identical(other.hard, hard) || other.hard == hard)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, hard); + + @override + String toString() { + return 'MessageDeleteScope.deleteForAll(hard: $hard)'; + } +} + +/// @nodoc +abstract mixin class $DeleteForAllCopyWith<$Res> + implements $MessageDeleteScopeCopyWith<$Res> { + factory $DeleteForAllCopyWith( + DeleteForAll value, $Res Function(DeleteForAll) _then) = + _$DeleteForAllCopyWithImpl; + @useResult + $Res call({bool hard}); +} + +/// @nodoc +class _$DeleteForAllCopyWithImpl<$Res> implements $DeleteForAllCopyWith<$Res> { + _$DeleteForAllCopyWithImpl(this._self, this._then); + + final DeleteForAll _self; + final $Res Function(DeleteForAll) _then; + + /// Create a copy of MessageDeleteScope + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + $Res call({ + Object? hard = null, + }) { + return _then(DeleteForAll( + hard: null == hard + ? _self.hard + : hard // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +// dart format on diff --git a/packages/stream_chat/lib/src/core/models/message_delete_scope.g.dart b/packages/stream_chat/lib/src/core/models/message_delete_scope.g.dart new file mode 100644 index 0000000000..0998631d2a --- /dev/null +++ b/packages/stream_chat/lib/src/core/models/message_delete_scope.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'message_delete_scope.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +DeleteForMe _$DeleteForMeFromJson(Map json) => DeleteForMe( + $type: json['runtimeType'] as String?, + ); + +Map _$DeleteForMeToJson(DeleteForMe instance) => + { + 'runtimeType': instance.$type, + }; + +DeleteForAll _$DeleteForAllFromJson(Map json) => DeleteForAll( + hard: json['hard'] as bool? ?? false, + $type: json['runtimeType'] as String?, + ); + +Map _$DeleteForAllToJson(DeleteForAll instance) => + { + 'hard': instance.hard, + 'runtimeType': instance.$type, + }; diff --git a/packages/stream_chat/lib/src/core/models/message_state.dart b/packages/stream_chat/lib/src/core/models/message_state.dart index cf98096843..141bdfd1d8 100644 --- a/packages/stream_chat/lib/src/core/models/message_state.dart +++ b/packages/stream_chat/lib/src/core/models/message_state.dart @@ -1,9 +1,9 @@ // ignore_for_file: avoid_positional_boolean_parameters import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:stream_chat/src/core/models/message_delete_scope.dart'; part 'message_state.freezed.dart'; - part 'message_state.g.dart'; /// Helper extension for [MessageState]. @@ -33,7 +33,7 @@ extension MessageStateX on MessageState { } /// Returns true if the message is in outgoing deleting state. - bool get isDeleting => isSoftDeleting || isHardDeleting; + bool get isDeleting => isSoftDeleting || isHardDeleting || isDeletingForMe; /// Returns true if the message is in outgoing soft deleting state. bool get isSoftDeleting { @@ -43,7 +43,10 @@ extension MessageStateX on MessageState { final outgoingState = messageState.state; if (outgoingState is! Deleting) return false; - return !outgoingState.hard; + final deletingScope = outgoingState.scope; + if (deletingScope is! DeleteForAll) return false; + + return !deletingScope.hard; } /// Returns true if the message is in outgoing hard deleting state. @@ -54,7 +57,22 @@ extension MessageStateX on MessageState { final outgoingState = messageState.state; if (outgoingState is! Deleting) return false; - return outgoingState.hard; + final deletingScope = outgoingState.scope; + if (deletingScope is! DeleteForAll) return false; + + return deletingScope.hard; + } + + /// Returns true if the message is in outgoing deleting for me state. + bool get isDeletingForMe { + final messageState = this; + if (messageState is! MessageOutgoing) return false; + + final outgoingState = messageState.state; + if (outgoingState is! Deleting) return false; + + final deletingScope = outgoingState.scope; + return deletingScope is DeleteForMe; } /// Returns true if the message is in completed sent state. @@ -70,7 +88,7 @@ extension MessageStateX on MessageState { } /// Returns true if the message is in completed deleted state. - bool get isDeleted => isSoftDeleted || isHardDeleted; + bool get isDeleted => isSoftDeleted || isHardDeleted || isDeletedForMe; /// Returns true if the message is in completed soft deleted state. bool get isSoftDeleted { @@ -80,7 +98,10 @@ extension MessageStateX on MessageState { final completedState = messageState.state; if (completedState is! Deleted) return false; - return !completedState.hard; + final deletingScope = completedState.scope; + if (deletingScope is! DeleteForAll) return false; + + return !deletingScope.hard; } /// Returns true if the message is in completed hard deleted state. @@ -91,7 +112,22 @@ extension MessageStateX on MessageState { final completedState = messageState.state; if (completedState is! Deleted) return false; - return completedState.hard; + final deletingScope = completedState.scope; + if (deletingScope is! DeleteForAll) return false; + + return deletingScope.hard; + } + + /// Returns true if the message is in completed deleted for me state. + bool get isDeletedForMe { + final messageState = this; + if (messageState is! MessageCompleted) return false; + + final completedState = messageState.state; + if (completedState is! Deleted) return false; + + final deletingScope = completedState.scope; + return deletingScope is DeleteForMe; } /// Returns true if the message is in failed sending state. @@ -111,7 +147,8 @@ extension MessageStateX on MessageState { } /// Returns true if the message is in failed deleting state. - bool get isDeletingFailed => isSoftDeletingFailed || isHardDeletingFailed; + bool get isDeletingFailed => + isSoftDeletingFailed || isHardDeletingFailed || isDeletingForMeFailed; /// Returns true if the message is in failed soft deleting state. bool get isSoftDeletingFailed { @@ -121,7 +158,10 @@ extension MessageStateX on MessageState { final failedState = messageState.state; if (failedState is! DeletingFailed) return false; - return !failedState.hard; + final deletingScope = failedState.scope; + if (deletingScope is! DeleteForAll) return false; + + return !deletingScope.hard; } /// Returns true if the message is in failed hard deleting state. @@ -132,7 +172,22 @@ extension MessageStateX on MessageState { final failedState = messageState.state; if (failedState is! DeletingFailed) return false; - return failedState.hard; + final deletingScope = failedState.scope; + if (deletingScope is! DeleteForAll) return false; + + return deletingScope.hard; + } + + /// Returns true if the message is in failed deleting for me state. + bool get isDeletingForMeFailed { + final messageState = this; + if (messageState is! MessageFailed) return false; + + final failedState = messageState.state; + if (failedState is! DeletingFailed) return false; + + final deletingScope = failedState.scope; + return deletingScope is DeleteForMe; } } @@ -163,24 +218,32 @@ sealed class MessageState with _$MessageState { factory MessageState.fromJson(Map json) => _$MessageStateFromJson(json); + // region Factory Constructors for Common States + /// Deleting state when the message is being deleted. - factory MessageState.deleting({required bool hard}) { + factory MessageState.deleting({ + required MessageDeleteScope scope, + }) { return MessageState.outgoing( - state: OutgoingState.deleting(hard: hard), + state: OutgoingState.deleting(scope: scope), ); } /// Deleting state when the message has been successfully deleted. - factory MessageState.deleted({required bool hard}) { + factory MessageState.deleted({ + required MessageDeleteScope scope, + }) { return MessageState.completed( - state: CompletedState.deleted(hard: hard), + state: CompletedState.deleted(scope: scope), ); } /// Deleting failed state when the message fails to be deleted. - factory MessageState.deletingFailed({required bool hard}) { + factory MessageState.deletingFailed({ + required MessageDeleteScope scope, + }) { return MessageState.failed( - state: FailedState.deletingFailed(hard: hard), + state: FailedState.deletingFailed(scope: scope), ); } @@ -224,6 +287,10 @@ sealed class MessageState with _$MessageState { ); } + // endregion + + // region Common Static Instances + /// Sending state when the message is being sent. static const sending = MessageState.outgoing( state: OutgoingState.sending(), @@ -241,7 +308,16 @@ sealed class MessageState with _$MessageState { /// Hard deleting state when the message is being hard deleted. static const hardDeleting = MessageState.outgoing( - state: OutgoingState.deleting(hard: true), + state: OutgoingState.deleting( + scope: MessageDeleteScope.hardDeleteForAll, + ), + ); + + /// Deleting for me state when the message is being deleted only for me. + static const deletingForMe = MessageState.outgoing( + state: OutgoingState.deleting( + scope: MessageDeleteScope.deleteForMe(), + ), ); /// Sent state when the message has been successfully sent. @@ -261,7 +337,17 @@ sealed class MessageState with _$MessageState { /// Hard deleted state when the message has been successfully hard deleted. static const hardDeleted = MessageState.completed( - state: CompletedState.deleted(hard: true), + state: CompletedState.deleted( + scope: MessageDeleteScope.hardDeleteForAll, + ), + ); + + /// Deleted for me state when the message has been successfully deleted only + /// for me. + static const deletedForMe = MessageState.completed( + state: CompletedState.deleted( + scope: MessageDeleteScope.deleteForMe(), + ), ); /// Deleting failed state when the message fails to be soft deleted. @@ -271,8 +357,19 @@ sealed class MessageState with _$MessageState { /// Hard deleting failed state when the message fails to be hard deleted. static const hardDeletingFailed = MessageState.failed( - state: FailedState.deletingFailed(hard: true), + state: FailedState.deletingFailed( + scope: MessageDeleteScope.hardDeleteForAll, + ), ); + + /// Deleting for me failed state when the message fails to be deleted only + static const deletingForMeFailed = MessageState.failed( + state: FailedState.deletingFailed( + scope: MessageDeleteScope.deleteForMe(), + ), + ); + + // endregion } /// Represents the state of an outgoing message. @@ -286,7 +383,7 @@ sealed class OutgoingState with _$OutgoingState { /// Deleting state when the message is being deleted. const factory OutgoingState.deleting({ - @Default(false) bool hard, + @Default(MessageDeleteScope.softDeleteForAll) MessageDeleteScope scope, }) = Deleting; /// Creates a new instance from a json @@ -305,7 +402,7 @@ sealed class CompletedState with _$CompletedState { /// Deleted state when the message has been successfully deleted. const factory CompletedState.deleted({ - @Default(false) bool hard, + @Default(MessageDeleteScope.softDeleteForAll) MessageDeleteScope scope, }) = Deleted; /// Creates a new instance from a json @@ -336,7 +433,7 @@ sealed class FailedState with _$FailedState { /// Deleting failed state when the message fails to be deleted. const factory FailedState.deletingFailed({ - @Default(false) bool hard, + @Default(MessageDeleteScope.softDeleteForAll) MessageDeleteScope scope, }) = DeletingFailed; /// Creates a new instance from a json @@ -464,13 +561,13 @@ extension OutgoingStatePatternMatching on OutgoingState { TResult when({ required TResult Function() sending, required TResult Function() updating, - required TResult Function(bool hard) deleting, + required TResult Function(MessageDeleteScope scope) deleting, }) { final outgoingState = this; return switch (outgoingState) { Sending() => sending(), Updating() => updating(), - Deleting() => deleting(outgoingState.hard), + Deleting() => deleting(outgoingState.scope), }; } @@ -479,13 +576,13 @@ extension OutgoingStatePatternMatching on OutgoingState { TResult? whenOrNull({ TResult? Function()? sending, TResult? Function()? updating, - TResult? Function(bool hard)? deleting, + TResult? Function(MessageDeleteScope scope)? deleting, }) { final outgoingState = this; return switch (outgoingState) { Sending() => sending?.call(), Updating() => updating?.call(), - Deleting() => deleting?.call(outgoingState.hard), + Deleting() => deleting?.call(outgoingState.scope), }; } @@ -494,14 +591,14 @@ extension OutgoingStatePatternMatching on OutgoingState { TResult maybeWhen({ TResult Function()? sending, TResult Function()? updating, - TResult Function(bool hard)? deleting, + TResult Function(MessageDeleteScope scope)? deleting, required TResult orElse(), }) { final outgoingState = this; final result = switch (outgoingState) { Sending() => sending?.call(), Updating() => updating?.call(), - Deleting() => deleting?.call(outgoingState.hard), + Deleting() => deleting?.call(outgoingState.scope), }; return result ?? orElse(); @@ -563,13 +660,13 @@ extension CompletedStatePatternMatching on CompletedState { TResult when({ required TResult Function() sent, required TResult Function() updated, - required TResult Function(bool hard) deleted, + required TResult Function(MessageDeleteScope scope) deleted, }) { final completedState = this; return switch (completedState) { Sent() => sent(), Updated() => updated(), - Deleted() => deleted(completedState.hard), + Deleted() => deleted(completedState.scope), }; } @@ -578,13 +675,13 @@ extension CompletedStatePatternMatching on CompletedState { TResult? whenOrNull({ TResult? Function()? sent, TResult? Function()? updated, - TResult? Function(bool hard)? deleted, + TResult? Function(MessageDeleteScope scope)? deleted, }) { final completedState = this; return switch (completedState) { Sent() => sent?.call(), Updated() => updated?.call(), - Deleted() => deleted?.call(completedState.hard), + Deleted() => deleted?.call(completedState.scope), }; } @@ -593,14 +690,14 @@ extension CompletedStatePatternMatching on CompletedState { TResult maybeWhen({ TResult Function()? sent, TResult Function()? updated, - TResult Function(bool hard)? deleted, + TResult Function(MessageDeleteScope scope)? deleted, required TResult orElse(), }) { final completedState = this; final result = switch (completedState) { Sent() => sent?.call(), Updated() => updated?.call(), - Deleted() => deleted?.call(completedState.hard), + Deleted() => deleted?.call(completedState.scope), }; return result ?? orElse(); @@ -665,7 +762,7 @@ extension FailedStatePatternMatching on FailedState { required TResult Function( Map? set, List? unset, bool skipEnrichUrl) partialUpdatingFailed, - required TResult Function(bool hard) deletingFailed, + required TResult Function(MessageDeleteScope scope) deletingFailed, }) { final failedState = this; return switch (failedState) { @@ -675,7 +772,7 @@ extension FailedStatePatternMatching on FailedState { updatingFailed(failedState.skipPush, failedState.skipEnrichUrl), PartialUpdatingFailed() => partialUpdatingFailed( failedState.set, failedState.unset, failedState.skipEnrichUrl), - DeletingFailed() => deletingFailed(failedState.hard), + DeletingFailed() => deletingFailed(failedState.scope), }; } @@ -687,7 +784,7 @@ extension FailedStatePatternMatching on FailedState { required TResult Function( Map? set, List? unset, bool skipEnrichUrl) partialUpdatingFailed, - TResult? Function(bool hard)? deletingFailed, + TResult? Function(MessageDeleteScope scope)? deletingFailed, }) { final failedState = this; return switch (failedState) { @@ -697,7 +794,7 @@ extension FailedStatePatternMatching on FailedState { updatingFailed?.call(failedState.skipPush, failedState.skipEnrichUrl), PartialUpdatingFailed() => partialUpdatingFailed( failedState.set, failedState.unset, failedState.skipEnrichUrl), - DeletingFailed() => deletingFailed?.call(failedState.hard), + DeletingFailed() => deletingFailed?.call(failedState.scope), }; } @@ -709,7 +806,7 @@ extension FailedStatePatternMatching on FailedState { required TResult Function( Map? set, List? unset, bool skipEnrichUrl) partialUpdatingFailed, - TResult Function(bool hard)? deletingFailed, + TResult Function(MessageDeleteScope scope)? deletingFailed, required TResult orElse(), }) { final failedState = this; @@ -720,7 +817,7 @@ extension FailedStatePatternMatching on FailedState { updatingFailed?.call(failedState.skipPush, failedState.skipEnrichUrl), PartialUpdatingFailed() => partialUpdatingFailed( failedState.set, failedState.unset, failedState.skipEnrichUrl), - DeletingFailed() => deletingFailed?.call(failedState.hard), + DeletingFailed() => deletingFailed?.call(failedState.scope), }; return result ?? orElse(); diff --git a/packages/stream_chat/lib/src/core/models/message_state.freezed.dart b/packages/stream_chat/lib/src/core/models/message_state.freezed.dart index 06cb77449e..f896483f61 100644 --- a/packages/stream_chat/lib/src/core/models/message_state.freezed.dart +++ b/packages/stream_chat/lib/src/core/models/message_state.freezed.dart @@ -473,13 +473,14 @@ class Updating implements OutgoingState { /// @nodoc @JsonSerializable() class Deleting implements OutgoingState { - const Deleting({this.hard = false, final String? $type}) + const Deleting( + {this.scope = MessageDeleteScope.softDeleteForAll, final String? $type}) : $type = $type ?? 'deleting'; factory Deleting.fromJson(Map json) => _$DeletingFromJson(json); @JsonKey() - final bool hard; + final MessageDeleteScope scope; @JsonKey(name: 'runtimeType') final String $type; @@ -503,16 +504,16 @@ class Deleting implements OutgoingState { return identical(this, other) || (other.runtimeType == runtimeType && other is Deleting && - (identical(other.hard, hard) || other.hard == hard)); + (identical(other.scope, scope) || other.scope == scope)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, hard); + int get hashCode => Object.hash(runtimeType, scope); @override String toString() { - return 'OutgoingState.deleting(hard: $hard)'; + return 'OutgoingState.deleting(scope: $scope)'; } } @@ -522,7 +523,9 @@ abstract mixin class $DeletingCopyWith<$Res> factory $DeletingCopyWith(Deleting value, $Res Function(Deleting) _then) = _$DeletingCopyWithImpl; @useResult - $Res call({bool hard}); + $Res call({MessageDeleteScope scope}); + + $MessageDeleteScopeCopyWith<$Res> get scope; } /// @nodoc @@ -536,15 +539,25 @@ class _$DeletingCopyWithImpl<$Res> implements $DeletingCopyWith<$Res> { /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') $Res call({ - Object? hard = null, + Object? scope = null, }) { return _then(Deleting( - hard: null == hard - ? _self.hard - : hard // ignore: cast_nullable_to_non_nullable - as bool, + scope: null == scope + ? _self.scope + : scope // ignore: cast_nullable_to_non_nullable + as MessageDeleteScope, )); } + + /// Create a copy of OutgoingState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $MessageDeleteScopeCopyWith<$Res> get scope { + return $MessageDeleteScopeCopyWith<$Res>(_self.scope, (value) { + return _then(_self.copyWith(scope: value)); + }); + } } CompletedState _$CompletedStateFromJson(Map json) { @@ -656,13 +669,14 @@ class Updated implements CompletedState { /// @nodoc @JsonSerializable() class Deleted implements CompletedState { - const Deleted({this.hard = false, final String? $type}) + const Deleted( + {this.scope = MessageDeleteScope.softDeleteForAll, final String? $type}) : $type = $type ?? 'deleted'; factory Deleted.fromJson(Map json) => _$DeletedFromJson(json); @JsonKey() - final bool hard; + final MessageDeleteScope scope; @JsonKey(name: 'runtimeType') final String $type; @@ -686,16 +700,16 @@ class Deleted implements CompletedState { return identical(this, other) || (other.runtimeType == runtimeType && other is Deleted && - (identical(other.hard, hard) || other.hard == hard)); + (identical(other.scope, scope) || other.scope == scope)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, hard); + int get hashCode => Object.hash(runtimeType, scope); @override String toString() { - return 'CompletedState.deleted(hard: $hard)'; + return 'CompletedState.deleted(scope: $scope)'; } } @@ -705,7 +719,9 @@ abstract mixin class $DeletedCopyWith<$Res> factory $DeletedCopyWith(Deleted value, $Res Function(Deleted) _then) = _$DeletedCopyWithImpl; @useResult - $Res call({bool hard}); + $Res call({MessageDeleteScope scope}); + + $MessageDeleteScopeCopyWith<$Res> get scope; } /// @nodoc @@ -719,15 +735,25 @@ class _$DeletedCopyWithImpl<$Res> implements $DeletedCopyWith<$Res> { /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') $Res call({ - Object? hard = null, + Object? scope = null, }) { return _then(Deleted( - hard: null == hard - ? _self.hard - : hard // ignore: cast_nullable_to_non_nullable - as bool, + scope: null == scope + ? _self.scope + : scope // ignore: cast_nullable_to_non_nullable + as MessageDeleteScope, )); } + + /// Create a copy of CompletedState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $MessageDeleteScopeCopyWith<$Res> get scope { + return $MessageDeleteScopeCopyWith<$Res>(_self.scope, (value) { + return _then(_self.copyWith(scope: value)); + }); + } } FailedState _$FailedStateFromJson(Map json) { @@ -1078,13 +1104,14 @@ class _$PartialUpdatingFailedCopyWithImpl<$Res> /// @nodoc @JsonSerializable() class DeletingFailed implements FailedState { - const DeletingFailed({this.hard = false, final String? $type}) + const DeletingFailed( + {this.scope = MessageDeleteScope.softDeleteForAll, final String? $type}) : $type = $type ?? 'deletingFailed'; factory DeletingFailed.fromJson(Map json) => _$DeletingFailedFromJson(json); @JsonKey() - final bool hard; + final MessageDeleteScope scope; @JsonKey(name: 'runtimeType') final String $type; @@ -1108,16 +1135,16 @@ class DeletingFailed implements FailedState { return identical(this, other) || (other.runtimeType == runtimeType && other is DeletingFailed && - (identical(other.hard, hard) || other.hard == hard)); + (identical(other.scope, scope) || other.scope == scope)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, hard); + int get hashCode => Object.hash(runtimeType, scope); @override String toString() { - return 'FailedState.deletingFailed(hard: $hard)'; + return 'FailedState.deletingFailed(scope: $scope)'; } } @@ -1128,7 +1155,9 @@ abstract mixin class $DeletingFailedCopyWith<$Res> DeletingFailed value, $Res Function(DeletingFailed) _then) = _$DeletingFailedCopyWithImpl; @useResult - $Res call({bool hard}); + $Res call({MessageDeleteScope scope}); + + $MessageDeleteScopeCopyWith<$Res> get scope; } /// @nodoc @@ -1143,15 +1172,25 @@ class _$DeletingFailedCopyWithImpl<$Res> /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') $Res call({ - Object? hard = null, + Object? scope = null, }) { return _then(DeletingFailed( - hard: null == hard - ? _self.hard - : hard // ignore: cast_nullable_to_non_nullable - as bool, + scope: null == scope + ? _self.scope + : scope // ignore: cast_nullable_to_non_nullable + as MessageDeleteScope, )); } + + /// Create a copy of FailedState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $MessageDeleteScopeCopyWith<$Res> get scope { + return $MessageDeleteScopeCopyWith<$Res>(_self.scope, (value) { + return _then(_self.copyWith(scope: value)); + }); + } } // dart format on diff --git a/packages/stream_chat/lib/src/core/models/message_state.g.dart b/packages/stream_chat/lib/src/core/models/message_state.g.dart index 3161a4f8c2..693a642e17 100644 --- a/packages/stream_chat/lib/src/core/models/message_state.g.dart +++ b/packages/stream_chat/lib/src/core/models/message_state.g.dart @@ -71,12 +71,14 @@ Map _$UpdatingToJson(Updating instance) => { }; Deleting _$DeletingFromJson(Map json) => Deleting( - hard: json['hard'] as bool? ?? false, + scope: json['scope'] == null + ? MessageDeleteScope.softDeleteForAll + : MessageDeleteScope.fromJson(json['scope'] as Map), $type: json['runtimeType'] as String?, ); Map _$DeletingToJson(Deleting instance) => { - 'hard': instance.hard, + 'scope': instance.scope.toJson(), 'runtimeType': instance.$type, }; @@ -97,12 +99,14 @@ Map _$UpdatedToJson(Updated instance) => { }; Deleted _$DeletedFromJson(Map json) => Deleted( - hard: json['hard'] as bool? ?? false, + scope: json['scope'] == null + ? MessageDeleteScope.softDeleteForAll + : MessageDeleteScope.fromJson(json['scope'] as Map), $type: json['runtimeType'] as String?, ); Map _$DeletedToJson(Deleted instance) => { - 'hard': instance.hard, + 'scope': instance.scope.toJson(), 'runtimeType': instance.$type, }; @@ -155,12 +159,14 @@ Map _$PartialUpdatingFailedToJson( DeletingFailed _$DeletingFailedFromJson(Map json) => DeletingFailed( - hard: json['hard'] as bool? ?? false, + scope: json['scope'] == null + ? MessageDeleteScope.softDeleteForAll + : MessageDeleteScope.fromJson(json['scope'] as Map), $type: json['runtimeType'] as String?, ); Map _$DeletingFailedToJson(DeletingFailed instance) => { - 'hard': instance.hard, + 'scope': instance.scope.toJson(), 'runtimeType': instance.$type, }; diff --git a/packages/stream_chat/lib/stream_chat.dart b/packages/stream_chat/lib/stream_chat.dart index 01b114e5e0..5070a56d9b 100644 --- a/packages/stream_chat/lib/stream_chat.dart +++ b/packages/stream_chat/lib/stream_chat.dart @@ -47,6 +47,7 @@ export 'src/core/models/location.dart'; export 'src/core/models/location_coordinates.dart'; export 'src/core/models/member.dart'; export 'src/core/models/message.dart'; +export 'src/core/models/message_delete_scope.dart'; export 'src/core/models/message_reminder.dart'; export 'src/core/models/message_state.dart'; export 'src/core/models/moderation.dart'; diff --git a/packages/stream_chat/test/fixtures/member.json b/packages/stream_chat/test/fixtures/member.json index 6d95d7ad02..63b7ddd175 100644 --- a/packages/stream_chat/test/fixtures/member.json +++ b/packages/stream_chat/test/fixtures/member.json @@ -12,5 +12,10 @@ "channel_role": "channel_member", "created_at": "2020-01-28T22:17:30.95443Z", "updated_at": "2020-01-28T22:17:30.95443Z", + "deleted_messages": [ + "msg-1", + "msg-2", + "msg-3" + ], "some_custom_field": "with_custom_data" } \ No newline at end of file diff --git a/packages/stream_chat/test/src/client/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart index 9c69178ac8..80430c1642 100644 --- a/packages/stream_chat/test/src/client/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel_test.dart @@ -2239,41 +2239,47 @@ void main() { uploadState: const UploadState.success(), ), ); + const messageId = 'test-message-id'; final message = Message( - attachments: attachments, id: messageId, + attachments: attachments, createdAt: DateTime.now(), state: MessageState.sent, ); - when(() => client.deleteMessage(messageId, hard: true)) - .thenAnswer((_) async => EmptyResponse()); + when( + () => client.deleteMessage(messageId, hard: true), + ).thenAnswer((_) async => EmptyResponse()); - when(() => client.deleteImage(any(), channelId, channelType)) - .thenAnswer((_) async => EmptyResponse()); + when( + () => client.deleteImage(any(), channelId, channelType), + ).thenAnswer((_) async => EmptyResponse()); - when(() => client.deleteFile(any(), channelId, channelType)) - .thenAnswer((_) async => EmptyResponse()); + when( + () => client.deleteFile(any(), channelId, channelType), + ).thenAnswer((_) async => EmptyResponse()); final res = await channel.deleteMessage(message, hard: true); - expect(res, isNotNull); verify(() => client.deleteMessage(messageId, hard: true)).called(1); - verify(() => client.deleteImage( - any(), - channelId, - channelType, - )).called(2); + + verify(() => client.deleteImage(any(), channelId, channelType)) + .called(2); + + verify(() => client.deleteFile(any(), channelId, channelType)) + .called(1); }); test( - '''should directly update the state with message as deleted if the state is sending or failed''', + 'should hard delete the message if the state is sending or failed', () async { const messageId = 'test-message-id'; final message = Message( id: messageId, + text: 'Hello World!', + state: MessageState.sending, ); expectLater( @@ -2282,13 +2288,17 @@ void main() { emitsInOrder([ [ isSameMessageAs( - message.copyWith(state: MessageState.softDeleted), + message.copyWith(state: MessageState.sending), matchMessageState: true, ), ], + const [], // message is hard deleted from state ]), ); + // Add message to channel state first + channel.state?.addNewMessage(message); + final res = await channel.deleteMessage(message); expect(res, isNotNull); @@ -2297,6 +2307,79 @@ void main() { ); }); + group('`.deleteMessageForMe`', () { + test('should work fine', () async { + const messageId = 'test-message-id'; + final message = Message( + id: messageId, + createdAt: DateTime.now(), + state: MessageState.sent, + ); + + when(() => client.deleteMessageForMe(messageId)) + .thenAnswer((_) async => EmptyResponse()); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(state: MessageState.deletingForMe), + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith(state: MessageState.deletedForMe), + matchMessageState: true, + ), + ], + ]), + ); + + final res = await channel.deleteMessageForMe(message); + + expect(res, isNotNull); + + verify(() => client.deleteMessageForMe(messageId)).called(1); + }); + + test( + 'should hard delete the message if the state is sending or failed', + () async { + const messageId = 'test-message-id'; + final message = Message( + id: messageId, + text: 'Hello World!', + state: MessageState.sending, + ); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(state: MessageState.sending), + matchMessageState: true, + ), + ], + const [], // message is hard deleted from state + ]), + ); + + // Add message to channel state first + channel.state?.addNewMessage(message); + + final res = await channel.deleteMessageForMe(message); + + expect(res, isNotNull); + verifyNever(() => client.deleteMessageForMe(messageId)); + }, + ); + }); + group('`.pinMessage`', () { test('should work fine without passing timeoutOrExpirationDate', () async { @@ -7199,7 +7282,7 @@ void main() { final message = Message( id: 'test-message-id', createdAt: DateTime.now(), - state: MessageState.deletingFailed(hard: true), + state: MessageState.hardDeletingFailed, ); when(() => client.deleteMessage( @@ -7223,7 +7306,7 @@ void main() { final message = Message( id: 'test-message-id', createdAt: DateTime.now(), - state: MessageState.deletingFailed(hard: false), + state: MessageState.softDeletingFailed, ); when(() => client.deleteMessage( @@ -7240,6 +7323,25 @@ void main() { )).called(1); }); + test('should call deleteMessageForMe for deletingForMeFailed state', + () async { + final message = Message( + id: 'test-message-id', + createdAt: DateTime.now(), + state: MessageState.deletingForMeFailed, + ); + + when(() => client.deleteMessageForMe(message.id)) + .thenAnswer((_) async => EmptyResponse()); + + final result = await channel.retryMessage(message); + + expect(result, isNotNull); + expect(result, isA()); + + verify(() => client.deleteMessageForMe(message.id)).called(1); + }); + test('should throw AssertionError when message state is not failed', () async { final message = Message( diff --git a/packages/stream_chat/test/src/client/client_test.dart b/packages/stream_chat/test/src/client/client_test.dart index 3a9f3bb66d..cc29b3d68a 100644 --- a/packages/stream_chat/test/src/client/client_test.dart +++ b/packages/stream_chat/test/src/client/client_test.dart @@ -9,6 +9,47 @@ import '../mocks.dart'; import '../utils.dart'; void main() { + test('description', () async { + /// Create a new instance of [StreamChatClient] + /// by passing the apikey obtained from your project dashboard. + final client = StreamChatClient( + 's2dxdhpxd94g', + logLevel: Level.ALL, + ); + + /// Set the current user and connect the websocket. In a production + /// scenario, this should be done using a backend to generate a user token + /// using our server SDK. + /// + /// Please see the following for more information: + /// https://getstream.io/chat/docs/ios_user_setup_and_tokens/ + await client.connectUser( + User(id: 'super-band-9'), + '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''', + ); + + final channels = await client.queryChannelsOnline( + filter: Filter.in_('members', const ['super-band-9']), + ); + + final channel = channels.first; + await channel.watch(); + + channel.on().listen((event) { + // handle event + print('Deleted For Me: ${event.message?.deletedOnlyForMe}'); + }); + + final message = Message(text: 'Hello, world!'); + final response = await channel.sendMessage(message); + print(response); + + final res = await channel.deleteMessageForMe(response.message); + print(res); + // + await Future.delayed(const Duration(seconds: 5)); + }); + group('Fake web-socket connection functions', () { const apiKey = 'test-api-key'; late final api = FakeChatApi(); @@ -3536,6 +3577,20 @@ void main() { verifyNoMoreInteractions(api.message); }); + test('`.deleteMessageForMe`', () async { + const messageId = 'test-message-id'; + + when(() => api.message.deleteMessage(messageId, deleteForMe: true)) + .thenAnswer((_) async => EmptyResponse()); + + final res = await client.deleteMessageForMe(messageId); + expect(res, isNotNull); + + verify(() => api.message.deleteMessage(messageId, deleteForMe: true)) + .called(1); + verifyNoMoreInteractions(api.message); + }); + test('`.getMessage`', () async { const messageId = 'test-message-id'; final message = Message(id: messageId); diff --git a/packages/stream_chat/test/src/core/models/member_test.dart b/packages/stream_chat/test/src/core/models/member_test.dart index 752ff46b60..86693a6504 100644 --- a/packages/stream_chat/test/src/core/models/member_test.dart +++ b/packages/stream_chat/test/src/core/models/member_test.dart @@ -12,6 +12,7 @@ void main() { expect(member.channelRole, 'channel_member'); expect(member.createdAt, DateTime.parse('2020-01-28T22:17:30.95443Z')); expect(member.updatedAt, DateTime.parse('2020-01-28T22:17:30.95443Z')); + expect(member.deletedMessages, ['msg-1', 'msg-2', 'msg-3']); expect(member.extraData['some_custom_field'], 'with_custom_data'); }); diff --git a/packages/stream_chat/test/src/core/models/message_state_test.dart b/packages/stream_chat/test/src/core/models/message_state_test.dart index 4d5ac13a31..f97291f06a 100644 --- a/packages/stream_chat/test/src/core/models/message_state_test.dart +++ b/packages/stream_chat/test/src/core/models/message_state_test.dart @@ -1,5 +1,6 @@ -// ignore_for_file: use_named_constants, lines_longer_than_80_chars +// ignore_for_file: use_named_constants, lines_longer_than_80_chars, avoid_redundant_argument_values +import 'package:stream_chat/src/core/models/message_delete_scope.dart'; import 'package:stream_chat/src/core/models/message_state.dart'; import 'package:test/test.dart'; @@ -74,25 +75,41 @@ void main() { ); test( - 'isSoftDeleting should return true if the message state is MessageOutgoing with Deleting state and not hard deleting', + 'isSoftDeleting should return true if the message state is MessageOutgoing with Deleting state and scope is softDeleteForAll', () { const messageState = MessageState.outgoing( - state: OutgoingState.deleting(), + state: OutgoingState.deleting( + scope: MessageDeleteScope.softDeleteForAll, + ), ); expect(messageState.isSoftDeleting, true); }, ); test( - 'isHardDeleting should return true if the message state is MessageOutgoing with Deleting state and hard deleting', + 'isHardDeleting should return true if the message state is MessageOutgoing with Deleting state and scope is HardDeleteForAll', () { const messageState = MessageState.outgoing( - state: OutgoingState.deleting(hard: true), + state: OutgoingState.deleting( + scope: MessageDeleteScope.hardDeleteForAll, + ), ); expect(messageState.isHardDeleting, true); }, ); + test( + 'isDeletingForMe should return true if the message state is MessageOutgoing with Deleting state and scope is DeleteForMe', + () { + const messageState = MessageState.outgoing( + state: OutgoingState.deleting( + scope: MessageDeleteScope.deleteForMe(), + ), + ); + expect(messageState.isDeletingForMe, true); + }, + ); + test( 'isSent should return true if the message state is MessageCompleted with Sent state', () { @@ -121,25 +138,41 @@ void main() { ); test( - 'isSoftDeleted should return true if the message state is MessageCompleted with Deleted state and not hard deleting', + 'isSoftDeleted should return true if the message state is MessageCompleted with Deleted state and scope is softDeleteForAll', () { const messageState = MessageState.completed( - state: CompletedState.deleted(), + state: CompletedState.deleted( + scope: MessageDeleteScope.softDeleteForAll, + ), ); expect(messageState.isSoftDeleted, true); }, ); test( - 'isHardDeleted should return true if the message state is MessageCompleted with Deleted state and hard deleting', + 'isHardDeleted should return true if the message state is MessageCompleted with Deleted state and scope is hardDeleteForAll', () { const messageState = MessageState.completed( - state: CompletedState.deleted(hard: true), + state: CompletedState.deleted( + scope: MessageDeleteScope.hardDeleteForAll, + ), ); expect(messageState.isHardDeleted, true); }, ); + test( + 'isDeletedForMe should return true if the message state is MessageCompleted with Deleted state and scope is DeleteForMe', + () { + const messageState = MessageState.completed( + state: CompletedState.deleted( + scope: MessageDeleteScope.deleteForMe(), + ), + ); + expect(messageState.isDeletedForMe, true); + }, + ); + test( 'isSendingFailed should return true if the message state is MessageFailed with SendingFailed state', () { @@ -169,24 +202,40 @@ void main() { ); test( - 'isSoftDeletingFailed should return true if the message state is MessageFailed with DeletingFailed state and not hard deleting', + 'isSoftDeletingFailed should return true if the message state is MessageFailed with DeletingFailed state and scope is softDeleteForAll', () { const messageState = MessageState.failed( - state: FailedState.deletingFailed(), + state: FailedState.deletingFailed( + scope: MessageDeleteScope.softDeleteForAll, + ), ); expect(messageState.isSoftDeletingFailed, true); }, ); test( - 'isHardDeletingFailed should return true if the message state is MessageFailed with DeletingFailed state and hard deleting', + 'isHardDeletingFailed should return true if the message state is MessageFailed with DeletingFailed state and scope is hardDeleteForAll', () { const messageState = MessageState.failed( - state: FailedState.deletingFailed(hard: true), + state: FailedState.deletingFailed( + scope: MessageDeleteScope.hardDeleteForAll, + ), ); expect(messageState.isHardDeletingFailed, true); }, ); + + test( + 'isDeletingForMeFailed should return true if the message state is MessageFailed with DeletingFailed state and scope is DeleteForMe', + () { + const messageState = MessageState.failed( + state: FailedState.deletingFailed( + scope: MessageDeleteScope.deleteForMe(), + ), + ); + expect(messageState.isDeletingForMeFailed, true); + }, + ); }, ); @@ -210,22 +259,41 @@ void main() { ); test( - 'MessageState.softDeleting should create a MessageOutgoing instance with Deleting state and not hard deleting', + 'MessageState.softDeleting should create a MessageOutgoing instance with Deleting state and softDeleteForAll scope', () { const messageState = MessageState.softDeleting; expect(messageState, isA()); expect((messageState as MessageOutgoing).state, isA()); - expect((messageState.state as Deleting).hard, false); + + final deletingState = messageState.state as Deleting; + expect(deletingState.scope, isA()); + expect((deletingState.scope as DeleteForAll).hard, false); }, ); test( - 'MessageState.hardDeleting should create a MessageOutgoing instance with Deleting state and hard deleting', + 'MessageState.hardDeleting should create a MessageOutgoing instance with Deleting state and hardDeleteForAll scope', () { const messageState = MessageState.hardDeleting; expect(messageState, isA()); expect((messageState as MessageOutgoing).state, isA()); - expect((messageState.state as Deleting).hard, true); + + final deletingState = messageState.state as Deleting; + expect(deletingState.scope, isA()); + expect((deletingState.scope as DeleteForAll).hard, true); + }, + ); + + test( + 'MessageState.deletingForMe should create a MessageOutgoing instance with Deleting state and DeleteForMe scope', + () { + const messageState = MessageState.deletingForMe; + expect(messageState, isA()); + expect((messageState as MessageOutgoing).state, isA()); + + final deletingState = messageState.state as Deleting; + expect(deletingState.scope, isA()); + expect((deletingState.scope as DeleteForMe).hard, false); }, ); @@ -248,22 +316,41 @@ void main() { ); test( - 'MessageState.softDeleted should create a MessageCompleted instance with Deleted state and not hard deleting', + 'MessageState.softDeleted should create a MessageCompleted instance with Deleted state and softDeleteForAll scope', () { const messageState = MessageState.softDeleted; expect(messageState, isA()); expect((messageState as MessageCompleted).state, isA()); - expect((messageState.state as Deleted).hard, false); + + final deletedState = messageState.state as Deleted; + expect(deletedState.scope, isA()); + expect((deletedState.scope as DeleteForAll).hard, false); }, ); test( - 'MessageState.hardDeleted should create a MessageCompleted instance with Deleted state and hard deleting', + 'MessageState.hardDeleted should create a MessageCompleted instance with Deleted state and hardDeleteForAll scope', () { const messageState = MessageState.hardDeleted; expect(messageState, isA()); expect((messageState as MessageCompleted).state, isA()); - expect((messageState.state as Deleted).hard, true); + + final deletedState = messageState.state as Deleted; + expect(deletedState.scope, isA()); + expect((deletedState.scope as DeleteForAll).hard, true); + }, + ); + + test( + 'MessageState.deletedForMe should create a MessageCompleted instance with Deleted state and DeleteForMe scope', + () { + const messageState = MessageState.deletedForMe; + expect(messageState, isA()); + expect((messageState as MessageCompleted).state, isA()); + + final deletedState = messageState.state as Deleted; + expect(deletedState.scope, isA()); + expect((deletedState.scope as DeleteForMe).hard, false); }, ); @@ -306,22 +393,41 @@ void main() { ); test( - 'MessageState.softDeletingFailed should create a MessageFailed instance with DeletingFailed state and not hard deleting', + 'MessageState.softDeletingFailed should create a MessageFailed instance with DeletingFailed state and softDeleteForAll scope', () { const messageState = MessageState.softDeletingFailed; expect(messageState, isA()); expect((messageState as MessageFailed).state, isA()); - expect((messageState.state as DeletingFailed).hard, false); + + final failedState = messageState.state as DeletingFailed; + expect(failedState.scope, isA()); + expect((failedState.scope as DeleteForAll).hard, false); }, ); test( - 'MessageState.hardDeletingFailed should create a MessageFailed instance with DeletingFailed state and hard deleting', + 'MessageState.hardDeletingFailed should create a MessageFailed instance with DeletingFailed state and hardDeleteForAll scope', () { const messageState = MessageState.hardDeletingFailed; expect(messageState, isA()); expect((messageState as MessageFailed).state, isA()); - expect((messageState.state as DeletingFailed).hard, true); + + final failedState = messageState.state as DeletingFailed; + expect(failedState.scope, isA()); + expect((failedState.scope as DeleteForAll).hard, true); + }, + ); + + test( + 'MessageState.deletingForMeFailed should create a MessageFailed instance with DeletingFailed state and DeleteForMe scope', + () { + const messageState = MessageState.deletingForMeFailed; + expect(messageState, isA()); + expect((messageState as MessageFailed).state, isA()); + + final failedState = messageState.state as DeletingFailed; + expect(failedState.scope, isA()); + expect((failedState.scope as DeleteForMe).hard, false); }, ); }); diff --git a/sample_app/lib/pages/channel_page.dart b/sample_app/lib/pages/channel_page.dart index 988ac96959..7e55149a17 100644 --- a/sample_app/lib/pages/channel_page.dart +++ b/sample_app/lib/pages/channel_page.dart @@ -1,4 +1,4 @@ -// ignore_for_file: deprecated_member_use +// ignore_for_file: deprecated_member_use, avoid_redundant_argument_values import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; @@ -212,40 +212,40 @@ class _ChannelPageState extends State { final channel = StreamChannel.of(context).channel; final channelConfig = channel.config; + final currentUser = StreamChat.of(context).currentUser; + final isSentByCurrentUser = message.user?.id == currentUser?.id; + final canDeleteAnyMessage = channel.canDeleteAnyMessage; + final canDeleteOwnMessage = channel.canDeleteOwnMessage; + final customOptions = [ + if (isSentByCurrentUser && (canDeleteAnyMessage || canDeleteOwnMessage)) + StreamMessageAction( + isDestructive: true, + title: const Text('Delete Message for Me'), + action: DeleteForMe(message: message), + leading: const StreamSvgIcon(icon: StreamSvgIcons.delete), + ), if (channelConfig?.userMessageReminders == true) ...[ if (reminder != null) ...[ StreamMessageAction( - leading: StreamSvgIcon( - icon: StreamSvgIcons.time, - color: colorTheme.textLowEmphasis, - ), title: const Text('Edit Reminder'), + leading: const StreamSvgIcon(icon: StreamSvgIcons.time), action: EditReminder(message: message, reminder: reminder), ), StreamMessageAction( - leading: StreamSvgIcon( - icon: StreamSvgIcons.checkAll, - color: colorTheme.textLowEmphasis, - ), title: const Text('Remove from later'), + leading: const StreamSvgIcon(icon: StreamSvgIcons.checkAll), action: RemoveReminder(message: message, reminder: reminder), ), ] else ...[ StreamMessageAction( - leading: StreamSvgIcon( - icon: StreamSvgIcons.time, - color: colorTheme.textLowEmphasis, - ), title: const Text('Remind me'), + leading: const StreamSvgIcon(icon: StreamSvgIcons.time), action: CreateReminder(message: message), ), StreamMessageAction( - leading: Icon( - Icons.bookmark_border, - color: colorTheme.textLowEmphasis, - ), title: const Text('Save for later'), + leading: const Icon(Icons.bookmark_border), action: CreateBookmark(message: message), ), ], @@ -301,6 +301,7 @@ class _ChannelPageState extends State { CreateBookmark() => _createBookmark(it.message), EditReminder() => _editReminder(it.message, it.reminder), RemoveReminder() => _removeReminder(it.message, it.reminder), + DeleteForMe() => _deleteMessageForMe(it.message), _ => null, }, attachmentBuilders: [locationAttachmentBuilder], @@ -374,6 +375,24 @@ class _ChannelPageState extends State { return client.createReminder(messageId).ignore(); } + Future _deleteMessageForMe(Message message) async { + final confirmDelete = await showStreamDialog( + context: context, + builder: (context) => const StreamMessageActionConfirmationModal( + isDestructiveAction: true, + title: Text('Delete for me'), + content: Text('Are you sure you want to delete this message for you?'), + cancelActionTitle: Text('Cancel'), + confirmActionTitle: Text('Delete'), + ), + ); + + if (confirmDelete != true) return; + + final channel = StreamChannel.of(context).channel; + return channel.deleteMessageForMe(message).ignore(); + } + bool defaultFilter(Message m) { final currentUser = StreamChat.of(context).currentUser; final isMyMessage = m.user?.id == currentUser?.id; @@ -419,3 +438,7 @@ final class RemoveReminder extends ReminderMessageAction { @override final MessageReminder reminder; } + +class DeleteForMe extends CustomMessageAction { + const DeleteForMe({required super.message}); +} From 4c762d5a785ffea27bdda04b36e9994c5be99cd6 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 25 Sep 2025 07:48:48 +0200 Subject: [PATCH 02/16] feat: Add sentenceCase extension and replace capitalize extension This commit introduces a new `sentenceCase` extension for strings and replaces all usages of the deprecated `capitalize` extension with it. The `sentenceCase` extension converts a string to sentence case, where the first letter is capitalized and the rest of the string is in lowercase. This provides more accurate capitalization for various UI elements. The `capitalize` extension has been deprecated and will be removed in a future release. --- .../builder/url_attachment_builder.dart | 4 ++-- .../attachment_actions_modal.dart | 2 +- .../stream_command_autocomplete_options.dart | 2 +- .../message_widget/giphy_ephemeral_message.dart | 6 +++--- .../lib/src/message_widget/message_widget.dart | 11 ----------- .../lib/src/utils/extensions.dart | 16 ++++++++++++++-- 6 files changed, 21 insertions(+), 20 deletions(-) diff --git a/packages/stream_chat_flutter/lib/src/attachment/builder/url_attachment_builder.dart b/packages/stream_chat_flutter/lib/src/attachment/builder/url_attachment_builder.dart index ab464d9ed0..58c85fe15b 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/builder/url_attachment_builder.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/builder/url_attachment_builder.dart @@ -65,9 +65,9 @@ class UrlAttachmentBuilder extends StreamAttachmentWidgetBuilder { final host = Uri.parse(urlPreview.titleLink!).host; final splitList = host.split('.'); final hostName = splitList.length == 3 ? splitList[1] : splitList[0]; - final hostDisplayName = urlPreview.authorName?.capitalize() ?? + final hostDisplayName = urlPreview.authorName?.sentenceCase ?? getWebsiteName(hostName.toLowerCase()) ?? - hostName.capitalize(); + hostName.sentenceCase; return InkWell( onTap: onTap, diff --git a/packages/stream_chat_flutter/lib/src/attachment_actions_modal/attachment_actions_modal.dart b/packages/stream_chat_flutter/lib/src/attachment_actions_modal/attachment_actions_modal.dart index f11e4e127e..c36963a7aa 100644 --- a/packages/stream_chat_flutter/lib/src/attachment_actions_modal/attachment_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/attachment_actions_modal/attachment_actions_modal.dart @@ -195,7 +195,7 @@ class AttachmentActionsModal extends StatelessWidget { showDelete) _buildButton( context, - context.translations.deleteLabel.capitalize(), + context.translations.deleteLabel.sentenceCase, StreamSvgIcon( size: 24, icon: StreamSvgIcons.delete, diff --git a/packages/stream_chat_flutter/lib/src/autocomplete/stream_command_autocomplete_options.dart b/packages/stream_chat_flutter/lib/src/autocomplete/stream_command_autocomplete_options.dart index fddd4abc72..d2556eb55a 100644 --- a/packages/stream_chat_flutter/lib/src/autocomplete/stream_command_autocomplete_options.dart +++ b/packages/stream_chat_flutter/lib/src/autocomplete/stream_command_autocomplete_options.dart @@ -65,7 +65,7 @@ class StreamCommandAutocompleteOptions extends StatelessWidget { title: Row( children: [ Text( - command.name.capitalize(), + command.name.sentenceCase, style: textTheme.bodyBold.copyWith( color: colorTheme.textHighEmphasis, ), diff --git a/packages/stream_chat_flutter/lib/src/message_widget/giphy_ephemeral_message.dart b/packages/stream_chat_flutter/lib/src/message_widget/giphy_ephemeral_message.dart index dea2cc8e2c..b47013df1b 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/giphy_ephemeral_message.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/giphy_ephemeral_message.dart @@ -159,7 +159,7 @@ class GiphyActions extends StatelessWidget { textStyle: textTheme.bodyBold, foregroundColor: colorTheme.textLowEmphasis, ), - child: Text(context.translations.cancelLabel.capitalize()), + child: Text(context.translations.cancelLabel.sentenceCase), ), ), VerticalDivider(thickness: 1, width: 4, color: colorTheme.borders), @@ -173,7 +173,7 @@ class GiphyActions extends StatelessWidget { textStyle: textTheme.bodyBold, foregroundColor: colorTheme.textLowEmphasis, ), - child: Text(context.translations.shuffleLabel.capitalize()), + child: Text(context.translations.shuffleLabel.sentenceCase), ), ), VerticalDivider(thickness: 1, width: 4, color: colorTheme.borders), @@ -187,7 +187,7 @@ class GiphyActions extends StatelessWidget { textStyle: textTheme.bodyBold, foregroundColor: colorTheme.accentPrimary, ), - child: Text(context.translations.sendLabel.capitalize()), + child: Text(context.translations.sendLabel.sentenceCase), ), ), ], diff --git a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart index ec54d488aa..dc82cf1914 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget/message_widget.dart @@ -1194,14 +1194,3 @@ extension on Poll { return copyWith(name: trimmedName); } } - -extension on String { - String get sentenceCase { - if (isEmpty) return this; - - final firstChar = this[0].toUpperCase(); - final restOfString = substring(1).toLowerCase(); - - return '$firstChar$restOfString'; - } -} diff --git a/packages/stream_chat_flutter/lib/src/utils/extensions.dart b/packages/stream_chat_flutter/lib/src/utils/extensions.dart index 3b9ea9ac21..4fbb58a52a 100644 --- a/packages/stream_chat_flutter/lib/src/utils/extensions.dart +++ b/packages/stream_chat_flutter/lib/src/utils/extensions.dart @@ -44,8 +44,20 @@ extension DurationExtension on Duration { /// String extension extension StringExtension on String { /// Returns the capitalized string - String capitalize() => - isNotEmpty ? '${this[0].toUpperCase()}${substring(1).toLowerCase()}' : ''; + @Deprecated('Use sentenceCase instead') + String capitalize() => sentenceCase; + + /// Returns the string in sentence case. + /// + /// Example: 'hello WORLD' -> 'Hello world' + String get sentenceCase { + if (isEmpty) return this; + + final firstChar = this[0].toUpperCase(); + final restOfString = substring(1).toLowerCase(); + + return '$firstChar$restOfString'; + } /// Returns the biggest line of a text. String biggestLine() { From aea85a0870b4753f411464775830fd14edd27ff7 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 25 Sep 2025 08:00:36 +0200 Subject: [PATCH 03/16] chore: remove demo tests --- .../test/src/client/client_test.dart | 41 ------------------- 1 file changed, 41 deletions(-) diff --git a/packages/stream_chat/test/src/client/client_test.dart b/packages/stream_chat/test/src/client/client_test.dart index cc29b3d68a..de03ea97f7 100644 --- a/packages/stream_chat/test/src/client/client_test.dart +++ b/packages/stream_chat/test/src/client/client_test.dart @@ -9,47 +9,6 @@ import '../mocks.dart'; import '../utils.dart'; void main() { - test('description', () async { - /// Create a new instance of [StreamChatClient] - /// by passing the apikey obtained from your project dashboard. - final client = StreamChatClient( - 's2dxdhpxd94g', - logLevel: Level.ALL, - ); - - /// Set the current user and connect the websocket. In a production - /// scenario, this should be done using a backend to generate a user token - /// using our server SDK. - /// - /// Please see the following for more information: - /// https://getstream.io/chat/docs/ios_user_setup_and_tokens/ - await client.connectUser( - User(id: 'super-band-9'), - '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''', - ); - - final channels = await client.queryChannelsOnline( - filter: Filter.in_('members', const ['super-band-9']), - ); - - final channel = channels.first; - await channel.watch(); - - channel.on().listen((event) { - // handle event - print('Deleted For Me: ${event.message?.deletedOnlyForMe}'); - }); - - final message = Message(text: 'Hello, world!'); - final response = await channel.sendMessage(message); - print(response); - - final res = await channel.deleteMessageForMe(response.message); - print(res); - // - await Future.delayed(const Duration(seconds: 5)); - }); - group('Fake web-socket connection functions', () { const apiKey = 'test-api-key'; late final api = FakeChatApi(); From 6bbb62494d64fc9b5d3f92fdc3634d25c423bd17 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 25 Sep 2025 08:02:18 +0200 Subject: [PATCH 04/16] fix(samples): only allow deleting own messages if user has permission --- sample_app/lib/pages/channel_page.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sample_app/lib/pages/channel_page.dart b/sample_app/lib/pages/channel_page.dart index 7e55149a17..1477d5cc86 100644 --- a/sample_app/lib/pages/channel_page.dart +++ b/sample_app/lib/pages/channel_page.dart @@ -214,11 +214,10 @@ class _ChannelPageState extends State { final currentUser = StreamChat.of(context).currentUser; final isSentByCurrentUser = message.user?.id == currentUser?.id; - final canDeleteAnyMessage = channel.canDeleteAnyMessage; final canDeleteOwnMessage = channel.canDeleteOwnMessage; final customOptions = [ - if (isSentByCurrentUser && (canDeleteAnyMessage || canDeleteOwnMessage)) + if (isSentByCurrentUser && canDeleteOwnMessage) StreamMessageAction( isDestructive: true, title: const Text('Delete Message for Me'), From 034268d2074b973bcf02a1da2b2257f2591b66d6 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 25 Sep 2025 08:07:06 +0200 Subject: [PATCH 05/16] refactor: rename DeleteForMe to DeleteMessageForMe This commit renames the `DeleteForMe` class and its usages to `DeleteMessageForMe` for clarity. --- sample_app/lib/pages/channel_page.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sample_app/lib/pages/channel_page.dart b/sample_app/lib/pages/channel_page.dart index 1477d5cc86..3c5acfe89e 100644 --- a/sample_app/lib/pages/channel_page.dart +++ b/sample_app/lib/pages/channel_page.dart @@ -221,7 +221,7 @@ class _ChannelPageState extends State { StreamMessageAction( isDestructive: true, title: const Text('Delete Message for Me'), - action: DeleteForMe(message: message), + action: DeleteMessageForMe(message: message), leading: const StreamSvgIcon(icon: StreamSvgIcons.delete), ), if (channelConfig?.userMessageReminders == true) ...[ @@ -300,7 +300,7 @@ class _ChannelPageState extends State { CreateBookmark() => _createBookmark(it.message), EditReminder() => _editReminder(it.message, it.reminder), RemoveReminder() => _removeReminder(it.message, it.reminder), - DeleteForMe() => _deleteMessageForMe(it.message), + DeleteMessageForMe() => _deleteMessageForMe(it.message), _ => null, }, attachmentBuilders: [locationAttachmentBuilder], @@ -438,6 +438,6 @@ final class RemoveReminder extends ReminderMessageAction { final MessageReminder reminder; } -class DeleteForMe extends CustomMessageAction { - const DeleteForMe({required super.message}); +class DeleteMessageForMe extends CustomMessageAction { + const DeleteMessageForMe({required super.message}); } From 2c6dd69b3ceba84173f7637f994da546438bea30 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 25 Sep 2025 08:10:48 +0200 Subject: [PATCH 06/16] docs: update MessageDeleteScope docs The documentation for MessageDeleteScope was updated to reflect the new names for the enum values. `forMe` was changed to `deleteForMe` and `forAll` was changed to `deleteForAll`. --- .../lib/src/core/models/message_delete_scope.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/stream_chat/lib/src/core/models/message_delete_scope.dart b/packages/stream_chat/lib/src/core/models/message_delete_scope.dart index 027ce9ce4c..a769fbae4e 100644 --- a/packages/stream_chat/lib/src/core/models/message_delete_scope.dart +++ b/packages/stream_chat/lib/src/core/models/message_delete_scope.dart @@ -5,9 +5,9 @@ part 'message_delete_scope.g.dart'; /// Represents the scope of deletion for a message. /// -/// - [forMe]: The message is deleted only for the current user. -/// - [forAll]: The message is deleted for all users. The [hard] -/// parameter indicates whether the deletion is permanent (hard) or soft. +/// - [deleteForMe]: The message is deleted only for the current user. +/// - [deleteForAll]: The message is deleted for all users. The [hard] +/// parameter indicates whether the deletion is permanent (hard) or soft. @freezed sealed class MessageDeleteScope with _$MessageDeleteScope { /// The message is deleted only for the current user. From 4bef4ffe1924d89b337438ba7d0b7deeed6fe9d7 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 25 Sep 2025 08:24:22 +0200 Subject: [PATCH 07/16] docs: update comment for MessageDeleteScope factory method --- .../stream_chat/lib/src/core/models/message_delete_scope.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/lib/src/core/models/message_delete_scope.dart b/packages/stream_chat/lib/src/core/models/message_delete_scope.dart index a769fbae4e..c08f1ea01c 100644 --- a/packages/stream_chat/lib/src/core/models/message_delete_scope.dart +++ b/packages/stream_chat/lib/src/core/models/message_delete_scope.dart @@ -27,7 +27,7 @@ sealed class MessageDeleteScope with _$MessageDeleteScope { @Default(false) bool hard, }) = DeleteForAll; - /// Creates a MessageDeletionScope from a JSON map. + /// Creates a instance of [MessageDeleteScope] from a JSON map. factory MessageDeleteScope.fromJson(Map json) => _$MessageDeleteScopeFromJson(json); From 3f7ee969378474ca8126591ee0f855c17445e2c4 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 25 Sep 2025 08:25:49 +0200 Subject: [PATCH 08/16] refactor: mark DeleteMessageForMe as final --- sample_app/lib/pages/channel_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample_app/lib/pages/channel_page.dart b/sample_app/lib/pages/channel_page.dart index 3c5acfe89e..be6537b8a9 100644 --- a/sample_app/lib/pages/channel_page.dart +++ b/sample_app/lib/pages/channel_page.dart @@ -438,6 +438,6 @@ final class RemoveReminder extends ReminderMessageAction { final MessageReminder reminder; } -class DeleteMessageForMe extends CustomMessageAction { +final class DeleteMessageForMe extends CustomMessageAction { const DeleteMessageForMe({required super.message}); } From b8a47b19f9780100d82bb4dd3153b2b895092404 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 25 Sep 2025 09:24:57 +0200 Subject: [PATCH 09/16] feat(ui): add delete action to failed message This commit introduces a new message action that allows users to delete a message that failed to send. The new action is added to the `MessageActionsBuilder` and is only displayed when the message state is `isSendingFailed`. --- .../src/message_action/message_actions_builder.dart | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/stream_chat_flutter/lib/src/message_action/message_actions_builder.dart b/packages/stream_chat_flutter/lib/src/message_action/message_actions_builder.dart index f9dbae07b4..527df91b09 100644 --- a/packages/stream_chat_flutter/lib/src/message_action/message_actions_builder.dart +++ b/packages/stream_chat_flutter/lib/src/message_action/message_actions_builder.dart @@ -79,6 +79,17 @@ class StreamMessageActionsBuilder { ), ), ), + if (messageState.isSendingFailed) + StreamMessageAction( + isDestructive: true, + action: HardDeleteMessage(message: message), + leading: const StreamSvgIcon(icon: StreamSvgIcons.delete), + title: Text( + context.translations.toggleDeleteRetryDeleteMessageText( + isDeleteFailed: false, + ), + ), + ), ], if (message.state.isDeletingFailed) StreamMessageAction( From 80e79641dcda2d6887b6b2c865569f092b588141 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 25 Sep 2025 09:25:19 +0200 Subject: [PATCH 10/16] chore: fix lint --- packages/stream_chat/lib/src/core/api/message_api.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/stream_chat/lib/src/core/api/message_api.dart b/packages/stream_chat/lib/src/core/api/message_api.dart index 46e0979c83..3dc132ea1c 100644 --- a/packages/stream_chat/lib/src/core/api/message_api.dart +++ b/packages/stream_chat/lib/src/core/api/message_api.dart @@ -179,7 +179,7 @@ class MessageApi { }) async { if (hard == true && deleteForMe == true) { throw ArgumentError( - 'Both hard and deleteForMe cannot be set at the same time.' + 'Both hard and deleteForMe cannot be set at the same time.', ); } From d7a4c066724597653b2aa2ec76588f6b4c6ed359 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 25 Sep 2025 10:00:39 +0200 Subject: [PATCH 11/16] refactor: rename `deletedOnlyForMe to` `deletedForMe` in `Message` model --- .../stream_chat/lib/src/client/channel.dart | 6 +++--- .../lib/src/core/models/message.dart | 18 +++++++++--------- .../lib/src/core/models/message.g.dart | 2 +- .../test/src/core/api/message_api_test.dart | 7 ++++--- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 2683585537..3da53ad054 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -971,7 +971,7 @@ class Channel { message = message.copyWith( type: MessageType.deleted, deletedAt: DateTime.now(), - deletedOnlyForMe: scope is DeleteForMe, + deletedForMe: scope is DeleteForMe, state: MessageState.deleting(scope: scope), ); @@ -988,7 +988,7 @@ class Channel { ); final deletedMessage = message.copyWith( - deletedOnlyForMe: scope is DeleteForMe, + deletedForMe: scope is DeleteForMe, state: MessageState.deleted(scope: scope), ); @@ -3056,7 +3056,7 @@ class ChannelClientState { final message = event.message!.copyWith( // TODO: Remove once deletedForMe is properly enriched on the backend. - deletedOnlyForMe: event.deletedForMe, + deletedForMe: event.deletedForMe, ); return deleteMessage(message, hardDelete: hardDelete); diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index f8a320d249..2c2968f700 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -51,7 +51,7 @@ class Message extends Equatable implements ComparableFieldProvider { this.localUpdatedAt, DateTime? deletedAt, this.localDeletedAt, - this.deletedOnlyForMe = false, + this.deletedForMe, this.messageTextUpdatedAt, this.user, this.pinned = false, @@ -85,12 +85,12 @@ class Message extends Equatable implements ComparableFieldProvider { // TODO: Remove this once type are properly enriched on the backend. var type = message.type; - if (message.deletedOnlyForMe) { + if (message.deletedForMe ?? false) { type = MessageType.deleted; } var state = MessageState.sent; - if (message.deletedOnlyForMe) { + if (message.deletedForMe ?? false) { state = MessageState.deletedForMe; } else if (message.deletedAt != null) { state = MessageState.softDeleted; @@ -321,8 +321,8 @@ class Message extends Equatable implements ComparableFieldProvider { final Location? sharedLocation; /// Whether the message was deleted only for the current user. - @JsonKey(name: 'deleted_for_me', defaultValue: false, includeToJson: false) - final bool deletedOnlyForMe; + @JsonKey(includeToJson: false) + final bool? deletedForMe; /// Message custom extraData. final Map extraData; @@ -433,7 +433,7 @@ class Message extends Equatable implements ComparableFieldProvider { Object? draft = _nullConst, Object? reminder = _nullConst, Location? sharedLocation, - bool? deletedOnlyForMe, + bool? deletedForMe, }) { assert(() { if (pinExpires is! DateTime && @@ -512,7 +512,7 @@ class Message extends Equatable implements ComparableFieldProvider { reminder: reminder == _nullConst ? this.reminder : reminder as MessageReminder?, sharedLocation: sharedLocation ?? this.sharedLocation, - deletedOnlyForMe: deletedOnlyForMe ?? this.deletedOnlyForMe, + deletedForMe: deletedForMe ?? this.deletedForMe, ); } @@ -559,7 +559,7 @@ class Message extends Equatable implements ComparableFieldProvider { draft: other.draft, reminder: other.reminder, sharedLocation: other.sharedLocation, - deletedOnlyForMe: other.deletedOnlyForMe, + deletedForMe: other.deletedForMe, ); } @@ -626,7 +626,7 @@ class Message extends Equatable implements ComparableFieldProvider { draft, reminder, sharedLocation, - deletedOnlyForMe, + deletedForMe, ]; @override diff --git a/packages/stream_chat/lib/src/core/models/message.g.dart b/packages/stream_chat/lib/src/core/models/message.g.dart index e549a16d87..52a82f6ab3 100644 --- a/packages/stream_chat/lib/src/core/models/message.g.dart +++ b/packages/stream_chat/lib/src/core/models/message.g.dart @@ -54,7 +54,7 @@ Message _$MessageFromJson(Map json) => Message( deletedAt: json['deleted_at'] == null ? null : DateTime.parse(json['deleted_at'] as String), - deletedOnlyForMe: json['deleted_for_me'] as bool? ?? false, + deletedForMe: json['deleted_for_me'] as bool?, messageTextUpdatedAt: json['message_text_updated_at'] == null ? null : DateTime.parse(json['message_text_updated_at'] as String), diff --git a/packages/stream_chat/test/src/core/api/message_api_test.dart b/packages/stream_chat/test/src/core/api/message_api_test.dart index e0572cb911..bfe9bd7815 100644 --- a/packages/stream_chat/test/src/core/api/message_api_test.dart +++ b/packages/stream_chat/test/src/core/api/message_api_test.dart @@ -205,16 +205,17 @@ void main() { const messageId = 'test-message-id'; const path = '/messages/$messageId'; + const params = {'delete_for_me': true}; - when(() => client.delete(path)).thenAnswer( + when(() => client.delete(path, queryParameters: params)).thenAnswer( (_) async => successResponse(path, data: {}), ); - final res = await messageApi.deleteMessage(messageId); + final res = await messageApi.deleteMessage(messageId, deleteForMe: true); expect(res, isNotNull); - verify(() => client.delete(path)).called(1); + verify(() => client.delete(path, queryParameters: params)).called(1); verifyNoMoreInteractions(client); }); From be9ebfc528107da88f1856458cbb351120f09fa7 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 25 Sep 2025 10:43:01 +0200 Subject: [PATCH 12/16] chore: update CHANGELOG.md and migration guide --- migrations/v10-migration.md | 84 +++++++++++++++++++++++++++++++ packages/stream_chat/CHANGELOG.md | 19 +++++++ 2 files changed, 103 insertions(+) diff --git a/migrations/v10-migration.md b/migrations/v10-migration.md index 951fd6c344..fbce8c2d25 100644 --- a/migrations/v10-migration.md +++ b/migrations/v10-migration.md @@ -8,6 +8,10 @@ This guide includes breaking changes grouped by release phase: +### 🚧 v10.0.0-beta.7 + +- [MessageState](#-messagestate) + ### 🚧 v10.0.0-beta.4 - [SendReaction](#-sendreaction) @@ -28,6 +32,77 @@ This guide includes breaking changes grouped by release phase: --- +## 🧪 Migration for v10.0.0-beta.7 + +### 🛠 MessageState + +#### Key Changes: + +- `MessageState` factory constructors now accept `MessageDeleteScope` instead of `bool hard` parameter +- Pattern matching callbacks in state classes now receive `MessageDeleteScope scope` instead of `bool hard` +- New delete-for-me functionality with dedicated states and methods + +#### Migration Steps: + +**Before:** +```dart +// Factory constructors with bool hard +final deletingState = MessageState.deleting(hard: true); +final deletedState = MessageState.deleted(hard: false); +final failedState = MessageState.deletingFailed(hard: true); + +// Pattern matching with bool hard +message.state.whenOrNull( + deleting: (hard) => handleDeleting(hard), + deleted: (hard) => handleDeleted(hard), + deletingFailed: (hard) => handleDeletingFailed(hard), +); +``` + +**After:** +```dart +// Factory constructors with MessageDeleteScope +final deletingState = MessageState.deleting( + scope: MessageDeleteScope.hardDeleteForAll, +); +final deletedState = MessageState.deleted( + scope: MessageDeleteScope.softDeleteForAll, +); +final failedState = MessageState.deletingFailed( + scope: MessageDeleteScope.deleteForMe(), +); + +// Pattern matching with MessageDeleteScope +message.state.whenOrNull( + deleting: (scope) => handleDeleting(scope.hard), + deleted: (scope) => handleDeleted(scope.hard), + deletingFailed: (scope) => handleDeletingFailed(scope.hard), +); + +// New delete-for-me functionality +channel.deleteMessageForMe(message); // Delete only for current user +client.deleteMessageForMe(messageId); // Delete only for current user + +// Check delete-for-me states +if (message.state.isDeletingForMe) { + // Handle deleting for me state +} +if (message.state.isDeletedForMe) { + // Handle deleted for me state +} +if (message.state.isDeletingForMeFailed) { + // Handle delete for me failed state +} +``` + +> ⚠️ **Important:** +> - All `MessageState` factory constructors now require `MessageDeleteScope` parameter +> - Pattern matching callbacks receive `MessageDeleteScope` instead of `bool hard` +> - Use `scope.hard` to access the hard delete boolean value +> - New delete-for-me methods are available on both `Channel` and `StreamChatClient` + +--- + ## 🧪 Migration for v10.0.0-beta.4 ### 🛠 SendReaction @@ -429,6 +504,15 @@ StreamMessageWidget( ## 🎉 You're Ready to Migrate! +### For v10.0.0-beta.7: +- ✅ Update `MessageState` factory constructors to use `MessageDeleteScope` parameter +- ✅ Update pattern matching callbacks to handle `MessageDeleteScope` instead of `bool hard` +- ✅ Leverage new delete-for-me functionality with `deleteMessageForMe` methods +- ✅ Use new state checking methods for delete-for-me operations + +### For v10.0.0-beta.4: +- ✅ Update `sendReaction` method calls to use `Reaction` object instead of individual parameters + ### For v10.0.0-beta.3: - ✅ Update attachment picker options to use `SystemAttachmentPickerOption` or `TabbedAttachmentPickerOption` - ✅ Handle new `StreamAttachmentPickerResult` return type from attachment picker diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index c1402692f8..c4d7aadebb 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,22 @@ +## Upcoming Beta + +🛑️ Breaking + +- **Changed `MessageState` factory constructors**: The `deleting`, `deleted`, and `deletingFailed` + factory constructors now accept a `MessageDeleteScope` parameter instead of `bool hard`. + Pattern matching callbacks also receive `MessageDeleteScope scope` instead of `bool hard`. + +✅ Added + +- Added support for deleting messages only for the current user: + - `Channel.deleteMessageForMe()` - Delete a message only for the current user + - `StreamChatClient.deleteMessageForMe()` - Delete a message only for the current user via client + - `MessageDeleteScope` - New sealed class to represent deletion scope + - `MessageState.deletingForMe`, `MessageState.deletedForMe`, `MessageState.deletingForMeFailed` states + - `Message.deletedOnlyForMe`, `Event.deletedForMe`, `Member.deletedMessages` model fields + +For more details, please refer to the [migration guide](../../migrations/v10-migration.md). + ## 10.0.0-beta.6 - Included the changes from version [`9.17.0`](https://pub.dev/packages/stream_chat/changelog). From a11af4c37b6a261eb87dec281788175e7132c184 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 25 Sep 2025 09:42:12 +0200 Subject: [PATCH 13/16] feat(persistence): Add deletedForMe and deletedMessages fields This commit introduces two new fields to the persistence layer: - `deletedForMe` (boolean) to `MessageEntity` and `PinnedMessageEntity`: Indicates if a message was deleted only for the current user. - `deletedMessages` (list of strings) to `MemberEntity`: Stores a list of message IDs that a member has deleted only for themselves. These changes allow for more granular control over message deletion states within the local database. The database schema version has been incremented to `1025` to reflect these changes. Mapper and test files have been updated accordingly. --- .../lib/src/db/drift_chat_database.dart | 2 +- .../lib/src/db/drift_chat_database.g.dart | 202 +++++++++++++++++- .../lib/src/entity/members.dart | 8 +- .../lib/src/entity/messages.dart | 3 + .../lib/src/mapper/member_mapper.dart | 2 + .../lib/src/mapper/message_mapper.dart | 4 +- .../lib/src/mapper/pinned_message_mapper.dart | 2 + .../test/src/mapper/member_mapper_test.dart | 4 + .../test/src/mapper/message_mapper_test.dart | 4 + .../mapper/pinned_message_mapper_test.dart | 4 + 10 files changed, 225 insertions(+), 10 deletions(-) diff --git a/packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart index aa8cd3ef71..1ffae6b1c7 100644 --- a/packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart +++ b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart @@ -57,7 +57,7 @@ class DriftChatDatabase extends _$DriftChatDatabase { // you should bump this number whenever you change or add a table definition. @override - int get schemaVersion => 1000 + 24; + int get schemaVersion => 1000 + 25; @override MigrationStrategy get migration => MigrationStrategy( diff --git a/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart index 2ec585f138..89776f1905 100644 --- a/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart +++ b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart @@ -787,6 +787,16 @@ class $MessagesTable extends Messages late final GeneratedColumn remoteDeletedAt = GeneratedColumn('remote_deleted_at', aliasedName, true, type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _deletedForMeMeta = + const VerificationMeta('deletedForMe'); + @override + late final GeneratedColumn deletedForMe = GeneratedColumn( + 'deleted_for_me', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("deleted_for_me" IN (0, 1))'), + defaultValue: const Constant(false)); static const VerificationMeta _messageTextUpdatedAtMeta = const VerificationMeta('messageTextUpdatedAt'); @override @@ -874,6 +884,7 @@ class $MessagesTable extends Messages remoteUpdatedAt, localDeletedAt, remoteDeletedAt, + deletedForMe, messageTextUpdatedAt, userId, pinned, @@ -986,6 +997,12 @@ class $MessagesTable extends Messages remoteDeletedAt.isAcceptableOrUnknown( data['remote_deleted_at']!, _remoteDeletedAtMeta)); } + if (data.containsKey('deleted_for_me')) { + context.handle( + _deletedForMeMeta, + deletedForMe.isAcceptableOrUnknown( + data['deleted_for_me']!, _deletedForMeMeta)); + } if (data.containsKey('message_text_updated_at')) { context.handle( _messageTextUpdatedAtMeta, @@ -1076,6 +1093,8 @@ class $MessagesTable extends Messages DriftSqlType.dateTime, data['${effectivePrefix}local_deleted_at']), remoteDeletedAt: attachedDatabase.typeMapping.read( DriftSqlType.dateTime, data['${effectivePrefix}remote_deleted_at']), + deletedForMe: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}deleted_for_me'])!, messageTextUpdatedAt: attachedDatabase.typeMapping.read( DriftSqlType.dateTime, data['${effectivePrefix}message_text_updated_at']), @@ -1190,6 +1209,9 @@ class MessageEntity extends DataClass implements Insertable { /// The DateTime on which the message was deleted on the server. final DateTime? remoteDeletedAt; + /// Whether the message was deleted only for the current user. + final bool deletedForMe; + /// The DateTime at which the message text was edited final DateTime? messageTextUpdatedAt; @@ -1240,6 +1262,7 @@ class MessageEntity extends DataClass implements Insertable { this.remoteUpdatedAt, this.localDeletedAt, this.remoteDeletedAt, + required this.deletedForMe, this.messageTextUpdatedAt, this.userId, required this.pinned, @@ -1308,6 +1331,7 @@ class MessageEntity extends DataClass implements Insertable { if (!nullToAbsent || remoteDeletedAt != null) { map['remote_deleted_at'] = Variable(remoteDeletedAt); } + map['deleted_for_me'] = Variable(deletedForMe); if (!nullToAbsent || messageTextUpdatedAt != null) { map['message_text_updated_at'] = Variable(messageTextUpdatedAt); } @@ -1365,6 +1389,7 @@ class MessageEntity extends DataClass implements Insertable { remoteUpdatedAt: serializer.fromJson(json['remoteUpdatedAt']), localDeletedAt: serializer.fromJson(json['localDeletedAt']), remoteDeletedAt: serializer.fromJson(json['remoteDeletedAt']), + deletedForMe: serializer.fromJson(json['deletedForMe']), messageTextUpdatedAt: serializer.fromJson(json['messageTextUpdatedAt']), userId: serializer.fromJson(json['userId']), @@ -1404,6 +1429,7 @@ class MessageEntity extends DataClass implements Insertable { 'remoteUpdatedAt': serializer.toJson(remoteUpdatedAt), 'localDeletedAt': serializer.toJson(localDeletedAt), 'remoteDeletedAt': serializer.toJson(remoteDeletedAt), + 'deletedForMe': serializer.toJson(deletedForMe), 'messageTextUpdatedAt': serializer.toJson(messageTextUpdatedAt), 'userId': serializer.toJson(userId), @@ -1441,6 +1467,7 @@ class MessageEntity extends DataClass implements Insertable { Value remoteUpdatedAt = const Value.absent(), Value localDeletedAt = const Value.absent(), Value remoteDeletedAt = const Value.absent(), + bool? deletedForMe, Value messageTextUpdatedAt = const Value.absent(), Value userId = const Value.absent(), bool? pinned, @@ -1485,6 +1512,7 @@ class MessageEntity extends DataClass implements Insertable { remoteDeletedAt: remoteDeletedAt.present ? remoteDeletedAt.value : this.remoteDeletedAt, + deletedForMe: deletedForMe ?? this.deletedForMe, messageTextUpdatedAt: messageTextUpdatedAt.present ? messageTextUpdatedAt.value : this.messageTextUpdatedAt, @@ -1546,6 +1574,9 @@ class MessageEntity extends DataClass implements Insertable { remoteDeletedAt: data.remoteDeletedAt.present ? data.remoteDeletedAt.value : this.remoteDeletedAt, + deletedForMe: data.deletedForMe.present + ? data.deletedForMe.value + : this.deletedForMe, messageTextUpdatedAt: data.messageTextUpdatedAt.present ? data.messageTextUpdatedAt.value : this.messageTextUpdatedAt, @@ -1590,6 +1621,7 @@ class MessageEntity extends DataClass implements Insertable { ..write('remoteUpdatedAt: $remoteUpdatedAt, ') ..write('localDeletedAt: $localDeletedAt, ') ..write('remoteDeletedAt: $remoteDeletedAt, ') + ..write('deletedForMe: $deletedForMe, ') ..write('messageTextUpdatedAt: $messageTextUpdatedAt, ') ..write('userId: $userId, ') ..write('pinned: $pinned, ') @@ -1626,6 +1658,7 @@ class MessageEntity extends DataClass implements Insertable { remoteUpdatedAt, localDeletedAt, remoteDeletedAt, + deletedForMe, messageTextUpdatedAt, userId, pinned, @@ -1661,6 +1694,7 @@ class MessageEntity extends DataClass implements Insertable { other.remoteUpdatedAt == this.remoteUpdatedAt && other.localDeletedAt == this.localDeletedAt && other.remoteDeletedAt == this.remoteDeletedAt && + other.deletedForMe == this.deletedForMe && other.messageTextUpdatedAt == this.messageTextUpdatedAt && other.userId == this.userId && other.pinned == this.pinned && @@ -1694,6 +1728,7 @@ class MessagesCompanion extends UpdateCompanion { final Value remoteUpdatedAt; final Value localDeletedAt; final Value remoteDeletedAt; + final Value deletedForMe; final Value messageTextUpdatedAt; final Value userId; final Value pinned; @@ -1726,6 +1761,7 @@ class MessagesCompanion extends UpdateCompanion { this.remoteUpdatedAt = const Value.absent(), this.localDeletedAt = const Value.absent(), this.remoteDeletedAt = const Value.absent(), + this.deletedForMe = const Value.absent(), this.messageTextUpdatedAt = const Value.absent(), this.userId = const Value.absent(), this.pinned = const Value.absent(), @@ -1759,6 +1795,7 @@ class MessagesCompanion extends UpdateCompanion { this.remoteUpdatedAt = const Value.absent(), this.localDeletedAt = const Value.absent(), this.remoteDeletedAt = const Value.absent(), + this.deletedForMe = const Value.absent(), this.messageTextUpdatedAt = const Value.absent(), this.userId = const Value.absent(), this.pinned = const Value.absent(), @@ -1796,6 +1833,7 @@ class MessagesCompanion extends UpdateCompanion { Expression? remoteUpdatedAt, Expression? localDeletedAt, Expression? remoteDeletedAt, + Expression? deletedForMe, Expression? messageTextUpdatedAt, Expression? userId, Expression? pinned, @@ -1829,6 +1867,7 @@ class MessagesCompanion extends UpdateCompanion { if (remoteUpdatedAt != null) 'remote_updated_at': remoteUpdatedAt, if (localDeletedAt != null) 'local_deleted_at': localDeletedAt, if (remoteDeletedAt != null) 'remote_deleted_at': remoteDeletedAt, + if (deletedForMe != null) 'deleted_for_me': deletedForMe, if (messageTextUpdatedAt != null) 'message_text_updated_at': messageTextUpdatedAt, if (userId != null) 'user_id': userId, @@ -1866,6 +1905,7 @@ class MessagesCompanion extends UpdateCompanion { Value? remoteUpdatedAt, Value? localDeletedAt, Value? remoteDeletedAt, + Value? deletedForMe, Value? messageTextUpdatedAt, Value? userId, Value? pinned, @@ -1898,6 +1938,7 @@ class MessagesCompanion extends UpdateCompanion { remoteUpdatedAt: remoteUpdatedAt ?? this.remoteUpdatedAt, localDeletedAt: localDeletedAt ?? this.localDeletedAt, remoteDeletedAt: remoteDeletedAt ?? this.remoteDeletedAt, + deletedForMe: deletedForMe ?? this.deletedForMe, messageTextUpdatedAt: messageTextUpdatedAt ?? this.messageTextUpdatedAt, userId: userId ?? this.userId, pinned: pinned ?? this.pinned, @@ -1978,6 +2019,9 @@ class MessagesCompanion extends UpdateCompanion { if (remoteDeletedAt.present) { map['remote_deleted_at'] = Variable(remoteDeletedAt.value); } + if (deletedForMe.present) { + map['deleted_for_me'] = Variable(deletedForMe.value); + } if (messageTextUpdatedAt.present) { map['message_text_updated_at'] = Variable(messageTextUpdatedAt.value); @@ -2042,6 +2086,7 @@ class MessagesCompanion extends UpdateCompanion { ..write('remoteUpdatedAt: $remoteUpdatedAt, ') ..write('localDeletedAt: $localDeletedAt, ') ..write('remoteDeletedAt: $remoteDeletedAt, ') + ..write('deletedForMe: $deletedForMe, ') ..write('messageTextUpdatedAt: $messageTextUpdatedAt, ') ..write('userId: $userId, ') ..write('pinned: $pinned, ') @@ -3377,6 +3422,16 @@ class $PinnedMessagesTable extends PinnedMessages late final GeneratedColumn remoteDeletedAt = GeneratedColumn('remote_deleted_at', aliasedName, true, type: DriftSqlType.dateTime, requiredDuringInsert: false); + static const VerificationMeta _deletedForMeMeta = + const VerificationMeta('deletedForMe'); + @override + late final GeneratedColumn deletedForMe = GeneratedColumn( + 'deleted_for_me', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: false, + defaultConstraints: GeneratedColumn.constraintIsAlways( + 'CHECK ("deleted_for_me" IN (0, 1))'), + defaultValue: const Constant(false)); static const VerificationMeta _messageTextUpdatedAtMeta = const VerificationMeta('messageTextUpdatedAt'); @override @@ -3462,6 +3517,7 @@ class $PinnedMessagesTable extends PinnedMessages remoteUpdatedAt, localDeletedAt, remoteDeletedAt, + deletedForMe, messageTextUpdatedAt, userId, pinned, @@ -3575,6 +3631,12 @@ class $PinnedMessagesTable extends PinnedMessages remoteDeletedAt.isAcceptableOrUnknown( data['remote_deleted_at']!, _remoteDeletedAtMeta)); } + if (data.containsKey('deleted_for_me')) { + context.handle( + _deletedForMeMeta, + deletedForMe.isAcceptableOrUnknown( + data['deleted_for_me']!, _deletedForMeMeta)); + } if (data.containsKey('message_text_updated_at')) { context.handle( _messageTextUpdatedAtMeta, @@ -3665,6 +3727,8 @@ class $PinnedMessagesTable extends PinnedMessages DriftSqlType.dateTime, data['${effectivePrefix}local_deleted_at']), remoteDeletedAt: attachedDatabase.typeMapping.read( DriftSqlType.dateTime, data['${effectivePrefix}remote_deleted_at']), + deletedForMe: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}deleted_for_me'])!, messageTextUpdatedAt: attachedDatabase.typeMapping.read( DriftSqlType.dateTime, data['${effectivePrefix}message_text_updated_at']), @@ -3781,6 +3845,9 @@ class PinnedMessageEntity extends DataClass /// The DateTime on which the message was deleted on the server. final DateTime? remoteDeletedAt; + /// Whether the message was deleted only for the current user. + final bool deletedForMe; + /// The DateTime at which the message text was edited final DateTime? messageTextUpdatedAt; @@ -3831,6 +3898,7 @@ class PinnedMessageEntity extends DataClass this.remoteUpdatedAt, this.localDeletedAt, this.remoteDeletedAt, + required this.deletedForMe, this.messageTextUpdatedAt, this.userId, required this.pinned, @@ -3899,6 +3967,7 @@ class PinnedMessageEntity extends DataClass if (!nullToAbsent || remoteDeletedAt != null) { map['remote_deleted_at'] = Variable(remoteDeletedAt); } + map['deleted_for_me'] = Variable(deletedForMe); if (!nullToAbsent || messageTextUpdatedAt != null) { map['message_text_updated_at'] = Variable(messageTextUpdatedAt); } @@ -3957,6 +4026,7 @@ class PinnedMessageEntity extends DataClass remoteUpdatedAt: serializer.fromJson(json['remoteUpdatedAt']), localDeletedAt: serializer.fromJson(json['localDeletedAt']), remoteDeletedAt: serializer.fromJson(json['remoteDeletedAt']), + deletedForMe: serializer.fromJson(json['deletedForMe']), messageTextUpdatedAt: serializer.fromJson(json['messageTextUpdatedAt']), userId: serializer.fromJson(json['userId']), @@ -3996,6 +4066,7 @@ class PinnedMessageEntity extends DataClass 'remoteUpdatedAt': serializer.toJson(remoteUpdatedAt), 'localDeletedAt': serializer.toJson(localDeletedAt), 'remoteDeletedAt': serializer.toJson(remoteDeletedAt), + 'deletedForMe': serializer.toJson(deletedForMe), 'messageTextUpdatedAt': serializer.toJson(messageTextUpdatedAt), 'userId': serializer.toJson(userId), @@ -4033,6 +4104,7 @@ class PinnedMessageEntity extends DataClass Value remoteUpdatedAt = const Value.absent(), Value localDeletedAt = const Value.absent(), Value remoteDeletedAt = const Value.absent(), + bool? deletedForMe, Value messageTextUpdatedAt = const Value.absent(), Value userId = const Value.absent(), bool? pinned, @@ -4077,6 +4149,7 @@ class PinnedMessageEntity extends DataClass remoteDeletedAt: remoteDeletedAt.present ? remoteDeletedAt.value : this.remoteDeletedAt, + deletedForMe: deletedForMe ?? this.deletedForMe, messageTextUpdatedAt: messageTextUpdatedAt.present ? messageTextUpdatedAt.value : this.messageTextUpdatedAt, @@ -4138,6 +4211,9 @@ class PinnedMessageEntity extends DataClass remoteDeletedAt: data.remoteDeletedAt.present ? data.remoteDeletedAt.value : this.remoteDeletedAt, + deletedForMe: data.deletedForMe.present + ? data.deletedForMe.value + : this.deletedForMe, messageTextUpdatedAt: data.messageTextUpdatedAt.present ? data.messageTextUpdatedAt.value : this.messageTextUpdatedAt, @@ -4182,6 +4258,7 @@ class PinnedMessageEntity extends DataClass ..write('remoteUpdatedAt: $remoteUpdatedAt, ') ..write('localDeletedAt: $localDeletedAt, ') ..write('remoteDeletedAt: $remoteDeletedAt, ') + ..write('deletedForMe: $deletedForMe, ') ..write('messageTextUpdatedAt: $messageTextUpdatedAt, ') ..write('userId: $userId, ') ..write('pinned: $pinned, ') @@ -4218,6 +4295,7 @@ class PinnedMessageEntity extends DataClass remoteUpdatedAt, localDeletedAt, remoteDeletedAt, + deletedForMe, messageTextUpdatedAt, userId, pinned, @@ -4253,6 +4331,7 @@ class PinnedMessageEntity extends DataClass other.remoteUpdatedAt == this.remoteUpdatedAt && other.localDeletedAt == this.localDeletedAt && other.remoteDeletedAt == this.remoteDeletedAt && + other.deletedForMe == this.deletedForMe && other.messageTextUpdatedAt == this.messageTextUpdatedAt && other.userId == this.userId && other.pinned == this.pinned && @@ -4286,6 +4365,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { final Value remoteUpdatedAt; final Value localDeletedAt; final Value remoteDeletedAt; + final Value deletedForMe; final Value messageTextUpdatedAt; final Value userId; final Value pinned; @@ -4318,6 +4398,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { this.remoteUpdatedAt = const Value.absent(), this.localDeletedAt = const Value.absent(), this.remoteDeletedAt = const Value.absent(), + this.deletedForMe = const Value.absent(), this.messageTextUpdatedAt = const Value.absent(), this.userId = const Value.absent(), this.pinned = const Value.absent(), @@ -4351,6 +4432,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { this.remoteUpdatedAt = const Value.absent(), this.localDeletedAt = const Value.absent(), this.remoteDeletedAt = const Value.absent(), + this.deletedForMe = const Value.absent(), this.messageTextUpdatedAt = const Value.absent(), this.userId = const Value.absent(), this.pinned = const Value.absent(), @@ -4388,6 +4470,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { Expression? remoteUpdatedAt, Expression? localDeletedAt, Expression? remoteDeletedAt, + Expression? deletedForMe, Expression? messageTextUpdatedAt, Expression? userId, Expression? pinned, @@ -4421,6 +4504,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { if (remoteUpdatedAt != null) 'remote_updated_at': remoteUpdatedAt, if (localDeletedAt != null) 'local_deleted_at': localDeletedAt, if (remoteDeletedAt != null) 'remote_deleted_at': remoteDeletedAt, + if (deletedForMe != null) 'deleted_for_me': deletedForMe, if (messageTextUpdatedAt != null) 'message_text_updated_at': messageTextUpdatedAt, if (userId != null) 'user_id': userId, @@ -4458,6 +4542,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { Value? remoteUpdatedAt, Value? localDeletedAt, Value? remoteDeletedAt, + Value? deletedForMe, Value? messageTextUpdatedAt, Value? userId, Value? pinned, @@ -4490,6 +4575,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { remoteUpdatedAt: remoteUpdatedAt ?? this.remoteUpdatedAt, localDeletedAt: localDeletedAt ?? this.localDeletedAt, remoteDeletedAt: remoteDeletedAt ?? this.remoteDeletedAt, + deletedForMe: deletedForMe ?? this.deletedForMe, messageTextUpdatedAt: messageTextUpdatedAt ?? this.messageTextUpdatedAt, userId: userId ?? this.userId, pinned: pinned ?? this.pinned, @@ -4572,6 +4658,9 @@ class PinnedMessagesCompanion extends UpdateCompanion { if (remoteDeletedAt.present) { map['remote_deleted_at'] = Variable(remoteDeletedAt.value); } + if (deletedForMe.present) { + map['deleted_for_me'] = Variable(deletedForMe.value); + } if (messageTextUpdatedAt.present) { map['message_text_updated_at'] = Variable(messageTextUpdatedAt.value); @@ -4636,6 +4725,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { ..write('remoteUpdatedAt: $remoteUpdatedAt, ') ..write('localDeletedAt: $localDeletedAt, ') ..write('remoteDeletedAt: $remoteDeletedAt, ') + ..write('deletedForMe: $deletedForMe, ') ..write('messageTextUpdatedAt: $messageTextUpdatedAt, ') ..write('userId: $userId, ') ..write('pinned: $pinned, ') @@ -7432,6 +7522,12 @@ class $MembersTable extends Members requiredDuringInsert: false, defaultValue: currentDateAndTime); @override + late final GeneratedColumnWithTypeConverter, String> + deletedMessages = GeneratedColumn( + 'deleted_messages', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true) + .withConverter>($MembersTable.$converterdeletedMessages); + @override List get $columns => [ userId, channelCid, @@ -7446,7 +7542,8 @@ class $MembersTable extends Members isModerator, extraData, createdAt, - updatedAt + updatedAt, + deletedMessages ]; @override String get aliasedName => _alias ?? actualTableName; @@ -7566,6 +7663,9 @@ class $MembersTable extends Members .read(DriftSqlType.dateTime, data['${effectivePrefix}created_at'])!, updatedAt: attachedDatabase.typeMapping .read(DriftSqlType.dateTime, data['${effectivePrefix}updated_at'])!, + deletedMessages: $MembersTable.$converterdeletedMessages.fromSql( + attachedDatabase.typeMapping.read(DriftSqlType.string, + data['${effectivePrefix}deleted_messages'])!), ); } @@ -7578,6 +7678,8 @@ class $MembersTable extends Members MapConverter(); static TypeConverter?, String?> $converterextraDatan = NullAwareTypeConverter.wrap($converterextraData); + static TypeConverter, String> $converterdeletedMessages = + ListConverter(); } class MemberEntity extends DataClass implements Insertable { @@ -7622,6 +7724,12 @@ class MemberEntity extends DataClass implements Insertable { /// The last date of update final DateTime updatedAt; + + /// List of message ids deleted by the member only for himself. + /// + /// These messages are now marked deleted for this member, but are still + /// kept as regular to other channel members. + final List deletedMessages; const MemberEntity( {required this.userId, required this.channelCid, @@ -7636,7 +7744,8 @@ class MemberEntity extends DataClass implements Insertable { required this.isModerator, this.extraData, required this.createdAt, - required this.updatedAt}); + required this.updatedAt, + required this.deletedMessages}); @override Map toColumns(bool nullToAbsent) { final map = {}; @@ -7667,6 +7776,10 @@ class MemberEntity extends DataClass implements Insertable { } map['created_at'] = Variable(createdAt); map['updated_at'] = Variable(updatedAt); + { + map['deleted_messages'] = Variable( + $MembersTable.$converterdeletedMessages.toSql(deletedMessages)); + } return map; } @@ -7690,6 +7803,8 @@ class MemberEntity extends DataClass implements Insertable { extraData: serializer.fromJson?>(json['extraData']), createdAt: serializer.fromJson(json['createdAt']), updatedAt: serializer.fromJson(json['updatedAt']), + deletedMessages: + serializer.fromJson>(json['deletedMessages']), ); } @override @@ -7710,6 +7825,7 @@ class MemberEntity extends DataClass implements Insertable { 'extraData': serializer.toJson?>(extraData), 'createdAt': serializer.toJson(createdAt), 'updatedAt': serializer.toJson(updatedAt), + 'deletedMessages': serializer.toJson>(deletedMessages), }; } @@ -7727,7 +7843,8 @@ class MemberEntity extends DataClass implements Insertable { bool? isModerator, Value?> extraData = const Value.absent(), DateTime? createdAt, - DateTime? updatedAt}) => + DateTime? updatedAt, + List? deletedMessages}) => MemberEntity( userId: userId ?? this.userId, channelCid: channelCid ?? this.channelCid, @@ -7747,6 +7864,7 @@ class MemberEntity extends DataClass implements Insertable { extraData: extraData.present ? extraData.value : this.extraData, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, + deletedMessages: deletedMessages ?? this.deletedMessages, ); MemberEntity copyWithCompanion(MembersCompanion data) { return MemberEntity( @@ -7774,6 +7892,9 @@ class MemberEntity extends DataClass implements Insertable { extraData: data.extraData.present ? data.extraData.value : this.extraData, createdAt: data.createdAt.present ? data.createdAt.value : this.createdAt, updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt, + deletedMessages: data.deletedMessages.present + ? data.deletedMessages.value + : this.deletedMessages, ); } @@ -7793,7 +7914,8 @@ class MemberEntity extends DataClass implements Insertable { ..write('isModerator: $isModerator, ') ..write('extraData: $extraData, ') ..write('createdAt: $createdAt, ') - ..write('updatedAt: $updatedAt') + ..write('updatedAt: $updatedAt, ') + ..write('deletedMessages: $deletedMessages') ..write(')')) .toString(); } @@ -7813,7 +7935,8 @@ class MemberEntity extends DataClass implements Insertable { isModerator, extraData, createdAt, - updatedAt); + updatedAt, + deletedMessages); @override bool operator ==(Object other) => identical(this, other) || @@ -7831,7 +7954,8 @@ class MemberEntity extends DataClass implements Insertable { other.isModerator == this.isModerator && other.extraData == this.extraData && other.createdAt == this.createdAt && - other.updatedAt == this.updatedAt); + other.updatedAt == this.updatedAt && + other.deletedMessages == this.deletedMessages); } class MembersCompanion extends UpdateCompanion { @@ -7849,6 +7973,7 @@ class MembersCompanion extends UpdateCompanion { final Value?> extraData; final Value createdAt; final Value updatedAt; + final Value> deletedMessages; final Value rowid; const MembersCompanion({ this.userId = const Value.absent(), @@ -7865,6 +7990,7 @@ class MembersCompanion extends UpdateCompanion { this.extraData = const Value.absent(), this.createdAt = const Value.absent(), this.updatedAt = const Value.absent(), + this.deletedMessages = const Value.absent(), this.rowid = const Value.absent(), }); MembersCompanion.insert({ @@ -7882,9 +8008,11 @@ class MembersCompanion extends UpdateCompanion { this.extraData = const Value.absent(), this.createdAt = const Value.absent(), this.updatedAt = const Value.absent(), + required List deletedMessages, this.rowid = const Value.absent(), }) : userId = Value(userId), - channelCid = Value(channelCid); + channelCid = Value(channelCid), + deletedMessages = Value(deletedMessages); static Insertable custom({ Expression? userId, Expression? channelCid, @@ -7900,6 +8028,7 @@ class MembersCompanion extends UpdateCompanion { Expression? extraData, Expression? createdAt, Expression? updatedAt, + Expression? deletedMessages, Expression? rowid, }) { return RawValuesInsertable({ @@ -7917,6 +8046,7 @@ class MembersCompanion extends UpdateCompanion { if (extraData != null) 'extra_data': extraData, if (createdAt != null) 'created_at': createdAt, if (updatedAt != null) 'updated_at': updatedAt, + if (deletedMessages != null) 'deleted_messages': deletedMessages, if (rowid != null) 'rowid': rowid, }); } @@ -7936,6 +8066,7 @@ class MembersCompanion extends UpdateCompanion { Value?>? extraData, Value? createdAt, Value? updatedAt, + Value>? deletedMessages, Value? rowid}) { return MembersCompanion( userId: userId ?? this.userId, @@ -7952,6 +8083,7 @@ class MembersCompanion extends UpdateCompanion { extraData: extraData ?? this.extraData, createdAt: createdAt ?? this.createdAt, updatedAt: updatedAt ?? this.updatedAt, + deletedMessages: deletedMessages ?? this.deletedMessages, rowid: rowid ?? this.rowid, ); } @@ -8002,6 +8134,10 @@ class MembersCompanion extends UpdateCompanion { if (updatedAt.present) { map['updated_at'] = Variable(updatedAt.value); } + if (deletedMessages.present) { + map['deleted_messages'] = Variable( + $MembersTable.$converterdeletedMessages.toSql(deletedMessages.value)); + } if (rowid.present) { map['rowid'] = Variable(rowid.value); } @@ -8025,6 +8161,7 @@ class MembersCompanion extends UpdateCompanion { ..write('extraData: $extraData, ') ..write('createdAt: $createdAt, ') ..write('updatedAt: $updatedAt, ') + ..write('deletedMessages: $deletedMessages, ') ..write('rowid: $rowid') ..write(')')) .toString(); @@ -9785,6 +9922,7 @@ typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({ Value remoteUpdatedAt, Value localDeletedAt, Value remoteDeletedAt, + Value deletedForMe, Value messageTextUpdatedAt, Value userId, Value pinned, @@ -9818,6 +9956,7 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ Value remoteUpdatedAt, Value localDeletedAt, Value remoteDeletedAt, + Value deletedForMe, Value messageTextUpdatedAt, Value userId, Value pinned, @@ -9979,6 +10118,9 @@ class $$MessagesTableFilterComposer column: $table.remoteDeletedAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get deletedForMe => $composableBuilder( + column: $table.deletedForMe, builder: (column) => ColumnFilters(column)); + ColumnFilters get messageTextUpdatedAt => $composableBuilder( column: $table.messageTextUpdatedAt, builder: (column) => ColumnFilters(column)); @@ -10179,6 +10321,10 @@ class $$MessagesTableOrderingComposer column: $table.remoteDeletedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get deletedForMe => $composableBuilder( + column: $table.deletedForMe, + builder: (column) => ColumnOrderings(column)); + ColumnOrderings get messageTextUpdatedAt => $composableBuilder( column: $table.messageTextUpdatedAt, builder: (column) => ColumnOrderings(column)); @@ -10302,6 +10448,9 @@ class $$MessagesTableAnnotationComposer GeneratedColumn get remoteDeletedAt => $composableBuilder( column: $table.remoteDeletedAt, builder: (column) => column); + GeneratedColumn get deletedForMe => $composableBuilder( + column: $table.deletedForMe, builder: (column) => column); + GeneratedColumn get messageTextUpdatedAt => $composableBuilder( column: $table.messageTextUpdatedAt, builder: (column) => column); @@ -10463,6 +10612,7 @@ class $$MessagesTableTableManager extends RootTableManager< Value remoteUpdatedAt = const Value.absent(), Value localDeletedAt = const Value.absent(), Value remoteDeletedAt = const Value.absent(), + Value deletedForMe = const Value.absent(), Value messageTextUpdatedAt = const Value.absent(), Value userId = const Value.absent(), Value pinned = const Value.absent(), @@ -10496,6 +10646,7 @@ class $$MessagesTableTableManager extends RootTableManager< remoteUpdatedAt: remoteUpdatedAt, localDeletedAt: localDeletedAt, remoteDeletedAt: remoteDeletedAt, + deletedForMe: deletedForMe, messageTextUpdatedAt: messageTextUpdatedAt, userId: userId, pinned: pinned, @@ -10530,6 +10681,7 @@ class $$MessagesTableTableManager extends RootTableManager< Value remoteUpdatedAt = const Value.absent(), Value localDeletedAt = const Value.absent(), Value remoteDeletedAt = const Value.absent(), + Value deletedForMe = const Value.absent(), Value messageTextUpdatedAt = const Value.absent(), Value userId = const Value.absent(), Value pinned = const Value.absent(), @@ -10563,6 +10715,7 @@ class $$MessagesTableTableManager extends RootTableManager< remoteUpdatedAt: remoteUpdatedAt, localDeletedAt: localDeletedAt, remoteDeletedAt: remoteDeletedAt, + deletedForMe: deletedForMe, messageTextUpdatedAt: messageTextUpdatedAt, userId: userId, pinned: pinned, @@ -11616,6 +11769,7 @@ typedef $$PinnedMessagesTableCreateCompanionBuilder = PinnedMessagesCompanion Value remoteUpdatedAt, Value localDeletedAt, Value remoteDeletedAt, + Value deletedForMe, Value messageTextUpdatedAt, Value userId, Value pinned, @@ -11650,6 +11804,7 @@ typedef $$PinnedMessagesTableUpdateCompanionBuilder = PinnedMessagesCompanion Value remoteUpdatedAt, Value localDeletedAt, Value remoteDeletedAt, + Value deletedForMe, Value messageTextUpdatedAt, Value userId, Value pinned, @@ -11771,6 +11926,9 @@ class $$PinnedMessagesTableFilterComposer column: $table.remoteDeletedAt, builder: (column) => ColumnFilters(column)); + ColumnFilters get deletedForMe => $composableBuilder( + column: $table.deletedForMe, builder: (column) => ColumnFilters(column)); + ColumnFilters get messageTextUpdatedAt => $composableBuilder( column: $table.messageTextUpdatedAt, builder: (column) => ColumnFilters(column)); @@ -11914,6 +12072,10 @@ class $$PinnedMessagesTableOrderingComposer column: $table.remoteDeletedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get deletedForMe => $composableBuilder( + column: $table.deletedForMe, + builder: (column) => ColumnOrderings(column)); + ColumnOrderings get messageTextUpdatedAt => $composableBuilder( column: $table.messageTextUpdatedAt, builder: (column) => ColumnOrderings(column)); @@ -12020,6 +12182,9 @@ class $$PinnedMessagesTableAnnotationComposer GeneratedColumn get remoteDeletedAt => $composableBuilder( column: $table.remoteDeletedAt, builder: (column) => column); + GeneratedColumn get deletedForMe => $composableBuilder( + column: $table.deletedForMe, builder: (column) => column); + GeneratedColumn get messageTextUpdatedAt => $composableBuilder( column: $table.messageTextUpdatedAt, builder: (column) => column); @@ -12121,6 +12286,7 @@ class $$PinnedMessagesTableTableManager extends RootTableManager< Value remoteUpdatedAt = const Value.absent(), Value localDeletedAt = const Value.absent(), Value remoteDeletedAt = const Value.absent(), + Value deletedForMe = const Value.absent(), Value messageTextUpdatedAt = const Value.absent(), Value userId = const Value.absent(), Value pinned = const Value.absent(), @@ -12154,6 +12320,7 @@ class $$PinnedMessagesTableTableManager extends RootTableManager< remoteUpdatedAt: remoteUpdatedAt, localDeletedAt: localDeletedAt, remoteDeletedAt: remoteDeletedAt, + deletedForMe: deletedForMe, messageTextUpdatedAt: messageTextUpdatedAt, userId: userId, pinned: pinned, @@ -12188,6 +12355,7 @@ class $$PinnedMessagesTableTableManager extends RootTableManager< Value remoteUpdatedAt = const Value.absent(), Value localDeletedAt = const Value.absent(), Value remoteDeletedAt = const Value.absent(), + Value deletedForMe = const Value.absent(), Value messageTextUpdatedAt = const Value.absent(), Value userId = const Value.absent(), Value pinned = const Value.absent(), @@ -12221,6 +12389,7 @@ class $$PinnedMessagesTableTableManager extends RootTableManager< remoteUpdatedAt: remoteUpdatedAt, localDeletedAt: localDeletedAt, remoteDeletedAt: remoteDeletedAt, + deletedForMe: deletedForMe, messageTextUpdatedAt: messageTextUpdatedAt, userId: userId, pinned: pinned, @@ -13969,6 +14138,7 @@ typedef $$MembersTableCreateCompanionBuilder = MembersCompanion Function({ Value?> extraData, Value createdAt, Value updatedAt, + required List deletedMessages, Value rowid, }); typedef $$MembersTableUpdateCompanionBuilder = MembersCompanion Function({ @@ -13986,6 +14156,7 @@ typedef $$MembersTableUpdateCompanionBuilder = MembersCompanion Function({ Value?> extraData, Value createdAt, Value updatedAt, + Value> deletedMessages, Value rowid, }); @@ -14062,6 +14233,11 @@ class $$MembersTableFilterComposer ColumnFilters get updatedAt => $composableBuilder( column: $table.updatedAt, builder: (column) => ColumnFilters(column)); + ColumnWithTypeConverterFilters, List, String> + get deletedMessages => $composableBuilder( + column: $table.deletedMessages, + builder: (column) => ColumnWithTypeConverterFilters(column)); + $$ChannelsTableFilterComposer get channelCid { final $$ChannelsTableFilterComposer composer = $composerBuilder( composer: this, @@ -14134,6 +14310,10 @@ class $$MembersTableOrderingComposer ColumnOrderings get updatedAt => $composableBuilder( column: $table.updatedAt, builder: (column) => ColumnOrderings(column)); + ColumnOrderings get deletedMessages => $composableBuilder( + column: $table.deletedMessages, + builder: (column) => ColumnOrderings(column)); + $$ChannelsTableOrderingComposer get channelCid { final $$ChannelsTableOrderingComposer composer = $composerBuilder( composer: this, @@ -14204,6 +14384,10 @@ class $$MembersTableAnnotationComposer GeneratedColumn get updatedAt => $composableBuilder(column: $table.updatedAt, builder: (column) => column); + GeneratedColumnWithTypeConverter, String> get deletedMessages => + $composableBuilder( + column: $table.deletedMessages, builder: (column) => column); + $$ChannelsTableAnnotationComposer get channelCid { final $$ChannelsTableAnnotationComposer composer = $composerBuilder( composer: this, @@ -14262,6 +14446,7 @@ class $$MembersTableTableManager extends RootTableManager< Value?> extraData = const Value.absent(), Value createdAt = const Value.absent(), Value updatedAt = const Value.absent(), + Value> deletedMessages = const Value.absent(), Value rowid = const Value.absent(), }) => MembersCompanion( @@ -14279,6 +14464,7 @@ class $$MembersTableTableManager extends RootTableManager< extraData: extraData, createdAt: createdAt, updatedAt: updatedAt, + deletedMessages: deletedMessages, rowid: rowid, ), createCompanionCallback: ({ @@ -14296,6 +14482,7 @@ class $$MembersTableTableManager extends RootTableManager< Value?> extraData = const Value.absent(), Value createdAt = const Value.absent(), Value updatedAt = const Value.absent(), + required List deletedMessages, Value rowid = const Value.absent(), }) => MembersCompanion.insert( @@ -14313,6 +14500,7 @@ class $$MembersTableTableManager extends RootTableManager< extraData: extraData, createdAt: createdAt, updatedAt: updatedAt, + deletedMessages: deletedMessages, rowid: rowid, ), withReferenceMapper: (p0) => p0 diff --git a/packages/stream_chat_persistence/lib/src/entity/members.dart b/packages/stream_chat_persistence/lib/src/entity/members.dart index 086effca7d..9e3734b38c 100644 --- a/packages/stream_chat_persistence/lib/src/entity/members.dart +++ b/packages/stream_chat_persistence/lib/src/entity/members.dart @@ -1,6 +1,6 @@ // coverage:ignore-file import 'package:drift/drift.dart'; -import 'package:stream_chat_persistence/src/converter/map_converter.dart'; +import 'package:stream_chat_persistence/src/converter/converter.dart'; import 'package:stream_chat_persistence/src/entity/channels.dart'; /// Represents a [Members] table in [MoorChatDatabase]. @@ -51,6 +51,12 @@ class Members extends Table { /// The last date of update DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)(); + /// List of message ids deleted by the member only for himself. + /// + /// These messages are now marked deleted for this member, but are still + /// kept as regular to other channel members. + TextColumn get deletedMessages => text().map(ListConverter())(); + @override Set get primaryKey => {userId, channelCid}; } diff --git a/packages/stream_chat_persistence/lib/src/entity/messages.dart b/packages/stream_chat_persistence/lib/src/entity/messages.dart index caf11a1a11..3c33c49221 100644 --- a/packages/stream_chat_persistence/lib/src/entity/messages.dart +++ b/packages/stream_chat_persistence/lib/src/entity/messages.dart @@ -99,6 +99,9 @@ class Messages extends Table { /// The DateTime on which the message was deleted on the server. DateTimeColumn get remoteDeletedAt => dateTime().nullable()(); + /// Whether the message was deleted only for the current user. + BoolColumn get deletedForMe => boolean().withDefault(const Constant(false))(); + /// The DateTime at which the message text was edited DateTimeColumn get messageTextUpdatedAt => dateTime().nullable()(); diff --git a/packages/stream_chat_persistence/lib/src/mapper/member_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/member_mapper.dart index 6c15e54e5d..f37aaf1bd4 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/member_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/member_mapper.dart @@ -18,6 +18,7 @@ extension MemberEntityX on MemberEntity { pinnedAt: pinnedAt, archivedAt: archivedAt, isModerator: isModerator, + deletedMessages: deletedMessages, extraData: extraData ?? {}, ); } @@ -39,6 +40,7 @@ extension MemberX on Member { archivedAt: archivedAt, channelRole: channelRole, updatedAt: updatedAt, + deletedMessages: deletedMessages, extraData: extraData, ); } diff --git a/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart index 4640c0f325..558651c97c 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart @@ -31,6 +31,7 @@ extension MessageEntityX on MessageEntity { localUpdatedAt: localUpdatedAt, deletedAt: remoteDeletedAt, localDeletedAt: localDeletedAt, + deletedOnlyForMe: deletedForMe, messageTextUpdatedAt: messageTextUpdatedAt, id: id, type: type, @@ -78,13 +79,14 @@ extension MessageX on Message { replyCount: replyCount, reactionGroups: reactionGroups, mentionedUsers: mentionedUsers.map(jsonEncode).toList(), - state: jsonEncode(state), + state: jsonEncode(state.toJson()), remoteUpdatedAt: remoteUpdatedAt, localUpdatedAt: localUpdatedAt, extraData: extraData, userId: user?.id, remoteDeletedAt: remoteDeletedAt, localDeletedAt: localDeletedAt, + deletedForMe: deletedOnlyForMe, messageTextUpdatedAt: messageTextUpdatedAt, messageText: text, pinned: pinned, diff --git a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart index fbd168c717..c4e60dbe87 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart @@ -31,6 +31,7 @@ extension PinnedMessageEntityX on PinnedMessageEntity { localUpdatedAt: localUpdatedAt, deletedAt: remoteDeletedAt, localDeletedAt: localDeletedAt, + deletedOnlyForMe: deletedForMe, messageTextUpdatedAt: messageTextUpdatedAt, id: id, type: type, @@ -86,6 +87,7 @@ extension PMessageX on Message { userId: user?.id, remoteDeletedAt: remoteDeletedAt, localDeletedAt: localDeletedAt, + deletedForMe: deletedOnlyForMe, messageTextUpdatedAt: messageTextUpdatedAt, messageText: text, pinned: pinned, diff --git a/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart index 0c2bea8fca..a577a6d078 100644 --- a/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart @@ -24,6 +24,7 @@ void main() { pinnedAt: DateTime.now(), archivedAt: DateTime.now(), isModerator: math.Random().nextBool(), + deletedMessages: ['msg1', 'msg2', 'msg3'], extraData: {'test_extra_data': 'testData'}, ); final member = entity.toMember(user: user); @@ -40,6 +41,7 @@ void main() { expect(member.pinnedAt, isSameDateAs(entity.pinnedAt)); expect(member.archivedAt, isSameDateAs(entity.archivedAt)); expect(member.isModerator, entity.isModerator); + expect(member.deletedMessages, entity.deletedMessages); expect(member.extraData, entity.extraData); }); @@ -59,6 +61,7 @@ void main() { pinnedAt: DateTime.now(), archivedAt: DateTime.now(), isModerator: math.Random().nextBool(), + deletedMessages: const ['msg1', 'msg2', 'msg3'], extraData: const {'test_extra_data': 'testData'}, ); final entity = member.toEntity(cid: cid); @@ -76,6 +79,7 @@ void main() { expect(entity.pinnedAt, isSameDateAs(member.pinnedAt)); expect(entity.archivedAt, isSameDateAs(member.archivedAt)); expect(entity.isModerator, member.isModerator); + expect(entity.deletedMessages, member.deletedMessages); expect(entity.extraData, member.extraData); }); } diff --git a/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart index 13d6bc3953..4a2f579971 100644 --- a/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart @@ -80,6 +80,7 @@ void main() { userId: user.id, localDeletedAt: DateTime.now(), remoteDeletedAt: DateTime.now().add(const Duration(seconds: 1)), + deletedForMe: false, messageText: 'Hello', pinned: true, pinExpires: DateTime.now().toUtc(), @@ -130,6 +131,7 @@ void main() { expect(message.user!.id, entity.userId); expect(message.localDeletedAt, isSameDateAs(entity.localDeletedAt)); expect(message.remoteDeletedAt, isSameDateAs(entity.remoteDeletedAt)); + expect(message.deletedOnlyForMe, entity.deletedForMe); expect(message.text, entity.messageText); expect(message.pinned, entity.pinned); expect(message.pinExpires, isSameDateAs(entity.pinExpires)); @@ -218,6 +220,7 @@ void main() { user: user, localDeletedAt: DateTime.now(), deletedAt: DateTime.now().add(const Duration(seconds: 1)), + deletedOnlyForMe: true, text: 'Hello', pinned: true, pinExpires: DateTime.now(), @@ -256,6 +259,7 @@ void main() { expect(entity.userId, message.user!.id); expect(entity.localDeletedAt, isSameDateAs(message.localDeletedAt)); expect(entity.remoteDeletedAt, isSameDateAs(message.remoteDeletedAt)); + expect(entity.deletedForMe, message.deletedOnlyForMe); expect(entity.messageText, message.text); expect(entity.pinned, message.pinned); expect(entity.pinExpires, isSameDateAs(message.pinExpires)); diff --git a/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart index 133a49c39e..8a371c3030 100644 --- a/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart @@ -80,6 +80,7 @@ void main() { userId: user.id, localDeletedAt: DateTime.now(), remoteDeletedAt: DateTime.now().add(const Duration(seconds: 1)), + deletedForMe: false, messageText: 'Hello', pinned: true, pinExpires: DateTime.now().toUtc(), @@ -130,6 +131,7 @@ void main() { expect(message.user!.id, entity.userId); expect(message.localDeletedAt, isSameDateAs(entity.localDeletedAt)); expect(message.remoteDeletedAt, isSameDateAs(entity.remoteDeletedAt)); + expect(message.deletedOnlyForMe, entity.deletedForMe); expect(message.text, entity.messageText); expect(message.pinned, entity.pinned); expect(message.pinExpires, isSameDateAs(entity.pinExpires)); @@ -218,6 +220,7 @@ void main() { user: user, localDeletedAt: DateTime.now(), deletedAt: DateTime.now().add(const Duration(seconds: 1)), + deletedOnlyForMe: true, text: 'Hello', pinned: true, pinExpires: DateTime.now(), @@ -256,6 +259,7 @@ void main() { expect(entity.userId, message.user!.id); expect(entity.localDeletedAt, isSameDateAs(message.localDeletedAt)); expect(entity.remoteDeletedAt, isSameDateAs(message.remoteDeletedAt)); + expect(entity.deletedForMe, message.deletedOnlyForMe); expect(entity.messageText, message.text); expect(entity.pinned, message.pinned); expect(entity.pinExpires, isSameDateAs(message.pinExpires)); From bf7793675a45927558ad60206262c48b5a6b8983 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 25 Sep 2025 09:43:25 +0200 Subject: [PATCH 14/16] chore: update CHANGELOG.md --- packages/stream_chat_persistence/CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index 7b4f6fe53f..f993f872e9 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,3 +1,7 @@ +## Upcoming Beta + +- Added support for `Messages.deleteForMe` and `Members.deletedMessages` fields. + ## 10.0.0-beta.6 - Included the changes from version [`9.17.0`](https://pub.dev/packages/stream_chat_persistence/changelog). From 56301ab2003673774c6952a0a6e93a73bce3a030 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 25 Sep 2025 10:05:44 +0200 Subject: [PATCH 15/16] fix(persistence): use `deletedForMe` instead of `deletedOnlyForMe` The `deletedOnlyForMe` field in `MessageEntity` has been renamed to `deletedForMe` to match the API. The `deletedForMe` field in `MessageEntity` is now nullable. --- packages/stream_chat_persistence/CHANGELOG.md | 2 +- .../lib/src/db/drift_chat_database.g.dart | 72 ++++++++++--------- .../lib/src/entity/messages.dart | 2 +- .../lib/src/mapper/message_mapper.dart | 4 +- .../lib/src/mapper/pinned_message_mapper.dart | 6 +- .../test/src/mapper/message_mapper_test.dart | 6 +- .../mapper/pinned_message_mapper_test.dart | 6 +- 7 files changed, 51 insertions(+), 47 deletions(-) diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index f993f872e9..ad9270c1f4 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,6 +1,6 @@ ## Upcoming Beta -- Added support for `Messages.deleteForMe` and `Members.deletedMessages` fields. +- Added support for `Messages.deletedForMe` and `Members.deletedMessages` fields. ## 10.0.0-beta.6 diff --git a/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart index 89776f1905..58b0074eb1 100644 --- a/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart +++ b/packages/stream_chat_persistence/lib/src/db/drift_chat_database.g.dart @@ -791,12 +791,11 @@ class $MessagesTable extends Messages const VerificationMeta('deletedForMe'); @override late final GeneratedColumn deletedForMe = GeneratedColumn( - 'deleted_for_me', aliasedName, false, + 'deleted_for_me', aliasedName, true, type: DriftSqlType.bool, requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("deleted_for_me" IN (0, 1))'), - defaultValue: const Constant(false)); + 'CHECK ("deleted_for_me" IN (0, 1))')); static const VerificationMeta _messageTextUpdatedAtMeta = const VerificationMeta('messageTextUpdatedAt'); @override @@ -1094,7 +1093,7 @@ class $MessagesTable extends Messages remoteDeletedAt: attachedDatabase.typeMapping.read( DriftSqlType.dateTime, data['${effectivePrefix}remote_deleted_at']), deletedForMe: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}deleted_for_me'])!, + .read(DriftSqlType.bool, data['${effectivePrefix}deleted_for_me']), messageTextUpdatedAt: attachedDatabase.typeMapping.read( DriftSqlType.dateTime, data['${effectivePrefix}message_text_updated_at']), @@ -1210,7 +1209,7 @@ class MessageEntity extends DataClass implements Insertable { final DateTime? remoteDeletedAt; /// Whether the message was deleted only for the current user. - final bool deletedForMe; + final bool? deletedForMe; /// The DateTime at which the message text was edited final DateTime? messageTextUpdatedAt; @@ -1262,7 +1261,7 @@ class MessageEntity extends DataClass implements Insertable { this.remoteUpdatedAt, this.localDeletedAt, this.remoteDeletedAt, - required this.deletedForMe, + this.deletedForMe, this.messageTextUpdatedAt, this.userId, required this.pinned, @@ -1331,7 +1330,9 @@ class MessageEntity extends DataClass implements Insertable { if (!nullToAbsent || remoteDeletedAt != null) { map['remote_deleted_at'] = Variable(remoteDeletedAt); } - map['deleted_for_me'] = Variable(deletedForMe); + if (!nullToAbsent || deletedForMe != null) { + map['deleted_for_me'] = Variable(deletedForMe); + } if (!nullToAbsent || messageTextUpdatedAt != null) { map['message_text_updated_at'] = Variable(messageTextUpdatedAt); } @@ -1389,7 +1390,7 @@ class MessageEntity extends DataClass implements Insertable { remoteUpdatedAt: serializer.fromJson(json['remoteUpdatedAt']), localDeletedAt: serializer.fromJson(json['localDeletedAt']), remoteDeletedAt: serializer.fromJson(json['remoteDeletedAt']), - deletedForMe: serializer.fromJson(json['deletedForMe']), + deletedForMe: serializer.fromJson(json['deletedForMe']), messageTextUpdatedAt: serializer.fromJson(json['messageTextUpdatedAt']), userId: serializer.fromJson(json['userId']), @@ -1429,7 +1430,7 @@ class MessageEntity extends DataClass implements Insertable { 'remoteUpdatedAt': serializer.toJson(remoteUpdatedAt), 'localDeletedAt': serializer.toJson(localDeletedAt), 'remoteDeletedAt': serializer.toJson(remoteDeletedAt), - 'deletedForMe': serializer.toJson(deletedForMe), + 'deletedForMe': serializer.toJson(deletedForMe), 'messageTextUpdatedAt': serializer.toJson(messageTextUpdatedAt), 'userId': serializer.toJson(userId), @@ -1467,7 +1468,7 @@ class MessageEntity extends DataClass implements Insertable { Value remoteUpdatedAt = const Value.absent(), Value localDeletedAt = const Value.absent(), Value remoteDeletedAt = const Value.absent(), - bool? deletedForMe, + Value deletedForMe = const Value.absent(), Value messageTextUpdatedAt = const Value.absent(), Value userId = const Value.absent(), bool? pinned, @@ -1512,7 +1513,8 @@ class MessageEntity extends DataClass implements Insertable { remoteDeletedAt: remoteDeletedAt.present ? remoteDeletedAt.value : this.remoteDeletedAt, - deletedForMe: deletedForMe ?? this.deletedForMe, + deletedForMe: + deletedForMe.present ? deletedForMe.value : this.deletedForMe, messageTextUpdatedAt: messageTextUpdatedAt.present ? messageTextUpdatedAt.value : this.messageTextUpdatedAt, @@ -1728,7 +1730,7 @@ class MessagesCompanion extends UpdateCompanion { final Value remoteUpdatedAt; final Value localDeletedAt; final Value remoteDeletedAt; - final Value deletedForMe; + final Value deletedForMe; final Value messageTextUpdatedAt; final Value userId; final Value pinned; @@ -1905,7 +1907,7 @@ class MessagesCompanion extends UpdateCompanion { Value? remoteUpdatedAt, Value? localDeletedAt, Value? remoteDeletedAt, - Value? deletedForMe, + Value? deletedForMe, Value? messageTextUpdatedAt, Value? userId, Value? pinned, @@ -3426,12 +3428,11 @@ class $PinnedMessagesTable extends PinnedMessages const VerificationMeta('deletedForMe'); @override late final GeneratedColumn deletedForMe = GeneratedColumn( - 'deleted_for_me', aliasedName, false, + 'deleted_for_me', aliasedName, true, type: DriftSqlType.bool, requiredDuringInsert: false, defaultConstraints: GeneratedColumn.constraintIsAlways( - 'CHECK ("deleted_for_me" IN (0, 1))'), - defaultValue: const Constant(false)); + 'CHECK ("deleted_for_me" IN (0, 1))')); static const VerificationMeta _messageTextUpdatedAtMeta = const VerificationMeta('messageTextUpdatedAt'); @override @@ -3728,7 +3729,7 @@ class $PinnedMessagesTable extends PinnedMessages remoteDeletedAt: attachedDatabase.typeMapping.read( DriftSqlType.dateTime, data['${effectivePrefix}remote_deleted_at']), deletedForMe: attachedDatabase.typeMapping - .read(DriftSqlType.bool, data['${effectivePrefix}deleted_for_me'])!, + .read(DriftSqlType.bool, data['${effectivePrefix}deleted_for_me']), messageTextUpdatedAt: attachedDatabase.typeMapping.read( DriftSqlType.dateTime, data['${effectivePrefix}message_text_updated_at']), @@ -3846,7 +3847,7 @@ class PinnedMessageEntity extends DataClass final DateTime? remoteDeletedAt; /// Whether the message was deleted only for the current user. - final bool deletedForMe; + final bool? deletedForMe; /// The DateTime at which the message text was edited final DateTime? messageTextUpdatedAt; @@ -3898,7 +3899,7 @@ class PinnedMessageEntity extends DataClass this.remoteUpdatedAt, this.localDeletedAt, this.remoteDeletedAt, - required this.deletedForMe, + this.deletedForMe, this.messageTextUpdatedAt, this.userId, required this.pinned, @@ -3967,7 +3968,9 @@ class PinnedMessageEntity extends DataClass if (!nullToAbsent || remoteDeletedAt != null) { map['remote_deleted_at'] = Variable(remoteDeletedAt); } - map['deleted_for_me'] = Variable(deletedForMe); + if (!nullToAbsent || deletedForMe != null) { + map['deleted_for_me'] = Variable(deletedForMe); + } if (!nullToAbsent || messageTextUpdatedAt != null) { map['message_text_updated_at'] = Variable(messageTextUpdatedAt); } @@ -4026,7 +4029,7 @@ class PinnedMessageEntity extends DataClass remoteUpdatedAt: serializer.fromJson(json['remoteUpdatedAt']), localDeletedAt: serializer.fromJson(json['localDeletedAt']), remoteDeletedAt: serializer.fromJson(json['remoteDeletedAt']), - deletedForMe: serializer.fromJson(json['deletedForMe']), + deletedForMe: serializer.fromJson(json['deletedForMe']), messageTextUpdatedAt: serializer.fromJson(json['messageTextUpdatedAt']), userId: serializer.fromJson(json['userId']), @@ -4066,7 +4069,7 @@ class PinnedMessageEntity extends DataClass 'remoteUpdatedAt': serializer.toJson(remoteUpdatedAt), 'localDeletedAt': serializer.toJson(localDeletedAt), 'remoteDeletedAt': serializer.toJson(remoteDeletedAt), - 'deletedForMe': serializer.toJson(deletedForMe), + 'deletedForMe': serializer.toJson(deletedForMe), 'messageTextUpdatedAt': serializer.toJson(messageTextUpdatedAt), 'userId': serializer.toJson(userId), @@ -4104,7 +4107,7 @@ class PinnedMessageEntity extends DataClass Value remoteUpdatedAt = const Value.absent(), Value localDeletedAt = const Value.absent(), Value remoteDeletedAt = const Value.absent(), - bool? deletedForMe, + Value deletedForMe = const Value.absent(), Value messageTextUpdatedAt = const Value.absent(), Value userId = const Value.absent(), bool? pinned, @@ -4149,7 +4152,8 @@ class PinnedMessageEntity extends DataClass remoteDeletedAt: remoteDeletedAt.present ? remoteDeletedAt.value : this.remoteDeletedAt, - deletedForMe: deletedForMe ?? this.deletedForMe, + deletedForMe: + deletedForMe.present ? deletedForMe.value : this.deletedForMe, messageTextUpdatedAt: messageTextUpdatedAt.present ? messageTextUpdatedAt.value : this.messageTextUpdatedAt, @@ -4365,7 +4369,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { final Value remoteUpdatedAt; final Value localDeletedAt; final Value remoteDeletedAt; - final Value deletedForMe; + final Value deletedForMe; final Value messageTextUpdatedAt; final Value userId; final Value pinned; @@ -4542,7 +4546,7 @@ class PinnedMessagesCompanion extends UpdateCompanion { Value? remoteUpdatedAt, Value? localDeletedAt, Value? remoteDeletedAt, - Value? deletedForMe, + Value? deletedForMe, Value? messageTextUpdatedAt, Value? userId, Value? pinned, @@ -9922,7 +9926,7 @@ typedef $$MessagesTableCreateCompanionBuilder = MessagesCompanion Function({ Value remoteUpdatedAt, Value localDeletedAt, Value remoteDeletedAt, - Value deletedForMe, + Value deletedForMe, Value messageTextUpdatedAt, Value userId, Value pinned, @@ -9956,7 +9960,7 @@ typedef $$MessagesTableUpdateCompanionBuilder = MessagesCompanion Function({ Value remoteUpdatedAt, Value localDeletedAt, Value remoteDeletedAt, - Value deletedForMe, + Value deletedForMe, Value messageTextUpdatedAt, Value userId, Value pinned, @@ -10612,7 +10616,7 @@ class $$MessagesTableTableManager extends RootTableManager< Value remoteUpdatedAt = const Value.absent(), Value localDeletedAt = const Value.absent(), Value remoteDeletedAt = const Value.absent(), - Value deletedForMe = const Value.absent(), + Value deletedForMe = const Value.absent(), Value messageTextUpdatedAt = const Value.absent(), Value userId = const Value.absent(), Value pinned = const Value.absent(), @@ -10681,7 +10685,7 @@ class $$MessagesTableTableManager extends RootTableManager< Value remoteUpdatedAt = const Value.absent(), Value localDeletedAt = const Value.absent(), Value remoteDeletedAt = const Value.absent(), - Value deletedForMe = const Value.absent(), + Value deletedForMe = const Value.absent(), Value messageTextUpdatedAt = const Value.absent(), Value userId = const Value.absent(), Value pinned = const Value.absent(), @@ -11769,7 +11773,7 @@ typedef $$PinnedMessagesTableCreateCompanionBuilder = PinnedMessagesCompanion Value remoteUpdatedAt, Value localDeletedAt, Value remoteDeletedAt, - Value deletedForMe, + Value deletedForMe, Value messageTextUpdatedAt, Value userId, Value pinned, @@ -11804,7 +11808,7 @@ typedef $$PinnedMessagesTableUpdateCompanionBuilder = PinnedMessagesCompanion Value remoteUpdatedAt, Value localDeletedAt, Value remoteDeletedAt, - Value deletedForMe, + Value deletedForMe, Value messageTextUpdatedAt, Value userId, Value pinned, @@ -12286,7 +12290,7 @@ class $$PinnedMessagesTableTableManager extends RootTableManager< Value remoteUpdatedAt = const Value.absent(), Value localDeletedAt = const Value.absent(), Value remoteDeletedAt = const Value.absent(), - Value deletedForMe = const Value.absent(), + Value deletedForMe = const Value.absent(), Value messageTextUpdatedAt = const Value.absent(), Value userId = const Value.absent(), Value pinned = const Value.absent(), @@ -12355,7 +12359,7 @@ class $$PinnedMessagesTableTableManager extends RootTableManager< Value remoteUpdatedAt = const Value.absent(), Value localDeletedAt = const Value.absent(), Value remoteDeletedAt = const Value.absent(), - Value deletedForMe = const Value.absent(), + Value deletedForMe = const Value.absent(), Value messageTextUpdatedAt = const Value.absent(), Value userId = const Value.absent(), Value pinned = const Value.absent(), diff --git a/packages/stream_chat_persistence/lib/src/entity/messages.dart b/packages/stream_chat_persistence/lib/src/entity/messages.dart index 3c33c49221..877c3c0970 100644 --- a/packages/stream_chat_persistence/lib/src/entity/messages.dart +++ b/packages/stream_chat_persistence/lib/src/entity/messages.dart @@ -100,7 +100,7 @@ class Messages extends Table { DateTimeColumn get remoteDeletedAt => dateTime().nullable()(); /// Whether the message was deleted only for the current user. - BoolColumn get deletedForMe => boolean().withDefault(const Constant(false))(); + BoolColumn get deletedForMe => boolean().nullable()(); /// The DateTime at which the message text was edited DateTimeColumn get messageTextUpdatedAt => dateTime().nullable()(); diff --git a/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart index 558651c97c..4b66826a82 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/message_mapper.dart @@ -31,7 +31,7 @@ extension MessageEntityX on MessageEntity { localUpdatedAt: localUpdatedAt, deletedAt: remoteDeletedAt, localDeletedAt: localDeletedAt, - deletedOnlyForMe: deletedForMe, + deletedForMe: deletedForMe, messageTextUpdatedAt: messageTextUpdatedAt, id: id, type: type, @@ -86,7 +86,7 @@ extension MessageX on Message { userId: user?.id, remoteDeletedAt: remoteDeletedAt, localDeletedAt: localDeletedAt, - deletedForMe: deletedOnlyForMe, + deletedForMe: deletedForMe, messageTextUpdatedAt: messageTextUpdatedAt, messageText: text, pinned: pinned, diff --git a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart index c4e60dbe87..1826fff80f 100644 --- a/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart +++ b/packages/stream_chat_persistence/lib/src/mapper/pinned_message_mapper.dart @@ -31,7 +31,7 @@ extension PinnedMessageEntityX on PinnedMessageEntity { localUpdatedAt: localUpdatedAt, deletedAt: remoteDeletedAt, localDeletedAt: localDeletedAt, - deletedOnlyForMe: deletedForMe, + deletedForMe: deletedForMe, messageTextUpdatedAt: messageTextUpdatedAt, id: id, type: type, @@ -80,14 +80,14 @@ extension PMessageX on Message { replyCount: replyCount, reactionGroups: reactionGroups, mentionedUsers: mentionedUsers.map(jsonEncode).toList(), - state: jsonEncode(state), + state: jsonEncode(state.toJson()), remoteUpdatedAt: remoteUpdatedAt, localUpdatedAt: localUpdatedAt, extraData: extraData, userId: user?.id, remoteDeletedAt: remoteDeletedAt, localDeletedAt: localDeletedAt, - deletedForMe: deletedOnlyForMe, + deletedForMe: deletedForMe, messageTextUpdatedAt: messageTextUpdatedAt, messageText: text, pinned: pinned, diff --git a/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart index 4a2f579971..11a81451e6 100644 --- a/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart @@ -131,7 +131,7 @@ void main() { expect(message.user!.id, entity.userId); expect(message.localDeletedAt, isSameDateAs(entity.localDeletedAt)); expect(message.remoteDeletedAt, isSameDateAs(entity.remoteDeletedAt)); - expect(message.deletedOnlyForMe, entity.deletedForMe); + expect(message.deletedForMe, entity.deletedForMe); expect(message.text, entity.messageText); expect(message.pinned, entity.pinned); expect(message.pinExpires, isSameDateAs(entity.pinExpires)); @@ -220,7 +220,7 @@ void main() { user: user, localDeletedAt: DateTime.now(), deletedAt: DateTime.now().add(const Duration(seconds: 1)), - deletedOnlyForMe: true, + deletedForMe: true, text: 'Hello', pinned: true, pinExpires: DateTime.now(), @@ -259,7 +259,7 @@ void main() { expect(entity.userId, message.user!.id); expect(entity.localDeletedAt, isSameDateAs(message.localDeletedAt)); expect(entity.remoteDeletedAt, isSameDateAs(message.remoteDeletedAt)); - expect(entity.deletedForMe, message.deletedOnlyForMe); + expect(entity.deletedForMe, message.deletedForMe); expect(entity.messageText, message.text); expect(entity.pinned, message.pinned); expect(entity.pinExpires, isSameDateAs(message.pinExpires)); diff --git a/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart index 8a371c3030..e9a76f4378 100644 --- a/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart @@ -131,7 +131,7 @@ void main() { expect(message.user!.id, entity.userId); expect(message.localDeletedAt, isSameDateAs(entity.localDeletedAt)); expect(message.remoteDeletedAt, isSameDateAs(entity.remoteDeletedAt)); - expect(message.deletedOnlyForMe, entity.deletedForMe); + expect(message.deletedForMe, entity.deletedForMe); expect(message.text, entity.messageText); expect(message.pinned, entity.pinned); expect(message.pinExpires, isSameDateAs(entity.pinExpires)); @@ -220,7 +220,7 @@ void main() { user: user, localDeletedAt: DateTime.now(), deletedAt: DateTime.now().add(const Duration(seconds: 1)), - deletedOnlyForMe: true, + deletedForMe: true, text: 'Hello', pinned: true, pinExpires: DateTime.now(), @@ -259,7 +259,7 @@ void main() { expect(entity.userId, message.user!.id); expect(entity.localDeletedAt, isSameDateAs(message.localDeletedAt)); expect(entity.remoteDeletedAt, isSameDateAs(message.remoteDeletedAt)); - expect(entity.deletedForMe, message.deletedOnlyForMe); + expect(entity.deletedForMe, message.deletedForMe); expect(entity.messageText, message.text); expect(entity.pinned, message.pinned); expect(entity.pinExpires, isSameDateAs(message.pinExpires)); From c3d39ce59e2f32303532214975aa8f34394c9535 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 25 Sep 2025 10:06:45 +0200 Subject: [PATCH 16/16] chore: update CHANGELOG.md --- packages/stream_chat_persistence/CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index ad9270c1f4..23a17d9471 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,6 +1,7 @@ ## Upcoming Beta -- Added support for `Messages.deletedForMe` and `Members.deletedMessages` fields. +- Added support for `Messages.deletedForMe`, `PinnedMessages.deletedForMe`, and + `Members.deletedMessages` fields. ## 10.0.0-beta.6