diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 82225b27..144cfacc 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -10,6 +10,9 @@ - [[#890]](https://github.com/GetStream/stream-chat-flutter/pull/890) Fixed Reactions not updating on thread messages. Thanks [bstolinski](https://github.com/bstolinski). - [[#897]](https://github.com/GetStream/stream-chat-flutter/issues/897) Fixed error type mis-match in `AuthInterceptor`. +- [[#891]](https://github.com/GetStream/stream-chat-flutter/pull/891) Fixed reply counter for parent message not + updating correctly after deleting thread message. +- Fix `channelState.copyWith` with respect to pinnedMessages. ## 3.4.0 diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index 76fbab7a..0493c0d7 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -407,7 +407,7 @@ class Channel { if (index != -1) { final newAttachments = [...message!.attachments]..[index] = attachment; final updatedMessage = message!.copyWith(attachments: newAttachments); - state?.addMessage(updatedMessage); + state?.updateMessage(updatedMessage); // updating original message for next iteration message = message!.merge(updatedMessage); } @@ -512,7 +512,7 @@ class Channel { ).toList(), ); - state!.addMessage(message); + state!.updateMessage(message); try { if (message.attachments.any((it) => !it.uploadState.isSuccess)) { @@ -535,7 +535,7 @@ class Channel { type, skipPush: skipPush, ); - state!.addMessage(response.message); + state!.updateMessage(response.message); if (cooldown > 0) cooldownStartedAt = DateTime.now(); return response; } catch (e) { @@ -571,7 +571,7 @@ class Channel { ).toList(), ); - state?.addMessage(message); + state?.updateMessage(message); try { if (message.attachments.any((it) => !it.uploadState.isSuccess)) { @@ -594,7 +594,7 @@ class Channel { ownReactions: message.ownReactions, ); - state?.addMessage(m); + state?.updateMessage(m); return response; } catch (e) { @@ -602,7 +602,7 @@ class Channel { if (e.isRetriable) { state!._retryQueue.add([message]); } else { - state?.addMessage(originalMessage); + state?.updateMessage(originalMessage); } } rethrow; @@ -630,7 +630,7 @@ class Channel { ownReactions: message.ownReactions, ); - state?.addMessage(updatedMessage); + state?.updateMessage(updatedMessage); return response; } catch (e) { @@ -643,13 +643,18 @@ class Channel { /// Deletes the [message] from the channel. Future deleteMessage(Message message, {bool? hard}) async { + final hardDelete = hard ?? false; + // Directly deleting the local messages which are not yet sent to server if (message.status == MessageSendingStatus.sending || message.status == MessageSendingStatus.failed) { - state!.addMessage(message.copyWith( - type: 'deleted', - status: MessageSendingStatus.sent, - )); + state!.deleteMessage( + message.copyWith( + type: 'deleted', + status: MessageSendingStatus.sent, + ), + hardDelete: hardDelete, + ); // Removing the attachments upload completer to stop the `sendMessage` // waiting for attachments to complete. @@ -667,11 +672,14 @@ class Channel { deletedAt: message.deletedAt ?? DateTime.now(), ); - state?.addMessage(message); + state?.deleteMessage(message, hardDelete: hardDelete); final response = await _client.deleteMessage(message.id, hard: hard); - state?.addMessage(message.copyWith(status: MessageSendingStatus.sent)); + state?.deleteMessage( + message.copyWith(status: MessageSendingStatus.sent), + hardDelete: hardDelete, + ); return response; } catch (e) { @@ -861,7 +869,7 @@ class Channel { ownReactions: ownReactions, ); - state?.addMessage(newMessage); + state?.updateMessage(newMessage); try { final reactionResp = await _client.sendReaction( @@ -874,7 +882,7 @@ class Channel { return reactionResp; } catch (_) { // Reset the message if the update fails - state?.addMessage(message); + state?.updateMessage(message); rethrow; } } @@ -914,7 +922,7 @@ class Channel { ownReactions: ownReactions, ); - state?.addMessage(newMessage); + state?.updateMessage(newMessage); try { final deleteResponse = await _client.deleteReaction( @@ -924,7 +932,7 @@ class Channel { return deleteResponse; } catch (_) { // Reset the message if the update fails - state?.addMessage(message); + state?.updateMessage(message); rethrow; } } @@ -1081,7 +1089,7 @@ class Channel { // update the passed message with response message if (res.message != null) { - state!.addMessage(res.message!); + state!.updateMessage(res.message!); } else { // remove the passed message if response does // not contain message @@ -1712,7 +1720,7 @@ class ChannelClientState { final message = event.message!.copyWith( ownReactions: ownReactions, ); - addMessage(message); + updateMessage(message); })); } @@ -1725,7 +1733,7 @@ class ChannelClientState { final message = event.message!.copyWith( ownReactions: oldMessage?.ownReactions, ); - addMessage(message); + updateMessage(message); })); } @@ -1743,7 +1751,7 @@ class ChannelClientState { final message = event.message!.copyWith( ownReactions: oldMessage?.ownReactions, ); - addMessage(message); + updateMessage(message); if (message.pinned) { _channelState = _channelState.copyWith( @@ -1760,9 +1768,9 @@ class ChannelClientState { _subscriptions.add(_channel.on(EventType.messageDeleted).listen((event) { final message = event.message!; if (event.hardDelete == true) { - removeMessage(message, hardDelete: true); + removeMessage(message); } else { - addMessage(message); + updateMessage(message); } })); } @@ -1777,7 +1785,7 @@ class ChannelClientState { final message = event.message!; if (isUpToDate || (message.parentId != null && message.showInChannel != true)) { - addMessage(message); + updateMessage(message); } if (_countMessageAsUnread(message)) { @@ -1787,9 +1795,13 @@ class ChannelClientState { } /// Add a [message] to this [channelState]. - void addMessage(Message message) { + @Deprecated('Use updateMessage instead') + void addMessage(Message message) => updateMessage(message); + + /// Updates the [message] in the state if it exists. Adds it otherwise. + void updateMessage(Message message) { if (message.parentId == null || message.showInChannel == true) { - final newMessages = List.from(_channelState.messages); + final newMessages = [...messages]; final oldIndex = newMessages.indexWhere((m) => m.id == message.id); if (oldIndex != -1) { Message? m; @@ -1804,8 +1816,24 @@ class ChannelClientState { newMessages.add(message); } + final newPinnedMessages = [...pinnedMessages]; + final oldPinnedIndex = + newPinnedMessages.indexWhere((m) => m.id == message.id); + + // Handle pinned messages + if (message.pinned) { + if (oldPinnedIndex != -1) { + newPinnedMessages[oldPinnedIndex] = message; + } else { + newPinnedMessages.add(message); + } + } else { + newPinnedMessages.removeWhere((m) => m.id == message.id); + } + _channelState = _channelState.copyWith( messages: newMessages..sort(_sortByCreatedAt), + pinnedMessages: newPinnedMessages, channel: _channelState.channel?.copyWith( lastMessageAt: message.createdAt, ), @@ -1818,41 +1846,35 @@ class ChannelClientState { } /// Remove a [message] from this [channelState]. - void removeMessage(Message message, {bool hardDelete = false}) { + void removeMessage(Message message) { final parentId = message.parentId; - // i.e. it's a thread message - // 1. Remove the thread message - // 2. Reduce total reply count of parent message + // i.e. it's a thread message, Remove it if (parentId != null) { - final allMessages = [...messages]; - final parentMessage = allMessages.firstWhereOrNull( - (it) => it.id == parentId, - ); + final newThreads = {...threads}; + // Early return in case the thread is not available + if (!newThreads.containsKey(parentId)) return; - // return if message not available in the memory - if (parentMessage == null) return; - final replyCount = parentMessage.replyCount; - // return if reply count is null or zero - if (replyCount == null || replyCount == 0) return; + _threads = newThreads + ..update( + parentId, + (messages) => messages..removeWhere((e) => e.id == message.id), + ); - addMessage(parentMessage.copyWith(replyCount: replyCount - 1)); - updateThreadInfo( - parentId, - threads[parentId]! - ..removeWhere( - (e) => e.id == message.id, - ), - ); - } else { - // Remove regular message - final allMessages = [...messages]; - if (hardDelete) { - allMessages.removeWhere((e) => e.id == message.id); - _channelState = _channelState.copyWith(messages: allMessages); - } else if (allMessages.remove(message)) { - _channelState = _channelState.copyWith(messages: allMessages); - } + // Early return if the thread message is not shown in channel. + if (message.showInChannel == false) return; } + + // Remove regular message, thread message shown in channel + final allMessages = [...messages]; + _channelState = _channelState.copyWith( + messages: allMessages..removeWhere((e) => e.id == message.id), + ); + } + + /// Removes/Updates the [message] based on the [hardDelete] value. + void deleteMessage(Message message, {bool hardDelete = false}) { + if (hardDelete) return removeMessage(message); + return updateMessage(message); } void _listenReadEvents() { @@ -1898,11 +1920,12 @@ class ChannelClientState { .distinct(const ListEquality().equals); /// Channel pinned message list. - List get pinnedMessages => _channelState.pinnedMessages.toList(); + List get pinnedMessages => _channelState.pinnedMessages; /// Channel pinned message list as a stream. - Stream> get pinnedMessagesStream => - channelStateStream.map((cs) => cs.pinnedMessages.toList()); + Stream> get pinnedMessagesStream => channelStateStream + .map((cs) => cs.pinnedMessages) + .distinct(const ListEquality().equals); /// Get channel last message. Message? get lastMessage => @@ -2213,7 +2236,7 @@ class ChannelClientState { .toList(); updateChannelState(_channelState.copyWith( - pinnedMessages: pinnedMessages.where(_pinIsValid()).toList(), + pinnedMessages: pinnedMessages.where(_pinIsValid).toList(), messages: expiredMessages, )); } @@ -2250,7 +2273,7 @@ class ChannelClientState { } } -bool Function(Message) _pinIsValid() { +bool _pinIsValid(Message message) { final now = DateTime.now(); - return (Message m) => m.pinExpires!.isAfter(now); + return message.pinExpires!.isAfter(now); } diff --git a/packages/stream_chat/lib/src/client/retry_queue.dart b/packages/stream_chat/lib/src/client/retry_queue.dart index 17b019a5..5dfa9aa3 100644 --- a/packages/stream_chat/lib/src/client/retry_queue.dart +++ b/packages/stream_chat/lib/src/client/retry_queue.dart @@ -158,7 +158,7 @@ class RetryQueue { : message.status == MessageSendingStatus.updating ? MessageSendingStatus.failed_update : MessageSendingStatus.failed_delete; - channel.state?.addMessage(message.copyWith(status: newStatus)); + channel.state?.updateMessage(message.copyWith(status: newStatus)); } Future _retryMessage(Message message) async { diff --git a/packages/stream_chat/lib/src/core/models/channel_state.dart b/packages/stream_chat/lib/src/core/models/channel_state.dart index 3ddc2b68..17439dc0 100644 --- a/packages/stream_chat/lib/src/core/models/channel_state.dart +++ b/packages/stream_chat/lib/src/core/models/channel_state.dart @@ -56,7 +56,7 @@ class ChannelState { ChannelModel? channel, List? messages, List? members, - List? pinnedMessages, + List pinnedMessages = _emptyPinnedMessages, int? watcherCount, List? watchers, List? read, @@ -69,7 +69,7 @@ class ChannelState { // FIXME: Use non-nullable by default instead of empty list. pinnedMessages: pinnedMessages == _emptyPinnedMessages ? this.pinnedMessages - : pinnedMessages ?? _emptyPinnedMessages, + : pinnedMessages, watcherCount: watcherCount ?? this.watcherCount, watchers: watchers ?? this.watchers, read: read ?? this.read, diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index b798b839..d8e01aa4 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -5,6 +5,7 @@ - [[#888]](https://github.com/GetStream/stream-chat-flutter/issues/888) Fix `unban` command not working in `MessageInput`. - [[#805]](https://github.com/GetStream/stream-chat-flutter/issues/805) Updated chewie dependency version to 1.3.0 - Fix `showScrollToBottom` in `MessageListView` not respecting false value. +- Fix default `Channel` route not opening from `ChannelListView` when `ChannelAvatar` is tapped ✅ Added diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index 0ceff00e..648226ce 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -599,7 +599,9 @@ class _ChannelListViewState extends State { child: ChannelPreview( onLongPress: widget.onChannelLongPress, channel: channel, - onImageTap: () => widget.onImageTap?.call(channel), + onImageTap: widget.onImageTap != null + ? () => widget.onImageTap!(channel) + : null, onTap: (channel) => onTap(channel, widget.channelWidget), ), ), @@ -612,7 +614,7 @@ class _ChannelListViewState extends State { if (widget.onChannelTap != null) { onTap = widget.onChannelTap!; } else { - onTap = (client, _) { + onTap = (channel, _) { if (widget.channelWidget == null) { return; } @@ -620,7 +622,7 @@ class _ChannelListViewState extends State { context, MaterialPageRoute( builder: (context) => StreamChannel( - channel: client, + channel: channel, child: widget.channelWidget!, ), ), diff --git a/packages/stream_chat_flutter/lib/src/full_screen_media.dart b/packages/stream_chat_flutter/lib/src/full_screen_media.dart index 83ae96b1..aa185597 100644 --- a/packages/stream_chat_flutter/lib/src/full_screen_media.dart +++ b/packages/stream_chat_flutter/lib/src/full_screen_media.dart @@ -65,24 +65,35 @@ class FullScreenMedia extends StatefulWidget { class _FullScreenMediaState extends State with SingleTickerProviderStateMixin { - bool _optionsShown = true; - - late final AnimationController _controller; + late final AnimationController _animationController; late final PageController _pageController; - late int _currentPage; + late final _curvedAnimation = CurvedAnimation( + parent: _animationController, + curve: Curves.easeOut, + reverseCurve: Curves.easeIn, + ); + + final _opacityTween = Tween(begin: 1, end: 0); + late final _opacityAnimation = _opacityTween.animate( + CurvedAnimation( + parent: _animationController, + curve: const Interval(0, 0.6, curve: Curves.easeOut), + ), + ); + + late final ValueNotifier _currentPage = ValueNotifier(widget.startIndex); final videoPackages = {}; @override void initState() { super.initState(); - _controller = AnimationController( + _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 300), ); _pageController = PageController(initialPage: widget.startIndex); - _currentPage = widget.startIndex; for (var i = 0; i < widget.mediaAttachments.length; i++) { final attachment = widget.mediaAttachments[i]; if (attachment.type != 'video') continue; @@ -116,41 +127,38 @@ class _FullScreenMediaState extends State resizeToAvoidBottomInset: false, body: Stack( children: [ - AnimatedBuilder( - animation: _controller, - builder: (context, snapshot) => PageView.builder( - controller: _pageController, - onPageChanged: (val) { - setState(() { - _currentPage = val; - }); + PageView.builder( + controller: _pageController, + onPageChanged: (val) { + _currentPage.value = val; - if (videoPackages.isEmpty) { - return; + if (videoPackages.isEmpty) { + return; + } + + final currentAttachment = widget.mediaAttachments[val]; + + for (final e in videoPackages.values) { + if (e._attachment != currentAttachment) { + e._chewieController?.pause(); } + } - final currentAttachment = widget.mediaAttachments[val]; - - for (final e in videoPackages.values) { - if (e._attachment != currentAttachment) { - e._chewieController?.pause(); - } - } - - if (widget.autoplayVideos && - currentAttachment.type == 'video') { - final controller = videoPackages[currentAttachment.id]!; - controller._chewieController?.play(); - } - }, - itemBuilder: (context, index) { - final attachment = widget.mediaAttachments[index]; - if (attachment.type == 'image' || - attachment.type == 'giphy') { - final imageUrl = attachment.imageUrl ?? - attachment.assetUrl ?? - attachment.thumbUrl; - return PhotoView( + if (widget.autoplayVideos && + currentAttachment.type == 'video') { + final controller = videoPackages[currentAttachment.id]!; + controller._chewieController?.play(); + } + }, + itemBuilder: (context, index) { + final attachment = widget.mediaAttachments[index]; + if (attachment.type == 'image' || attachment.type == 'giphy') { + final imageUrl = attachment.imageUrl ?? + attachment.assetUrl ?? + attachment.thumbUrl; + return AnimatedBuilder( + animation: _curvedAnimation, + builder: (context, child) => PhotoView( loadingBuilder: (context, image) => const Offstage(), imageProvider: (imageUrl == null && attachment.localUri != null && @@ -166,97 +174,91 @@ class _FullScreenMediaState extends State color: ColorTween( begin: ChannelHeaderTheme.of(context).color, end: Colors.black, - ).lerp(_controller.value), + ).lerp(_curvedAnimation.value), ), onTapUp: (a, b, c) { - setState(() { - _optionsShown = !_optionsShown; - }); - if (_controller.isCompleted) { - _controller.reverse(); + if (_animationController.isCompleted) { + _animationController.reverse(); } else { - _controller.forward(); + _animationController.forward(); } }, - ); - } else if (attachment.type == 'video') { - final controller = videoPackages[attachment.id]!; - if (!controller.initialized) { - return const Center( - child: CircularProgressIndicator(), - ); - } - return InkWell( - onTap: () { - setState(() { - _optionsShown = !_optionsShown; - }); - if (_controller.isCompleted) { - _controller.reverse(); - } else { - _controller.forward(); - } - }, - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 50, - ), - child: Chewie( - controller: controller.chewieController!, - ), - ), + ), + ); + } else if (attachment.type == 'video') { + final controller = videoPackages[attachment.id]!; + if (!controller.initialized) { + return const Center( + child: CircularProgressIndicator(), ); } - return Container(); - }, - itemCount: widget.mediaAttachments.length, - ), - ), - AnimatedOpacity( - opacity: _optionsShown ? 1.0 : 0.0, - duration: const Duration(milliseconds: 300), - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GalleryHeader( - userName: widget.userName, - sentAt: context.translations.sentAtText( - date: widget.message.createdAt, - time: widget.message.createdAt, + return InkWell( + onTap: () { + if (_animationController.isCompleted) { + _animationController.reverse(); + } else { + _animationController.forward(); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: 50, + ), + child: Chewie( + controller: controller.chewieController!, + ), ), - onBackPressed: () { - Navigator.of(context).pop(); - }, - message: widget.message, - currentIndex: _currentPage, - onShowMessage: () { - widget.onShowMessage?.call( - widget.message, - StreamChannel.of(context).channel, - ); - }, - attachmentActionsModalBuilder: - widget.attachmentActionsModalBuilder, - ), - if (!widget.message.isEphemeral) - GalleryFooter( - currentPage: _currentPage, - totalPages: widget.mediaAttachments.length, - mediaAttachments: widget.mediaAttachments, + ); + } + return const SizedBox(); + }, + itemCount: widget.mediaAttachments.length, + ), + FadeTransition( + opacity: _opacityAnimation, + child: ValueListenableBuilder( + valueListenable: _currentPage, + builder: (context, value, child) => Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GalleryHeader( + userName: widget.userName, + sentAt: context.translations.sentAtText( + date: widget.message.createdAt, + time: widget.message.createdAt, + ), + onBackPressed: () { + Navigator.of(context).pop(); + }, message: widget.message, - mediaSelectedCallBack: (val) { - setState(() { - _currentPage = val; + currentIndex: value, + onShowMessage: () { + widget.onShowMessage?.call( + widget.message, + StreamChannel.of(context).channel, + ); + }, + attachmentActionsModalBuilder: + widget.attachmentActionsModalBuilder, + ), + if (!widget.message.isEphemeral) + GalleryFooter( + currentPage: value, + totalPages: widget.mediaAttachments.length, + mediaAttachments: widget.mediaAttachments, + message: widget.message, + mediaSelectedCallBack: (val) { + _currentPage.value = val; _pageController.animateToPage( val, duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, ); Navigator.pop(context); - }); - }, - ), - ], + }, + ), + ], + ), ), ), ], @@ -264,9 +266,11 @@ class _FullScreenMediaState extends State ); @override - void dispose() async { + void dispose() { + _animationController.dispose(); + _pageController.dispose(); for (final package in videoPackages.values) { - await package.dispose(); + package.dispose(); } super.dispose(); }