From 5b29057ccbc460d76d996416c6f2a7df36a4a327 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 11 May 2020 16:20:03 +0200 Subject: [PATCH] refactor message widget --- analysis_options.yaml | 122 +- example/lib/custom_message.dart | 7 +- example/lib/main.dart | 17 +- lib/src/attachment_title.dart | 62 +- lib/src/date_divider.dart | 2 +- lib/src/deleted_message.dart | 25 +- lib/src/image_attachment.dart | 4 +- lib/src/message_actions_bottom_sheet.dart | 209 +++ lib/src/message_input.dart | 1 - lib/src/message_list_view.dart | 275 ++-- lib/src/message_text.dart | 64 + lib/src/message_widget.dart | 1436 +++++++++------------ lib/src/reaction_picker.dart | 81 +- lib/src/reply_indicator.dart | 1 + lib/src/sending_indicator.dart | 57 +- lib/stream_chat_flutter.dart | 1 + pubspec.yaml | 9 +- 17 files changed, 1189 insertions(+), 1184 deletions(-) create mode 100644 lib/src/message_actions_bottom_sheet.dart create mode 100644 lib/src/message_text.dart diff --git a/analysis_options.yaml b/analysis_options.yaml index 7feb4342..fa8ecab2 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,61 +1,61 @@ -include: package:pedantic/analysis_options.yaml - -analyzer: - exclude: - - lib/**/*.g.dart - - example/* - -linter: - rules: - # these rules are documented on and in the same order as - # the Dart Lint rules page to make maintenance easier - # https://github.com/dart-lang/linter/blob/master/example/all.yaml - # - always_declare_return_types - # - always_specify_types - # - annotate_overrides - # - avoid_as - - avoid_empty_else - - avoid_init_to_null - - avoid_return_types_on_setters - - avoid_web_libraries_in_flutter - - await_only_futures - - camel_case_types - - cancel_subscriptions - - close_sinks - # - comment_references # we do not presume as to what people want to reference in their dartdocs - # - constant_identifier_names # https://github.com/dart-lang/linter/issues/204 - - control_flow_in_finally - - empty_constructor_bodies - - empty_statements - - hash_and_equals - - implementation_imports - # - invariant_booleans - # - iterable_contains_unrelated_type - - library_names - # - library_prefixes - # - list_remove_unrelated_type - # - literal_only_boolean_expressions - - non_constant_identifier_names - # - one_member_abstracts - # - only_throw_errors - # - overridden_fields -# - package_api_docs - - package_names - - package_prefixed_library_names - - prefer_is_not_empty - # - prefer_mixin # https://github.com/dart-lang/language/issues/32 - - public_member_api_docs - - slash_for_doc_comments - # - sort_constructors_first - # - sort_unnamed_constructors_first - # - super_goes_last # no longer needed w/ Dart 2 - - test_types_in_equals - - throw_in_finally - # - type_annotate_public_apis # subset of always_specify_types - - type_init_formals - # - unawaited_futures - - unnecessary_brace_in_string_interps - - unnecessary_getters_setters - - unnecessary_statements - - unrelated_type_equality_checks - - valid_regexps +#include: package:pedantic/analysis_options.yaml +# +#analyzer: +# exclude: +# - lib/**/*.g.dart +# - example/* +# +#linter: +# rules: +# # these rules are documented on and in the same order as +# # the Dart Lint rules page to make maintenance easier +# # https://github.com/dart-lang/linter/blob/master/example/all.yaml +# # - always_declare_return_types +# # - always_specify_types +# # - annotate_overrides +# # - avoid_as +# - avoid_empty_else +# - avoid_init_to_null +# - avoid_return_types_on_setters +# - avoid_web_libraries_in_flutter +# - await_only_futures +# - camel_case_types +# - cancel_subscriptions +# - close_sinks +# # - comment_references # we do not presume as to what people want to reference in their dartdocs +# # - constant_identifier_names # https://github.com/dart-lang/linter/issues/204 +# - control_flow_in_finally +# - empty_constructor_bodies +# - empty_statements +# - hash_and_equals +# - implementation_imports +# # - invariant_booleans +# # - iterable_contains_unrelated_type +# - library_names +# # - library_prefixes +# # - list_remove_unrelated_type +# # - literal_only_boolean_expressions +# - non_constant_identifier_names +# # - one_member_abstracts +# # - only_throw_errors +# # - overridden_fields +## - package_api_docs +# - package_names +# - package_prefixed_library_names +# - prefer_is_not_empty +# # - prefer_mixin # https://github.com/dart-lang/language/issues/32 +# - public_member_api_docs +# - slash_for_doc_comments +# # - sort_constructors_first +# # - sort_unnamed_constructors_first +# # - super_goes_last # no longer needed w/ Dart 2 +# - test_types_in_equals +# - throw_in_finally +# # - type_annotate_public_apis # subset of always_specify_types +# - type_init_formals +# # - unawaited_futures +# - unnecessary_brace_in_string_interps +# - unnecessary_getters_setters +# - unnecessary_statements +# - unrelated_type_equality_checks +# - valid_regexps diff --git a/example/lib/custom_message.dart b/example/lib/custom_message.dart index b084baa2..4f1aa2ca 100644 --- a/example/lib/custom_message.dart +++ b/example/lib/custom_message.dart @@ -90,7 +90,12 @@ class ChannelPage extends StatelessWidget { ); } - Widget _messageBuilder(context, message, index) { + Widget _messageBuilder( + BuildContext context, + MessageDetails details, + List messages, + ) { + final message = details.message; final isCurrentUser = StreamChat.of(context).user.id == message.user.id; final textAlign = isCurrentUser ? TextAlign.right : TextAlign.left; final color = isCurrentUser ? Colors.blueGrey : Colors.blue; diff --git a/example/lib/main.dart b/example/lib/main.dart index 4e997156..251af27e 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_apns/apns.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart' @@ -66,9 +67,11 @@ void _initNotifications(Client client) { void main() async { final client = Client( 's2dxdhpxd94g', - logLevel: Level.INFO, + logLevel: Level.SEVERE, showLocalNotification: Platform.isAndroid ? showLocalNotification : null, backgroundKeepAlive: Duration.zero, + persistenceEnabled: true, +// baseURL: 'chat-us-east-staging.stream-io-api.com', ); await client.setUser( @@ -108,11 +111,11 @@ class ChannelListPage extends StatelessWidget { return Scaffold( body: ChannelsBloc( child: ChannelListView( - filter: { - 'members': { - '\$in': [StreamChat.of(context).user.id], - } - }, +// filter: { +// 'members': { +// '\$in': [StreamChat.of(context).user.id], +// } +// }, sort: [SortOption('last_message_at')], pagination: PaginationParams( limit: 20, @@ -139,8 +142,6 @@ class ChannelPage extends StatelessWidget { child: Stack( children: [ MessageListView( - showOtherMessageUsername: true, - showVideoFullScreen: false, threadBuilder: (_, parentMessage) { return ThreadPage( parent: parentMessage, diff --git a/lib/src/attachment_title.dart b/lib/src/attachment_title.dart index d23c4bf8..4d30f7b2 100644 --- a/lib/src/attachment_title.dart +++ b/lib/src/attachment_title.dart @@ -22,43 +22,33 @@ class AttachmentTitle extends StatelessWidget { launchURL(context, attachment.titleLink); } }, - child: Container( - constraints: BoxConstraints.loose( - Size( - MediaQuery.of(context).size.width * 0.7, - 500, - ), - ), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - attachment.title, - overflow: TextOverflow.ellipsis, - style: messageTheme.messageText.copyWith( - color: StreamChatTheme.of(context).accentColor, - fontWeight: FontWeight.bold, - ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + attachment.title, + overflow: TextOverflow.ellipsis, + style: messageTheme.messageText.copyWith( + color: StreamChatTheme.of(context).accentColor, + fontWeight: FontWeight.bold, ), - if (attachment.titleLink != null || - attachment.ogScrapeUrl != null) - Text( - Uri.parse(attachment.titleLink ?? attachment.ogScrapeUrl) - .authority - .split('.') - .reversed - .take(2) - .toList() - .reversed - .join('.'), - overflow: TextOverflow.ellipsis, - style: messageTheme.createdAt, - ), - ], - ), + ), + if (attachment.titleLink != null || attachment.ogScrapeUrl != null) + Text( + Uri.parse(attachment.titleLink ?? attachment.ogScrapeUrl) + .authority + .split('.') + .reversed + .take(2) + .toList() + .reversed + .join('.'), + style: messageTheme.createdAt, + ), + ], ), ), ); diff --git a/lib/src/date_divider.dart b/lib/src/date_divider.dart index 1e00f2ae..970169b8 100644 --- a/lib/src/date_divider.dart +++ b/lib/src/date_divider.dart @@ -44,7 +44,7 @@ class DateDivider extends StatelessWidget { } return Padding( - padding: const EdgeInsets.only(top: 24.0), + padding: const EdgeInsets.symmetric(vertical: 12.0), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ diff --git a/lib/src/deleted_message.dart b/lib/src/deleted_message.dart index 6b63176d..a132f48d 100644 --- a/lib/src/deleted_message.dart +++ b/lib/src/deleted_message.dart @@ -5,30 +5,19 @@ class DeletedMessage extends StatelessWidget { const DeletedMessage({ Key key, @required this.messageTheme, - this.alignment, }) : super(key: key); final MessageTheme messageTheme; - final Alignment alignment; @override Widget build(BuildContext context) { - return Align( - alignment: alignment, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 14, - ), - child: Text( - 'This message was deleted...', - style: messageTheme.messageText.copyWith( - fontStyle: FontStyle.italic, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white - : Colors.black, - ), - ), + return Text( + 'This message was deleted...', + style: messageTheme.messageText.copyWith( + fontStyle: FontStyle.italic, + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white + : Colors.black, ), ); } diff --git a/lib/src/image_attachment.dart b/lib/src/image_attachment.dart index 9fb7f05c..eab4ada6 100644 --- a/lib/src/image_attachment.dart +++ b/lib/src/image_attachment.dart @@ -26,10 +26,9 @@ class ImageAttachment extends StatelessWidget { attachment: attachment, ); } - return Column( mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Stack( children: [ @@ -42,7 +41,6 @@ class ImageAttachment extends StatelessWidget { return GestureDetector( child: Image( image: provider, - width: MediaQuery.of(context).size.width * 0.7, fit: BoxFit.cover, ), onTap: () { diff --git a/lib/src/message_actions_bottom_sheet.dart b/lib/src/message_actions_bottom_sheet.dart new file mode 100644 index 00000000..1e81900f --- /dev/null +++ b/lib/src/message_actions_bottom_sheet.dart @@ -0,0 +1,209 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/reaction_picker.dart'; +import 'package:stream_chat_flutter/src/stream_channel.dart'; + +import '../stream_chat_flutter.dart'; +import 'message_input.dart'; +import 'stream_chat.dart'; + +class MessageActionsBottomSheet extends StatelessWidget { + final Widget Function(BuildContext, Message) editMessageInputBuilder; + final void Function(Message) onThreadTap; + final Message message; + final bool showReactions; + final bool showDeleteMessage; + final bool showEditMessage; + final bool showReply; + final Map reactionToEmoji = const { + 'love': '❤️️', + 'haha': '😂', + 'like': '👍', + 'sad': '😕', + 'angry': '😡', + 'wow': '😲', + }; + + const MessageActionsBottomSheet({ + Key key, + this.message, + this.showReactions, + this.showDeleteMessage, + this.showEditMessage, + this.onThreadTap, + this.showReply, + this.editMessageInputBuilder, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final channel = StreamChannel.of(context).channel; + return SafeArea( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (showReactions && + (message.status == MessageSendingStatus.SENT || + message.status == null)) + ReactionPicker( + channel: channel, + reactionToEmoji: reactionToEmoji, + message: message, + ), + if (showDeleteMessage) _buildDeleteButton(context), + if (showEditMessage) _buildEditMessage(context), + if (showReply && + (message.status == MessageSendingStatus.SENT || + message.status == null) && + message.parentId == null) + _buildReplyButton(context), + ], + ), + ); + } + + FlatButton _buildDeleteButton(BuildContext context) { + return FlatButton( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + 'Delete message', + style: + Theme.of(context).textTheme.headline5.copyWith(color: Colors.red), + ), + ), + onPressed: () { + Navigator.pop(context); + StreamChat.of(context).client.deleteMessage( + message, + StreamChannel.of(context).channel.cid, + ); + }, + ); + } + + FlatButton _buildEditMessage(BuildContext context) { + return FlatButton( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + 'Edit message', + style: Theme.of(context).textTheme.headline5, + ), + ), + onPressed: () async { + Navigator.pop(context); + _showEditBottomSheet(context); + }, + ); + } + + void _showEditBottomSheet(BuildContext context) { + final channel = StreamChannel.of(context).channel; + showModalBottomSheet( + context: context, + elevation: 2, + clipBehavior: Clip.hardEdge, + isScrollControlled: true, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + ), + builder: (context) { + return StreamChannel( + channel: channel, + child: Flex( + direction: Axis.vertical, + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.only( + top: 16.0, + left: 16.0, + right: 16.0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Edit message', + style: Theme.of(context).textTheme.headline6, + ), + Container( + height: 30, + padding: const EdgeInsets.all(2.0), + child: AspectRatio( + aspectRatio: 1, + child: RawMaterialButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(4), + ), + elevation: 0, + highlightElevation: 0, + focusElevation: 0, + disabledElevation: 0, + hoverElevation: 0, + onPressed: () { + Navigator.of(context).pop(); + }, + fillColor: + Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(.1) + : Colors.black.withOpacity(.1), + padding: EdgeInsets.all(4), + child: Icon( + Icons.close, + size: 15, + color: StreamChatTheme.of(context) + .primaryIconTheme + .color, + ), + ), + ), + ), + ], + ), + ), + Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, + ), + child: editMessageInputBuilder != null + ? editMessageInputBuilder(context, message) + : MessageInput( + editMessage: message, + onMessageSent: (_) { + FocusScope.of(context).unfocus(); + Navigator.pop(context); + }, + ), + ), + ], + ), + ); + }, + ); + } + + FlatButton _buildReplyButton(BuildContext context) { + return FlatButton( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Text( + 'Start a thread', + style: Theme.of(context).textTheme.headline5, + ), + ), + onPressed: () { + Navigator.pop(context); + if (onThreadTap != null) { + onThreadTap(message); + } + }, + ); + } +} diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index e4de6915..1c48070a 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -861,7 +861,6 @@ class MessageInputState extends State { Message message; if (widget.editMessage != null) { message = widget.editMessage.copyWith( - parentId: widget.parentMessage?.id, text: text, attachments: _getAttachments(attachments).toList(), mentionedUsers: diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index b7c4ef8b..da67bcc9 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -3,19 +3,46 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/message_widget.dart'; import 'package:visibility_detector/visibility_detector.dart'; import '../stream_chat_flutter.dart'; import 'date_divider.dart'; -import 'message_widget.dart'; import 'stream_channel.dart'; typedef MessageBuilder = Widget Function( - BuildContext, Message, List, int index); -typedef ParentMessageBuilder = Widget Function(BuildContext, Message); + BuildContext, + MessageDetails, + List, +); +typedef ParentMessageBuilder = Widget Function( + BuildContext, + Message, +); typedef ThreadBuilder = Widget Function(BuildContext context, Message parent); typedef ThreadTapCallback = void Function(Message, Widget); +class MessageDetails { + bool isMyMessage; + bool isLastUser; + bool isNextUser; + Message message; + int index; + + MessageDetails( + BuildContext context, + this.message, + List messages, + this.index, + ) { + isMyMessage = message.user.id == StreamChat.of(context).user.id; + isLastUser = index + 1 < messages.length && + message.user.id == messages[index + 1]?.user?.id; + isNextUser = + index - 1 >= 0 && message.user.id == messages[index - 1]?.user?.id; + } +} + /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_listview_paint.png) /// @@ -65,15 +92,7 @@ class MessageListView extends StatefulWidget { this.parentMessage, this.threadBuilder, this.onThreadTap, - this.showOtherMessageUsername = false, - this.showVideoFullScreen = true, - this.onMentionTap, - this.onUserAvatarTap, - this.onMessageActions, - this.attachmentBuilders, this.dateDividerBuilder, - this.showAvatar = true, - this.editMessageInputBuilder, this.scrollPhysics = const AlwaysScrollableScrollPhysics(), }) : super(key: key); @@ -93,38 +112,12 @@ class MessageListView extends StatefulWidget { /// Parent message in case of a thread final Message parentMessage; - /// If true show the other users username next to the timestamp of the message - final bool showOtherMessageUsername; - - /// True if the video player will allow fullscreen mode - final bool showVideoFullScreen; - - /// Function called on message mention tap - final void Function(User) onMentionTap; - - /// Function called on User Avatar tap - final void Function(User) onUserAvatarTap; - - /// Function called on message long press - final Function(BuildContext, Message) onMessageActions; - - /// Map that defines a builder for an attachment type - final Map attachmentBuilders; - /// Builder used to render date dividers final Widget Function(DateTime) dateDividerBuilder; - /// if true shows the user avatar - final bool showAvatar; - - - /// Builder used to build the message input to edit a message - final Widget Function(BuildContext, Message) editMessageInputBuilder; - /// The ScrollPhysics used by the ListView final ScrollPhysics scrollPhysics; - @override _MessageListViewState createState() => _MessageListViewState(); } @@ -171,24 +164,7 @@ class _MessageListViewState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - MessageWidget( - key: ValueKey( - 'PARENT-MESSAGE-${widget.parentMessage.id}'), - previousMessage: null, - message: widget.parentMessage, - nextMessage: null, - onThreadTap: _onThreadTap, - isParent: true, - showVideoFullScreen: widget.showVideoFullScreen, - showOtherMessageUsername: - widget.showOtherMessageUsername, - onMentionTap: widget.onMentionTap, - onUserAvatarTap: widget.onUserAvatarTap, - onMessageActions: widget.onMessageActions, - attachmentBuilders: widget.attachmentBuilders, - showAvatar: widget.showAvatar, - editMessageInputBuilder: widget.editMessageInputBuilder, - ), + buildParentMessage(widget.parentMessage), Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Container( @@ -212,50 +188,40 @@ class _MessageListViewState extends State { return _buildLoadingIndicator(streamChannel); } final message = _messages[i]; - - final previousMessage = - i < _messages.length - 1 ? _messages[i + 1] : null; final nextMessage = i > 0 ? _messages[i - 1] : null; Widget messageWidget; if (i == 0) { messageWidget = _buildBottomMessage( - streamChannel, - previousMessage, - message, context, + message, + _messages, + streamChannel, ); } else if (i == _messages.length - 1) { messageWidget = _buildTopMessage( - message, - nextMessage, - streamChannel, context, + message, + _messages, + streamChannel, ); } else { if (widget.messageBuilder != null) { messageWidget = Builder( key: ValueKey('MESSAGE-${message.id}'), - builder: (_) => - widget.messageBuilder(context, message, _messages, i), + builder: (_) => widget.messageBuilder( + context, + MessageDetails( + context, + message, + _messages, + i, + ), + _messages), ); } else { - messageWidget = MessageWidget( - key: ValueKey('MESSAGE-${message.id}'), - previousMessage: previousMessage, - message: message, - nextMessage: nextMessage, - onThreadTap: _onThreadTap, - showOtherMessageUsername: widget.showOtherMessageUsername, - showVideoFullScreen: widget.showVideoFullScreen, - onMentionTap: widget.onMentionTap, - onUserAvatarTap: widget.onUserAvatarTap, - onMessageActions: widget.onMessageActions, - attachmentBuilders: widget.attachmentBuilders, - showAvatar: widget.showAvatar, - editMessageInputBuilder: widget.editMessageInputBuilder, - ); + messageWidget = buildMessage(message, _messages, i); } } @@ -319,38 +285,28 @@ class _MessageListViewState extends State { } Widget _buildTopMessage( - Message message, - Message nextMessage, - StreamChannelState streamChannelState, BuildContext context, + Message message, + List messages, + StreamChannelState streamChannel, ) { Widget messageWidget; if (widget.messageBuilder != null) { messageWidget = Builder( - key: ValueKey('MESSAGE-${message.id}'), + key: ValueKey('TOP-MESSAGE'), builder: (_) => widget.messageBuilder( context, - message, + MessageDetails( + context, + message, + _messages, + _messages.length - 1, + ), _messages, - _messages.length - 1, ), ); } else { - messageWidget = MessageWidget( - key: ValueKey('MESSAGE-${message.id}'), - previousMessage: null, - message: message, - nextMessage: nextMessage, - onThreadTap: _onThreadTap, - showVideoFullScreen: widget.showVideoFullScreen, - showOtherMessageUsername: widget.showOtherMessageUsername, - onMentionTap: widget.onMentionTap, - onUserAvatarTap: widget.onUserAvatarTap, - onMessageActions: widget.onMessageActions, - attachmentBuilders: widget.attachmentBuilders, - showAvatar: widget.showAvatar, - editMessageInputBuilder: widget.editMessageInputBuilder, - ); + messageWidget = buildMessage(message, messages, _messages.length - 1); } return VisibilityDetector( @@ -359,7 +315,7 @@ class _MessageListViewState extends State { onVisibilityChanged: (visibility) { final topIsVisible = visibility.visibleBounds != Rect.zero; if (topIsVisible && !_topWasVisible) { - streamChannelState.queryMessages(); + streamChannel.queryMessages(); } _topWasVisible = topIsVisible; }, @@ -367,33 +323,28 @@ class _MessageListViewState extends State { } Widget _buildBottomMessage( - StreamChannelState streamChannel, - Message previousMessage, - Message message, BuildContext context, + Message message, + List messages, + StreamChannelState streamChannel, ) { Widget messageWidget; if (widget.messageBuilder != null) { messageWidget = Builder( - key: ValueKey('MESSAGE-${message.id}'), - builder: (_) => widget.messageBuilder(context, message, _messages, 0), + key: ValueKey('BOTTOM-MESSAGE'), + builder: (_) => widget.messageBuilder( + context, + MessageDetails( + context, + message, + _messages, + 0, + ), + _messages, + ), ); } else { - messageWidget = MessageWidget( - key: ValueKey('MESSAGE-${message.id}'), - previousMessage: previousMessage, - message: message, - nextMessage: null, - onThreadTap: _onThreadTap, - showVideoFullScreen: widget.showVideoFullScreen, - showOtherMessageUsername: widget.showOtherMessageUsername, - onMentionTap: widget.onMentionTap, - onUserAvatarTap: widget.onUserAvatarTap, - onMessageActions: widget.onMessageActions, - attachmentBuilders: widget.attachmentBuilders, - showAvatar: widget.showAvatar, - editMessageInputBuilder: widget.editMessageInputBuilder, - ); + messageWidget = buildMessage(message, messages, 0); } return VisibilityDetector( @@ -410,6 +361,84 @@ class _MessageListViewState extends State { ); } + Widget buildParentMessage( + Message message, + ) { + final isMyMessage = message.user.id == StreamChat.of(context).user.id; + + return MessageWidget( + showReplyIndicator: false, + message: message, + reverse: isMyMessage, + showUsername: !isMyMessage, + padding: EdgeInsets.only( + top: 8.0, + left: 8.0, + right: 8.0, + bottom: 16.0, + ), + showSendingIndicator: DisplayWidget.hide, + onThreadTap: _onThreadTap, + showEditMessage: false, + showDeleteMessage: false, + borderRadiusGeometry: BorderRadius.only( + topLeft: Radius.circular(16), + bottomLeft: Radius.circular(2), + topRight: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + borderSide: isMyMessage ? BorderSide.none : null, + showUserAvatar: DisplayWidget.show, + messageTheme: isMyMessage + ? StreamChatTheme.of(context).ownMessageTheme + : StreamChatTheme.of(context).otherMessageTheme, + ); + } + + Widget buildMessage( + Message message, + List messages, + int index, + ) { + final isMyMessage = message.user.id == StreamChat.of(context).user.id; + final isLastUser = index + 1 < messages.length && + message.user.id == messages[index + 1]?.user?.id; + final isNextUser = + index - 1 >= 0 && message.user.id == messages[index - 1]?.user?.id; + + return MessageWidget( + message: message, + reverse: isMyMessage, + showReactions: !message.isDeleted, + padding: EdgeInsets.only( + left: 8.0, + right: 8.0, + bottom: index == 0 ? 30 : (isLastUser ? 5 : 10), + ), + showUsername: !isMyMessage && !isNextUser, + showSendingIndicator: isMyMessage && + (index == 0 || message.status != MessageSendingStatus.SENT) + ? DisplayWidget.show + : DisplayWidget.hide, + showTimestamp: !isNextUser, + showEditMessage: isMyMessage, + showDeleteMessage: isMyMessage, + borderSide: isMyMessage ? BorderSide.none : null, + onThreadTap: _onThreadTap, + attachmentBorderRadiusGeometry: BorderRadius.circular(16), + borderRadiusGeometry: BorderRadius.only( + topLeft: Radius.circular(isLastUser ? 2 : 16), + bottomLeft: Radius.circular(2), + topRight: Radius.circular(16), + bottomRight: Radius.circular(16), + ), + showUserAvatar: isNextUser ? DisplayWidget.hide : DisplayWidget.show, + messageTheme: isMyMessage + ? StreamChatTheme.of(context).ownMessageTheme + : StreamChatTheme.of(context).otherMessageTheme, + ); + } + StreamSubscription _streamListener; @override diff --git a/lib/src/message_text.dart b/lib/src/message_text.dart new file mode 100644 index 00000000..f35b8549 --- /dev/null +++ b/lib/src/message_text.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:stream_chat/stream_chat.dart'; + +import 'stream_chat_theme.dart'; +import 'utils.dart'; + +class MessageText extends StatelessWidget { + const MessageText({ + Key key, + @required this.message, + @required this.messageTheme, + this.onMentionTap, + }) : super(key: key); + + final Message message; + final void Function(User) onMentionTap; + final MessageTheme messageTheme; + + @override + Widget build(BuildContext context) { + final text = _replaceMentions(message.text); + return MarkdownBody( + data: text, + onTapLink: (link) { + if (link.startsWith('@')) { + final mentionedUser = message.mentionedUsers.firstWhere( + (u) => '@${u.name.replaceAll(' ', '')}' == link, + orElse: () => null, + ); + + if (onMentionTap != null) { + onMentionTap(mentionedUser); + } else { + print('tap on ${mentionedUser.name}'); + } + } else { + launchURL(context, link); + } + }, + styleSheet: MarkdownStyleSheet.fromTheme( + Theme.of(context).copyWith( + textTheme: Theme.of(context).textTheme.apply( + bodyColor: messageTheme.messageText.color, + decoration: messageTheme.messageText.decoration, + decorationColor: messageTheme.messageText.decorationColor, + decorationStyle: messageTheme.messageText.decorationStyle, + fontFamily: messageTheme.messageText.fontFamily, + ), + ), + ).copyWith( + p: messageTheme.messageText, + ), + ); + } + + String _replaceMentions(String text) { + message.mentionedUsers?.forEach((u) { + text = text.replaceAll( + '@${u.name}', '[@${u.name}](@${u.name.replaceAll(' ', '')})'); + }); + return text; + } +} diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index 84100826..25c40cd1 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -1,822 +1,53 @@ import 'dart:math'; +import 'dart:ui'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; -import 'package:flutter_markdown/flutter_markdown.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter_portal/flutter_portal.dart'; import 'package:jiffy/jiffy.dart'; -import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_flutter/src/message_input.dart'; -import 'package:stream_chat_flutter/src/message_list_view.dart'; -import 'package:stream_chat_flutter/src/reaction_picker.dart'; -import 'package:stream_chat_flutter/src/reply_indicator.dart'; -import 'package:stream_chat_flutter/src/sending_indicator.dart'; -import 'package:stream_chat_flutter/src/stream_channel.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; -import 'package:stream_chat_flutter/src/user_avatar.dart'; -import 'package:stream_chat_flutter/src/utils.dart'; -import 'package:stream_chat_flutter/src/video_attachment.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'deleted_message.dart'; -import 'file_attachment.dart'; -import 'giphy_attachment.dart'; -import 'image_attachment.dart'; -import 'stream_chat.dart'; +import 'message_actions_bottom_sheet.dart'; +import 'message_text.dart'; typedef AttachmentBuilder = Widget Function(BuildContext, Message, Attachment); -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_widget.png) -/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/message_widget_paint.png) -/// -/// It shows a message with reactions, replies and user avatar. -/// -/// Usually you don't use this widget as it's the default message widget used by [MessageListView]. -/// -/// The widget components render the ui based on the first ancestor of type [StreamChatTheme]. -/// Modify it to change the widget appearance. -class MessageWidget extends StatefulWidget { - /// Instantiate a new MessageWidget - const MessageWidget({ - Key key, - @required this.previousMessage, - @required this.message, - @required this.nextMessage, - this.onThreadTap, - this.onUserAvatarTap, - this.onMessageActions, - this.isParent = false, - this.onMentionTap, - this.showOtherMessageUsername = false, - this.showVideoFullScreen = true, - this.attachmentBuilders, - this.showAvatar = true, - this.editMessageInputBuilder, - }) : super(key: key); - - /// Function called on mention tap - final void Function(User) onMentionTap; - - /// Function called on long press - final Function(BuildContext, Message) onMessageActions; - - /// If true show the other users username next to the timestamp of the message - final bool showOtherMessageUsername; - - /// This message - final Message message; - - /// The previous message - final Message previousMessage; - - /// The next message - final Message nextMessage; - - /// The function called when tapping on replies - final void Function(Message) onThreadTap; - - /// The function called when tapping on UserAvatar - final void Function(User) onUserAvatarTap; - - /// True if this is the parent of the thread being showed - final bool isParent; - - /// True if the video player will allow fullscreen mode - final bool showVideoFullScreen; - - /// Map that defines a builder for an attachment type - final Map attachmentBuilders; - - /// if true shows the user avatar - final bool showAvatar; - - /// Builder used to build the message input to edit a message - final Widget Function(BuildContext, Message) editMessageInputBuilder; - - @override - _MessageWidgetState createState() => _MessageWidgetState(); +enum DisplayWidget { + hide, + gone, + show, } -class _MessageWidgetState extends State - with AutomaticKeepAliveClientMixin, TickerProviderStateMixin { - MessageTheme _messageTheme; - StreamChatState _streamChat; - StreamChannelState _streamChannel; - bool _isMyMessage; - - String _currentUserId; - String _messageUserId; - String _previousUserId; - String _nextUserId; - bool _isLastUser; - bool _isNextUser; - - Map _attachmentBuilders; - - @override - Widget build(BuildContext context) { - super.build(context); - - _messageTheme = _isMyMessage - ? StreamChatTheme.of(context).ownMessageTheme - : StreamChatTheme.of(context).otherMessageTheme; - - final alignment = - _isMyMessage ? Alignment.centerRight : Alignment.centerLeft; - - var row = List.from([ - Column( - crossAxisAlignment: - _isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start, - children: [ - (widget.message.isDeleted && - widget.message.status != MessageSendingStatus.FAILED_DELETE) - ? _buildDeletedMessage(alignment) - : _buildBubble(context), - if (_streamChannel.channel.config?.replies == true) - _buildThreadIndicator(context), - if (!_isNextUser) _buildTimestamp(alignment), - ], - ), - _isNextUser - ? Container( - width: widget.showAvatar ? 40 : 8, - ) - : _buildUserAvatar(), - ]); - - if (!_isMyMessage) { - row = row.reversed.toList(); - } - - return Container( - padding: EdgeInsets.symmetric( - horizontal: (_isMyMessage && widget.nextMessage == null) ? 0.0 : 10, - ), - margin: EdgeInsets.only( - top: _isLastUser ? 5 : 24, - bottom: widget.nextMessage == null ? 30 : 0, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: - _isMyMessage ? MainAxisAlignment.end : MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: row, - ), - ); - } - - Padding _buildUserAvatar() { - return Padding( - padding: EdgeInsets.only( - left: _isMyMessage ? 8.0 : 0, - right: _isMyMessage ? 0 : 8.0, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (widget.showAvatar) - UserAvatar( - user: widget.message.user, - onTap: widget.onUserAvatarTap, - ), - if (_isMyMessage && widget.nextMessage == null) - SendingIndicator( - message: widget.message, - ), - ], - ), - ); - } - - @override - void initState() { - super.initState(); - - _streamChat = StreamChat.of(context); - _streamChannel = StreamChannel.of(context); - - _currentUserId = _streamChat.client.state.user.id; - _messageUserId = widget.message.user.id; - _previousUserId = widget.previousMessage?.user?.id; - _nextUserId = widget.nextMessage?.user?.id; - _isLastUser = _previousUserId == _messageUserId; - _isNextUser = _nextUserId == _messageUserId; - - _isMyMessage = _messageUserId == _currentUserId; - - _mergeAttachmentBuilders(); - } - - @override - void didUpdateWidget(MessageWidget oldWidget) { - super.didUpdateWidget(oldWidget); - - _mergeAttachmentBuilders(); - } - - void _mergeAttachmentBuilders() { - _attachmentBuilders = { - 'image': (context, message, attachment) { - return ImageAttachment( - attachment: attachment, - messageTheme: _messageTheme, - ); - }, - 'video': (context, message, attachment) { - return VideoAttachment( - enableFullScreen: widget.showVideoFullScreen, - attachment: attachment, - messageTheme: _messageTheme, - ); - }, - 'giphy': (context, message, attachment) { - return GiphyAttachment( - attachment: attachment, - messageTheme: _messageTheme, - message: message, - ); - }, - 'file': (context, message, attachment) { - return FileAttachment( - attachment: attachment, - ); - }, - }..addAll(widget.attachmentBuilders ?? {}); - } - - Widget _buildDeletedMessage(Alignment alignment) { - return DeletedMessage( - messageTheme: _messageTheme, - alignment: alignment, - ); - } - - Widget _buildThreadIndicator(BuildContext context) { - if (widget.message?.replyCount != null && widget.message.replyCount > 0) { - return ReplyIndicator( - onTap: !widget.isParent - ? () { - widget.onThreadTap(widget.message); - } - : null, - message: widget.message, - messageTheme: _messageTheme, - reversed: _isMyMessage, - ); - } - return SizedBox(); - } - - Widget _buildBubble( - BuildContext context, - ) { - var nOfAttachmentWidgets = 0; - - final column = - List.from(widget.message.attachments.map((attachment) { - nOfAttachmentWidgets++; - - Widget attachmentWidget; - final attachmentBuilder = _attachmentBuilders[attachment.type]; - if (attachmentBuilder == null) { - return SizedBox(); - } - - attachmentWidget = attachmentBuilder( - context, - widget.message, - attachment, - ); - - if (attachmentWidget != null) { - return _buildAttachment( - attachmentWidget, - attachment, - nOfAttachmentWidgets, - context, - ); - } - - nOfAttachmentWidgets--; - return SizedBox(); - }) ?? - []); - - if (widget.message.text.trim().isNotEmpty) { - var text = widget.message.text; - text = _replaceMentions(text); - - column.addAll( - [ - Column( - crossAxisAlignment: _isMyMessage - ? CrossAxisAlignment.end - : CrossAxisAlignment.start, - children: [ - if (_streamChannel.channel.config?.reactions == true && - nOfAttachmentWidgets == 0) - Align( - child: _buildReactions(), - alignment: _isMyMessage - ? Alignment.centerLeft - : Alignment.centerRight, - ), - Stack( - overflow: Overflow.visible, - children: [ - if (nOfAttachmentWidgets == 0 && - _streamChannel.channel.config?.reactions == true) - _buildReactionPaint(), - _buildMessageText(nOfAttachmentWidgets, text, context), - ], - ), - ], - ), - ], - ); - } - - if (_streamChannel.channel.config?.reactions == true && - nOfAttachmentWidgets > 0) { - column.insert( - 0, - Align( - child: _buildReactions(), - alignment: - _isMyMessage ? Alignment.centerLeft : Alignment.centerRight, - ), - ); - column[1] = Stack( - overflow: Overflow.visible, - children: [ - Padding( - padding: EdgeInsets.only( - right: _isMyMessage ? 0.0 : 8.0, - left: _isMyMessage ? 8.0 : 0.0, - ), - child: column[1], - ), - _buildReactionPaint(), - ], - ); - } - - return GestureDetector( - child: IntrinsicWidth( - child: Column( - children: column, - crossAxisAlignment: - _isMyMessage ? CrossAxisAlignment.end : CrossAxisAlignment.start, - ), - ), - onTap: () { - final channel = StreamChannel.of(context).channel; - if (widget.message.status == MessageSendingStatus.FAILED) { - channel.sendMessage(widget.message); - return; - } - if (widget.message.status == MessageSendingStatus.FAILED_UPDATE) { - StreamChat.of(context).client.updateMessage( - widget.message, - channel.cid, - ); - return; - } - - if (widget.message.status == MessageSendingStatus.FAILED_DELETE) { - StreamChat.of(context).client.deleteMessage( - widget.message, - channel.cid, - ); - return; - } - }, - onLongPress: () { - if (widget.message.isEphemeral || - widget.message.status == MessageSendingStatus.SENDING) { - return; - } - - if (widget.onMessageActions != null) { - widget.onMessageActions(context, widget.message); - } else { - _showMessageBottomSheet(context); - } - }, - ); - } - - Padding _buildAttachment( - Widget attachmentWidget, - Attachment attachment, - int nOfAttachmentWidgets, - BuildContext context, - ) { - final boxDecoration = _buildBoxDecoration(_isLastUser); - return Padding( - padding: const EdgeInsets.only(bottom: 2.0), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ClipRRect( - borderRadius: boxDecoration.borderRadius, - child: Container( - decoration: boxDecoration, - constraints: BoxConstraints.loose( - Size.fromWidth(MediaQuery.of(context).size.width * 0.7), - ), - child: attachmentWidget, - margin: EdgeInsets.only( - top: nOfAttachmentWidgets > 1 ? 5 : 0, - ), - ), - ), - ], - ), - ); - } - - Widget _buildSendingError(Widget child) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.message.status == MessageSendingStatus.FAILED) - Text( - 'MESSAGE FAILED · CLICK TO TRY AGAIN', - style: _messageTheme.messageText.copyWith( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(.5) - : Colors.black.withOpacity(.5), - fontSize: 11, - ), - ), - if (widget.message.status == MessageSendingStatus.FAILED_UPDATE) - Text( - 'MESSAGE UPDATE FAILED · CLICK TO TRY AGAIN', - style: _messageTheme.messageText.copyWith( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(.5) - : Colors.black.withOpacity(.5), - fontSize: 11, - ), - ), - if (widget.message.status == MessageSendingStatus.FAILED_DELETE) - Text( - 'MESSAGE DELETE FAILED · CLICK TO TRY AGAIN', - style: _messageTheme.messageText.copyWith( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(.5) - : Colors.black.withOpacity(.5), - fontSize: 11, - ), - ), - child, - ], - ); - } - - Widget _buildMessageText( - int nOfAttachmentWidgets, - String text, - BuildContext context, - ) { - return Padding( - padding: EdgeInsets.only( - right: _isMyMessage ? 0.0 : 8.0, - left: _isMyMessage ? 8.0 : 0.0, - ), - child: Container( - decoration: - _buildBoxDecoration(_isLastUser || nOfAttachmentWidgets > 0), - padding: EdgeInsets.all(10), - constraints: BoxConstraints.loose( - Size.fromWidth(MediaQuery.of(context).size.width * 0.7), - ), - child: _buildSendingError( - MarkdownBody( - data: text, - onTapLink: (link) { - if (link.startsWith('@')) { - final mentionedUser = widget.message.mentionedUsers.firstWhere( - (u) => '@${u.name.replaceAll(' ', '')}' == link, - orElse: () => null, - ); - - if (widget.onMentionTap != null) { - widget.onMentionTap(mentionedUser); - } else { - print('tap on ${mentionedUser.name}'); - } - } else { - launchURL(context, link); - } - }, - styleSheet: MarkdownStyleSheet.fromTheme( - Theme.of(context).copyWith( - textTheme: Theme.of(context).textTheme.apply( - bodyColor: _messageTheme.messageText.color, - decoration: _messageTheme.messageText.decoration, - decorationColor: - _messageTheme.messageText.decorationColor, - decorationStyle: - _messageTheme.messageText.decorationStyle, - fontFamily: _messageTheme.messageText.fontFamily, - ), - ), - ).copyWith( - p: _messageTheme.messageText, - ), - ), - ), - ), - ); - } - - String _replaceMentions(String text) { - widget.message.mentionedUsers?.forEach((u) { - text = text.replaceAll( - '@${u.name}', '[@${u.name}](@${u.name.replaceAll(' ', '')})'); - }); - return text; - } - - Widget _buildReactionPaint() { - return widget.message.reactionCounts?.isNotEmpty == true - ? Positioned( - left: _isMyMessage ? 4 : null, - right: !_isMyMessage ? 4 : null, - top: -6, - child: Transform( - transform: - !_isMyMessage ? Matrix4.rotationY(pi) : Matrix4.identity(), - alignment: Alignment.center, - child: CustomPaint( - painter: _ReactionBubblePainter( - Theme.of(context).brightness == Brightness.dark - ? Colors.white - : Colors.black, - ), - ), - ), - ) - : SizedBox(); - } - - void _showMessageBottomSheet(BuildContext context) { - if (!_streamChannel.channel.config.reactions && - !_streamChannel.channel.config.replies) { - return; - } - - final theme = Theme.of(context); - - showModalBottomSheet( - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(32), - topRight: Radius.circular(32), - ), - ), - context: context, - builder: (_) { - return SafeArea( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Container( - color: Colors.black87, - child: (_streamChannel.channel.config.reactions && - widget.message.status != MessageSendingStatus.FAILED) - ? ReactionPicker( - channel: StreamChannel.of(context).channel, - reactionToEmoji: reactionToEmoji, - message: widget.message, - ) - : SizedBox(), - ), - _isMyMessage - ? FlatButton( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - 'Delete message', - style: theme.textTheme.headline - .copyWith(color: Colors.red), - ), - ), - onPressed: () { - Navigator.pop(context); - StreamChat.of(context).client.deleteMessage( - widget.message, - _streamChannel.channel.cid, - ); - }, - ) - : SizedBox(), - _isMyMessage - ? FlatButton( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - 'Edit message', - style: theme.textTheme.headline, - ), - ), - onPressed: () async { - Navigator.pop(context); - - _showEditBottomSheet(context); - }, - ) - : SizedBox(), - (_streamChannel.channel.config.replies && - widget.message.status != MessageSendingStatus.FAILED && - widget.message.parentId == null && - !widget.isParent) - ? FlatButton( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Text( - 'Start a thread', - style: theme.textTheme.headline, - ), - ), - onPressed: () { - Navigator.pop(context); - widget.onThreadTap(widget.message); - }, - ) - : SizedBox(), - ], - ), - ); - }); - } - - void _showEditBottomSheet(BuildContext context) { - showModalBottomSheet( - context: context, - elevation: 2, - clipBehavior: Clip.hardEdge, - isScrollControlled: true, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(32), - topRight: Radius.circular(32), - ), - ), - builder: (context) { - return StreamChannel( - channel: _streamChannel.channel, - child: Flex( - direction: Axis.vertical, - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.only( - top: 16.0, - left: 16.0, - right: 16.0, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'Edit message', - style: Theme.of(context).textTheme.title, - ), - Container( - height: 30, - padding: const EdgeInsets.all(2.0), - child: AspectRatio( - aspectRatio: 1, - child: RawMaterialButton( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(4), - ), - elevation: 0, - highlightElevation: 0, - focusElevation: 0, - disabledElevation: 0, - hoverElevation: 0, - onPressed: () { - Navigator.of(context).pop(); - }, - fillColor: - Theme.of(context).brightness == Brightness.dark - ? Colors.white.withOpacity(.1) - : Colors.black.withOpacity(.1), - padding: EdgeInsets.all(4), - child: Icon( - Icons.close, - size: 15, - color: StreamChatTheme.of(context) - .primaryIconTheme - .color, - ), - ), - ), - ), - ], - ), - ), - Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewInsets.bottom, - ), - child: widget.editMessageInputBuilder != null - ? widget.editMessageInputBuilder(context, widget.message) - : MessageInput( - editMessage: widget.message, - parentMessage: widget.isParent - ? StreamChannel.of(context) - .channel - .state - .messages - .firstWhere((message) => - message.id == widget.message.parentId) - : null, - onMessageSent: (_) { - FocusScope.of(context).unfocus(); - Navigator.pop(context); - }, - ), - ), - ], - ), - ); - }, - ); - } - - Widget _buildReactions() { - return GestureDetector( - onTap: () { - if (widget.onMessageActions != null) { - widget.onMessageActions(context, widget.message); - } else { - _showMessageBottomSheet(context); - } - }, - child: Padding( - padding: EdgeInsets.symmetric( - vertical: widget.message.reactionCounts?.isNotEmpty == true ? 4.0 : 0, - ), - child: Container( - padding: widget.message.reactionCounts?.isNotEmpty == true - ? const EdgeInsets.all(8) - : EdgeInsets.zero, - decoration: BoxDecoration( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white - : Colors.black, - borderRadius: BorderRadius.all(Radius.circular(14))), - child: AnimatedSwitcher( - duration: Duration(milliseconds: 300), - reverseDuration: Duration(milliseconds: 0), - child: (widget.message.reactionCounts != null && - widget.message.reactionCounts.isNotEmpty) - ? _buildReactionRow() - : SizedBox(), - ), - ), - ), - ); - } - - Row _buildReactionRow() { - return Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - ...widget.message.reactionCounts.keys.map((reactionType) { - return Text( - reactionToEmoji[reactionType] ?? '?', - ); - }), - Padding( - padding: const EdgeInsets.only(left: 4.0), - child: Text( - widget.message.reactionCounts.values - .fold(0, (t, v) => v + t) - .toString(), - style: TextStyle( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.black - : Colors.white, - ), - ), - ), - ], - ); - } - +class MessageWidget extends StatelessWidget { + final void Function(User) onMentionTap; + final void Function(Message) onThreadTap; + final Widget Function(BuildContext, Message) editMessageInputBuilder; + final Widget Function(BuildContext, Message) textBuilder; + final void Function(BuildContext, Message) onMessageActions; + final Message message; + final MessageTheme messageTheme; + final bool reverse; + final ShapeBorder shape; + final ShapeBorder attachmentShape; + final BorderSide borderSide; + final BorderSide attachmentBorderSide; + final BorderRadiusGeometry borderRadiusGeometry; + final BorderRadiusGeometry attachmentBorderRadiusGeometry; + final EdgeInsetsGeometry padding; + final EdgeInsetsGeometry textPadding; + final EdgeInsetsGeometry attachmentPadding; + final DisplayWidget showUserAvatar; + final DisplayWidget showSendingIndicator; + final bool showReactions; + final bool showReplyIndicator; + final bool isParent; + final bool showUsername; + final bool showVideoFullScreen; + final bool showTimestamp; + final bool showDeleteMessage; + final bool showEditMessage; + final Map attachmentBuilders; final Map reactionToEmoji = { 'love': '❤️️', 'haha': '😂', @@ -826,67 +57,566 @@ class _MessageWidgetState extends State 'wow': '😲', }; - @override - void dispose() { - super.dispose(); - } + MessageWidget({ + Key key, + @required this.message, + @required this.messageTheme, + this.reverse = false, + this.shape, + this.attachmentShape, + this.borderSide, + this.attachmentBorderSide, + this.borderRadiusGeometry, + this.attachmentBorderRadiusGeometry, + this.onMentionTap, + this.showUserAvatar = DisplayWidget.show, + this.showSendingIndicator = DisplayWidget.show, + this.showReplyIndicator = true, + this.isParent = false, + this.onThreadTap, + this.showUsername = true, + this.showTimestamp = true, + this.showReactions = true, + this.showDeleteMessage = true, + this.showEditMessage = true, + this.onMessageActions, + this.editMessageInputBuilder, + this.textBuilder, + Map customAttachmentBuilders, + this.showVideoFullScreen = true, + this.padding, + this.textPadding = const EdgeInsets.all(8.0), + this.attachmentPadding = EdgeInsets.zero, + }) : attachmentBuilders = { + 'image': (context, message, attachment) { + return ImageAttachment( + attachment: attachment, + messageTheme: messageTheme, + ); + }, + 'video': (context, message, attachment) { + return VideoAttachment( + enableFullScreen: showVideoFullScreen, + attachment: attachment, + messageTheme: messageTheme, + ); + }, + 'giphy': (context, message, attachment) { + return GiphyAttachment( + attachment: attachment, + messageTheme: messageTheme, + message: message, + ); + }, + 'file': (context, message, attachment) { + return FileAttachment( + attachment: attachment, + ); + }, + }..addAll(customAttachmentBuilders ?? {}), + super(key: key); - Widget _buildTimestamp(Alignment alignment) { - return Padding( - padding: const EdgeInsets.only(top: 5.0), - child: RichText( - text: TextSpan( - style: _messageTheme.createdAt, - children: [ - if (!_isMyMessage && widget.showOtherMessageUsername) - TextSpan( - text: widget.message.user.name, - style: TextStyle(fontWeight: FontWeight.bold), + @override + Widget build(BuildContext context) { + final leftPadding = showUserAvatar != DisplayWidget.gone + ? messageTheme.avatarTheme.constraints.maxWidth + 22.0 + : 12.0; + return Portal( + child: Padding( + padding: padding ?? EdgeInsets.all(8), + child: Transform( + alignment: Alignment.center, + transform: Matrix4.rotationY(reverse ? pi : 0), + child: Container( + alignment: Alignment.centerLeft, + child: Container( + constraints: BoxConstraints.loose( + Size.fromWidth(MediaQuery.of(context).size.width * 0.8), ), - if (widget.message.createdAt != null) - TextSpan( - text: - Jiffy(widget.message.createdAt.toLocal()).format(' HH:mm'), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + IntrinsicWidth( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + if (showSendingIndicator == DisplayWidget.show) + _buildSendingIndicator(), + SizedBox( + width: 2, + ), + if (showSendingIndicator == DisplayWidget.hide) + SizedBox( + width: 6, + ), + if (showUserAvatar == DisplayWidget.show) + _buildUserAvatar(), + SizedBox( + width: 6, + ), + if (showUserAvatar == DisplayWidget.hide) + SizedBox( + width: messageTheme + .avatarTheme.constraints.maxWidth + + 10, + ), + Flexible( + child: Padding( + padding: (message.reactionCounts?.isNotEmpty == + true && + showReactions) + ? EdgeInsets.only( + top: _getReactionsTopPadding()) + : EdgeInsets.zero, + child: PortalEntry( + portalAnchor: Alignment(0, 1), + childAnchor: Alignment.topRight, + portal: _buildReactionIndicator(context), + child: (message.isDeleted && + message.status != + MessageSendingStatus + .FAILED_DELETE) + ? Transform( + alignment: Alignment.center, + transform: Matrix4.rotationY( + reverse ? pi : 0), + child: DeletedMessage( + messageTheme: messageTheme, + ), + ) + : Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + ..._parseAttachments(context), + if (message.text.trim().isNotEmpty) + _buildTextBubble(context), + ], + ), + ), + ), + ), + ], + ), + if (showReplyIndicator && message.replyCount > 0) + _buildReplyIndicator(leftPadding), + ], + ), + ), + if ((message.createdAt != null && showTimestamp) || + showUsername) + _buildUsernameAndTimestamp(leftPadding), + ], ), - ], + ), + ), ), ), ); } - BoxDecoration _buildBoxDecoration(bool rectBorders) { - return BoxDecoration( - border: _isMyMessage - ? null - : Border.all( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white.withAlpha(24) - : Colors.black.withAlpha(24), - ), - borderRadius: BorderRadius.only( - topLeft: Radius.circular((_isMyMessage || !rectBorders) ? 16 : 2), - bottomLeft: Radius.circular(_isMyMessage ? 16 : 2), - topRight: Radius.circular((_isMyMessage && rectBorders) ? 2 : 16), - bottomRight: Radius.circular(_isMyMessage ? 2 : 16), - ), - color: (widget.message.status == MessageSendingStatus.FAILED || - widget.message.status == MessageSendingStatus.FAILED_UPDATE || - widget.message.status == MessageSendingStatus.FAILED_DELETE) - ? Color(0xffd0021B).withOpacity(.1) - : _messageTheme.messageBackgroundColor, + double _getReactionsTopPadding() { + return 36.0 * + ((message.reactionCounts.values + .where((element) => element > 0) + .length ~/ + 4) + + 1); + } + + Widget _buildReactionsTail(BuildContext context) { + return AnimatedSwitcher( + duration: Duration(milliseconds: 300), + child: message.reactionCounts?.isNotEmpty == true + ? Transform.translate( + offset: Offset(4, 0), + child: CustomPaint( + painter: ReactionBubblePainter( + Theme.of(context).brightness == Brightness.dark + ? Colors.white + : Colors.black, + ), + ), + ) + : SizedBox(), ); } - @override - bool get wantKeepAlive { - return widget.message.attachments?.isNotEmpty == true; + Padding _buildUsernameAndTimestamp(double leftPadding) { + return Padding( + padding: EdgeInsets.only( + left: leftPadding, + top: 2, + ), + child: Transform( + alignment: Alignment.center, + transform: Matrix4.rotationY(reverse ? pi : 0), + child: RichText( + text: TextSpan( + style: messageTheme.createdAt, + children: [ + if (showUsername) + TextSpan( + text: message.user.name, + style: TextStyle(fontWeight: FontWeight.bold), + ), + if (message.createdAt != null && showTimestamp) + TextSpan( + text: Jiffy(message.createdAt.toLocal()).format(' HH:mm'), + ), + ], + ), + ), + ), + ); + } + + Widget _buildReactionIndicator(BuildContext context) { + return AnimatedSwitcher( + duration: Duration(milliseconds: 300), + child: (showReactions && + message.reactionCounts?.isNotEmpty == true && + !message.isDeleted) + ? Container( + child: GestureDetector( + onTap: () => onLongPress(context), + child: Container( + width: MediaQuery.of(context).size.width * 0.3, + padding: const EdgeInsets.only( + bottom: 4.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Transform( + transform: Matrix4.rotationY(reverse ? pi : 0), + alignment: Alignment.center, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: + Theme.of(context).brightness == Brightness.dark + ? Colors.white + : Colors.black, + borderRadius: BorderRadius.all(Radius.circular(14)), + ), + child: _buildReactionsText(context), + ), + ), + _buildReactionsTail(context), + ], + ), + ), + ), + ) + : SizedBox(), + ); + } + + Text _buildReactionsText(BuildContext context) { + return Text( + message.reactionCounts.keys.map((reactionType) { + return reactionToEmoji[reactionType] ?? '?'; + }).join(' ') + + ' ${message.reactionCounts.values.fold(0, (t, v) => v + t).toString()}', + style: TextStyle( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.black + : Colors.white, + ), + textAlign: TextAlign.justify, + ); + } + + void _showMessageBottomSheet(BuildContext context) { + final channel = StreamChannel.of(context).channel; + showModalBottomSheet( + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(32), + topRight: Radius.circular(32), + ), + ), + context: context, + builder: (context) { + return StreamChannel( + channel: channel, + child: MessageActionsBottomSheet( + showDeleteMessage: showDeleteMessage, + message: message, + editMessageInputBuilder: editMessageInputBuilder, + onThreadTap: onThreadTap, + showEditMessage: showEditMessage, + showReactions: showReactions, + showReply: showReplyIndicator, + ), + ); + }); + } + + List _parseAttachments(BuildContext context) { + return message.attachments?.map((attachment) { + final attachmentBuilder = attachmentBuilders[attachment.type]; + + if (attachmentBuilder == null) { + return SizedBox(); + } + + return Padding( + padding: EdgeInsets.only( + bottom: 4, + ), + child: GestureDetector( + onTap: () => retryMessage(context), + onLongPress: () => onLongPress(context), + child: Material( + color: _getBackgroundColor(), + clipBehavior: Clip.hardEdge, + shape: attachmentShape ?? + shape ?? + ContinuousRectangleBorder( + side: attachmentBorderSide ?? + borderSide ?? + BorderSide( + color: + Theme.of(context).brightness == Brightness.dark + ? Colors.white.withAlpha(24) + : Colors.black.withAlpha(24), + ), + borderRadius: attachmentBorderRadiusGeometry ?? + borderRadiusGeometry ?? + BorderRadius.zero, + ), + child: Padding( + padding: attachmentPadding, + child: Transform( + transform: Matrix4.rotationY(reverse ? pi : 0), + alignment: Alignment.center, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + getFailedMessageWidget( + context, + padding: const EdgeInsets.all(8.0), + ), + attachmentBuilder( + context, + message, + attachment, + ), + ], + ), + ), + ), + ), + ), + ); + })?.toList() ?? + []; + } + + void onLongPress(BuildContext context) { + if (message.isEphemeral || message.status == MessageSendingStatus.SENDING) { + return; + } + + if (onMessageActions != null) { + onMessageActions(context, message); + } else { + _showMessageBottomSheet(context); + } + return; + } + + Widget _buildReplyIndicator(double leftPadding) { + return Padding( + padding: EdgeInsets.only( + left: leftPadding, + ), + child: Transform( + transform: Matrix4.rotationY(reverse ? pi : 0), + alignment: Alignment.center, + child: ReplyIndicator( + message: message, + reversed: reverse, + messageTheme: messageTheme, + onTap: onThreadTap != null + ? () { + onThreadTap(message); + } + : null, + ), + ), + ); + } + + Widget _buildSendingIndicator() { + return Transform.translate( + offset: Offset( + 0, + 4, + ), + child: Transform( + transform: Matrix4.rotationY(reverse ? pi : 0), + alignment: Alignment.center, + child: SendingIndicator( + message: message, + ), + ), + ); + } + + Widget _buildUserAvatar() => Transform( + transform: Matrix4.rotationY(reverse ? pi : 0), + alignment: Alignment.center, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Transform.translate( + offset: + Offset(0, messageTheme.avatarTheme.constraints.maxHeight / 2), + child: UserAvatar( + user: message.user, + constraints: messageTheme.avatarTheme.constraints, + ), + ), + ), + ); + + Widget getFailedMessageWidget( + BuildContext context, { + EdgeInsetsGeometry padding, + }) { + Widget widget; + if (message.status == MessageSendingStatus.FAILED) + widget = Text( + 'MESSAGE FAILED · CLICK TO TRY AGAIN', + style: messageTheme.messageText.copyWith( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(.5) + : Colors.black.withOpacity(.5), + fontSize: 11, + ), + ); + if (message.status == MessageSendingStatus.FAILED_UPDATE) + widget = Text( + 'MESSAGE UPDATE FAILED · CLICK TO TRY AGAIN', + style: messageTheme.messageText.copyWith( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(.5) + : Colors.black.withOpacity(.5), + fontSize: 11, + ), + ); + if (message.status == MessageSendingStatus.FAILED_DELETE) + widget = Text( + 'MESSAGE DELETE FAILED · CLICK TO TRY AGAIN', + style: messageTheme.messageText.copyWith( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withOpacity(.5) + : Colors.black.withOpacity(.5), + fontSize: 11, + ), + ); + + if (widget != null) { + return Padding( + padding: padding ?? EdgeInsets.zero, + child: widget, + ); + } + + return SizedBox(); + } + + Widget _buildTextBubble(BuildContext context) { + return GestureDetector( + onTap: () => retryMessage(context), + onLongPress: () => onLongPress(context), + child: Material( + shape: shape ?? + ContinuousRectangleBorder( + side: borderSide ?? + BorderSide( + color: Theme.of(context).brightness == Brightness.dark + ? Colors.white.withAlpha(24) + : Colors.black.withAlpha(24), + ), + borderRadius: borderRadiusGeometry ?? BorderRadius.zero, + ), + color: _getBackgroundColor(), + child: Transform( + transform: Matrix4.rotationY(reverse ? pi : 0), + alignment: Alignment.center, + child: Padding( + padding: textPadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + getFailedMessageWidget(context), + _buildText(context), + ], + ), + ), + ), + ), + ); + } + + Color _getBackgroundColor() { + return (message.status == MessageSendingStatus.FAILED || + message.status == MessageSendingStatus.FAILED_UPDATE || + message.status == MessageSendingStatus.FAILED_DELETE) + ? Color(0xffd0021B).withOpacity(.1) + : messageTheme.messageBackgroundColor; + } + + void retryMessage(BuildContext context) { + final channel = StreamChannel.of(context).channel; + if (message.status == MessageSendingStatus.FAILED) { + channel.sendMessage(message); + return; + } + if (message.status == MessageSendingStatus.FAILED_UPDATE) { + StreamChat.of(context).client.updateMessage( + message, + channel.cid, + ); + return; + } + + if (message.status == MessageSendingStatus.FAILED_DELETE) { + StreamChat.of(context).client.deleteMessage( + message, + channel.cid, + ); + return; + } + } + + Widget _buildText(BuildContext context) { + return textBuilder != null + ? textBuilder(context, message) + : MessageText( + message: message, + onMentionTap: onMentionTap, + messageTheme: messageTheme, + ); } } -class _ReactionBubblePainter extends CustomPainter { +class ReactionBubblePainter extends CustomPainter { final Color color; - _ReactionBubblePainter(this.color); + ReactionBubblePainter(this.color); @override void paint(Canvas canvas, Size size) { diff --git a/lib/src/reaction_picker.dart b/lib/src/reaction_picker.dart index d24d4e18..56127476 100644 --- a/lib/src/reaction_picker.dart +++ b/lib/src/reaction_picker.dart @@ -24,47 +24,50 @@ class ReactionPicker extends StatelessWidget { @override Widget build(BuildContext context) { - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - mainAxisSize: MainAxisSize.min, - children: reactionToEmoji.keys.map((reactionType) { - final ownReactionIndex = message.ownReactions - ?.indexWhere((reaction) => reaction.type == reactionType) ?? - -1; - return Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - IconButton( - iconSize: size, - icon: Text( - reactionToEmoji[reactionType], - style: TextStyle( - fontSize: size - 10, + return Container( + color: Colors.black87, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: reactionToEmoji.keys.map((reactionType) { + final ownReactionIndex = message.ownReactions + ?.indexWhere((reaction) => reaction.type == reactionType) ?? + -1; + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + IconButton( + iconSize: size, + icon: Text( + reactionToEmoji[reactionType], + style: TextStyle( + fontSize: size - 10, + ), ), + onPressed: () { + if (ownReactionIndex != -1) { + removeReaction( + context, message.ownReactions[ownReactionIndex]); + } else { + sendReaction(context, reactionType); + } + }, ), - onPressed: () { - if (ownReactionIndex != -1) { - removeReaction( - context, message.ownReactions[ownReactionIndex]); - } else { - sendReaction(context, reactionType); - } - }, - ), - ownReactionIndex != -1 - ? Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Text( - message.ownReactions[ownReactionIndex].score.toString(), - style: TextStyle(color: Colors.white), - ), - ) - : SizedBox(), - ], - ); - }).toList(), + ownReactionIndex != -1 + ? Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Text( + message.ownReactions[ownReactionIndex].score.toString(), + style: TextStyle(color: Colors.white), + ), + ) + : SizedBox(), + ], + ); + }).toList(), + ), ); } diff --git a/lib/src/reply_indicator.dart b/lib/src/reply_indicator.dart index e7a8f3c5..cdb1cd57 100644 --- a/lib/src/reply_indicator.dart +++ b/lib/src/reply_indicator.dart @@ -47,6 +47,7 @@ class ReplyIndicator extends StatelessWidget { child: Padding( padding: const EdgeInsets.symmetric(vertical: 2.0), child: Row( + mainAxisSize: MainAxisSize.min, children: row, ), ), diff --git a/lib/src/sending_indicator.dart b/lib/src/sending_indicator.dart index 9c7ebb57..81e7dfef 100644 --- a/lib/src/sending_indicator.dart +++ b/lib/src/sending_indicator.dart @@ -13,53 +13,38 @@ class SendingIndicator extends StatelessWidget { @override Widget build(BuildContext context) { if (message.status == MessageSendingStatus.SENT || message.status == null) { - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 1.0, - ), - child: CircleAvatar( - radius: 4, - backgroundColor: StreamChatTheme.of(context).accentColor, - child: Icon( - Icons.done, - color: Colors.white, - size: 4, - ), + return CircleAvatar( + radius: 4, + backgroundColor: StreamChatTheme.of(context).accentColor, + child: Icon( + Icons.done, + color: Colors.white, + size: 4, ), ); } if (message.status == MessageSendingStatus.SENDING || message.status == MessageSendingStatus.UPDATING) { - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 1.0, - ), - child: CircleAvatar( - radius: 4, - backgroundColor: Colors.grey, - child: Icon( - Icons.access_time, - size: 4, - color: Colors.white, - ), + return CircleAvatar( + radius: 4, + backgroundColor: Colors.grey, + child: Icon( + Icons.access_time, + size: 4, + color: Colors.white, ), ); } if (message.status == MessageSendingStatus.FAILED || message.status == MessageSendingStatus.FAILED_UPDATE || message.status == MessageSendingStatus.FAILED_DELETE) { - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 1.0, - ), - child: CircleAvatar( - radius: 4, - backgroundColor: Color(0xffd0021B).withOpacity(.1), - child: Icon( - Icons.error_outline, - size: 4, - color: Colors.white, - ), + return CircleAvatar( + radius: 4, + backgroundColor: Color(0xffd0021B).withOpacity(.1), + child: Icon( + Icons.error_outline, + size: 4, + color: Colors.white, ), ); } diff --git a/lib/stream_chat_flutter.dart b/lib/stream_chat_flutter.dart index 09c67b6d..17b018a6 100644 --- a/lib/stream_chat_flutter.dart +++ b/lib/stream_chat_flutter.dart @@ -13,6 +13,7 @@ export 'src/giphy_attachment.dart'; export 'src/image_attachment.dart'; export 'src/message_input.dart'; export 'src/message_list_view.dart'; +export 'src/message_text.dart'; export 'src/message_widget.dart'; export 'src/reaction_picker.dart'; export 'src/reply_indicator.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index d7661473..06d6b00a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -12,18 +12,19 @@ dependencies: photo_view: ^0.9.2 rxdart: ^0.24.0 jiffy: ^3.0.1 - cached_network_image: ^2.2.0 + flutter_portal: ^0.1.0 + cached_network_image: ^2.2.0+1 flutter_markdown: ^0.3.5 url_launcher: ^5.4.5 - video_player: ^0.10.9+1 + video_player: ^0.10.10 chewie: ^0.9.10 file_picker: ^1.8.0+2 - image_picker: ^0.6.5+3 + image_picker: ^0.6.6+1 flutter_keyboard_visibility: ^2.0.0 stream_chat: path: ../stream_chat_dart mime: ^0.9.6+3 - visibility_detector: ^0.1.4 + visibility_detector: ^0.1.5 http_parser: ^3.1.4 dev_dependencies: