From dc6c3094e96f49cd182a72335f22f30762281f6e Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 9 Jun 2021 16:11:59 +0200 Subject: [PATCH] performance fixes --- packages/stream_chat/lib/src/api/channel.dart | 2 +- packages/stream_chat/lib/src/client.dart | 6 +- .../stream_chat_flutter/analysis_options.yaml | 4 +- .../lib/src/attachment/file_attachment.dart | 6 +- .../lib/src/attachment/giphy_attachment.dart | 2 +- .../lib/src/attachment/image_attachment.dart | 12 +- .../lib/src/channel_header.dart | 3 +- .../lib/src/channel_image.dart | 10 +- .../lib/src/channel_list_view.dart | 194 ++-- .../lib/src/channel_preview.dart | 35 +- .../lib/src/connection_status_builder.dart | 44 +- .../lib/src/emoji/emoji.dart | 3 + .../lib/src/extension.dart | 8 +- .../lib/src/info_tile.dart | 3 + .../lib/src/message_actions_modal.dart | 244 +++-- .../lib/src/message_input.dart | 945 +++++++++--------- .../lib/src/message_list_view.dart | 218 ++-- .../lib/src/message_reactions_modal.dart | 192 ++-- .../lib/src/message_widget.dart | 146 +-- .../lib/src/quoted_message_widget.dart | 6 +- .../lib/src/reaction_picker.dart | 171 ++-- .../lib/src/typing_indicator.dart | 21 +- .../lib/src/url_attachment.dart | 2 +- .../stream_chat_flutter/lib/src/utils.dart | 2 +- .../lib/src/better_stream_builder.dart | 43 +- .../lib/src/stream_channel.dart | 60 +- 26 files changed, 1224 insertions(+), 1158 deletions(-) diff --git a/packages/stream_chat/lib/src/api/channel.dart b/packages/stream_chat/lib/src/api/channel.dart index a2fc4ed3..1e7f824e 100644 --- a/packages/stream_chat/lib/src/api/channel.dart +++ b/packages/stream_chat/lib/src/api/channel.dart @@ -1709,7 +1709,7 @@ class ChannelClientState { final BehaviorSubject _unreadCountController = BehaviorSubject.seeded(0); /// Unread count getter as a stream - Stream get unreadCountStream => _unreadCountController.stream; + Stream get unreadCountStream => _unreadCountController.stream.distinct(); /// Unread count getter int? get unreadCount => _unreadCountController.value; diff --git a/packages/stream_chat/lib/src/client.dart b/packages/stream_chat/lib/src/client.dart index 1b98762d..17b5654c 100644 --- a/packages/stream_chat/lib/src/client.dart +++ b/packages/stream_chat/lib/src/client.dart @@ -1532,13 +1532,15 @@ class ClientState { int? get unreadChannels => _unreadChannelsController.valueOrNull; /// The current unread channels count as a stream - Stream get unreadChannelsStream => _unreadChannelsController.stream; + Stream get unreadChannelsStream => + _unreadChannelsController.stream.distinct(); /// The current total unread messages count int? get totalUnreadCount => _totalUnreadCountController.valueOrNull; /// The current total unread messages count as a stream - Stream get totalUnreadCountStream => _totalUnreadCountController.stream; + Stream get totalUnreadCountStream => + _totalUnreadCountController.stream.distinct(); /// The current list of channels in memory as a stream Stream?> get channelsStream => diff --git a/packages/stream_chat_flutter/analysis_options.yaml b/packages/stream_chat_flutter/analysis_options.yaml index a37a0fb7..f0a87ea5 100644 --- a/packages/stream_chat_flutter/analysis_options.yaml +++ b/packages/stream_chat_flutter/analysis_options.yaml @@ -3,10 +3,10 @@ analyzer: - extension-methods exclude: - lib/**/*.g.dart -# - example/** + - example/** - lib/src/emoji - lib/**/*.freezed.dart -# - test/** + - test/** linter: rules: diff --git a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart index a676e576..7ec5fb04 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart @@ -3,10 +3,10 @@ import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/upload_progress_indicator.dart'; import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import 'package:stream_chat_flutter/src/upload_progress_indicator.dart'; // ignore: always_use_package_imports import 'attachment_widget.dart'; @@ -103,7 +103,7 @@ class FileAttachment extends AttachmentWidget { Widget _getFileTypeImage(BuildContext context) { if (isImageAttachment) { return Material( - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, type: MaterialType.transparency, shape: _getDefaultShape(context), child: source.when( @@ -154,7 +154,7 @@ class FileAttachment extends AttachmentWidget { if (isVideoAttachment) { return Material( - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, type: MaterialType.transparency, shape: _getDefaultShape(context), child: source.when( diff --git a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart index 1bbbd54f..1572061e 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart @@ -53,7 +53,7 @@ class GiphyAttachment extends AttachmentWidget { Card( color: StreamChatTheme.of(context).colorTheme.white, elevation: 2, - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( topRight: Radius.circular(16), diff --git a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart index 5c431d6e..c677435a 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/image_attachment.dart @@ -74,16 +74,16 @@ class ImageAttachment extends AttachmentWidget { if (imageUri.host == 'stream-io-cdn.com') { imageUri = imageUri.replace(queryParameters: { ...imageUri.queryParameters, - 'h': '500', - 'w': '500', + 'h': '400', + 'w': '400', 'crop': 'center', 'resize': 'crop', }); } else if (imageUri.host == 'stream-cloud-uploads.imgix.net') { imageUri = imageUri.replace(queryParameters: { ...imageUri.queryParameters, - 'height': '500', - 'width': '500', + 'height': '400', + 'width': '400', 'fit': 'crop', }); } @@ -92,10 +92,10 @@ class ImageAttachment extends AttachmentWidget { return _buildImageAttachment( context, CachedNetworkImage( - cacheKey: imageUri.path, + cacheKey: imageUrl, height: size?.height, width: size?.width, - placeholder: (_, __) { + placeholder: (context, __) { final image = Image.asset( 'images/placeholder.png', fit: BoxFit.cover, diff --git a/packages/stream_chat_flutter/lib/src/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel_header.dart index 7773b36f..16c1f1b7 100644 --- a/packages/stream_chat_flutter/lib/src/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_header.dart @@ -133,8 +133,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { } return InfoTile( - // ignore: avoid_bool_literals_in_conditional_expressions - showMessage: showConnectionStateTile ? showStatus : false, + showMessage: showConnectionStateTile && showStatus, message: statusString, child: AppBar( textTheme: Theme.of(context).textTheme, diff --git a/packages/stream_chat_flutter/lib/src/channel_image.dart b/packages/stream_chat_flutter/lib/src/channel_image.dart index aefdb44e..dd9bb314 100644 --- a/packages/stream_chat_flutter/lib/src/channel_image.dart +++ b/packages/stream_chat_flutter/lib/src/channel_image.dart @@ -94,15 +94,17 @@ class ChannelImage extends StatelessWidget { } else if (channel.state?.members.length == 2) { final otherMember = channel.state?.members .firstWhere((member) => member.user?.id != streamChat.user?.id); - return StreamBuilder( - stream: streamChat.client.state.usersStream.map( - (users) => users[otherMember?.userId] ?? otherMember!.user!), + return BetterStreamBuilder( + stream: streamChat.client.state.usersStream + .map((users) => + users[otherMember?.userId] ?? otherMember!.user!) + .distinct(), initialData: otherMember!.user, builder: (context, snapshot) => UserAvatar( borderRadius: borderRadius ?? chatThemeData .channelPreviewTheme.avatarTheme?.borderRadius, - user: snapshot.data ?? otherMember.user!, + user: snapshot ?? otherMember.user!, constraints: constraints ?? chatThemeData .channelPreviewTheme.avatarTheme?.constraints, 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 f5780d73..12b73f72 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -1,11 +1,10 @@ -import 'package:collection/collection.dart' show IterableExtension; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:shimmer/shimmer.dart'; import 'package:stream_chat_flutter/src/channel_bottom_sheet.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; -import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; @@ -245,10 +244,7 @@ class _ChannelListViewState extends State { } } - return AnimatedSwitcher( - duration: const Duration(milliseconds: 500), - child: child, - ); + return child; } Widget _buildEmptyWidget(BuildContext context) => LayoutBuilder( @@ -473,97 +469,97 @@ class _ChannelListViewState extends State { final channel = channels[i]; return StreamChannel( - key: ValueKey('CHANNEL-${channel.id}'), + key: ValueKey('CHANNEL-${channel.cid}'), channel: channel, - child: Builder( - builder: (context) => Slidable( - controller: _slideController, - enabled: widget.swipeToAction, - actionPane: const SlidableBehindActionPane(), - actionExtentRatio: 0.12, - secondaryActions: widget.swipeActions - ?.map((e) => IconSlideAction( - color: e.color, - iconWidget: e.iconWidget, - onTap: () { - e.onTap?.call(channel); - }, - )) - .toList() ?? - [ + child: Slidable( + controller: _slideController, + enabled: widget.swipeToAction, + actionPane: const SlidableBehindActionPane(), + actionExtentRatio: 0.12, + secondaryActions: widget.swipeActions + ?.map((e) => IconSlideAction( + color: e.color, + iconWidget: e.iconWidget, + onTap: () { + e.onTap?.call(channel); + }, + )) + .toList() ?? + [ + IconSlideAction( + color: backgroundColor, + icon: Icons.more_horiz, + onTap: widget.onMoreDetailsPressed != null + ? () { + widget.onMoreDetailsPressed!(channel); + } + : () { + showModalBottomSheet( + clipBehavior: Clip.hardEdge, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + ), + context: context, + builder: (context) => StreamChannel( + channel: channel, + child: ChannelBottomSheet( + onViewInfoTap: () { + widget.onViewInfoTap?.call(channel); + }, + ), + ), + ); + }, + ), + if ([ + 'admin', + 'owner', + ].contains(channel.state!.members + .firstWhereOrNull( + (m) => m.userId == channel.client.state.user?.id) + ?.role)) IconSlideAction( color: backgroundColor, - icon: Icons.more_horiz, - onTap: widget.onMoreDetailsPressed != null + iconWidget: StreamSvgIcon.delete( + color: chatThemeData.colorTheme.accentRed, + ), + onTap: widget.onDeletePressed != null ? () { - widget.onMoreDetailsPressed!(channel); + widget.onDeletePressed!(channel); } - : () { - showModalBottomSheet( - clipBehavior: Clip.hardEdge, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(32), - topRight: Radius.circular(32), - ), - ), - context: context, - builder: (context) => StreamChannel( - channel: channel, - child: ChannelBottomSheet( - onViewInfoTap: () { - widget.onViewInfoTap?.call(channel); - }, - ), + : () async { + final res = await showConfirmationDialog( + context, + title: 'Delete Conversation', + okText: 'DELETE', + question: + // ignore: lines_longer_than_80_chars + 'Are you sure you want to delete this conversation?', + cancelText: 'CANCEL', + icon: StreamSvgIcon.delete( + color: chatThemeData.colorTheme.accentRed, ), ); + if (res == true) { + await channel.delete(); + } }, ), - if ([ - 'admin', - 'owner', - ].contains(channel.state!.members - .firstWhereOrNull( - (m) => m.userId == channel.client.state.user?.id) - ?.role)) - IconSlideAction( - color: backgroundColor, - iconWidget: StreamSvgIcon.delete( - color: chatThemeData.colorTheme.accentRed, - ), - onTap: widget.onDeletePressed != null - ? () { - widget.onDeletePressed!(channel); - } - : () async { - final res = await showConfirmationDialog( - context, - title: 'Delete Conversation', - okText: 'DELETE', - question: - // ignore: lines_longer_than_80_chars - 'Are you sure you want to delete this conversation?', - cancelText: 'CANCEL', - icon: StreamSvgIcon.delete( - color: chatThemeData.colorTheme.accentRed, - ), - ); - if (res == true) { - await channel.delete(); - } - }, - ), - ], - child: Container( + ], + child: DecoratedBox( + decoration: BoxDecoration( color: chatThemeData.colorTheme.whiteSnow, - child: widget.channelPreviewBuilder?.call(context, channel) ?? - ChannelPreview( - onLongPress: widget.onChannelLongPress, - channel: channel, - onImageTap: () => widget.onImageTap?.call(channel), - onTap: (channel) => onTap(channel, widget.channelWidget), - ), ), + child: widget.channelPreviewBuilder?.call(context, channel) ?? + ChannelPreview( + onLongPress: widget.onChannelLongPress, + channel: channel, + onImageTap: () => widget.onImageTap?.call(channel), + onTap: (channel) => onTap(channel, widget.channelWidget), + ), ), ), ); @@ -637,12 +633,10 @@ class _ChannelListViewState extends State { context, ChannelsBlocState channelsProvider, ) => - StreamBuilder( + BetterStreamBuilder( stream: channelsProvider.queryChannelsLoading, initialData: false, - builder: (context, snapshot) { - if (snapshot.hasError) { - return Container( + errorBuilder: (context, err) => Container( color: StreamChatTheme.of(context) .colorTheme .accentRed @@ -653,17 +647,15 @@ class _ChannelListViewState extends State { child: Text('Error loading channels'), ), ), - ); - } - return snapshot.data! - ? const Center( - child: Padding( - padding: EdgeInsets.all(16), - child: CircularProgressIndicator(), - ), - ) - : const Offstage(); - }); + ), + builder: (context, snapshot) => snapshot + ? const Center( + child: Padding( + padding: EdgeInsets.all(16), + child: CircularProgressIndicator(), + ), + ) + : const Offstage()); Widget _separatorBuilder(context, i) { final effect = StreamChatTheme.of(context).colorTheme.borderBottom; diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index 3189b0cb..bb41f652 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -1,4 +1,5 @@ -import 'package:collection/collection.dart' show IterableExtension; +import 'package:collection/collection.dart' + show IterableExtension, ListEquality; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:jiffy/jiffy.dart'; @@ -69,12 +70,12 @@ class ChannelPreview extends StatelessWidget { Widget build(BuildContext context) { final channelPreviewTheme = StreamChatTheme.of(context).channelPreviewTheme; final streamChatState = StreamChat.of(context); - - return StreamBuilder( + return BetterStreamBuilder( stream: channel.isMutedStream, initialData: channel.isMuted, - builder: (context, snapshot) => Opacity( - opacity: snapshot.data! ? 0.5 : 1, + builder: (context, snapshot) => AnimatedOpacity( + opacity: snapshot ? 0.5 : 1, + duration: const Duration(milliseconds: 300), child: ListTile( visualDensity: VisualDensity.compact, contentPadding: const EdgeInsets.symmetric( @@ -103,14 +104,16 @@ class ChannelPreview extends StatelessWidget { textStyle: channelPreviewTheme.title, ), ), - StreamBuilder>( + BetterStreamBuilder?>( stream: channel.state?.membersStream, initialData: channel.state?.members, + comparator: const ListEquality().equals, builder: (context, snapshot) { - if (!snapshot.hasData || - snapshot.data!.isEmpty || - !snapshot.data!.any((Member e) => - e.user!.id == channel.client.state.user?.id)) { + if (snapshot?.isEmpty == true || + snapshot?.any((Member e) => + e.user!.id == + channel.client.state.user?.id) != + true) { return const SizedBox(); } return UnreadIndicator( @@ -159,14 +162,14 @@ class ChannelPreview extends StatelessWidget { )); } - Widget _buildDate(BuildContext context) => StreamBuilder( + Widget _buildDate(BuildContext context) => BetterStreamBuilder( stream: channel.lastMessageAtStream, initialData: channel.lastMessageAt, builder: (context, snapshot) { - if (!snapshot.hasData) { - return const SizedBox(); + if (snapshot == null) { + return const Offstage(); } - final lastMessageAt = snapshot.data!.toLocal(); + final lastMessageAt = snapshot.toLocal(); String stringDate; final now = DateTime.now(); @@ -219,11 +222,11 @@ class ChannelPreview extends StatelessWidget { } Widget _buildLastMessage(BuildContext context) => - StreamBuilder?>( + BetterStreamBuilder?>( stream: channel.state!.messagesStream, initialData: channel.state!.messages, builder: (context, snapshot) { - final lastMessage = snapshot.data + final lastMessage = snapshot ?.lastWhereOrNull((m) => m.shadowed != true && !m.isDeleted); if (lastMessage == null) { return const SizedBox(); diff --git a/packages/stream_chat_flutter/lib/src/connection_status_builder.dart b/packages/stream_chat_flutter/lib/src/connection_status_builder.dart index 9137265f..98daa580 100644 --- a/packages/stream_chat_flutter/lib/src/connection_status_builder.dart +++ b/packages/stream_chat_flutter/lib/src/connection_status_builder.dart @@ -8,7 +8,7 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// /// The widget will use the closest [StreamChatClient.wsConnectionStatusStream] /// in case no stream is provided. -class ConnectionStatusBuilder extends StatelessWidget { +class ConnectionStatusBuilder extends StatefulWidget { /// Creates a new ConnectionStatusBuilder const ConnectionStatusBuilder({ Key? key, @@ -36,20 +36,32 @@ class ConnectionStatusBuilder extends StatelessWidget { statusBuilder; @override - Widget build(BuildContext context) { - final client = StreamChat.of(context).client; - final stream = connectionStatusStream ?? client.wsConnectionStatusStream; - return BetterStreamBuilder( - initialData: initialStatus ?? client.wsConnectionStatus, - stream: stream, - loadingBuilder: loadingBuilder, - errorBuilder: (context, error) { - if (errorBuilder != null) { - return errorBuilder!(context, error); - } - return const Offstage(); - }, - builder: statusBuilder, - ); + _ConnectionStatusBuilderState createState() => + _ConnectionStatusBuilderState(); +} + +class _ConnectionStatusBuilderState extends State { + late StreamChatClient client; + late Stream stream; + + @override + Widget build(BuildContext context) => BetterStreamBuilder( + initialData: widget.initialStatus ?? client.wsConnectionStatus, + stream: stream, + loadingBuilder: widget.loadingBuilder, + errorBuilder: (context, error) { + if (widget.errorBuilder != null) { + return widget.errorBuilder!(context, error); + } + return const Offstage(); + }, + builder: widget.statusBuilder, + ); + + @override + void didChangeDependencies() { + client = StreamChat.of(context).client; + stream = widget.connectionStatusStream ?? client.wsConnectionStatusStream; + super.didChangeDependencies(); } } diff --git a/packages/stream_chat_flutter/lib/src/emoji/emoji.dart b/packages/stream_chat_flutter/lib/src/emoji/emoji.dart index 617e372a..38af85af 100644 --- a/packages/stream_chat_flutter/lib/src/emoji/emoji.dart +++ b/packages/stream_chat_flutter/lib/src/emoji/emoji.dart @@ -114325,6 +114325,9 @@ class Emoji { /// Get all Emojis static List all() => List.unmodifiable(_emojis); + static Iterable chars() => + _emojis.map((e) => e.char).whereType(); + /// Returns Emoji by [char] and character static Emoji? byChar(String char) { return _emojis.firstWhereOrNull((Emoji emoji) => emoji.char == char); diff --git a/packages/stream_chat_flutter/lib/src/extension.dart b/packages/stream_chat_flutter/lib/src/extension.dart index 3b6ba3e2..5161c8e8 100644 --- a/packages/stream_chat_flutter/lib/src/extension.dart +++ b/packages/stream_chat_flutter/lib/src/extension.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/emoji/emoji.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -final _emojis = Emoji.all(); +final _emojiChars = Emoji.chars(); /// String extension extension StringExtension on String { @@ -17,10 +17,10 @@ extension StringExtension on String { /// 1 to 3 emojis: big size with no text bubble. /// 4+ emojis or emojis+text: standard size with text bubble. bool get isOnlyEmoji { + if (isEmpty) return false; + if (length > 3) return false; final characters = trim().characters; - if (characters.isEmpty) return false; - if (characters.length > 3) return false; - return characters.every((c) => _emojis.map((e) => e.char).contains(c)); + return characters.every(_emojiChars.contains); } } diff --git a/packages/stream_chat_flutter/lib/src/info_tile.dart b/packages/stream_chat_flutter/lib/src/info_tile.dart index 1e935942..b420cadc 100644 --- a/packages/stream_chat_flutter/lib/src/info_tile.dart +++ b/packages/stream_chat_flutter/lib/src/info_tile.dart @@ -40,6 +40,9 @@ class InfoTile extends StatelessWidget { @override Widget build(BuildContext context) { final chatThemeData = StreamChatTheme.of(context); + if (!showMessage) { + return child; + } return PortalEntry( visible: showMessage, portalAnchor: tileAnchor ?? Alignment.topCenter, diff --git a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart index 1da4fcea..bdee86a0 100644 --- a/packages/stream_chat_flutter/lib/src/message_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_actions_modal.dart @@ -147,6 +147,119 @@ class _MessageActionsModalState extends State { widget.message.attachments.any((it) => it.type == 'file') == true; final streamChatThemeData = StreamChatTheme.of(context); + + final child = Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: widget.reverse + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, + children: [ + if (widget.showReactions && + (widget.message.status == MessageSendingStatus.sent)) + Align( + alignment: Alignment( + user?.id == widget.message.user?.id + ? (divFactor >= 1.0 ? -0.2 : (1.2 - divFactor)) + : (divFactor >= 1.0 ? 0.2 : -(1.2 - divFactor)), + 0), + child: ReactionPicker( + message: widget.message, + ), + ), + const SizedBox(height: 8), + IgnorePointer( + child: MessageWidget( + key: const Key('MessageWidget'), + reverse: widget.reverse, + attachmentBorderRadiusGeometry: widget + .attachmentBorderRadiusGeometry + ?.mirrorBorderIfReversed(reverse: !widget.reverse), + message: widget.message.copyWith( + text: widget.message.text!.length > 200 + // ignore: lines_longer_than_80_chars + ? '${widget.message.text!.substring(0, 200)}...' + : widget.message.text, + ), + messageTheme: widget.messageTheme, + showReactions: false, + showUsername: false, + showReplyMessage: false, + showUserAvatar: widget.showUserAvatar, + attachmentPadding: EdgeInsets.all( + hasFileAttachment ? 4 : 2, + ), + showTimestamp: false, + translateUserAvatar: false, + padding: const EdgeInsets.all(0), + textPadding: EdgeInsets.symmetric( + vertical: 8, + horizontal: widget.message.text!.isOnlyEmoji ? 0 : 16.0, + ), + showReactionPickerIndicator: widget.showReactions && + (widget.message.status == MessageSendingStatus.sent), + showSendingIndicator: false, + shape: widget.messageShape, + attachmentShape: widget.attachmentShape, + showPinHighlight: false, + textBuilder: widget.textBuilder, + ), + ), + const SizedBox(height: 8), + Padding( + padding: EdgeInsets.only( + left: widget.reverse ? 0 : 40, + ), + child: SizedBox( + width: mediaQueryData.size.width * 0.75, + child: Material( + color: streamChatThemeData.colorTheme.whiteSnow, + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (widget.showReplyMessage && + widget.message.status == MessageSendingStatus.sent) + _buildReplyButton(context), + if (widget.showThreadReplyMessage && + (widget.message.status == + MessageSendingStatus.sent) && + widget.message.parentId == null) + _buildThreadReplyButton(context), + if (widget.showResendMessage) + _buildResendMessage(context), + if (widget.showEditMessage) _buildEditMessage(context), + if (widget.showCopyMessage) _buildCopyButton(context), + if (widget.showFlagButton) _buildFlagButton(context), + if (widget.showPinButton) _buildPinButton(context), + if (widget.showDeleteMessage) + _buildDeleteButton(context), + ...widget.customActions + .map((action) => _buildCustomAction( + context, + action, + )) + ].insertBetween( + Container( + height: 1, + color: streamChatThemeData.colorTheme.greyWhisper, + ), + ), + ), + ), + ), + ), + ], + ), + ), + ), + ); + return GestureDetector( behavior: HitTestBehavior.translucent, onTap: () => Navigator.maybePop(context), @@ -168,136 +281,11 @@ class _MessageActionsModalState extends State { tween: Tween(begin: 0, end: 1), duration: const Duration(milliseconds: 300), curve: Curves.easeInOutBack, - builder: (context, val, snapshot) => Transform.scale( + builder: (context, val, child) => Transform.scale( scale: val, - child: Center( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(8), - child: Column( - crossAxisAlignment: widget.reverse - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - if (widget.showReactions && - (widget.message.status == - MessageSendingStatus.sent)) - Align( - alignment: Alignment( - user?.id == widget.message.user?.id - ? (divFactor >= 1.0 - ? -0.2 - : (1.2 - divFactor)) - : (divFactor >= 1.0 - ? 0.2 - : -(1.2 - divFactor)), - 0), - child: ReactionPicker( - message: widget.message, - ), - ), - const SizedBox(height: 8), - IgnorePointer( - child: MessageWidget( - key: const Key('MessageWidget'), - reverse: widget.reverse, - attachmentBorderRadiusGeometry: widget - .attachmentBorderRadiusGeometry - ?.mirrorBorderIfReversed( - reverse: !widget.reverse), - message: widget.message.copyWith( - text: widget.message.text!.length > 200 - // ignore: lines_longer_than_80_chars - ? '${widget.message.text!.substring(0, 200)}...' - : widget.message.text, - ), - messageTheme: widget.messageTheme, - showReactions: false, - showUsername: false, - showReplyMessage: false, - showUserAvatar: widget.showUserAvatar, - attachmentPadding: EdgeInsets.all( - hasFileAttachment ? 4 : 2, - ), - showTimestamp: false, - translateUserAvatar: false, - padding: const EdgeInsets.all(0), - textPadding: EdgeInsets.symmetric( - vertical: 8, - horizontal: - widget.message.text!.isOnlyEmoji ? 0 : 16.0, - ), - showReactionPickerIndicator: - widget.showReactions && - (widget.message.status == - MessageSendingStatus.sent), - showSendingIndicator: false, - shape: widget.messageShape, - attachmentShape: widget.attachmentShape, - showPinHighlight: false, - textBuilder: widget.textBuilder, - ), - ), - const SizedBox(height: 8), - Padding( - padding: EdgeInsets.only( - left: widget.reverse ? 0 : 40, - ), - child: SizedBox( - width: mediaQueryData.size.width * 0.75, - child: Material( - color: streamChatThemeData.colorTheme.whiteSnow, - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: [ - if (widget.showReplyMessage && - widget.message.status == - MessageSendingStatus.sent) - _buildReplyButton(context), - if (widget.showThreadReplyMessage && - (widget.message.status == - MessageSendingStatus.sent) && - widget.message.parentId == null) - _buildThreadReplyButton(context), - if (widget.showResendMessage) - _buildResendMessage(context), - if (widget.showEditMessage) - _buildEditMessage(context), - if (widget.showCopyMessage) - _buildCopyButton(context), - if (widget.showFlagButton) - _buildFlagButton(context), - if (widget.showPinButton) - _buildPinButton(context), - if (widget.showDeleteMessage) - _buildDeleteButton(context), - ...widget.customActions - .map((action) => _buildCustomAction( - context, - action, - )) - ].insertBetween( - Container( - height: 1, - color: streamChatThemeData - .colorTheme.greyWhisper, - ), - ), - ), - ), - ), - ), - ], - ), - ), - ), - ), + child: child, ), + child: child, ), ], ), diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 0d2070e7..7e62f1c3 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -268,6 +268,8 @@ class MessageInputState extends State { /// The editing controller passed to the input TextField late final TextEditingController textEditingController; + late StreamChatThemeData _streamChatTheme; + bool get _hasQuotedMessage => widget.quotedMessage != null; @override @@ -305,9 +307,10 @@ class MessageInputState extends State { @override Widget build(BuildContext context) { - final streamChatThemeData = StreamChatTheme.of(context); - Widget child = Container( - color: streamChatThemeData.messageInputTheme.inputBackground, + Widget child = DecoratedBox( + decoration: BoxDecoration( + color: _streamChatTheme.messageInputTheme.inputBackground, + ), child: SafeArea( child: GestureDetector( onPanUpdate: (details) { @@ -332,7 +335,7 @@ class MessageInputState extends State { Padding( padding: const EdgeInsets.all(8), child: StreamSvgIcon.reply( - color: streamChatThemeData.colorTheme.greyGainsboro, + color: _streamChatTheme.colorTheme.greyGainsboro, ), ), const Text( @@ -390,65 +393,62 @@ class MessageInputState extends State { ], ); - Widget _buildDmCheckbox() { - final streamChatThemeData = StreamChatTheme.of(context); - return Row( - children: [ - Container( - height: 16, - width: 16, - foregroundDecoration: BoxDecoration( - border: _sendAsDm - ? null - : Border.all( - color: streamChatThemeData.colorTheme.black.withOpacity(.5), - width: 2, - ), - borderRadius: BorderRadius.circular(3), - ), - child: Center( - child: Material( + Widget _buildDmCheckbox() => Row( + children: [ + Container( + height: 16, + width: 16, + foregroundDecoration: BoxDecoration( + border: _sendAsDm + ? null + : Border.all( + color: _streamChatTheme.colorTheme.black.withOpacity(.5), + width: 2, + ), borderRadius: BorderRadius.circular(3), - color: _sendAsDm - ? streamChatThemeData.colorTheme.accentBlue - : streamChatThemeData.colorTheme.white, - child: InkWell( - onTap: () { - setState(() { - _sendAsDm = !_sendAsDm; - }); - }, - child: AnimatedCrossFade( - duration: const Duration(milliseconds: 300), - reverseDuration: const Duration(milliseconds: 300), - crossFadeState: _sendAsDm - ? CrossFadeState.showFirst - : CrossFadeState.showSecond, - firstChild: StreamSvgIcon.check( - size: 16, - color: streamChatThemeData.colorTheme.white, - ), - secondChild: const SizedBox( - height: 16, - width: 16, + ), + child: Center( + child: Material( + borderRadius: BorderRadius.circular(3), + color: _sendAsDm + ? _streamChatTheme.colorTheme.accentBlue + : _streamChatTheme.colorTheme.white, + child: InkWell( + onTap: () { + setState(() { + _sendAsDm = !_sendAsDm; + }); + }, + child: AnimatedCrossFade( + duration: const Duration(milliseconds: 300), + reverseDuration: const Duration(milliseconds: 300), + crossFadeState: _sendAsDm + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + firstChild: StreamSvgIcon.check( + size: 16, + color: _streamChatTheme.colorTheme.white, + ), + secondChild: const SizedBox( + height: 16, + width: 16, + ), ), ), ), ), ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Text( - 'Also send as direct message', - style: streamChatThemeData.textTheme.footnote.copyWith( - color: streamChatThemeData.colorTheme.black.withOpacity(0.5), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + 'Also send as direct message', + style: _streamChatTheme.textTheme.footnote.copyWith( + color: _streamChatTheme.colorTheme.black.withOpacity(0.5), + ), ), ), - ), - ], - ); - } + ], + ); Widget _animateSendButton(BuildContext context) { final sendButton = widget.activeSendButton != null @@ -463,8 +463,7 @@ class MessageInputState extends State { : CrossFadeState.showSecond, firstChild: sendButton, secondChild: widget.idleSendButton ?? _buildIdleSendButton(context), - duration: - StreamChatTheme.of(context).messageInputTheme.sendAnimationDuration!, + duration: _streamChatTheme.messageInputTheme.sendAnimationDuration!, alignment: Alignment.center, ); } @@ -478,16 +477,18 @@ class MessageInputState extends State { ? CrossFadeState.showFirst : CrossFadeState.showSecond, firstChild: IconButton( - onPressed: () => setState(() => _actionsShrunk = false), + onPressed: () { + if (_actionsShrunk) { + setState(() => _actionsShrunk = false); + } + }, icon: Transform.rotate( angle: (widget.actionsLocation == ActionsLocation.right || widget.actionsLocation == ActionsLocation.rightInside) ? pi : 0, child: StreamSvgIcon.emptyCircleLeft( - color: StreamChatTheme.of(context) - .messageInputTheme - .expandButtonColor, + color: _streamChatTheme.messageInputTheme.expandButtonColor, ), ), padding: const EdgeInsets.all(0), @@ -522,7 +523,6 @@ class MessageInputState extends State { } Expanded _buildTextInput(BuildContext context) { - final theme = StreamChatTheme.of(context); final margin = (widget.sendButtonLocation == SendButtonLocation.inside ? const EdgeInsets.only(right: 8) : EdgeInsets.zero) + @@ -530,49 +530,46 @@ class MessageInputState extends State { ? const EdgeInsets.only(left: 8) : EdgeInsets.zero); return Expanded( - child: Center( - child: Container( - clipBehavior: Clip.antiAlias, - margin: margin, - decoration: BoxDecoration( - borderRadius: theme.messageInputTheme.borderRadius, - gradient: _focusNode.hasFocus - ? theme.messageInputTheme.activeBorderGradient - : theme.messageInputTheme.idleBorderGradient, - ), - child: Padding( - padding: const EdgeInsets.all(1.5), - child: Container( - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( - borderRadius: theme.messageInputTheme.borderRadius, - color: theme.messageInputTheme.inputBackground, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildReplyToMessage(), - _buildAttachments(), - LimitedBox( - maxHeight: widget.maxHeight, - child: TextField( - key: const Key('messageInputText'), - enabled: _inputEnabled, - maxLines: null, - onSubmitted: (_) => sendMessage(), - keyboardType: widget.keyboardType, - controller: textEditingController, - focusNode: _focusNode, - style: theme.messageInputTheme.inputTextStyle, - autofocus: widget.autofocus, - textAlignVertical: TextAlignVertical.center, - decoration: _getInputDecoration(), - textCapitalization: TextCapitalization.sentences, - ), - ) - ], - ), + child: Container( + clipBehavior: Clip.hardEdge, + margin: margin, + decoration: BoxDecoration( + borderRadius: _streamChatTheme.messageInputTheme.borderRadius, + gradient: _focusNode.hasFocus + ? _streamChatTheme.messageInputTheme.activeBorderGradient + : _streamChatTheme.messageInputTheme.idleBorderGradient, + ), + child: Padding( + padding: const EdgeInsets.all(1.5), + child: DecoratedBox( + decoration: BoxDecoration( + borderRadius: _streamChatTheme.messageInputTheme.borderRadius, + color: _streamChatTheme.messageInputTheme.inputBackground, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildReplyToMessage(), + _buildAttachments(), + LimitedBox( + maxHeight: widget.maxHeight, + child: TextField( + key: const Key('messageInputText'), + enabled: _inputEnabled, + maxLines: null, + onSubmitted: (_) => sendMessage(), + keyboardType: widget.keyboardType, + controller: textEditingController, + focusNode: _focusNode, + style: _streamChatTheme.messageInputTheme.inputTextStyle, + autofocus: widget.autofocus, + textAlignVertical: TextAlignVertical.center, + decoration: _getInputDecoration(), + textCapitalization: TextCapitalization.sentences, + ), + ) + ], ), ), ), @@ -581,13 +578,12 @@ class MessageInputState extends State { } InputDecoration _getInputDecoration() { - final theme = StreamChatTheme.of(context); - final passedDecoration = theme.messageInputTheme.inputDecoration; + final passedDecoration = _streamChatTheme.messageInputTheme.inputDecoration; return InputDecoration( isDense: true, hintText: _getHint(), - hintStyle: theme.messageInputTheme.inputTextStyle!.copyWith( - color: theme.colorTheme.grey, + hintStyle: _streamChatTheme.messageInputTheme.inputTextStyle!.copyWith( + color: _streamChatTheme.colorTheme.grey, ), border: const OutlineInputBorder( borderSide: BorderSide( @@ -625,7 +621,7 @@ class MessageInputState extends State { constraints: BoxConstraints.tight(const Size(64, 24)), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), - color: theme.colorTheme.accentBlue, + color: _streamChatTheme.colorTheme.accentBlue, ), alignment: Alignment.center, child: Row( @@ -637,7 +633,8 @@ class MessageInputState extends State { ), Text( _chosenCommand?.name.toUpperCase() ?? '', - style: theme.textTheme.footnoteBold.copyWith( + style: + _streamChatTheme.textTheme.footnoteBold.copyWith( color: Colors.white, ), ), @@ -828,129 +825,126 @@ class MessageInputState extends State { final renderBox = context.findRenderObject() as RenderBox; final size = renderBox.size; + final child = Padding( + padding: const EdgeInsets.all(8), + child: Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + color: _streamChatTheme.colorTheme.white, + clipBehavior: Clip.hardEdge, + child: Container( + constraints: BoxConstraints.loose(const Size.fromHeight(400)), + decoration: BoxDecoration( + color: _streamChatTheme.colorTheme.white, + borderRadius: BorderRadius.circular(8)), + child: ListView( + padding: const EdgeInsets.all(0), + shrinkWrap: true, + children: [ + if (commands.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + ), + child: StreamSvgIcon.lightning( + color: _streamChatTheme.colorTheme.accentBlue, + ), + ), + Text( + 'Instant Commands', + style: TextStyle( + color: + _streamChatTheme.colorTheme.black.withOpacity(.5), + ), + ) + ], + ), + ), + const SizedBox( + height: 10, + ), + ...commands + .map( + (c) => InkWell( + onTap: () { + _setCommand(c); + }, + child: SizedBox( + height: 40, + child: Row( + children: [ + const SizedBox( + width: 16, + ), + _buildCommandIcon(c.name), + const SizedBox( + width: 8, + ), + Text.rich( + TextSpan( + text: c.name.capitalize(), + style: const TextStyle( + fontWeight: FontWeight.bold), + children: [ + TextSpan( + text: ' /${c.name} ${c.args}', + style: _streamChatTheme.textTheme.body + .copyWith( + // ignore: lines_longer_than_80_chars + color: _streamChatTheme + // ignore: lines_longer_than_80_chars + .colorTheme + .grey, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ) + .toList(), + ], + ), + ), + ), + ); return OverlayEntry( builder: (context) => Positioned( bottom: size.height + MediaQuery.of(context).viewInsets.bottom, left: 0, right: 0, child: TweenAnimationBuilder( - tween: Tween(begin: 0, end: 1), - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOutExpo, - builder: (context, val, wid) { - final streamChatThemeData = StreamChatTheme.of(context); - return Transform.scale( - scale: val, - child: Padding( - padding: const EdgeInsets.all(8), - child: Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - color: streamChatThemeData.colorTheme.white, - clipBehavior: Clip.antiAlias, - child: Container( - constraints: BoxConstraints.loose( - const Size.fromHeight(400)), - decoration: BoxDecoration( - color: streamChatThemeData.colorTheme.white, - borderRadius: BorderRadius.circular(8)), - child: ListView( - padding: const EdgeInsets.all(0), - shrinkWrap: true, - children: [ - if (commands.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 8), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - ), - child: StreamSvgIcon.lightning( - color: streamChatThemeData - .colorTheme.accentBlue, - ), - ), - Text( - 'Instant Commands', - style: TextStyle( - color: streamChatThemeData - .colorTheme.black - .withOpacity(.5), - ), - ) - ], - ), - ), - const SizedBox( - height: 10, - ), - ...commands - .map( - (c) => InkWell( - onTap: () { - _setCommand(c); - }, - child: SizedBox( - height: 40, - child: Row( - children: [ - const SizedBox( - width: 16, - ), - _buildCommandIcon(c.name), - const SizedBox( - width: 8, - ), - Text.rich( - TextSpan( - text: c.name.capitalize(), - style: const TextStyle( - fontWeight: - FontWeight.bold), - children: [ - TextSpan( - text: - ' /${c.name} ${c.args}', - style: streamChatThemeData - .textTheme.body - .copyWith( - // ignore: lines_longer_than_80_chars - color: streamChatThemeData - // ignore: lines_longer_than_80_chars - .colorTheme - .grey, - ), - ), - ], - ), - ), - ], - ), - ), - ), - ) - .toList(), - ], - ), - ), - ), - ), - ); - }), + tween: Tween(begin: 0, end: 1), + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOutExpo, + builder: (context, val, child) => Transform.scale( + scale: val, + child: child, + ), + child: child, + ), )); } Widget _buildFilePickerSection() { + if (!_openFilePickerSection) { + return const Offstage(); + } + final _attachmentContainsFile = _attachments.values.any((it) => it.type == 'file'); - final chatThemeData = StreamChatTheme.of(context); Color _getIconColor(int index) { - final streamChatThemeData = chatThemeData; + final streamChatThemeData = _streamChatTheme; switch (index) { case 0: return _attachments.isEmpty @@ -982,7 +976,7 @@ class MessageInputState extends State { _animateContainer ? const Duration(milliseconds: 300) : Duration.zero, height: _openFilePickerSection ? _filePickerSize : 0, child: Material( - color: chatThemeData.colorTheme.whiteSmoke, + color: _streamChatTheme.colorTheme.whiteSmoke, child: Column( mainAxisSize: MainAxisSize.min, children: [ @@ -1044,9 +1038,9 @@ class MessageInputState extends State { ); }); }, - child: Container( + child: DecoratedBox( decoration: BoxDecoration( - color: chatThemeData.colorTheme.white, + color: _streamChatTheme.colorTheme.white, borderRadius: const BorderRadius.only( topLeft: Radius.circular(16), topRight: Radius.circular(16), @@ -1057,12 +1051,14 @@ class MessageInputState extends State { child: Center( child: Padding( padding: const EdgeInsets.all(8), - child: Container( + child: SizedBox( width: 40, height: 4, - decoration: BoxDecoration( - color: chatThemeData.colorTheme.whiteSmoke, - borderRadius: BorderRadius.circular(4), + child: DecoratedBox( + decoration: BoxDecoration( + color: _streamChatTheme.colorTheme.whiteSmoke, + borderRadius: BorderRadius.circular(4), + ), ), ), ), @@ -1072,13 +1068,14 @@ class MessageInputState extends State { ), if (_openFilePickerSection) Expanded( - child: Container( + child: DecoratedBox( decoration: BoxDecoration( - color: chatThemeData.colorTheme.white, + color: _streamChatTheme.colorTheme.white, borderRadius: BorderRadius.circular(8), ), child: _PickerWidget( filePickerIndex: _filePickerIndex, + streamChatTheme: _streamChatTheme, containsFile: _attachmentContainsFile, selectedMedias: _attachments.keys.toList(), onAddMoreFilesClick: pickFile, @@ -1150,7 +1147,6 @@ class MessageInputState extends State { } Widget _buildCommandIcon(String iconType) { - final chatThemeData = StreamChatTheme.of(context); switch (iconType) { case 'giphy': return CircleAvatar( @@ -1161,7 +1157,7 @@ class MessageInputState extends State { ); case 'ban': return CircleAvatar( - backgroundColor: chatThemeData.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentBlue, radius: 12, child: StreamSvgIcon.iconUserDelete( size: 16, @@ -1170,7 +1166,7 @@ class MessageInputState extends State { ); case 'flag': return CircleAvatar( - backgroundColor: chatThemeData.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentBlue, radius: 12, child: StreamSvgIcon.flag( size: 14, @@ -1179,7 +1175,7 @@ class MessageInputState extends State { ); case 'imgur': return CircleAvatar( - backgroundColor: chatThemeData.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentBlue, radius: 12, child: ClipOval( child: StreamSvgIcon.imgur( @@ -1189,7 +1185,7 @@ class MessageInputState extends State { ); case 'mute': return CircleAvatar( - backgroundColor: chatThemeData.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentBlue, radius: 12, child: StreamSvgIcon.mute( size: 16, @@ -1198,7 +1194,7 @@ class MessageInputState extends State { ); case 'unban': return CircleAvatar( - backgroundColor: chatThemeData.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentBlue, radius: 12, child: StreamSvgIcon.userAdd( size: 16, @@ -1207,7 +1203,7 @@ class MessageInputState extends State { ); case 'unmute': return CircleAvatar( - backgroundColor: chatThemeData.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentBlue, radius: 12, child: StreamSvgIcon.volumeUp( size: 16, @@ -1216,7 +1212,7 @@ class MessageInputState extends State { ); default: return CircleAvatar( - backgroundColor: chatThemeData.colorTheme.accentBlue, + backgroundColor: _streamChatTheme.colorTheme.accentBlue, radius: 12, child: StreamSvgIcon.lightning( size: 16, @@ -1253,7 +1249,70 @@ class MessageInputState extends State { // ignore: cast_nullable_to_non_nullable final renderBox = context.findRenderObject() as RenderBox; final size = renderBox.size; + final child = Card( + margin: const EdgeInsets.all(8), + elevation: 2, + color: _streamChatTheme.colorTheme.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + clipBehavior: Clip.hardEdge, + child: Container( + constraints: BoxConstraints.loose(const Size.fromHeight(240)), + decoration: BoxDecoration( + color: _streamChatTheme.colorTheme.white, + ), + child: FutureBuilder>( + future: queryMembers ?? Future.value(members), + initialData: members, + builder: (context, snapshot) => ListView( + padding: const EdgeInsets.all(0), + shrinkWrap: true, + children: [ + const SizedBox( + height: 8, + ), + ...snapshot.data! + .where((it) => it.user != null) + .map( + (m) => Material( + color: _streamChatTheme.colorTheme.white, + child: InkWell( + onTap: () { + if (m.user != null) { + _mentionedUsers.add(m.user!); + } + splits[splits.length - 1] = m.user!.name; + final rejoin = splits.join('@'); + + textEditingController.value = TextEditingValue( + text: rejoin + + textEditingController.text.substring( + textEditingController.selection.start), + selection: TextSelection.collapsed( + offset: rejoin.length, + ), + ); + _debounce!.cancel(); + _mentionsOverlay?.remove(); + _mentionsOverlay = null; + }, + child: widget.mentionsTileBuilder != null + ? widget.mentionsTileBuilder!(context, m) + : MentionTile(m), + ), + ), + ) + .toList(), + const SizedBox( + height: 8, + ), + ], + ), + ), + ), + ); return OverlayEntry( builder: (context) => Positioned( bottom: size.height + MediaQuery.of(context).viewInsets.bottom, @@ -1263,78 +1322,11 @@ class MessageInputState extends State { tween: Tween(begin: 0, end: 1), duration: const Duration(milliseconds: 300), curve: Curves.easeInOutExpo, - builder: (context, val, wid) { - final chatThemeData = StreamChatTheme.of(context); - return Transform.scale( - scale: val, - child: Card( - margin: const EdgeInsets.all(8), - elevation: 2, - color: chatThemeData.colorTheme.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - clipBehavior: Clip.antiAlias, - child: Container( - constraints: BoxConstraints.loose(const Size.fromHeight(240)), - decoration: BoxDecoration( - color: chatThemeData.colorTheme.white, - ), - child: FutureBuilder>( - future: queryMembers ?? Future.value(members), - initialData: members, - builder: (context, snapshot) => ListView( - padding: const EdgeInsets.all(0), - shrinkWrap: true, - children: [ - const SizedBox( - height: 8, - ), - ...snapshot.data! - .where((it) => it.user != null) - .map( - (m) => Material( - color: chatThemeData.colorTheme.white, - child: InkWell( - onTap: () { - if (m.user != null) { - _mentionedUsers.add(m.user!); - } - - splits[splits.length - 1] = m.user!.name; - final rejoin = splits.join('@'); - - textEditingController.value = - TextEditingValue( - text: rejoin + - textEditingController.text.substring( - textEditingController - .selection.start), - selection: TextSelection.collapsed( - offset: rejoin.length, - ), - ); - _debounce!.cancel(); - _mentionsOverlay?.remove(); - _mentionsOverlay = null; - }, - child: widget.mentionsTileBuilder != null - ? widget.mentionsTileBuilder!(context, m) - : MentionTile(m), - ), - ), - ) - .toList(), - const SizedBox( - height: 8, - ), - ], - ), - ), - ), - ), - ); - }, + builder: (context, val, child) => Transform.scale( + scale: val, + child: child, + ), + child: child, ), ), ); @@ -1363,88 +1355,88 @@ class MessageInputState extends State { final renderBox = context.findRenderObject() as RenderBox; final size = renderBox.size; - return OverlayEntry(builder: (context) { - final chatThemeData = StreamChatTheme.of(context); - return Positioned( - bottom: size.height + MediaQuery.of(context).viewInsets.bottom, - left: 0, - right: 0, - child: Card( - margin: const EdgeInsets.all(8), - elevation: 2, - color: chatThemeData.colorTheme.white, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - clipBehavior: Clip.antiAlias, - child: Container( - constraints: BoxConstraints.loose(const Size.fromHeight(200)), - decoration: BoxDecoration( - boxShadow: const [ - BoxShadow( - spreadRadius: -8, - blurRadius: 5, - offset: Offset(0, -4), + return OverlayEntry( + builder: (context) => Positioned( + bottom: size.height + MediaQuery.of(context).viewInsets.bottom, + left: 0, + right: 0, + child: Card( + margin: const EdgeInsets.all(8), + elevation: 2, + color: _streamChatTheme.colorTheme.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), ), - ], - color: chatThemeData.colorTheme.white, - ), - child: ListView.builder( - padding: const EdgeInsets.all(0), - shrinkWrap: true, - itemCount: emojis.length + 1, - itemBuilder: (context, i) { - if (i == 0) { - return Padding( - padding: const EdgeInsets.only(left: 8, top: 8), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 8), - child: StreamSvgIcon.smile( - color: chatThemeData.colorTheme.accentBlue, + clipBehavior: Clip.hardEdge, + child: Container( + constraints: BoxConstraints.loose(const Size.fromHeight(200)), + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + spreadRadius: -8, + blurRadius: 5, + offset: Offset(0, -4), + ), + ], + color: _streamChatTheme.colorTheme.white, + ), + child: ListView.builder( + padding: const EdgeInsets.all(0), + shrinkWrap: true, + itemCount: emojis.length + 1, + itemBuilder: (context, i) { + if (i == 0) { + return Padding( + padding: const EdgeInsets.only(left: 8, top: 8), + child: Row( + children: [ + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8), + child: StreamSvgIcon.smile( + color: + _streamChatTheme.colorTheme.accentBlue, + ), + ), + Flexible( + child: Text( + 'Emoji matching "$query"', + style: TextStyle( + color: _streamChatTheme.colorTheme.black + .withOpacity(.5), + ), + ), + ) + ], + ), + ); + } + + final emoji = emojis.elementAt(i - 1)!; + final themeData = Theme.of(context); + return ListTile( + title: SubstringHighlight( + text: + // ignore: lines_longer_than_80_chars + "${emoji.char} ${emoji.name!.replaceAll('_', ' ')}", + term: query, + textStyleHighlight: + themeData.textTheme.headline6!.copyWith( + fontSize: 14.5, + fontWeight: FontWeight.bold, + ), + textStyle: themeData.textTheme.headline6!.copyWith( + fontSize: 14.5, ), ), - Flexible( - child: Text( - 'Emoji matching "$query"', - style: TextStyle( - color: chatThemeData.colorTheme.black - .withOpacity(.5), - ), - ), - ) - ], - ), - ); - } - - final emoji = emojis.elementAt(i - 1)!; - final themeData = Theme.of(context); - return ListTile( - title: SubstringHighlight( - text: - // ignore: lines_longer_than_80_chars - "${emoji.char} ${emoji.name!.replaceAll('_', ' ')}", - term: query, - textStyleHighlight: - themeData.textTheme.headline6!.copyWith( - fontSize: 14.5, - fontWeight: FontWeight.bold, - ), - textStyle: themeData.textTheme.headline6!.copyWith( - fontSize: 14.5, - ), - ), - onTap: () { - _chooseEmoji(splits, emoji); - }, - ); - }), - ), - ), - ); - }); + onTap: () { + _chooseEmoji(splits, emoji); + }, + ); + }), + ), + ), + )); } void _chooseEmoji(List splits, Emoji emoji) { @@ -1483,7 +1475,7 @@ class MessageInputState extends State { reverse: true, showBorder: !containsUrl, message: widget.quotedMessage!, - messageTheme: StreamChatTheme.of(context).otherMessageTheme, + messageTheme: _streamChatTheme.otherMessageTheme, padding: const EdgeInsets.fromLTRB(8, 8, 8, 0), ); } @@ -1568,32 +1560,29 @@ class MessageInputState extends State { ); } - Widget _buildRemoveButton(Attachment attachment) { - final chatThemeData = StreamChatTheme.of(context); - return SizedBox( - height: 24, - width: 24, - child: RawMaterialButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - elevation: 0, - highlightElevation: 0, - focusElevation: 0, - hoverElevation: 0, - onPressed: () { - setState(() => _attachments.remove(attachment.id)); - }, - fillColor: chatThemeData.colorTheme.black.withOpacity(.5), - child: Center( - child: StreamSvgIcon.close( - size: 24, - color: chatThemeData.colorTheme.white, + Widget _buildRemoveButton(Attachment attachment) => SizedBox( + height: 24, + width: 24, + child: RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 0, + highlightElevation: 0, + focusElevation: 0, + hoverElevation: 0, + onPressed: () { + setState(() => _attachments.remove(attachment.id)); + }, + fillColor: _streamChatTheme.colorTheme.black.withOpacity(.5), + child: Center( + child: StreamSvgIcon.close( + size: 24, + color: _streamChatTheme.colorTheme.white, + ), ), ), - ), - ); - } + ); Widget _buildAttachment(Attachment attachment) { if (widget.attachmentThumbnailBuilders?.containsKey(attachment.type) == @@ -1623,18 +1612,15 @@ class MessageInputState extends State { fit: BoxFit.cover, errorWidget: (_, obj, trace) => getFileTypeImage(attachment.extraData['other'] as String?), - progressIndicatorBuilder: (context, _, progress) { - final chatThemeData = StreamChatTheme.of(context); - return Shimmer.fromColors( - baseColor: chatThemeData.colorTheme.greyGainsboro, - highlightColor: chatThemeData.colorTheme.whiteSmoke, - child: Image.asset( - 'images/placeholder.png', - fit: BoxFit.cover, - package: 'stream_chat_flutter', - ), - ); - }, + placeholder: (context, _) => Shimmer.fromColors( + baseColor: _streamChatTheme.colorTheme.greyGainsboro, + highlightColor: _streamChatTheme.colorTheme.whiteSmoke, + child: Image.asset( + 'images/placeholder.png', + fit: BoxFit.cover, + package: 'stream_chat_flutter', + ), + ), ); case 'video': return Stack( @@ -1666,14 +1652,13 @@ class MessageInputState extends State { Widget _buildCommandButton() { final s = textEditingController.text.trim(); - final chatThemeData = StreamChatTheme.of(context); return IconButton( icon: StreamSvgIcon.lightning( color: s.isNotEmpty - ? chatThemeData.colorTheme.greyGainsboro + ? _streamChatTheme.colorTheme.greyGainsboro : (_commandsOverlay != null - ? chatThemeData.messageInputTheme.actionButtonColor - : chatThemeData.messageInputTheme.actionButtonIdleColor), + ? _streamChatTheme.messageInputTheme.actionButtonColor + : _streamChatTheme.messageInputTheme.actionButtonIdleColor), ), padding: const EdgeInsets.all(0), constraints: const BoxConstraints.tightFor( @@ -1708,40 +1693,37 @@ class MessageInputState extends State { ); } - Widget _buildAttachmentButton() { - final chatThemeData = StreamChatTheme.of(context); - return IconButton( - icon: StreamSvgIcon.attach( - color: _openFilePickerSection - ? chatThemeData.messageInputTheme.actionButtonColor - : chatThemeData.messageInputTheme.actionButtonIdleColor, - ), - padding: const EdgeInsets.all(0), - constraints: const BoxConstraints.tightFor( - height: 24, - width: 24, - ), - splashRadius: 24, - onPressed: () async { - _emojiOverlay?.remove(); - _emojiOverlay = null; - _commandsOverlay?.remove(); - _commandsOverlay = null; - _mentionsOverlay?.remove(); - _mentionsOverlay = null; + Widget _buildAttachmentButton() => IconButton( + icon: StreamSvgIcon.attach( + color: _openFilePickerSection + ? _streamChatTheme.messageInputTheme.actionButtonColor + : _streamChatTheme.messageInputTheme.actionButtonIdleColor, + ), + padding: const EdgeInsets.all(0), + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, + ), + splashRadius: 24, + onPressed: () async { + _emojiOverlay?.remove(); + _emojiOverlay = null; + _commandsOverlay?.remove(); + _commandsOverlay = null; + _mentionsOverlay?.remove(); + _mentionsOverlay = null; - if (_openFilePickerSection) { - setState(() { - _animateContainer = true; - _openFilePickerSection = false; - _filePickerSize = _kMinMediaPickerSize; - }); - } else { - showAttachmentModal(); - } - }, - ); - } + if (_openFilePickerSection) { + setState(() { + _animateContainer = true; + _openFilePickerSection = false; + _filePickerSize = _kMinMediaPickerSize; + }); + } else { + showAttachmentModal(); + } + }, + ); /// Show the attachment modal, making the user choose where to /// pick a media from @@ -1948,8 +1930,7 @@ class MessageInputState extends State { padding: const EdgeInsets.all(8), child: StreamSvgIcon( assetName: _getIdleSendIcon(), - color: - StreamChatTheme.of(context).messageInputTheme.sendButtonIdleColor, + color: _streamChatTheme.messageInputTheme.sendButtonIdleColor, ), ); @@ -1965,8 +1946,7 @@ class MessageInputState extends State { ), icon: StreamSvgIcon( assetName: _getSendIcon(), - color: - StreamChatTheme.of(context).messageInputTheme.sendButtonColor, + color: _streamChatTheme.messageInputTheme.sendButtonColor, ), ), ); @@ -2083,9 +2063,8 @@ class MessageInputState extends State { StreamSubscription? _keyboardListener; void _showErrorAlert(String description) { - final chatThemeData = StreamChatTheme.of(context); showModalBottomSheet( - backgroundColor: chatThemeData.colorTheme.white, + backgroundColor: _streamChatTheme.colorTheme.white, context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.only( @@ -2099,7 +2078,7 @@ class MessageInputState extends State { height: 26, ), StreamSvgIcon.error( - color: chatThemeData.colorTheme.accentRed, + color: _streamChatTheme.colorTheme.accentRed, size: 24, ), const SizedBox( @@ -2107,7 +2086,7 @@ class MessageInputState extends State { ), Text( 'Something went wrong', - style: chatThemeData.textTheme.headlineBold, + style: _streamChatTheme.textTheme.headlineBold, ), const SizedBox( height: 7, @@ -2123,7 +2102,7 @@ class MessageInputState extends State { height: 36, ), Container( - color: chatThemeData.colorTheme.black.withOpacity(.08), + color: _streamChatTheme.colorTheme.black.withOpacity(.08), height: 1, ), Row( @@ -2135,8 +2114,8 @@ class MessageInputState extends State { }, child: Text( 'OK', - style: chatThemeData.textTheme.bodyBold - .copyWith(color: chatThemeData.colorTheme.accentBlue), + style: _streamChatTheme.textTheme.bodyBold + .copyWith(color: _streamChatTheme.colorTheme.accentBlue), ), ), ], @@ -2169,6 +2148,7 @@ class MessageInputState extends State { @override void didChangeDependencies() { + _streamChatTheme = StreamChatTheme.of(context); if (widget.editMessage != null && !_initialized) { FocusScope.of(context).requestFocus(_focusNode); _initialized = true; @@ -2233,6 +2213,7 @@ class _PickerWidget extends StatefulWidget { required this.selectedMedias, required this.onAddMoreFilesClick, required this.onMediaSelected, + required this.streamChatTheme, }) : super(key: key); final int filePickerIndex; @@ -2240,6 +2221,7 @@ class _PickerWidget extends StatefulWidget { final List selectedMedias; final void Function(DefaultAttachmentTypes) onAddMoreFilesClick; final void Function(AssetEntity) onMediaSelected; + final StreamChatThemeData streamChatTheme; @override __PickerWidgetState createState() => __PickerWidgetState(); @@ -2266,7 +2248,6 @@ class __PickerWidgetState extends State<_PickerWidget> { return const Center(child: CircularProgressIndicator()); } - final chatThemeData = StreamChatTheme.of(context); if (snapshot.data!) { if (widget.containsFile) { return GestureDetector( @@ -2275,12 +2256,12 @@ class __PickerWidgetState extends State<_PickerWidget> { }, child: Container( constraints: const BoxConstraints.expand(), - color: chatThemeData.colorTheme.whiteSmoke, + color: widget.streamChatTheme.colorTheme.whiteSmoke, alignment: Alignment.center, child: Text( 'Add more files', style: TextStyle( - color: chatThemeData.colorTheme.accentBlue, + color: widget.streamChatTheme.colorTheme.accentBlue, fontWeight: FontWeight.bold, ), ), @@ -2298,7 +2279,7 @@ class __PickerWidgetState extends State<_PickerWidget> { PhotoManager.openSetting(); }, child: Container( - color: chatThemeData.colorTheme.whiteSmoke, + color: widget.streamChatTheme.colorTheme.whiteSmoke, child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -2307,21 +2288,21 @@ class __PickerWidgetState extends State<_PickerWidget> { 'svgs/icon_picture_empty_state.svg', package: 'stream_chat_flutter', height: 140, - color: chatThemeData.colorTheme.greyGainsboro, + color: widget.streamChatTheme.colorTheme.greyGainsboro, ), Text( // ignore: lines_longer_than_80_chars 'Please enable access to your photos \nand videos so you can share them with friends.', - style: chatThemeData.textTheme.body - .copyWith(color: chatThemeData.colorTheme.grey), + style: widget.streamChatTheme.textTheme.body.copyWith( + color: widget.streamChatTheme.colorTheme.grey), textAlign: TextAlign.center, ), const SizedBox(height: 6), Center( child: Text( 'Allow access to your gallery', - style: chatThemeData.textTheme.bodyBold.copyWith( - color: chatThemeData.colorTheme.accentBlue, + style: widget.streamChatTheme.textTheme.bodyBold.copyWith( + color: widget.streamChatTheme.colorTheme.accentBlue, ), ), ), diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index cb1f66b7..468f2d9f 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:math'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; import 'package:rxdart/rxdart.dart'; @@ -281,8 +282,10 @@ class _MessageListViewState extends State { void Function(Message)? _onThreadTap; bool _showScrollToBottom = false; late final ItemPositionsListener _itemPositionListener; + late final Stream> _itemPositionStream; int? _messageListLength; StreamChannelState? streamChannel; + late StreamChatThemeData _streamTheme; int? get _initialIndex { if (widget.initialScrollIndex != null) return widget.initialScrollIndex; @@ -324,37 +327,34 @@ class _MessageListViewState extends State { final MessageListController _messageListController = MessageListController(); @override - Widget build(BuildContext context) { - final chatThemeData = StreamChatTheme.of(context); - return MessageListCore( - messageFilter: widget.messageFilter, - loadingBuilder: widget.loadingBuilder ?? - (context) => const Center( - child: CircularProgressIndicator(), - ), - emptyBuilder: widget.emptyBuilder ?? - (context) => Center( - child: Text( - 'No chats here yet...', - style: chatThemeData.textTheme.footnote.copyWith( - color: chatThemeData.colorTheme.black.withOpacity(.5)), + Widget build(BuildContext context) => MessageListCore( + messageFilter: widget.messageFilter, + loadingBuilder: widget.loadingBuilder ?? + (context) => const Center( + child: CircularProgressIndicator(), ), - ), - messageListBuilder: - widget.messageListBuilder ?? (context, list) => _buildListView(list), - messageListController: _messageListController, - parentMessage: widget.parentMessage, - showScrollToBottom: widget.showScrollToBottom, - errorWidgetBuilder: widget.errorWidgetBuilder ?? - (BuildContext context, Object error) => Center( - child: Text( - 'Something went wrong', - style: chatThemeData.textTheme.footnote.copyWith( - color: chatThemeData.colorTheme.black.withOpacity(.5)), + emptyBuilder: widget.emptyBuilder ?? + (context) => Center( + child: Text( + 'No chats here yet...', + style: _streamTheme.textTheme.footnote.copyWith( + color: _streamTheme.colorTheme.black.withOpacity(.5)), + ), ), - ), - ); - } + messageListBuilder: widget.messageListBuilder ?? + (context, list) => _buildListView(list), + messageListController: _messageListController, + parentMessage: widget.parentMessage, + showScrollToBottom: widget.showScrollToBottom, + errorWidgetBuilder: widget.errorWidgetBuilder ?? + (BuildContext context, Object error) => Center( + child: Text( + 'Something went wrong', + style: _streamTheme.textTheme.footnote.copyWith( + color: _streamTheme.colorTheme.black.withOpacity(.5)), + ), + ), + ); Widget _buildListView(List data) { messages = data; @@ -400,8 +400,7 @@ class _MessageListViewState extends State { } return InfoTile( - // ignore: avoid_bool_literals_in_conditional_expressions - showMessage: widget.showConnectionStateTile ? showStatus : false, + showMessage: widget.showConnectionStateTile && showStatus, tileAnchor: Alignment.topCenter, childAnchor: Alignment.topCenter, message: statusString, @@ -448,10 +447,9 @@ class _MessageListViewState extends State { if (i == 0) return const SizedBox(height: 30); if (i == messages.length + 1) { final replyCount = widget.parentMessage!.replyCount; - final chatThemeData = StreamChatTheme.of(context); return Container( decoration: BoxDecoration( - gradient: chatThemeData.colorTheme.bgGradient, + gradient: _streamTheme.colorTheme.bgGradient, ), child: Padding( padding: const EdgeInsets.all(8), @@ -459,7 +457,7 @@ class _MessageListViewState extends State { // ignore: lines_longer_than_80_chars '$replyCount ${replyCount == 1 ? 'Reply' : 'Replies'}', textAlign: TextAlign.center, - style: chatThemeData + style: _streamTheme .channelTheme.channelHeaderTheme.subtitle, ), ), @@ -571,9 +569,18 @@ class _MessageListViewState extends State { if (widget.showScrollToBottom) _buildScrollToBottom(), Positioned( top: 20, - child: ValueListenableBuilder>( - valueListenable: _itemPositionListener.itemPositions, - builder: (context, values, _) { + child: BetterStreamBuilder>( + initialData: _itemPositionListener.itemPositions.value, + stream: _itemPositionStream, + comparator: (a, b) { + if (a == null) { + return false; + } + final aTop = _getTopElement(a)?.index; + final bTop = _getTopElement(b)?.index; + return aTop == bTop; + }, + builder: (context, values) { final items = _itemPositionListener.itemPositions.value; if (items.isEmpty || messages.isEmpty) { return const SizedBox(); @@ -640,7 +647,6 @@ class _MessageListViewState extends State { final showUnreadCount = unreadCount > 0 && streamChannel!.channel.state!.members.any((e) => e.userId == streamChannel!.channel.client.state.user!.id); - final chatThemeData = StreamChatTheme.of(context); return Positioned( bottom: 8, right: 8, @@ -650,7 +656,7 @@ class _MessageListViewState extends State { clipBehavior: Clip.none, children: [ FloatingActionButton( - backgroundColor: chatThemeData.colorTheme.white, + backgroundColor: _streamTheme.colorTheme.white, onPressed: () { if (unreadCount > 0) { streamChannel!.channel.markRead(); @@ -669,7 +675,7 @@ class _MessageListViewState extends State { } }, child: StreamSvgIcon.down( - color: chatThemeData.colorTheme.black, + color: _streamTheme.colorTheme.black, ), ), if (showUnreadCount) @@ -700,39 +706,13 @@ class _MessageListViewState extends State { Widget _buildLoadingIndicator( StreamChannelState streamChannel, QueryDirection direction, - ) { - final stream = direction == QueryDirection.top - ? streamChannel.queryTopMessages - : streamChannel.queryBottomMessages; - return BetterStreamBuilder( - key: Key('LOADING-INDICATOR $direction'), - stream: stream, - initialData: false, - errorBuilder: (context, error) => Container( - color: StreamChatTheme.of(context).colorTheme.accentRed.withOpacity(.2), - child: const Center( - child: Text('Error loading messages'), - ), - ), - builder: (context, snapshot) { - if (!snapshot) { - if (!_isThreadConversation && direction == QueryDirection.top) { - return const SizedBox( - height: 52, - width: double.infinity, - ); - } - return const Offstage(); - } - return const Center( - child: Padding( - padding: EdgeInsets.all(8), - child: CircularProgressIndicator(), - ), - ); - }, - ); - } + ) => + _LoadingIndicator( + direction: direction, + streamTheme: _streamTheme, + streamChannel: streamChannel, + isThreadConversation: _isThreadConversation, + ); Widget _buildTopMessage( BuildContext context, @@ -818,7 +798,6 @@ class _MessageListViewState extends State { final currentUserMember = members.firstWhere((e) => e.user!.id == currentUser!.id); - final chatThemeData = StreamChatTheme.of(context); return MessageWidget( showReplyMessage: false, showResendMessage: false, @@ -847,8 +826,8 @@ class _MessageListViewState extends State { borderSide: isMyMessage || isOnlyEmoji ? BorderSide.none : null, showUserAvatar: isMyMessage ? DisplayWidget.gone : DisplayWidget.show, messageTheme: isMyMessage - ? chatThemeData.ownMessageTheme - : chatThemeData.otherMessageTheme, + ? _streamTheme.ownMessageTheme + : _streamTheme.otherMessageTheme, onShowMessage: widget.onShowMessage, onReturnAction: (action) { switch (action) { @@ -962,7 +941,6 @@ class _MessageListViewState extends State { final currentUserMember = members.firstWhere((e) => e.user!.id == currentUser!.id); - final chatThemeData = StreamChatTheme.of(context); Widget child = MessageWidget( key: ValueKey('MESSAGE-${message.id}'), message: message, @@ -1049,8 +1027,8 @@ class _MessageListViewState extends State { horizontal: isOnlyEmoji ? 0 : 16.0, ), messageTheme: isMyMessage - ? chatThemeData.ownMessageTheme - : chatThemeData.otherMessageTheme, + ? _streamTheme.ownMessageTheme + : _streamTheme.otherMessageTheme, readList: readList, allRead: allRead, onShowMessage: widget.onShowMessage, @@ -1091,7 +1069,7 @@ class _MessageListViewState extends State { widget.onMessageSwiped?.call(message); }, backgroundIcon: StreamSvgIcon.reply( - color: chatThemeData.colorTheme.accentBlue, + color: _streamTheme.colorTheme.accentBlue, ), child: child, ), @@ -1101,7 +1079,7 @@ class _MessageListViewState extends State { if (!initialMessageHighlightComplete && widget.highlightInitialMessage && _isInitialMessage(message.id)) { - final colorTheme = chatThemeData.colorTheme; + final colorTheme = _streamTheme.colorTheme; final highlightColor = widget.messageHighlightColor ?? colorTheme.highlight; child = TweenAnimationBuilder( @@ -1131,6 +1109,8 @@ class _MessageListViewState extends State { _scrollController = widget.scrollController ?? ItemScrollController(); _itemPositionListener = widget.itemPositionListener ?? ItemPositionsListener.create(); + _itemPositionStream = + valueListenableToStreamAdapter(_itemPositionListener.itemPositions); _getOnThreadTap(); super.initState(); @@ -1139,6 +1119,7 @@ class _MessageListViewState extends State { @override void didChangeDependencies() { final newStreamChannel = StreamChannel.of(context); + _streamTheme = StreamChatTheme.of(context); if (newStreamChannel != streamChannel) { streamChannel = newStreamChannel; @@ -1209,3 +1190,78 @@ class _MessageListViewState extends State { super.dispose(); } } + +class _LoadingIndicator extends StatelessWidget { + const _LoadingIndicator({ + Key? key, + required this.streamTheme, + required this.isThreadConversation, + required this.direction, + required this.streamChannel, + }) : super(key: key); + + final StreamChatThemeData streamTheme; + final bool isThreadConversation; + final QueryDirection direction; + final StreamChannelState streamChannel; + + @override + Widget build(BuildContext context) { + final stream = direction == QueryDirection.top + ? streamChannel.queryTopMessages + : streamChannel.queryBottomMessages; + return BetterStreamBuilder( + key: Key('LOADING-INDICATOR $direction'), + stream: stream, + initialData: false, + errorBuilder: (context, error) => Container( + color: streamTheme.colorTheme.accentRed.withOpacity(.2), + child: const Center( + child: Text('Error loading messages'), + ), + ), + builder: (context, snapshot) { + if (!snapshot) { + if (!isThreadConversation && direction == QueryDirection.top) { + return const SizedBox( + height: 52, + width: double.infinity, + ); + } + return const Offstage(); + } + return const Center( + child: Padding( + padding: EdgeInsets.all(8), + child: CircularProgressIndicator(), + ), + ); + }, + ); + } +} + +Stream valueListenableToStreamAdapter(ValueListenable listenable) { + late StreamController controller; + + void listener() { + controller.add(listenable.value); + } + + void start() { + listenable.addListener(listener); + } + + void end() { + listenable.removeListener(listener); + } + + controller = StreamController( + onListen: start, + onPause: end, + onResume: start, + onCancel: end, + ); + + return controller.stream; +} diff --git a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart index 9d78e3d9..e9369e99 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -77,113 +77,111 @@ class MessageReactionsModal extends StatelessWidget { final divFactor = message.attachments.isNotEmpty == true ? 1 : (roughSentenceSize == 0 ? 1 : (roughSentenceSize / roughMaxSize)); + final hasFileAttachment = + message.attachments.any((it) => it.type == 'file') == true; - return TweenAnimationBuilder( - tween: Tween(begin: 0, end: 1), - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOutBack, - builder: (context, val, snapshot) { - final hasFileAttachment = - message.attachments.any((it) => it.type == 'file') == true; - return GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () => Navigator.maybePop(context), - child: Stack( - children: [ - Positioned.fill( - child: BackdropFilter( - filter: ImageFilter.blur( - sigmaX: 10, - sigmaY: 10, - ), - child: Container( - color: StreamChatTheme.of(context).colorTheme.overlay, + final child = Center( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (showReactions && + (message.status == MessageSendingStatus.sent)) + Align( + alignment: Alignment( + user!.id == message.user!.id + ? (divFactor >= 1.0 ? -0.2 : (1.2 - divFactor)) + : (divFactor >= 1.0 ? 0.2 : -(1.2 - divFactor)), + 0), + child: ReactionPicker( + message: message, ), ), - ), - Transform.scale( - scale: val, - child: Center( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(8), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (showReactions && - (message.status == MessageSendingStatus.sent)) - Align( - alignment: Alignment( - user!.id == message.user!.id - ? (divFactor >= 1.0 - ? -0.2 - : (1.2 - divFactor)) - : (divFactor >= 1.0 - ? 0.2 - : -(1.2 - divFactor)), - 0), - child: ReactionPicker( - message: message, - ), - ), - const SizedBox(height: 8), - IgnorePointer( - child: MessageWidget( - key: const Key('MessageWidget'), - reverse: reverse, - message: message.copyWith( - text: message.text!.length > 200 - ? '${message.text!.substring(0, 200)}...' - : message.text, - ), - messageTheme: messageTheme, - showReactions: false, - showUsername: false, - showUserAvatar: showUserAvatar, - showTimestamp: false, - translateUserAvatar: false, - showSendingIndicator: false, - shape: messageShape, - attachmentShape: attachmentShape, - padding: const EdgeInsets.all(0), - attachmentBorderRadiusGeometry: - attachmentBorderRadiusGeometry - ?.mirrorBorderIfReversed( - reverse: !reverse), - attachmentPadding: EdgeInsets.all( - hasFileAttachment ? 4 : 2, - ), - textPadding: EdgeInsets.symmetric( - vertical: 8, - horizontal: - message.text!.isOnlyEmoji ? 0 : 16.0, - ), - showReactionPickerIndicator: showReactions && - (message.status == MessageSendingStatus.sent), - textBuilder: textBuilder, - showPinHighlight: false, - ), - ), - if (message.latestReactions?.isNotEmpty == true) ...[ - const SizedBox(height: 8), - _buildReactionCard(context), - ] - ], - ), - ), + const SizedBox(height: 8), + IgnorePointer( + child: MessageWidget( + key: const Key('MessageWidget'), + reverse: reverse, + message: message.copyWith( + text: message.text!.length > 200 + ? '${message.text!.substring(0, 200)}...' + : message.text, ), + messageTheme: messageTheme, + showReactions: false, + showUsername: false, + showUserAvatar: showUserAvatar, + showTimestamp: false, + translateUserAvatar: false, + showSendingIndicator: false, + shape: messageShape, + attachmentShape: attachmentShape, + padding: const EdgeInsets.all(0), + attachmentBorderRadiusGeometry: attachmentBorderRadiusGeometry + ?.mirrorBorderIfReversed(reverse: !reverse), + attachmentPadding: EdgeInsets.all( + hasFileAttachment ? 4 : 2, + ), + textPadding: EdgeInsets.symmetric( + vertical: 8, + horizontal: message.text!.isOnlyEmoji ? 0 : 16.0, + ), + showReactionPickerIndicator: showReactions && + (message.status == MessageSendingStatus.sent), + textBuilder: textBuilder, + showPinHighlight: false, ), ), + if (message.latestReactions?.isNotEmpty == true) ...[ + const SizedBox(height: 8), + _buildReactionCard( + context, + user, + ), + ] ], ), - ); - }, + ), + ), + ); + + return GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () => Navigator.maybePop(context), + child: Stack( + children: [ + Positioned.fill( + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 10, + sigmaY: 10, + ), + child: DecoratedBox( + decoration: BoxDecoration( + color: StreamChatTheme.of(context).colorTheme.overlay, + ), + ), + ), + ), + TweenAnimationBuilder( + tween: Tween(begin: 0, end: 1), + duration: const Duration(milliseconds: 300), + curve: Curves.easeInOutBack, + builder: (context, val, widget) => Transform.scale( + scale: val, + child: widget, + ), + child: child, + ), + ], + ), ); } - Widget _buildReactionCard(BuildContext context) { - final currentUser = StreamChat.of(context).user; + Widget _buildReactionCard(BuildContext context, User? user) { final chatThemeData = StreamChatTheme.of(context); return Card( color: chatThemeData.colorTheme.white, @@ -210,7 +208,7 @@ class MessageReactionsModal extends StatelessWidget { children: message.latestReactions! .map((e) => _buildReaction( e, - currentUser!, + user!, context, )) .toList(), diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index f1b5428d..7fb092d6 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -413,37 +413,39 @@ class _MessageWidgetState extends State bool get showTimeStamp => widget.showTimestamp; - bool get isMessageRead => widget.readList?.isNotEmpty == true; + late final bool isMessageRead = widget.readList?.isNotEmpty == true; bool get showInChannel => widget.showInChannelIndicator; bool get hasQuotedMessage => widget.message.quotedMessage != null; - bool get isSendFailed => widget.message.status == MessageSendingStatus.failed; + late final bool isSendFailed = + widget.message.status == MessageSendingStatus.failed; - bool get isUpdateFailed => + late final bool isUpdateFailed = widget.message.status == MessageSendingStatus.failed_update; - bool get isDeleteFailed => + late final bool isDeleteFailed = widget.message.status == MessageSendingStatus.failed_delete; - bool get isFailedState => isSendFailed || isUpdateFailed || isDeleteFailed; + late final bool isFailedState = + isSendFailed || isUpdateFailed || isDeleteFailed; - bool get isGiphy => + late final bool isGiphy = widget.message.attachments.any((element) => element.type == 'giphy') == - true; + true; - bool get hasNonUrlAttachments => - widget.message.attachments + late final bool isOnlyEmoji = widget.message.text?.isOnlyEmoji == true; + + late final bool hasNonUrlAttachments = widget.message.attachments .where((it) => it.ogScrapeUrl == null) .isNotEmpty == true; - bool get hasUrlAttachments => + late final bool hasUrlAttachments = widget.message.attachments.any((it) => it.ogScrapeUrl != null) == true; - bool get showBottomRow => - showThreadReplyIndicator || + late final bool showBottomRow = showThreadReplyIndicator || showUsername || showTimeStamp || showInChannel || @@ -453,6 +455,9 @@ class _MessageWidgetState extends State @override bool get wantKeepAlive => widget.message.attachments.isNotEmpty == true; + late StreamChatThemeData _streamChatTheme; + late StreamChatState _streamChat; + @override Widget build(BuildContext context) { super.build(context); @@ -466,7 +471,7 @@ class _MessageWidgetState extends State ? MaterialType.card : MaterialType.transparency, color: widget.message.pinned && widget.showPinHighlight - ? StreamChatTheme.of(context).colorTheme.highlight + ? _streamChatTheme.colorTheme.highlight : null, child: Portal( child: InkWell( @@ -527,7 +532,8 @@ class _MessageWidgetState extends State transform: Matrix4.translationValues( widget.reverse ? 12 : -12, 0, 0), constraints: const BoxConstraints( - maxWidth: 22 * 6.0), + maxWidth: 22 * 6.0, + ), child: _buildReactionIndicator(context), ), portalAnchor: @@ -572,7 +578,7 @@ class _MessageWidgetState extends State ), ) : Card( - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, elevation: 0, margin: EdgeInsets.symmetric( horizontal: (isFailedState @@ -626,9 +632,8 @@ class _MessageWidgetState extends State top: -8, child: CustomPaint( painter: ReactionBubblePainter( - StreamChatTheme.of(context) - .colorTheme - .white, + _streamChatTheme + .colorTheme.white, Colors.transparent, Colors.transparent, tailCirclesSpace: 1, @@ -673,14 +678,20 @@ class _MessageWidgetState extends State ); } + @override + void didChangeDependencies() { + _streamChatTheme = StreamChatTheme.of(context); + _streamChat = StreamChat.of(context); + super.didChangeDependencies(); + } + Widget _buildQuotedMessage() { - final isMyMessage = - widget.message.user?.id == StreamChat.of(context).user?.id; + final isMyMessage = widget.message.user?.id == _streamChat.user?.id; final onTap = widget.message.quotedMessage?.isDeleted != true && widget.onQuotedMessageTap != null ? () => widget.onQuotedMessageTap!(widget.message.quotedMessageId) : null; - final chatThemeData = StreamChatTheme.of(context); + final chatThemeData = _streamChatTheme; return QuotedMessageWidget( onTap: onTap, message: widget.message.quotedMessage!, @@ -695,7 +706,7 @@ class _MessageWidgetState extends State Widget get _bottomRow { if (isDeleted) { - final chatThemeData = StreamChatTheme.of(context); + final chatThemeData = _streamChatTheme; return Row( mainAxisSize: MainAxisSize.min, children: [ @@ -849,36 +860,16 @@ class _MessageWidgetState extends State ); } - Widget _buildThreadParticipantsIndicator(Iterable threadParticipants) { - var padding = 0.0; - return Stack( - children: threadParticipants.map((user) { - padding += 8.0; - return Positioned( - right: padding - 8, - bottom: 0, - top: 0, - child: Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: StreamChatTheme.of(context).colorTheme.white, - ), - padding: const EdgeInsets.all(1), - child: UserAvatar( - user: user, - constraints: BoxConstraints.loose(const Size.fromRadius(7)), - showOnlineStatus: false, - ), - ), - ); - }).toList(), - ); - } + Widget _buildThreadParticipantsIndicator(Iterable threadParticipants) => + _ThreadParticipants( + streamChatTheme: _streamChatTheme, + threadParticipants: threadParticipants, + ); Widget _buildReactionIndicator( BuildContext context, ) { - final ownId = StreamChat.of(context).user!.id; + final ownId = _streamChat.user!.id; final reactionsMap = {}; widget.message.latestReactions?.forEach((element) { if (!reactionsMap.containsKey(element.type) || @@ -918,7 +909,7 @@ class _MessageWidgetState extends State showDialog( context: context, - barrierColor: StreamChatTheme.of(context).colorTheme.overlay, + barrierColor: _streamChatTheme.colorTheme.overlay, builder: (context) => StreamChannel( channel: channel, child: MessageActionsModal( @@ -969,7 +960,7 @@ class _MessageWidgetState extends State final channel = StreamChannel.of(context).channel; showDialog( context: context, - barrierColor: StreamChatTheme.of(context).colorTheme.overlay, + barrierColor: _streamChatTheme.colorTheme.overlay, builder: (context) => StreamChannel( channel: channel, child: MessageReactionsModal( @@ -1000,7 +991,7 @@ class _MessageWidgetState extends State side: hasFiles ? widget.attachmentBorderSide ?? BorderSide( - color: StreamChatTheme.of(context).colorTheme.greyWhisper, + color: _streamChatTheme.colorTheme.greyWhisper, ) : BorderSide.none, borderRadius: widget.attachmentBorderRadiusGeometry ?? BorderRadius.zero, @@ -1010,7 +1001,7 @@ class _MessageWidgetState extends State ShapeBorder _getDefaultShape(BuildContext context) => RoundedRectangleBorder( side: widget.borderSide ?? BorderSide( - color: StreamChatTheme.of(context).colorTheme.greyWhisper, + color: _streamChatTheme.colorTheme.greyWhisper, ), borderRadius: widget.borderRadiusGeometry ?? BorderRadius.zero, ); @@ -1101,7 +1092,7 @@ class _MessageWidgetState extends State Text( widget.readList!.length.toString(), style: style.copyWith( - color: StreamChatTheme.of(context).colorTheme.accentBlue, + color: _streamChatTheme.colorTheme.accentBlue, ), ), const SizedBox(width: 2), @@ -1158,7 +1149,7 @@ class _MessageWidgetState extends State Widget _buildPinnedMessage(Message message) { final pinnedBy = message.pinnedBy; - final pinnedByMe = StreamChat.of(context).user!.id == pinnedBy!.id; + final pinnedByMe = _streamChat.user!.id == pinnedBy!.id; return Padding( padding: const EdgeInsets.only(left: 8, right: 8, top: 4, bottom: 8), @@ -1174,7 +1165,7 @@ class _MessageWidgetState extends State Text( 'Pinned by ${pinnedByMe ? 'You' : pinnedBy.name}', style: TextStyle( - color: StreamChatTheme.of(context).colorTheme.grey, + color: _streamChatTheme.colorTheme.grey, fontSize: 13, fontWeight: FontWeight.w400, ), @@ -1184,9 +1175,7 @@ class _MessageWidgetState extends State ); } - bool get isOnlyEmoji => widget.message.text!.isOnlyEmoji; - - bool get isPinned => widget.message.pinned; + late final bool isPinned = widget.message.pinned; Color? _getBackgroundColor() { if (hasQuotedMessage) { @@ -1194,7 +1183,7 @@ class _MessageWidgetState extends State } if (hasUrlAttachments) { - return StreamChatTheme.of(context).colorTheme.blueAlice; + return _streamChatTheme.colorTheme.blueAlice; } if (isOnlyEmoji) { @@ -1226,6 +1215,45 @@ class _MessageWidgetState extends State } } +class _ThreadParticipants extends StatelessWidget { + const _ThreadParticipants({ + Key? key, + required StreamChatThemeData streamChatTheme, + required this.threadParticipants, + }) : _streamChatTheme = streamChatTheme, + super(key: key); + + final StreamChatThemeData _streamChatTheme; + final Iterable threadParticipants; + + @override + Widget build(BuildContext context) { + var padding = 0.0; + return Stack( + children: threadParticipants.map((user) { + padding += 8.0; + return Positioned( + right: padding - 8, + bottom: 0, + top: 0, + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: _streamChatTheme.colorTheme.white, + ), + padding: const EdgeInsets.all(1), + child: UserAvatar( + user: user, + constraints: BoxConstraints.loose(const Size.fromRadius(7)), + showOnlineStatus: false, + ), + ), + ); + }).toList(), + ); + } +} + class _ThreadReplyPainter extends CustomPainter { const _ThreadReplyPainter({ this.context, diff --git a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart index 07a820b9..43620640 100644 --- a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart @@ -1,9 +1,9 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:video_player/video_player.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; /// Widget builder for quoted message attachment thumnail typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function( @@ -217,7 +217,7 @@ class QuotedMessageWidget extends StatelessWidget { } child = AbsorbPointer(child: child); return Material( - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, type: MaterialType.transparency, shape: attachment.type == 'file' ? null : _getDefaultShape(context), child: child, diff --git a/packages/stream_chat_flutter/lib/src/reaction_picker.dart b/packages/stream_chat_flutter/lib/src/reaction_picker.dart index 8a89b8c3..2e47b933 100644 --- a/packages/stream_chat_flutter/lib/src/reaction_picker.dart +++ b/packages/stream_chat_flutter/lib/src/reaction_picker.dart @@ -1,5 +1,3 @@ -import 'dart:math'; - import 'package:ezanimation/ezanimation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/extension.dart'; @@ -50,95 +48,90 @@ class _ReactionPickerState extends State triggerAnimations(); } - return TweenAnimationBuilder( - tween: Tween(begin: 0, end: 1), - curve: Curves.easeInOutBack, - duration: const Duration(milliseconds: 500), - builder: (context, val, wid) => Transform.scale( - scale: val, - child: Material( - borderRadius: BorderRadius.circular(24), - color: chatThemeData.colorTheme.white, - clipBehavior: Clip.hardEdge, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: reactionIcons - .map((reactionIcon) { - final ownReactionIndex = widget.message.ownReactions - ?.indexWhere((reaction) => - reaction.type == reactionIcon.type) ?? - -1; - final index = reactionIcons.indexOf(reactionIcon); + final child = Material( + borderRadius: BorderRadius.circular(24), + color: chatThemeData.colorTheme.white, + clipBehavior: Clip.hardEdge, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 8, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: reactionIcons + .map((reactionIcon) { + final ownReactionIndex = widget.message.ownReactions + ?.indexWhere( + (reaction) => reaction.type == reactionIcon.type) ?? + -1; + final index = reactionIcons.indexOf(reactionIcon); - return ConstrainedBox( - constraints: const BoxConstraints.tightFor( - height: 24, - width: 24, - ), - child: RawMaterialButton( - elevation: 0, - shape: ContinuousRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - constraints: const BoxConstraints.tightFor( - height: 24, - width: 24, - ), - onPressed: () { - if (ownReactionIndex != -1) { - removeReaction( - context, - widget.message - .ownReactions![ownReactionIndex], - ); - } else { - sendReaction( - context, - reactionIcon.type, - ); - } - }, - child: AnimatedBuilder( - animation: animations[index], - builder: (context, val) => Transform.scale( - scale: animations[index].value, - child: StreamSvgIcon( - assetName: reactionIcon.assetName, - height: max( - 0, - animations[index].value * 24.0, - ), - width: max( - 0, - animations[index].value * 24.0, - ), - color: ownReactionIndex != -1 - ? chatThemeData - .colorTheme.accentBlue - : Theme.of(context) - .iconTheme - .color! - .withOpacity(.5), - ), - )), - ), - ); - }) - .insertBetween(const SizedBox( - width: 16, - )) - .toList(), + final child = StreamSvgIcon( + assetName: reactionIcon.assetName, + color: ownReactionIndex != -1 + ? chatThemeData.colorTheme.accentBlue + : Theme.of(context).iconTheme.color!.withOpacity(.5), + ); + + return ConstrainedBox( + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, ), - ), - ), - )); + child: RawMaterialButton( + elevation: 0, + shape: ContinuousRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + constraints: const BoxConstraints.tightFor( + height: 24, + width: 24, + ), + onPressed: () { + if (ownReactionIndex != -1) { + removeReaction( + context, + widget.message.ownReactions![ownReactionIndex], + ); + } else { + sendReaction( + context, + reactionIcon.type, + ); + } + }, + child: AnimatedBuilder( + animation: animations[index], + builder: (context, child) => Transform.scale( + scale: animations[index].value, + child: child, + ), + child: child, + ), + ), + ); + }) + .insertBetween(const SizedBox( + width: 16, + )) + .toList(), + ), + ), + ); + + return TweenAnimationBuilder( + tween: Tween(begin: 0, end: 1), + curve: Curves.easeInOutBack, + duration: const Duration(milliseconds: 500), + builder: (context, val, widget) => Transform.scale( + scale: val, + child: widget, + ), + child: child, + ); } void triggerAnimations() async { diff --git a/packages/stream_chat_flutter/lib/src/typing_indicator.dart b/packages/stream_chat_flutter/lib/src/typing_indicator.dart index 8f974d6e..5609c18b 100644 --- a/packages/stream_chat_flutter/lib/src/typing_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/typing_indicator.dart @@ -1,10 +1,9 @@ import 'package:flutter/material.dart'; import 'package:lottie/lottie.dart'; -import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Widget to show the current list of typing users -class TypingIndicator extends StatelessWidget { +class TypingIndicator extends StatefulWidget { /// Instantiate a new TypingIndicator const TypingIndicator({ Key? key, @@ -30,16 +29,21 @@ class TypingIndicator extends StatelessWidget { /// Alignment of the typing indicator final Alignment alignment; + @override + _TypingIndicatorState createState() => _TypingIndicatorState(); +} + +class _TypingIndicatorState extends State { @override Widget build(BuildContext context) { final channelState = - channel?.state ?? StreamChannel.of(context).channel.state!; + widget.channel?.state ?? StreamChannel.of(context).channel.state!; final altWidget = Align( key: const Key('alternative'), - alignment: alignment, + alignment: widget.alignment, child: Container( - child: alternativeWidget ?? const Offstage(), + child: widget.alternativeWidget ?? const Offstage(), ), ); return BetterStreamBuilder>( @@ -49,10 +53,11 @@ class TypingIndicator extends StatelessWidget { duration: const Duration(milliseconds: 300), child: snapshot.isNotEmpty == true ? Padding( - padding: padding, + key: const Key('main'), + padding: widget.padding, child: Align( key: const Key('typings'), - alignment: alignment, + alignment: widget.alignment, child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -65,7 +70,7 @@ class TypingIndicator extends StatelessWidget { // ignore: lines_longer_than_80_chars ' ${snapshot[0].name}${snapshot.length == 1 ? '' : ' and ${snapshot.length - 1} more'} ${snapshot.length == 1 ? 'is' : 'are'} typing', maxLines: 1, - style: style, + style: widget.style, ), ], ), diff --git a/packages/stream_chat_flutter/lib/src/url_attachment.dart b/packages/stream_chat_flutter/lib/src/url_attachment.dart index 995475a8..4944c73c 100644 --- a/packages/stream_chat_flutter/lib/src/url_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/url_attachment.dart @@ -40,7 +40,7 @@ class UrlAttachment extends StatelessWidget { children: [ if (urlAttachment.imageUrl != null) Container( - clipBehavior: Clip.antiAliasWithSaveLayer, + clipBehavior: Clip.hardEdge, margin: const EdgeInsets.symmetric(horizontal: 8), decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), diff --git a/packages/stream_chat_flutter/lib/src/utils.dart b/packages/stream_chat_flutter/lib/src/utils.dart index 644ea99f..e98112fc 100644 --- a/packages/stream_chat_flutter/lib/src/utils.dart +++ b/packages/stream_chat_flutter/lib/src/utils.dart @@ -334,7 +334,7 @@ Widget wrapAttachmentWidget( bool reverse, ) => Material( - clipBehavior: Clip.antiAlias, + clipBehavior: Clip.hardEdge, shape: attachmentShape, type: MaterialType.transparency, child: attachmentWidget, diff --git a/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart b/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart index bf50966b..9691618a 100644 --- a/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart +++ b/packages/stream_chat_flutter_core/lib/src/better_stream_builder.dart @@ -25,29 +25,31 @@ class BetterStreamBuilder extends StatefulWidget { } class _BetterStreamBuilderState extends State> { - Widget? _child; T? _lastEvent; StreamSubscription? _subscription; Object? _lastError; @override - Widget build(BuildContext context) => _child ?? const Offstage(); + Widget build(BuildContext context) { + if (_lastError != null) { + return widget.errorBuilder!(context, _lastError!); + } + + if (_lastEvent == null) { + return widget.loadingBuilder?.call(context) ?? const Offstage(); + } + return widget.builder(context, _lastEvent ?? widget.initialData); + } bool _firstTime = true; @override void didChangeDependencies() { if (_firstTime) { - if (widget.initialData == null && widget.loadingBuilder != null) { - _child = widget.loadingBuilder!(context); - } else { - _onEvent(widget.initialData); - } _lastEvent = widget.initialData; _subscription = widget.stream?.listen( _onEvent, onError: _onError, ); - _firstTime = false; } super.didChangeDependencies(); @@ -55,12 +57,6 @@ class _BetterStreamBuilderState extends State> { @override void didUpdateWidget(covariant BetterStreamBuilder oldWidget) { - if (_lastError != null && oldWidget.errorBuilder != widget.errorBuilder) { - _onError(_lastError); - } else if (oldWidget.builder != widget.builder) { - _onEvent(_lastEvent); - } - if (oldWidget.stream != widget.stream) { _subscription?.cancel(); _subscription = widget.stream?.listen( @@ -79,21 +75,22 @@ class _BetterStreamBuilderState extends State> { void _onError(error) { if (widget.errorBuilder != null && error != _lastError) { - setState(() { - _child = widget.errorBuilder!(context, error); - }); + if (mounted) { + setState(() {}); + } _lastError = error; } } - void _onEvent(event) { + void _onEvent(T event) { _lastError = null; - if (widget.comparator != null + final isEqual = widget.comparator != null ? widget.comparator!(_lastEvent, event) - : event != _lastEvent) { - setState(() { - _child = widget.builder(context, event); - }); + : event == _lastEvent; + if (!isEqual) { + if (mounted) { + setState(() {}); + } _lastEvent = event; } } diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart index 20304cdd..ad1f11ae 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart @@ -346,36 +346,40 @@ class StreamChannelState extends State { @override Widget build(BuildContext context) { - Widget child = FutureBuilder>( - future: Future.wait(_futures), - initialData: [ - channel.state != null, - if (initialMessageId != null) false, - ], - builder: (context, snapshot) { - if (snapshot.hasError) { - var message = snapshot.error.toString(); - if (snapshot.error is DioError) { - final dioError = snapshot.error as DioError?; - if (dioError?.type == DioErrorType.response) { - message = dioError!.message; - } else { - message = 'Check your connection and retry'; + var child = widget.child; + if (widget.showLoading && + (initialMessageId != null || channel.state == null)) { + child = FutureBuilder>( + future: Future.wait(_futures), + initialData: [ + channel.state != null, + if (initialMessageId != null) false, + ], + builder: (context, snapshot) { + if (snapshot.hasError) { + var message = snapshot.error.toString(); + if (snapshot.error is DioError) { + final dioError = snapshot.error as DioError?; + if (dioError?.type == DioErrorType.response) { + message = dioError!.message; + } else { + message = 'Check your connection and retry'; + } } + return Center(child: Text(message)); } - return Center(child: Text(message)); - } - final initialized = snapshot.data![0]; - // ignore: avoid_bool_literals_in_conditional_expressions - final dataLoaded = initialMessageId == null ? true : snapshot.data![1]; - if (widget.showLoading && (!initialized || !dataLoaded)) { - return const Center( - child: CircularProgressIndicator(), - ); - } - return widget.child; - }, - ); + final initialized = snapshot.data![0]; + final dataLoaded = initialMessageId == null || snapshot.data![1]; + if (!initialized || !dataLoaded) { + return const Center( + child: CircularProgressIndicator(), + ); + } + return widget.child; + }, + ); + } + if (initialMessageId != null) { child = Material(child: child); }