diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index ee4b1e1ce..0e4dae96b 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,3 +1,9 @@ +## Upcoming + +🐞 Fixed + +- Fixed `skipPush` and `skipEnrichUrl` not preserving during message send or update retry + ## 10.0.0-beta.4 🛑️ Breaking diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index d0f878432..b8bd7d867 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -732,7 +732,10 @@ class Channel { state!._retryQueue.add([ message.copyWith( // Update the message state to failed. - state: MessageState.sendingFailed, + state: MessageState.sendingFailed( + skipPush: skipPush, + skipEnrichUrl: skipEnrichUrl, + ), ), ]); } @@ -815,14 +818,20 @@ class Channel { state!._retryQueue.add([ message.copyWith( // Update the message state to failed. - state: MessageState.updatingFailed, + state: MessageState.updatingFailed( + skipPush: skipPush, + skipEnrichUrl: skipEnrichUrl, + ), ), ]); } else { // Reset the message to original state if the update fails and is not // retriable. state?.updateMessage(originalMessage.copyWith( - state: MessageState.updatingFailed, + state: MessageState.updatingFailed( + skipPush: skipPush, + skipEnrichUrl: skipEnrichUrl, + ), )); } } @@ -885,15 +894,25 @@ class Channel { state!._retryQueue.add([ message.copyWith( // Update the message state to failed. - state: MessageState.updatingFailed, + state: MessageState.partialUpdatingFailed( + set: set, + unset: unset, + skipEnrichUrl: skipEnrichUrl, + ), ), ]); } else { // Reset the message to original state if the update fails and is not // retriable. - state?.updateMessage(originalMessage.copyWith( - state: MessageState.updatingFailed, - )); + state?.updateMessage( + originalMessage.copyWith( + state: MessageState.partialUpdatingFailed( + set: set, + unset: unset, + skipEnrichUrl: skipEnrichUrl, + ), + ), + ); } } @@ -997,8 +1016,23 @@ class Channel { return message.state.maybeWhen( failed: (state, _) => state.when( - sendingFailed: () => sendMessage(message), - updatingFailed: () => updateMessage(message), + sendingFailed: (skipPush, skipEnrichUrl) => sendMessage( + message, + skipPush: skipPush, + skipEnrichUrl: skipEnrichUrl, + ), + updatingFailed: (skipPush, skipEnrichUrl) => updateMessage( + message, + skipPush: skipPush, + skipEnrichUrl: skipEnrichUrl, + ), + partialUpdatingFailed: (set, unset, skipEnrichUrl) => + partialUpdateMessage( + message, + set: set, + unset: unset, + skipEnrichUrl: skipEnrichUrl, + ), deletingFailed: (hard) => deleteMessage(message, hard: hard), ), orElse: () { diff --git a/packages/stream_chat/lib/src/client/retry_queue.dart b/packages/stream_chat/lib/src/client/retry_queue.dart index 8675ee68b..c1f884140 100644 --- a/packages/stream_chat/lib/src/client/retry_queue.dart +++ b/packages/stream_chat/lib/src/client/retry_queue.dart @@ -119,10 +119,11 @@ class RetryQueue { } static DateTime? _getMessageDate(Message message) { - return message.state.maybeWhen( - failed: (state, _) => state.when( - sendingFailed: () => message.createdAt, - updatingFailed: () => message.updatedAt, + return message.state.maybeMap( + failed: (it) => it.state.map( + sendingFailed: (_) => message.createdAt, + updatingFailed: (_) => message.updatedAt, + partialUpdatingFailed: (_) => message.updatedAt, deletingFailed: (_) => message.deletedAt, ), orElse: () => null, 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 0cbefec15..cf9809684 100644 --- a/packages/stream_chat/lib/src/core/models/message_state.dart +++ b/packages/stream_chat/lib/src/core/models/message_state.dart @@ -103,9 +103,11 @@ extension MessageStateX on MessageState { /// Returns true if the message is in failed updating state. bool get isUpdatingFailed { - final messageState = this; - if (messageState is! MessageFailed) return false; - return messageState.state is UpdatingFailed; + return switch (this) { + MessageFailed(state: UpdatingFailed()) => true, + MessageFailed(state: PartialUpdatingFailed()) => true, + _ => false, + }; } /// Returns true if the message is in failed deleting state. @@ -182,6 +184,46 @@ sealed class MessageState with _$MessageState { ); } + /// Sending failed state when the message fails to be sent. + factory MessageState.sendingFailed({ + required bool skipPush, + required bool skipEnrichUrl, + }) { + return MessageState.failed( + state: FailedState.sendingFailed( + skipPush: skipPush, + skipEnrichUrl: skipEnrichUrl, + ), + ); + } + + /// Updating failed state when the message fails to be updated. + factory MessageState.updatingFailed({ + required bool skipPush, + required bool skipEnrichUrl, + }) { + return MessageState.failed( + state: FailedState.updatingFailed( + skipPush: skipPush, + skipEnrichUrl: skipEnrichUrl, + ), + ); + } + + factory MessageState.partialUpdatingFailed({ + Map? set, + List? unset, + required bool skipEnrichUrl, + }) { + return MessageState.failed( + state: FailedState.partialUpdatingFailed( + set: set, + unset: unset, + skipEnrichUrl: skipEnrichUrl, + ), + ); + } + /// Sending state when the message is being sent. static const sending = MessageState.outgoing( state: OutgoingState.sending(), @@ -222,16 +264,6 @@ sealed class MessageState with _$MessageState { state: CompletedState.deleted(hard: true), ); - /// Sending failed state when the message fails to be sent. - static const sendingFailed = MessageState.failed( - state: FailedState.sendingFailed(), - ); - - /// Updating failed state when the message fails to be updated. - static const updatingFailed = MessageState.failed( - state: FailedState.updatingFailed(), - ); - /// Deleting failed state when the message fails to be soft deleted. static const softDeletingFailed = MessageState.failed( state: FailedState.deletingFailed(), @@ -285,10 +317,22 @@ sealed class CompletedState with _$CompletedState { @freezed sealed class FailedState with _$FailedState { /// Sending failed state when the message fails to be sent. - const factory FailedState.sendingFailed() = SendingFailed; + const factory FailedState.sendingFailed({ + @Default(false) bool skipPush, + @Default(false) bool skipEnrichUrl, + }) = SendingFailed; /// Updating failed state when the message fails to be updated. - const factory FailedState.updatingFailed() = UpdatingFailed; + const factory FailedState.updatingFailed({ + @Default(false) bool skipPush, + @Default(false) bool skipEnrichUrl, + }) = UpdatingFailed; + + const factory FailedState.partialUpdatingFailed({ + Map? set, + List? unset, + @Default(false) bool skipEnrichUrl, + }) = PartialUpdatingFailed; /// Deleting failed state when the message fails to be deleted. const factory FailedState.deletingFailed({ @@ -616,14 +660,21 @@ extension FailedStatePatternMatching on FailedState { /// @nodoc @optionalTypeArgs TResult when({ - required TResult Function() sendingFailed, - required TResult Function() updatingFailed, + required TResult Function(bool skipPush, bool skipEnrichUrl) sendingFailed, + required TResult Function(bool skipPush, bool skipEnrichUrl) updatingFailed, + required TResult Function( + Map? set, List? unset, bool skipEnrichUrl) + partialUpdatingFailed, required TResult Function(bool hard) deletingFailed, }) { final failedState = this; return switch (failedState) { - SendingFailed() => sendingFailed(), - UpdatingFailed() => updatingFailed(), + SendingFailed() => + sendingFailed(failedState.skipPush, failedState.skipEnrichUrl), + UpdatingFailed() => + updatingFailed(failedState.skipPush, failedState.skipEnrichUrl), + PartialUpdatingFailed() => partialUpdatingFailed( + failedState.set, failedState.unset, failedState.skipEnrichUrl), DeletingFailed() => deletingFailed(failedState.hard), }; } @@ -631,14 +682,21 @@ extension FailedStatePatternMatching on FailedState { /// @nodoc @optionalTypeArgs TResult? whenOrNull({ - TResult? Function()? sendingFailed, - TResult? Function()? updatingFailed, + TResult? Function(bool skipPush, bool skipEnrichUrl)? sendingFailed, + TResult? Function(bool skipPush, bool skipEnrichUrl)? updatingFailed, + required TResult Function( + Map? set, List? unset, bool skipEnrichUrl) + partialUpdatingFailed, TResult? Function(bool hard)? deletingFailed, }) { final failedState = this; return switch (failedState) { - SendingFailed() => sendingFailed?.call(), - UpdatingFailed() => updatingFailed?.call(), + SendingFailed() => + sendingFailed?.call(failedState.skipPush, failedState.skipEnrichUrl), + UpdatingFailed() => + updatingFailed?.call(failedState.skipPush, failedState.skipEnrichUrl), + PartialUpdatingFailed() => partialUpdatingFailed( + failedState.set, failedState.unset, failedState.skipEnrichUrl), DeletingFailed() => deletingFailed?.call(failedState.hard), }; } @@ -646,15 +704,22 @@ extension FailedStatePatternMatching on FailedState { /// @nodoc @optionalTypeArgs TResult maybeWhen({ - TResult Function()? sendingFailed, - TResult Function()? updatingFailed, + TResult Function(bool skipPush, bool skipEnrichUrl)? sendingFailed, + TResult Function(bool skipPush, bool skipEnrichUrl)? updatingFailed, + required TResult Function( + Map? set, List? unset, bool skipEnrichUrl) + partialUpdatingFailed, TResult Function(bool hard)? deletingFailed, required TResult orElse(), }) { final failedState = this; final result = switch (failedState) { - SendingFailed() => sendingFailed?.call(), - UpdatingFailed() => updatingFailed?.call(), + SendingFailed() => + sendingFailed?.call(failedState.skipPush, failedState.skipEnrichUrl), + UpdatingFailed() => + updatingFailed?.call(failedState.skipPush, failedState.skipEnrichUrl), + PartialUpdatingFailed() => partialUpdatingFailed( + failedState.set, failedState.unset, failedState.skipEnrichUrl), DeletingFailed() => deletingFailed?.call(failedState.hard), }; @@ -666,12 +731,15 @@ extension FailedStatePatternMatching on FailedState { TResult map({ required TResult Function(SendingFailed value) sendingFailed, required TResult Function(UpdatingFailed value) updatingFailed, + required TResult Function(PartialUpdatingFailed value) + partialUpdatingFailed, required TResult Function(DeletingFailed value) deletingFailed, }) { final failedState = this; return switch (failedState) { SendingFailed() => sendingFailed(failedState), UpdatingFailed() => updatingFailed(failedState), + PartialUpdatingFailed() => partialUpdatingFailed(failedState), DeletingFailed() => deletingFailed(failedState), }; } @@ -681,12 +749,14 @@ extension FailedStatePatternMatching on FailedState { TResult? mapOrNull({ TResult? Function(SendingFailed value)? sendingFailed, TResult? Function(UpdatingFailed value)? updatingFailed, + TResult? Function(PartialUpdatingFailed value)? partialUpdatingFailed, TResult? Function(DeletingFailed value)? deletingFailed, }) { final failedState = this; return switch (failedState) { SendingFailed() => sendingFailed?.call(failedState), UpdatingFailed() => updatingFailed?.call(failedState), + PartialUpdatingFailed() => partialUpdatingFailed?.call(failedState), DeletingFailed() => deletingFailed?.call(failedState), }; } @@ -696,6 +766,7 @@ extension FailedStatePatternMatching on FailedState { TResult maybeMap({ TResult Function(SendingFailed value)? sendingFailed, TResult Function(UpdatingFailed value)? updatingFailed, + TResult Function(PartialUpdatingFailed value)? partialUpdatingFailed, TResult Function(DeletingFailed value)? deletingFailed, required TResult orElse(), }) { @@ -703,6 +774,7 @@ extension FailedStatePatternMatching on FailedState { final result = switch (failedState) { SendingFailed() => sendingFailed?.call(failedState), UpdatingFailed() => updatingFailed?.call(failedState), + PartialUpdatingFailed() => partialUpdatingFailed?.call(failedState), DeletingFailed() => deletingFailed?.call(failedState), }; 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 079d8b369..06cb77449 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 @@ -736,6 +736,8 @@ FailedState _$FailedStateFromJson(Map json) { return SendingFailed.fromJson(json); case 'updatingFailed': return UpdatingFailed.fromJson(json); + case 'partialUpdatingFailed': + return PartialUpdatingFailed.fromJson(json); case 'deletingFailed': return DeletingFailed.fromJson(json); @@ -774,13 +776,27 @@ class $FailedStateCopyWith<$Res> { /// @nodoc @JsonSerializable() class SendingFailed implements FailedState { - const SendingFailed({final String? $type}) : $type = $type ?? 'sendingFailed'; + const SendingFailed( + {this.skipPush = false, this.skipEnrichUrl = false, final String? $type}) + : $type = $type ?? 'sendingFailed'; factory SendingFailed.fromJson(Map json) => _$SendingFailedFromJson(json); + @JsonKey() + final bool skipPush; + @JsonKey() + final bool skipEnrichUrl; + @JsonKey(name: 'runtimeType') final String $type; + /// Create a copy of FailedState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $SendingFailedCopyWith get copyWith => + _$SendingFailedCopyWithImpl(this, _$identity); + @override Map toJson() { return _$SendingFailedToJson( @@ -791,30 +807,86 @@ class SendingFailed implements FailedState { @override bool operator ==(Object other) { return identical(this, other) || - (other.runtimeType == runtimeType && other is SendingFailed); + (other.runtimeType == runtimeType && + other is SendingFailed && + (identical(other.skipPush, skipPush) || + other.skipPush == skipPush) && + (identical(other.skipEnrichUrl, skipEnrichUrl) || + other.skipEnrichUrl == skipEnrichUrl)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => runtimeType.hashCode; + int get hashCode => Object.hash(runtimeType, skipPush, skipEnrichUrl); @override String toString() { - return 'FailedState.sendingFailed()'; + return 'FailedState.sendingFailed(skipPush: $skipPush, skipEnrichUrl: $skipEnrichUrl)'; + } +} + +/// @nodoc +abstract mixin class $SendingFailedCopyWith<$Res> + implements $FailedStateCopyWith<$Res> { + factory $SendingFailedCopyWith( + SendingFailed value, $Res Function(SendingFailed) _then) = + _$SendingFailedCopyWithImpl; + @useResult + $Res call({bool skipPush, bool skipEnrichUrl}); +} + +/// @nodoc +class _$SendingFailedCopyWithImpl<$Res> + implements $SendingFailedCopyWith<$Res> { + _$SendingFailedCopyWithImpl(this._self, this._then); + + final SendingFailed _self; + final $Res Function(SendingFailed) _then; + + /// Create a copy of FailedState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + $Res call({ + Object? skipPush = null, + Object? skipEnrichUrl = null, + }) { + return _then(SendingFailed( + skipPush: null == skipPush + ? _self.skipPush + : skipPush // ignore: cast_nullable_to_non_nullable + as bool, + skipEnrichUrl: null == skipEnrichUrl + ? _self.skipEnrichUrl + : skipEnrichUrl // ignore: cast_nullable_to_non_nullable + as bool, + )); } } /// @nodoc @JsonSerializable() class UpdatingFailed implements FailedState { - const UpdatingFailed({final String? $type}) + const UpdatingFailed( + {this.skipPush = false, this.skipEnrichUrl = false, final String? $type}) : $type = $type ?? 'updatingFailed'; factory UpdatingFailed.fromJson(Map json) => _$UpdatingFailedFromJson(json); + @JsonKey() + final bool skipPush; + @JsonKey() + final bool skipEnrichUrl; + @JsonKey(name: 'runtimeType') final String $type; + /// Create a copy of FailedState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $UpdatingFailedCopyWith get copyWith => + _$UpdatingFailedCopyWithImpl(this, _$identity); + @override Map toJson() { return _$UpdatingFailedToJson( @@ -825,16 +897,181 @@ class UpdatingFailed implements FailedState { @override bool operator ==(Object other) { return identical(this, other) || - (other.runtimeType == runtimeType && other is UpdatingFailed); + (other.runtimeType == runtimeType && + other is UpdatingFailed && + (identical(other.skipPush, skipPush) || + other.skipPush == skipPush) && + (identical(other.skipEnrichUrl, skipEnrichUrl) || + other.skipEnrichUrl == skipEnrichUrl)); } @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => runtimeType.hashCode; + int get hashCode => Object.hash(runtimeType, skipPush, skipEnrichUrl); @override String toString() { - return 'FailedState.updatingFailed()'; + return 'FailedState.updatingFailed(skipPush: $skipPush, skipEnrichUrl: $skipEnrichUrl)'; + } +} + +/// @nodoc +abstract mixin class $UpdatingFailedCopyWith<$Res> + implements $FailedStateCopyWith<$Res> { + factory $UpdatingFailedCopyWith( + UpdatingFailed value, $Res Function(UpdatingFailed) _then) = + _$UpdatingFailedCopyWithImpl; + @useResult + $Res call({bool skipPush, bool skipEnrichUrl}); +} + +/// @nodoc +class _$UpdatingFailedCopyWithImpl<$Res> + implements $UpdatingFailedCopyWith<$Res> { + _$UpdatingFailedCopyWithImpl(this._self, this._then); + + final UpdatingFailed _self; + final $Res Function(UpdatingFailed) _then; + + /// Create a copy of FailedState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + $Res call({ + Object? skipPush = null, + Object? skipEnrichUrl = null, + }) { + return _then(UpdatingFailed( + skipPush: null == skipPush + ? _self.skipPush + : skipPush // ignore: cast_nullable_to_non_nullable + as bool, + skipEnrichUrl: null == skipEnrichUrl + ? _self.skipEnrichUrl + : skipEnrichUrl // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +@JsonSerializable() +class PartialUpdatingFailed implements FailedState { + const PartialUpdatingFailed( + {final Map? set, + final List? unset, + this.skipEnrichUrl = false, + final String? $type}) + : _set = set, + _unset = unset, + $type = $type ?? 'partialUpdatingFailed'; + factory PartialUpdatingFailed.fromJson(Map json) => + _$PartialUpdatingFailedFromJson(json); + + final Map? _set; + Map? get set { + final value = _set; + if (value == null) return null; + if (_set is EqualUnmodifiableMapView) return _set; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); + } + + final List? _unset; + List? get unset { + final value = _unset; + if (value == null) return null; + if (_unset is EqualUnmodifiableListView) return _unset; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); + } + + @JsonKey() + final bool skipEnrichUrl; + + @JsonKey(name: 'runtimeType') + final String $type; + + /// Create a copy of FailedState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @pragma('vm:prefer-inline') + $PartialUpdatingFailedCopyWith get copyWith => + _$PartialUpdatingFailedCopyWithImpl( + this, _$identity); + + @override + Map toJson() { + return _$PartialUpdatingFailedToJson( + this, + ); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is PartialUpdatingFailed && + const DeepCollectionEquality().equals(other._set, _set) && + const DeepCollectionEquality().equals(other._unset, _unset) && + (identical(other.skipEnrichUrl, skipEnrichUrl) || + other.skipEnrichUrl == skipEnrichUrl)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_set), + const DeepCollectionEquality().hash(_unset), + skipEnrichUrl); + + @override + String toString() { + return 'FailedState.partialUpdatingFailed(set: $set, unset: $unset, skipEnrichUrl: $skipEnrichUrl)'; + } +} + +/// @nodoc +abstract mixin class $PartialUpdatingFailedCopyWith<$Res> + implements $FailedStateCopyWith<$Res> { + factory $PartialUpdatingFailedCopyWith(PartialUpdatingFailed value, + $Res Function(PartialUpdatingFailed) _then) = + _$PartialUpdatingFailedCopyWithImpl; + @useResult + $Res call( + {Map? set, List? unset, bool skipEnrichUrl}); +} + +/// @nodoc +class _$PartialUpdatingFailedCopyWithImpl<$Res> + implements $PartialUpdatingFailedCopyWith<$Res> { + _$PartialUpdatingFailedCopyWithImpl(this._self, this._then); + + final PartialUpdatingFailed _self; + final $Res Function(PartialUpdatingFailed) _then; + + /// Create a copy of FailedState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + $Res call({ + Object? set = freezed, + Object? unset = freezed, + Object? skipEnrichUrl = null, + }) { + return _then(PartialUpdatingFailed( + set: freezed == set + ? _self._set + : set // ignore: cast_nullable_to_non_nullable + as Map?, + unset: freezed == unset + ? _self._unset + : unset // ignore: cast_nullable_to_non_nullable + as List?, + skipEnrichUrl: null == skipEnrichUrl + ? _self.skipEnrichUrl + : skipEnrichUrl // ignore: cast_nullable_to_non_nullable + as bool, + )); } } 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 e39b40667..3161a4f8c 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 @@ -108,21 +108,48 @@ Map _$DeletedToJson(Deleted instance) => { SendingFailed _$SendingFailedFromJson(Map json) => SendingFailed( + skipPush: json['skip_push'] as bool? ?? false, + skipEnrichUrl: json['skip_enrich_url'] as bool? ?? false, $type: json['runtimeType'] as String?, ); Map _$SendingFailedToJson(SendingFailed instance) => { + 'skip_push': instance.skipPush, + 'skip_enrich_url': instance.skipEnrichUrl, 'runtimeType': instance.$type, }; UpdatingFailed _$UpdatingFailedFromJson(Map json) => UpdatingFailed( + skipPush: json['skip_push'] as bool? ?? false, + skipEnrichUrl: json['skip_enrich_url'] as bool? ?? false, $type: json['runtimeType'] as String?, ); Map _$UpdatingFailedToJson(UpdatingFailed instance) => { + 'skip_push': instance.skipPush, + 'skip_enrich_url': instance.skipEnrichUrl, + 'runtimeType': instance.$type, + }; + +PartialUpdatingFailed _$PartialUpdatingFailedFromJson( + Map json) => + PartialUpdatingFailed( + set: json['set'] as Map?, + unset: + (json['unset'] as List?)?.map((e) => e as String).toList(), + skipEnrichUrl: json['skip_enrich_url'] as bool? ?? false, + $type: json['runtimeType'] as String?, + ); + +Map _$PartialUpdatingFailedToJson( + PartialUpdatingFailed instance) => + { + 'set': instance.set, + 'unset': instance.unset, + 'skip_enrich_url': instance.skipEnrichUrl, 'runtimeType': instance.$type, }; diff --git a/packages/stream_chat/test/src/client/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart index f8bbb21e6..756f6f956 100644 --- a/packages/stream_chat/test/src/client/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel_test.dart @@ -215,6 +215,7 @@ void main() { tearDown(() { channel.dispose(); + clearInteractions(client); }); test('should throw if trying to set `extraData`', () { @@ -288,6 +289,269 @@ void main() { )).called(1); }); + test( + 'should handle StreamChatNetworkError by adding message to retry queue with skipPush: true, skipEnrichUrl: false', + () async { + final message = Message( + id: 'test-message-id', + user: client.state.currentUser, + ); + + when(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + skipPush: true, + )).thenThrow(StreamChatNetworkError(ChatErrorCode.notAllowed)); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(state: MessageState.sending), + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.sendingFailed( + skipPush: true, + skipEnrichUrl: false, + ), + ), + matchMessageState: true, + ), + ], + ]), + ); + + try { + await channel.sendMessage( + message, + skipPush: true, + ); + } catch (e) { + expect(e, isA()); + + final networkError = e as StreamChatNetworkError; + expect(networkError.code, equals(ChatErrorCode.notAllowed.code)); + } + }); + + test( + 'should handle StreamChatNetworkError by adding message to retry queue with skipPush: true, skipEnrichUrl: true', + () async { + final message = Message( + id: 'test-message-id-2', + user: client.state.currentUser, + ); + + when(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + skipPush: true, + skipEnrichUrl: true, + )).thenThrow(StreamChatNetworkError(ChatErrorCode.notAllowed)); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(state: MessageState.sending), + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.sendingFailed( + skipPush: true, + skipEnrichUrl: true, + ), + ), + matchMessageState: true, + ), + ], + ]), + ); + + try { + await channel.sendMessage( + message, + skipPush: true, + skipEnrichUrl: true, + ); + } catch (e) { + expect(e, isA()); + + final networkError = e as StreamChatNetworkError; + expect(networkError.code, equals(ChatErrorCode.notAllowed.code)); + } + }); + + test( + 'should handle StreamChatNetworkError by adding message to retry queue with skipPush: false, skipEnrichUrl: true', + () async { + final message = Message( + id: 'test-message-id-3', + user: client.state.currentUser, + ); + + when(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + skipEnrichUrl: true, + )).thenThrow(StreamChatNetworkError(ChatErrorCode.notAllowed)); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(state: MessageState.sending), + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.sendingFailed( + skipPush: false, + skipEnrichUrl: true, + ), + ), + matchMessageState: true, + ), + ], + ]), + ); + + try { + await channel.sendMessage( + message, + skipEnrichUrl: true, + ); + } catch (e) { + expect(e, isA()); + + final networkError = e as StreamChatNetworkError; + expect(networkError.code, equals(ChatErrorCode.notAllowed.code)); + } + }); + + test( + 'should handle StreamChatNetworkError by adding message to retry queue with skipPush: false, skipEnrichUrl: false', + () async { + final message = Message( + id: 'test-message-id-4', + user: client.state.currentUser, + ); + + when(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + )).thenThrow(StreamChatNetworkError(ChatErrorCode.notAllowed)); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(state: MessageState.sending), + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.sendingFailed( + skipPush: false, + skipEnrichUrl: false, + ), + ), + matchMessageState: true, + ), + ], + ]), + ); + + try { + await channel.sendMessage( + message, + ); + } catch (e) { + expect(e, isA()); + + final networkError = e as StreamChatNetworkError; + expect(networkError.code, equals(ChatErrorCode.notAllowed.code)); + } + }); + + test('should not update message state when non-retriable error occurs', + () async { + final message = Message( + id: 'test-message-id', + user: client.state.currentUser, + ); + + when(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + )).thenThrow(StreamChatNetworkError.raw( + code: ChatErrorCode.inputError.code, + message: 'Input error', + data: ErrorResponse() + ..code = ChatErrorCode.inputError.code + ..message = 'Input error' + ..statusCode = 400, + )); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(state: MessageState.sending), + matchMessageState: true, + ), + ], + ]), + ); + + try { + await channel.sendMessage(message); + } catch (e) { + expect(e, isA()); + } + + final updatedMessage = + channel.state!.messages.firstWhere((m) => m.id == message.id); + expect(updatedMessage.state, isA()); + expect( + updatedMessage.state.maybeWhen( + failed: (state, _) => state.map( + sendingFailed: (_) => false, + updatingFailed: (_) => false, + partialUpdatingFailed: (_) => false, + deletingFailed: (_) => false, + ), + orElse: () => true, + ), + isTrue); + }); + test('with attachments should work just fine', () async { final attachments = List.generate( 3, @@ -990,64 +1254,626 @@ void main() { any(that: isSameMessageAs(message)), )).called(1); }); - }); - test('`.partialUpdateMessage`', () async { - final message = Message( - id: 'test-message-id', - state: MessageState.sent, - ); + test( + 'should not update message state when error is not StreamChatNetworkError', + () async { + final message = Message( + id: 'test-message-id-error-1', + state: MessageState.sent, + ); - const set = {'text': 'Update Message text'}; - const unset = ['pinExpires']; + when(() => client.updateMessage( + any(that: isSameMessageAs(message)), + skipEnrichUrl: true, + )).thenThrow(ArgumentError('Invalid argument')); - final updateMessageResponse = UpdateMessageResponse() - ..message = message.copyWith(text: set['text'], pinExpires: null); + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(state: MessageState.updating), + matchMessageState: true, + ), + ], + ]), + ); - when( - () => client.partialUpdateMessage(message.id, set: set, unset: unset), - ).thenAnswer((_) async => updateMessageResponse); + try { + await channel.updateMessage(message, skipEnrichUrl: true); + } catch (e) { + expect(e, isA()); + } + }); - expectLater( - // skipping first seed message list -> [] messages - channel.state?.messagesStream.skip(1), - emitsInOrder([ - [ - isSameMessageAs( - message.copyWith( - state: MessageState.updating, - ), - matchText: true, - matchMessageState: true, - ), - ], - [ - isSameMessageAs( - updateMessageResponse.message.copyWith( - state: MessageState.updated, - ), - matchText: true, - matchMessageState: true, - ), + test( + 'should add message to retry queue when retriable StreamChatNetworkError occurs with skipPush: false, skipEnrichUrl: true', + () async { + final message = Message( + id: 'test-message-id-retry-1', + state: MessageState.sent, + ); + + // Create a retriable error (data == null) + when(() => client.updateMessage( + any(that: isSameMessageAs(message)), + skipEnrichUrl: true, + )).thenThrow(StreamChatNetworkError.raw( + code: ChatErrorCode.requestTimeout.code, + message: 'Request timed out', + )); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(state: MessageState.updating), + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.updatingFailed( + skipPush: false, + skipEnrichUrl: true, + ), + ), + matchMessageState: true, + ), + ], + ]), + ); + + try { + await channel.updateMessage(message, skipEnrichUrl: true); + } catch (e) { + expect(e, isA()); + + final networkError = e as StreamChatNetworkError; + expect(networkError.code, equals(ChatErrorCode.requestTimeout.code)); + expect(networkError.isRetriable, isTrue); + } + }); + + test( + 'should add message to retry queue when retriable StreamChatNetworkError occurs with skipPush: true, skipEnrichUrl: false', + () async { + final message = Message( + id: 'test-message-id-retry-2', + state: MessageState.sent, + ); + + // Create a retriable error (data == null) + when(() => client.updateMessage( + any(that: isSameMessageAs(message)), + skipPush: true, + )).thenThrow(StreamChatNetworkError.raw( + code: ChatErrorCode.internalSystemError.code, + message: 'Internal system error', + )); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(state: MessageState.updating), + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.updatingFailed( + skipPush: true, + skipEnrichUrl: false, + ), + ), + matchMessageState: true, + ), + ], + ]), + ); + + try { + await channel.updateMessage(message, skipPush: true); + } catch (e) { + expect(e, isA()); + + final networkError = e as StreamChatNetworkError; + expect(networkError.code, + equals(ChatErrorCode.internalSystemError.code)); + expect(networkError.isRetriable, isTrue); + } + }); + + test( + 'should handle non-retriable StreamChatNetworkError with skipPush: true, skipEnrichUrl: true', + () async { + final message = Message( + id: 'test-message-id-error-2', + state: MessageState.sent, + ); + + when(() => client.updateMessage( + any(that: isSameMessageAs(message)), + skipPush: true, + skipEnrichUrl: true, + )).thenThrow(StreamChatNetworkError(ChatErrorCode.notAllowed)); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(state: MessageState.updating), + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.updatingFailed( + skipPush: true, + skipEnrichUrl: true, + ), + ), + matchMessageState: true, + ), + ], + ]), + ); + + try { + await channel.updateMessage( + message, + skipPush: true, + skipEnrichUrl: true, + ); + } catch (e) { + expect(e, isA()); + + final networkError = e as StreamChatNetworkError; + expect(networkError.code, equals(ChatErrorCode.notAllowed.code)); + } + }); + + test( + 'should handle non-retriable StreamChatNetworkError with skipPush: false, skipEnrichUrl: false', + () async { + final message = Message( + id: 'test-message-id-error-3', + state: MessageState.sent, + ); + + when(() => client.updateMessage( + any(that: isSameMessageAs(message)), + )).thenThrow(StreamChatNetworkError(ChatErrorCode.notAllowed)); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith(state: MessageState.updating), + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.updatingFailed( + skipPush: false, + skipEnrichUrl: false, + ), + ), + matchMessageState: true, + ), + ], + ]), + ); + + try { + await channel.updateMessage(message); + } catch (e) { + expect(e, isA()); + + final networkError = e as StreamChatNetworkError; + expect(networkError.code, equals(ChatErrorCode.notAllowed.code)); + } + }); + }); + + test('`.partialUpdateMessage`', () async { + final message = Message( + id: 'test-message-id', + state: MessageState.sent, + ); + + const set = {'text': 'Update Message text'}; + const unset = ['pinExpires']; + + final updateMessageResponse = UpdateMessageResponse() + ..message = message.copyWith(text: set['text'], pinExpires: null); + + when( + () => client.partialUpdateMessage(message.id, set: set, unset: unset), + ).thenAnswer((_) async => updateMessageResponse); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith( + state: MessageState.updating, + ), + matchText: true, + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + updateMessageResponse.message.copyWith( + state: MessageState.updated, + ), + matchText: true, + matchMessageState: true, + ), ], ]), ); - final res = await channel.partialUpdateMessage( - message, - set: set, - unset: unset, - ); + final res = await channel.partialUpdateMessage( + message, + set: set, + unset: unset, + ); + + expect(res, isNotNull); + expect(res.message.id, message.id); + expect(res.message.id, message.id); + expect(res.message.text, set['text']); + expect(res.message.pinExpires, isNull); + + verify( + () => client.partialUpdateMessage(message.id, set: set, unset: unset), + ).called(1); + }); + + group('`.partialUpdateMessage` error handling', () { + test( + 'should not update message state when error is not StreamChatNetworkError', + () async { + final message = Message( + id: 'test-message-id-error-partial-1', + state: MessageState.sent, + ); + + // Add message to channel state first + channel.state?.updateMessage(message); + + const set = {'text': 'Update Message text'}; + const unset = ['pinExpires']; + + when( + () => client.partialUpdateMessage( + message.id, + set: set, + unset: unset, + ), + ).thenThrow(ArgumentError('Invalid argument')); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith( + state: MessageState.updating, + ), + matchText: true, + matchMessageState: true, + ), + ], + ]), + ); + + try { + await channel.partialUpdateMessage( + message, + set: set, + unset: unset, + ); + } catch (e) { + expect(e, isA()); + } + }); + + test( + 'should add message to retry queue when retriable StreamChatNetworkError occurs with skipEnrichUrl: true', + () async { + final message = Message( + id: 'test-message-id-retry-partial-1', + state: MessageState.sent, + ); + + // Add message to channel state first + channel.state?.updateMessage(message); + + const set = {'text': 'Update Message text'}; + const unset = ['pinExpires']; + + // Create a retriable error (data == null) + when( + () => client.partialUpdateMessage( + message.id, + set: set, + unset: unset, + skipEnrichUrl: true, + ), + ).thenThrow(StreamChatNetworkError.raw( + code: ChatErrorCode.requestTimeout.code, + message: 'Request timed out', + )); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith( + state: MessageState.updating, + ), + matchText: true, + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.partialUpdatingFailed( + set: set, + unset: unset, + skipEnrichUrl: true, + ), + ), + matchText: true, + matchMessageState: true, + ), + ], + ]), + ); + + try { + await channel.partialUpdateMessage( + message, + set: set, + unset: unset, + skipEnrichUrl: true, + ); + } catch (e) { + expect(e, isA()); + + final networkError = e as StreamChatNetworkError; + expect(networkError.code, equals(ChatErrorCode.requestTimeout.code)); + expect(networkError.isRetriable, isTrue); + } + }); + + test( + 'should add message to retry queue when retriable StreamChatNetworkError occurs with skipEnrichUrl: false', + () async { + final message = Message( + id: 'test-message-id-retry-partial-2', + state: MessageState.sent, + ); + + // Add message to channel state first + channel.state?.updateMessage(message); + + const set = {'text': 'Update Message text'}; + const unset = ['pinExpires']; + + // Create a retriable error (data == null) + when( + () => client.partialUpdateMessage( + message.id, + set: set, + unset: unset, + ), + ).thenThrow(StreamChatNetworkError.raw( + code: ChatErrorCode.internalSystemError.code, + message: 'Internal system error', + )); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith( + state: MessageState.updating, + ), + matchText: true, + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.partialUpdatingFailed( + set: set, + unset: unset, + skipEnrichUrl: false, + ), + ), + matchText: true, + matchMessageState: true, + ), + ], + ]), + ); + + try { + await channel.partialUpdateMessage( + message, + set: set, + unset: unset, + ); + } catch (e) { + expect(e, isA()); + + final networkError = e as StreamChatNetworkError; + expect(networkError.code, + equals(ChatErrorCode.internalSystemError.code)); + expect(networkError.isRetriable, isTrue); + } + }); + + test( + 'should handle non-retriable StreamChatNetworkError with skipEnrichUrl: true', + () async { + final message = Message( + id: 'test-message-id-error-partial-2', + state: MessageState.sent, + ); + + // Add message to channel state first + channel.state?.updateMessage(message); + + const set = {'text': 'Update Message text'}; + const unset = ['pinExpires']; + + when( + () => client.partialUpdateMessage( + message.id, + set: set, + unset: unset, + skipEnrichUrl: true, + ), + ).thenThrow(StreamChatNetworkError(ChatErrorCode.notAllowed)); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith( + state: MessageState.updating, + ), + matchText: true, + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.partialUpdatingFailed( + set: set, + unset: unset, + skipEnrichUrl: true, + ), + ), + matchText: true, + matchMessageState: true, + ), + ], + ]), + ); + + try { + await channel.partialUpdateMessage( + message, + set: set, + unset: unset, + skipEnrichUrl: true, + ); + } catch (e) { + expect(e, isA()); + + final networkError = e as StreamChatNetworkError; + expect(networkError.code, equals(ChatErrorCode.notAllowed.code)); + } + }); + + test( + 'should handle non-retriable StreamChatNetworkError with skipEnrichUrl: false', + () async { + final message = Message( + id: 'test-message-id-error-partial-3', + state: MessageState.sent, + ); + + // Add message to channel state first + channel.state?.updateMessage(message); + + const set = {'text': 'Update Message text'}; + const unset = ['pinExpires']; + + when( + () => client.partialUpdateMessage( + message.id, + set: set, + unset: unset, + ), + ).thenThrow(StreamChatNetworkError(ChatErrorCode.notAllowed)); + + expectLater( + // skipping first seed message list -> [] messages + channel.state?.messagesStream.skip(1), + emitsInOrder([ + [ + isSameMessageAs( + message.copyWith( + state: MessageState.updating, + ), + matchText: true, + matchMessageState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + state: MessageState.partialUpdatingFailed( + set: set, + unset: unset, + skipEnrichUrl: false, + ), + ), + matchText: true, + matchMessageState: true, + ), + ], + ]), + ); - expect(res, isNotNull); - expect(res.message.id, message.id); - expect(res.message.id, message.id); - expect(res.message.text, set['text']); - expect(res.message.pinExpires, isNull); + try { + await channel.partialUpdateMessage( + message, + set: set, + unset: unset, + ); + } catch (e) { + expect(e, isA()); - verify( - () => client.partialUpdateMessage(message.id, set: set, unset: unset), - ).called(1); + final networkError = e as StreamChatNetworkError; + expect(networkError.code, equals(ChatErrorCode.notAllowed.code)); + } + }); }); group('`.deleteMessage`', () { @@ -5562,4 +6388,295 @@ void main() { expect(channel.canUpdateChannel, false); }); }); + + group('Retry functionality with parameter preservation', () { + late final client = MockStreamChatClient(); + const channelId = 'test-channel-id'; + const channelType = 'test-channel-type'; + late Channel channel; + + setUpAll(() { + registerFallbackValue(FakeMessage()); + registerFallbackValue([]); + registerFallbackValue(FakeAttachmentFile()); + + when(() => client.detachedLogger(any())).thenAnswer((invocation) { + final name = invocation.positionalArguments.first; + return _createLogger(name); + }); + + when(() => client.logger).thenReturn(_createLogger('mock-client-logger')); + + final clientState = FakeClientState(); + when(() => client.state).thenReturn(clientState); + + final retryPolicy = RetryPolicy( + shouldRetry: (_, __, error) { + return error is StreamChatNetworkError && error.isRetriable; + }, + ); + when(() => client.retryPolicy).thenReturn(retryPolicy); + }); + + setUp(() { + final channelState = _generateChannelState(channelId, channelType); + channel = Channel.fromState(client, channelState); + }); + + tearDown(() { + channel.dispose(); + }); + + group('retryMessage method', () { + test( + 'should call sendMessage with preserved skipPush and skipEnrichUrl parameters', + () async { + final message = Message( + id: 'test-message-id', + state: MessageState.sendingFailed( + skipPush: true, + skipEnrichUrl: true, + ), + ); + + final sendMessageResponse = SendMessageResponse() + ..message = message.copyWith(state: MessageState.sent); + + when(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + skipPush: true, + skipEnrichUrl: true, + )).thenAnswer((_) async => sendMessageResponse); + + final result = await channel.retryMessage(message); + + expect(result, isNotNull); + expect(result, isA()); + + verify(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + skipPush: true, + skipEnrichUrl: true, + )).called(1); + }); + + test('should call sendMessage with preserved skipPush parameter', + () async { + final message = Message( + id: 'test-message-id', + state: MessageState.sendingFailed( + skipPush: true, + skipEnrichUrl: false, + ), + ); + + final sendMessageResponse = SendMessageResponse() + ..message = message.copyWith(state: MessageState.sent); + + when(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + skipPush: true, + )).thenAnswer((_) async => sendMessageResponse); + + final result = await channel.retryMessage(message); + + expect(result, isNotNull); + expect(result, isA()); + + verify(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + skipPush: true, + )).called(1); + }); + + test('should call sendMessage with preserved skipEnrichUrl parameter', + () async { + final message = Message( + id: 'test-message-id', + state: MessageState.sendingFailed( + skipPush: false, + skipEnrichUrl: true, + ), + ); + + final sendMessageResponse = SendMessageResponse() + ..message = message.copyWith(state: MessageState.sent); + + when(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + skipEnrichUrl: true, + )).thenAnswer((_) async => sendMessageResponse); + + final result = await channel.retryMessage(message); + + expect(result, isNotNull); + expect(result, isA()); + + verify(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + skipEnrichUrl: true, + )).called(1); + }); + + test( + 'should call sendMessage with preserved false skipPush and skipEnrichUrl parameters', + () async { + final message = Message( + id: 'test-message-id', + state: MessageState.sendingFailed( + skipPush: false, + skipEnrichUrl: false, + ), + ); + + final sendMessageResponse = SendMessageResponse() + ..message = message.copyWith(state: MessageState.sent); + + when(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + )).thenAnswer((_) async => sendMessageResponse); + + final result = await channel.retryMessage(message); + + expect(result, isNotNull); + expect(result, isA()); + + verify(() => client.sendMessage( + any(that: isSameMessageAs(message)), + channelId, + channelType, + )).called(1); + }); + + test( + 'should call updateMessage with preserved skipPush, skipEnrichUrl parameter', + () async { + final message = Message( + id: 'test-message-id', + state: MessageState.updatingFailed( + skipPush: true, + skipEnrichUrl: true, + ), + ); + + final updateMessageResponse = UpdateMessageResponse() + ..message = message.copyWith(state: MessageState.updated); + + when(() => client.updateMessage( + any(that: isSameMessageAs(message)), + skipPush: true, + skipEnrichUrl: true, + )).thenAnswer((_) async => updateMessageResponse); + + final result = await channel.retryMessage(message); + + expect(result, isNotNull); + expect(result, isA()); + + verify(() => client.updateMessage( + any(that: isSameMessageAs(message)), + skipPush: true, + skipEnrichUrl: true, + )).called(1); + }); + + test( + 'should call updateMessage with preserved false skipPush, skipEnrichUrl parameter', + () async { + final message = Message( + id: 'test-message-id', + state: MessageState.updatingFailed( + skipPush: false, + skipEnrichUrl: false, + ), + ); + + final updateMessageResponse = UpdateMessageResponse() + ..message = message.copyWith(state: MessageState.updated); + + when(() => client.updateMessage( + any(that: isSameMessageAs(message)), + )).thenAnswer((_) async => updateMessageResponse); + + final result = await channel.retryMessage(message); + + expect(result, isNotNull); + expect(result, isA()); + + verify(() => client.updateMessage( + any(that: isSameMessageAs(message)), + )).called(1); + }); + + test('should call deleteMessage with preserved hard parameter', () async { + final message = Message( + id: 'test-message-id', + createdAt: DateTime.now(), + state: MessageState.deletingFailed(hard: true), + ); + + when(() => client.deleteMessage( + message.id, + hard: true, + )).thenAnswer((_) async => EmptyResponse()); + + final result = await channel.retryMessage(message); + + expect(result, isNotNull); + expect(result, isA()); + + verify(() => client.deleteMessage( + message.id, + hard: true, + )).called(1); + }); + + test('should call deleteMessage with preserved false hard parameter', + () async { + final message = Message( + id: 'test-message-id', + createdAt: DateTime.now(), + state: MessageState.deletingFailed(hard: false), + ); + + when(() => client.deleteMessage( + message.id, + )).thenAnswer((_) async => EmptyResponse()); + + final result = await channel.retryMessage(message); + + expect(result, isNotNull); + expect(result, isA()); + + verify(() => client.deleteMessage( + message.id, + )).called(1); + }); + + test('should throw AssertionError when message state is not failed', + () async { + final message = Message( + id: 'test-message-id', + state: MessageState.sent, + ); + + expect(() => channel.retryMessage(message), + throwsA(isA())); + }); + }); + }); } diff --git a/packages/stream_chat/test/src/client/retry_queue_test.dart b/packages/stream_chat/test/src/client/retry_queue_test.dart index 1150c40b8..59e2e51f4 100644 --- a/packages/stream_chat/test/src/client/retry_queue_test.dart +++ b/packages/stream_chat/test/src/client/retry_queue_test.dart @@ -46,7 +46,10 @@ void main() { final message = Message( id: 'test-message-id', text: 'Sample message test', - state: MessageState.sendingFailed, + state: MessageState.sendingFailed( + skipPush: false, + skipEnrichUrl: false, + ), ); retryQueue.add([message]); expect(() => retryQueue.add([message]), returnsNormally); @@ -58,7 +61,10 @@ void main() { final message = Message( id: 'test-message-id', text: 'Sample message test', - state: MessageState.sendingFailed, + state: MessageState.sendingFailed( + skipPush: false, + skipEnrichUrl: false, + ), ); retryQueue.add([message]); expect(retryQueue.hasMessages, isTrue); 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 9bdb250af..4d5ac13a3 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 @@ -270,7 +270,10 @@ void main() { test( 'MessageState.sendingFailed should create a MessageFailed instance with SendingFailed state', () { - const messageState = MessageState.sendingFailed; + final messageState = MessageState.sendingFailed( + skipPush: false, + skipEnrichUrl: false, + ); expect(messageState, isA()); expect((messageState as MessageFailed).state, isA()); }, @@ -279,12 +282,29 @@ void main() { test( 'MessageState.updatingFailed should create a MessageFailed instance with UpdatingFailed state', () { - const messageState = MessageState.updatingFailed; + final messageState = MessageState.updatingFailed( + skipPush: false, + skipEnrichUrl: false, + ); expect(messageState, isA()); expect((messageState as MessageFailed).state, isA()); }, ); + test( + 'MessageState.partialUpdatingFailed should create a MessageFailed instance with UpdatingFailed state', + () { + final messageState = MessageState.partialUpdatingFailed( + skipEnrichUrl: false, + ); + expect(messageState, isA()); + expect( + (messageState as MessageFailed).state, + isA(), + ); + }, + ); + test( 'MessageState.softDeletingFailed should create a MessageFailed instance with DeletingFailed state and not hard deleting', () {