diff --git a/packages/flutter_widgets/example/ios/fastlane/report.xml b/packages/flutter_widgets/example/ios/fastlane/report.xml index d6fdf2d7..3a11e14a 100644 --- a/packages/flutter_widgets/example/ios/fastlane/report.xml +++ b/packages/flutter_widgets/example/ios/fastlane/report.xml @@ -5,39 +5,27 @@ - + - + - + - + - - - - - - - - - - - - - + diff --git a/packages/flutter_widgets/example/lib/advanced_options_page.dart b/packages/flutter_widgets/example/lib/advanced_options_page.dart index 7555f286..2da11406 100644 --- a/packages/flutter_widgets/example/lib/advanced_options_page.dart +++ b/packages/flutter_widgets/example/lib/advanced_options_page.dart @@ -1,13 +1,16 @@ import 'dart:io'; +import 'package:example/routes/app_routes.dart'; +import 'package:example/routes/routes.dart'; import 'package:example/stream_version.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:streaming_shared_preferences/streaming_shared_preferences.dart'; import 'choose_user_page.dart'; -import 'main.dart'; import 'notifications_service.dart'; class AdvancedOptionsPage extends StatefulWidget { @@ -312,17 +315,57 @@ class _AdvancedOptionsPageState extends State { context, MaterialPageRoute( builder: (context) { - return MaterialApp( - theme: ThemeData.light(), - darkTheme: ThemeData.dark(), - themeMode: ThemeMode.system, - builder: (context, widget) { - return StreamChat( - child: widget, - client: client, + return FutureBuilder( + future: StreamingSharedPreferences.instance, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return SizedBox(); + } + return PreferenceBuilder( + preference: snapshot.data.getInt( + 'theme', + defaultValue: 0, + ), + builder: (context, snapshot) => MaterialApp( + builder: (context, child) { + return StreamChat( + client: client, + child: Builder( + builder: (context) => + AnnotatedRegion< + SystemUiOverlayStyle>( + child: child, + value: SystemUiOverlayStyle( + systemNavigationBarColor: + StreamChatTheme.of(context) + .colorTheme + .white, + systemNavigationBarIconBrightness: + Theme.of(context) + .brightness == + Brightness.dark + ? Brightness.light + : Brightness.dark, + ), + ), + ), + ); + }, + debugShowCheckedModeBanner: false, + theme: ThemeData.light(), + darkTheme: ThemeData.dark(), + themeMode: { + -1: ThemeMode.dark, + 0: ThemeMode.system, + 1: ThemeMode.light, + }[snapshot], + onGenerateRoute: AppRoutes.generateRoute, + initialRoute: client.state.user == null + ? Routes.CHOOSE_USER + : Routes.HOME, + ), ); }, - home: ChannelListPage(), ); }, ), diff --git a/packages/flutter_widgets/example/lib/customize_message_widget.dart b/packages/flutter_widgets/example/lib/customize_message_widget.dart index a3713b69..84e65eab 100644 --- a/packages/flutter_widgets/example/lib/customize_message_widget.dart +++ b/packages/flutter_widgets/example/lib/customize_message_widget.dart @@ -120,7 +120,7 @@ class ChannelPage extends StatelessWidget { top: !details.isLastUser ? Radius.circular(16) : Radius.zero, bottom: !details.isNextUser ? Radius.circular(16) : Radius.zero, ), - showSendingIndicator: DisplayWidget.gone, + showSendingIndicator: false, reverse: false, showUserAvatar: details.isNextUser ? DisplayWidget.hide : DisplayWidget.show, diff --git a/packages/flutter_widgets/example/lib/group_info_screen.dart b/packages/flutter_widgets/example/lib/group_info_screen.dart index 985fe62d..ed5f2c61 100644 --- a/packages/flutter_widgets/example/lib/group_info_screen.dart +++ b/packages/flutter_widgets/example/lib/group_info_screen.dart @@ -169,12 +169,18 @@ class _GroupInfoScreenState extends State { } Widget _buildMembers(List members) { - int groupMemberListLength; + final groupMembers = members + ..sort((prev, curr) { + if (curr.role == 'owner') return 1; + return 0; + }); + + int groupMembersLength; if (listExpanded) { - groupMemberListLength = members.length; + groupMembersLength = groupMembers.length; } else { - groupMemberListLength = members.length > 6 ? 6 : members.length; + groupMembersLength = groupMembers.length > 6 ? 6 : groupMembers.length; } return Column( @@ -182,15 +188,15 @@ class _GroupInfoScreenState extends State { ListView.builder( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), - itemCount: groupMemberListLength, - itemBuilder: (context, position) { + itemCount: groupMembersLength, + itemBuilder: (context, index) { + final member = groupMembers[index]; return Material( child: InkWell( onTap: () { - var userMember = members.firstWhere( + var userMember = groupMembers.firstWhere( (e) => e.user.id == StreamChat.of(context).user.id); - _showUserInfoModal( - members[position].user, userMember.role == 'owner'); + _showUserInfoModal(member.user, userMember.role == 'owner'); }, child: Container( height: 65.0, @@ -202,7 +208,7 @@ class _GroupInfoScreenState extends State { padding: const EdgeInsets.symmetric( horizontal: 8.0, vertical: 12.0), child: UserAvatar( - user: members[position].user, + user: member.user, constraints: BoxConstraints( maxHeight: 40.0, maxWidth: 40.0), ), @@ -213,14 +219,14 @@ class _GroupInfoScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - members[position].user.name, + member.user.name, style: TextStyle(fontWeight: FontWeight.bold), ), SizedBox( height: 1.0, ), Text( - _getLastSeen(members[position].user), + _getLastSeen(member.user), style: TextStyle( color: StreamChatTheme.of(context) .colorTheme @@ -233,7 +239,7 @@ class _GroupInfoScreenState extends State { Padding( padding: const EdgeInsets.all(8.0), child: Text( - members[position].role == 'owner' ? 'Owner' : '', + member.role == 'owner' ? 'Owner' : '', style: TextStyle( color: StreamChatTheme.of(context) .colorTheme @@ -257,7 +263,7 @@ class _GroupInfoScreenState extends State { ); }, ), - if (groupMemberListLength != members.length) + if (groupMembersLength != groupMembers.length) InkWell( onTap: () { setState(() { @@ -287,7 +293,7 @@ class _GroupInfoScreenState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - '${members.length - groupMemberListLength} more', + '${members.length - groupMembersLength} more', style: TextStyle( color: StreamChatTheme.of(context) .colorTheme diff --git a/packages/flutter_widgets/example/lib/main.dart b/packages/flutter_widgets/example/lib/main.dart index 3f1112ba..98fa71a2 100644 --- a/packages/flutter_widgets/example/lib/main.dart +++ b/packages/flutter_widgets/example/lib/main.dart @@ -10,6 +10,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:streaming_shared_preferences/streaming_shared_preferences.dart'; import 'notifications_service.dart'; import 'routes/app_routes.dart'; @@ -52,32 +53,50 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( - builder: (context, child) { - return StreamChat( - client: client, - child: Builder( - builder: (context) => AnnotatedRegion( - child: child, - value: SystemUiOverlayStyle( - systemNavigationBarColor: - StreamChatTheme.of(context).colorTheme.white, - systemNavigationBarIconBrightness: - Theme.of(context).brightness == Brightness.dark - ? Brightness.light - : Brightness.dark, - ), - ), + return FutureBuilder( + future: StreamingSharedPreferences.instance, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return SizedBox(); + } + return PreferenceBuilder( + preference: snapshot.data.getInt( + 'theme', + defaultValue: 0, + ), + builder: (context, snapshot) => MaterialApp( + builder: (context, child) { + return StreamChat( + client: client, + child: Builder( + builder: (context) => AnnotatedRegion( + child: child, + value: SystemUiOverlayStyle( + systemNavigationBarColor: + StreamChatTheme.of(context).colorTheme.white, + systemNavigationBarIconBrightness: + Theme.of(context).brightness == Brightness.dark + ? Brightness.light + : Brightness.dark, + ), + ), + ), + ); + }, + debugShowCheckedModeBanner: false, + theme: ThemeData.light(), + darkTheme: ThemeData.dark(), + themeMode: { + -1: ThemeMode.dark, + 0: ThemeMode.system, + 1: ThemeMode.light, + }[snapshot], + onGenerateRoute: AppRoutes.generateRoute, + initialRoute: + client.state.user == null ? Routes.CHOOSE_USER : Routes.HOME, ), ); }, - debugShowCheckedModeBanner: false, - theme: ThemeData.light(), - darkTheme: ThemeData.dark(), - themeMode: ThemeMode.system, - onGenerateRoute: AppRoutes.generateRoute, - initialRoute: - client.state.user == null ? Routes.CHOOSE_USER : Routes.HOME, ); } } @@ -271,6 +290,21 @@ class _HomePageState extends State { fontSize: 14.5, ), ), + trailing: IconButton( + icon: StreamSvgIcon.Icon_moon( + size: 24, + ), + color: StreamChatTheme.of(context).colorTheme.grey, + onPressed: () async { + final sp = await StreamingSharedPreferences.instance; + sp.setInt( + 'theme', + Theme.of(context).brightness == Brightness.dark + ? 1 + : -1, + ); + }, + ), ), ), ), diff --git a/packages/flutter_widgets/example/pubspec.yaml b/packages/flutter_widgets/example/pubspec.yaml index 512ccadf..7053486c 100644 --- a/packages/flutter_widgets/example/pubspec.yaml +++ b/packages/flutter_widgets/example/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: flutter_secure_storage: ^3.3.5 yaml: ^2.2.1 uuid: ^2.2.2 + streaming_shared_preferences: ^1.0.2 dev_dependencies: flutter_test: diff --git a/packages/flutter_widgets/lib/src/back_button.dart b/packages/flutter_widgets/lib/src/back_button.dart index 52adeade..9d787517 100644 --- a/packages/flutter_widgets/lib/src/back_button.dart +++ b/packages/flutter_widgets/lib/src/back_button.dart @@ -9,11 +9,15 @@ class StreamBackButton extends StatelessWidget { Key key, this.onPressed, this.showUnreads = false, + this.cid, }) : super(key: key); final VoidCallback onPressed; final bool showUnreads; + /// Channel cid used to retrieve unread count + final String cid; + @override Widget build(BuildContext context) { return Stack( @@ -45,7 +49,9 @@ class StreamBackButton extends StatelessWidget { Positioned( top: 7, right: 7, - child: UnreadIndicator(), + child: UnreadIndicator( + cid: cid, + ), ), ], ); diff --git a/packages/flutter_widgets/lib/src/channel_media_display_screen.dart b/packages/flutter_widgets/lib/src/channel_media_display_screen.dart index cd5238bc..ebc95516 100644 --- a/packages/flutter_widgets/lib/src/channel_media_display_screen.dart +++ b/packages/flutter_widgets/lib/src/channel_media_display_screen.dart @@ -142,7 +142,9 @@ class _ChannelMediaDisplayScreenState extends State { for (var item in snapshot.data) { item.message.attachments - .where((e) => e.type == 'image' || e.type == 'video') + .where((e) => + (e.type == 'image' || e.type == 'video') && + e.ogScrapeUrl == null) .forEach((e) { VideoPlayerController controller; if (e.type == 'video') { diff --git a/packages/flutter_widgets/lib/src/date_divider.dart b/packages/flutter_widgets/lib/src/date_divider.dart index 12070e51..c7cfe296 100644 --- a/packages/flutter_widgets/lib/src/date_divider.dart +++ b/packages/flutter_widgets/lib/src/date_divider.dart @@ -5,10 +5,12 @@ import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; /// It shows a date divider depending on the date difference class DateDivider extends StatelessWidget { final DateTime dateTime; + final bool uppercase; const DateDivider({ Key key, @required this.dateTime, + this.uppercase = false, }) : super(key: key); @override @@ -18,38 +20,37 @@ class DateDivider extends StatelessWidget { String dayInfo; if (Jiffy(createdAt).isSame(now, Units.DAY)) { - dayInfo = 'TODAY'; + dayInfo = 'Today'; } else if (Jiffy(createdAt) .isSame(now.subtract(Duration(days: 1)), Units.DAY)) { - dayInfo = 'YESTERDAY'; + dayInfo = 'Yesterday'; } else if (Jiffy(createdAt).isAfter( now.subtract(Duration(days: 7)), Units.DAY, )) { - dayInfo = createdAt.format('EEEE').toUpperCase(); + dayInfo = createdAt.format('EEEE'); } else if (Jiffy(createdAt).isAfter( Jiffy(now).subtract(years: 1), Units.DAY, )) { - dayInfo = createdAt.format('MMMM d').toUpperCase(); + dayInfo = createdAt.format('MMMM d'); } else { - dayInfo = createdAt.format('MMMM d').toUpperCase(); + dayInfo = createdAt.format('MMMM d'); } + if (uppercase) dayInfo = dayInfo.toUpperCase(); + return Center( child: Container( - padding: const EdgeInsets.symmetric(horizontal: 8), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1), decoration: BoxDecoration( - color: StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), - borderRadius: BorderRadius.circular( - 8, - ), + color: StreamChatTheme.of(context).colorTheme.overlayDark, + borderRadius: BorderRadius.circular(8), ), child: Text( dayInfo, - style: StreamChatTheme.of(context).textTheme.footnoteBold.copyWith( + style: StreamChatTheme.of(context).textTheme.footnote.copyWith( color: StreamChatTheme.of(context).colorTheme.white, - fontWeight: FontWeight.bold, ), ), ), diff --git a/packages/flutter_widgets/lib/src/deleted_message.dart b/packages/flutter_widgets/lib/src/deleted_message.dart index 5921df43..bd014770 100644 --- a/packages/flutter_widgets/lib/src/deleted_message.dart +++ b/packages/flutter_widgets/lib/src/deleted_message.dart @@ -63,10 +63,7 @@ class DeletedMessage extends StatelessWidget { 'Message deleted', style: messageTheme.messageText.copyWith( fontStyle: FontStyle.italic, - color: (Theme.of(context).brightness == Brightness.dark - ? StreamChatTheme.of(context).colorTheme.white - : StreamChatTheme.of(context).colorTheme.black) - .withOpacity(.5), + color: messageTheme.createdAt.color, ), ), ), diff --git a/packages/flutter_widgets/lib/src/image_footer.dart b/packages/flutter_widgets/lib/src/image_footer.dart index 7ee9a128..e43eb3fb 100644 --- a/packages/flutter_widgets/lib/src/image_footer.dart +++ b/packages/flutter_widgets/lib/src/image_footer.dart @@ -116,7 +116,7 @@ class _ImageFooterState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ IconButton( - icon: StreamSvgIcon.icon_SHARE( + icon: StreamSvgIcon.Icon_SHARE( size: 24.0, color: StreamChatTheme.of(context).colorTheme.black, ), diff --git a/packages/flutter_widgets/lib/src/message_actions_modal.dart b/packages/flutter_widgets/lib/src/message_actions_modal.dart index c7ff56e0..9896edc4 100644 --- a/packages/flutter_widgets/lib/src/message_actions_modal.dart +++ b/packages/flutter_widgets/lib/src/message_actions_modal.dart @@ -24,8 +24,8 @@ class MessageActionsModal extends StatelessWidget { final bool showCopyMessage; final bool showEditMessage; final bool showResendMessage; - final bool showReply; - final bool showThreadReply; + final bool showReplyMessage; + final bool showThreadReplyMessage; final bool reverse; final ShapeBorder messageShape; final DisplayWidget showUserAvatar; @@ -40,9 +40,9 @@ class MessageActionsModal extends StatelessWidget { this.onReplyTap, this.onThreadReplyTap, this.showCopyMessage = true, - this.showReply = true, + this.showReplyMessage = true, this.showResendMessage = true, - this.showThreadReply = true, + this.showThreadReplyMessage = true, this.showUserAvatar = DisplayWidget.show, this.editMessageInputBuilder, this.messageShape, @@ -92,8 +92,9 @@ class MessageActionsModal extends StatelessWidget { child: Padding( padding: const EdgeInsets.symmetric(vertical: 8.0), child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, + crossAxisAlignment: reverse + ? CrossAxisAlignment.end + : CrossAxisAlignment.start, children: [ if (showReactions && (message.status == MessageSendingStatus.SENT || @@ -128,7 +129,7 @@ class MessageActionsModal extends StatelessWidget { showReactions: false, showUsername: false, showThreadReplyIndicator: false, - showReplyIndicator: false, + showReplyMessage: false, showUserAvatar: showUserAvatar, showTimestamp: false, translateUserAvatar: false, @@ -137,13 +138,12 @@ class MessageActionsModal extends StatelessWidget { MessageSendingStatus.SENT || message.status == null), showInChannelIndicator: false, - showSendingIndicator: DisplayWidget.gone, + showSendingIndicator: false, shape: messageShape, ), ), ); }), - SizedBox(height: 8), TweenAnimationBuilder( tween: Tween(begin: 0.0, end: 1.0), duration: Duration(milliseconds: 300), @@ -153,47 +153,56 @@ class MessageActionsModal extends StatelessWidget { transform: Matrix4.identity() ..scale(val) ..rotateZ(-1.0 + val), - alignment: Alignment.topRight, + alignment: reverse + ? Alignment.topRight + : Alignment.topLeft, child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 48.0, + padding: EdgeInsets.only( + right: reverse ? 16 : 0, + left: reverse ? 0 : 48, ), - child: Material( - color: StreamChatTheme.of(context) - .colorTheme - .whiteSnow, - clipBehavior: Clip.hardEdge, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - ), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.stretch, - children: ListTile.divideTiles( - context: context, - tiles: [ - if (showReply && - (message.status == - MessageSendingStatus.SENT || - message.status == null) && - message.parentId == null) - _buildReplyButton(context), - if (showThreadReply && - (message.status == - MessageSendingStatus.SENT || - message.status == null) && - message.parentId == null) - _buildThreadReplyButton(context), - if (showResendMessage) - _buildResendMessage(context), - if (showEditMessage) - _buildEditMessage(context), - if (showDeleteMessage) - _buildDeleteButton(context), - if (showCopyMessage) - _buildCopyButton(context), - ], - ).toList(), + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.75, + child: Material( + color: StreamChatTheme.of(context) + .colorTheme + .whiteSnow, + clipBehavior: Clip.hardEdge, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.stretch, + children: ListTile.divideTiles( + color: StreamChatTheme.of(context) + .colorTheme + .greyWhisper, + context: context, + tiles: [ + if (showReplyMessage && + (message.status == + MessageSendingStatus.SENT || + message.status == null) && + message.parentId == null) + _buildReplyButton(context), + if (showThreadReplyMessage && + (message.status == + MessageSendingStatus.SENT || + message.status == null) && + message.parentId == null) + _buildThreadReplyButton(context), + if (showResendMessage) + _buildResendMessage(context), + if (showEditMessage) + _buildEditMessage(context), + if (showCopyMessage) + _buildCopyButton(context), + if (showDeleteMessage) + _buildDeleteButton(context), + ], + ).toList(), + ), ), ), ), @@ -211,12 +220,18 @@ class MessageActionsModal extends StatelessWidget { Widget _buildReplyButton(BuildContext context) { return ListTile( - title: Text( - 'Reply', - style: Theme.of(context).textTheme.headline6, - ), - leading: StreamSvgIcon.reply( - color: StreamChatTheme.of(context).primaryIconTheme.color, + dense: true, + title: Row( + children: [ + StreamSvgIcon.reply( + color: StreamChatTheme.of(context).primaryIconTheme.color, + ), + const SizedBox(width: 16), + Text( + 'Reply', + style: StreamChatTheme.of(context).textTheme.headline, + ), + ], ), onTap: () { Navigator.pop(context); @@ -230,13 +245,21 @@ class MessageActionsModal extends StatelessWidget { Widget _buildDeleteButton(BuildContext context) { final isDeleteFailed = message.status == MessageSendingStatus.FAILED_DELETE; return ListTile( - title: Text( - isDeleteFailed ? 'Retry deleting message' : 'Delete message', - style: - Theme.of(context).textTheme.headline6.copyWith(color: Colors.red), - ), - leading: StreamSvgIcon.delete( - color: Colors.red, + dense: true, + title: Row( + children: [ + StreamSvgIcon.delete( + color: Colors.red, + ), + const SizedBox(width: 16), + Text( + isDeleteFailed ? 'Retry Deleting Message' : 'Delete Message', + style: StreamChatTheme.of(context) + .textTheme + .headline + .copyWith(color: Colors.red), + ), + ], ), onTap: () { Navigator.pop(context); @@ -250,12 +273,19 @@ class MessageActionsModal extends StatelessWidget { Widget _buildCopyButton(BuildContext context) { return ListTile( - title: Text( - 'Copy message', - style: Theme.of(context).textTheme.headline6, - ), - leading: StreamSvgIcon.copy( - color: StreamChatTheme.of(context).primaryIconTheme.color, + dense: true, + title: Row( + children: [ + StreamSvgIcon.copy( + size: 24, + color: StreamChatTheme.of(context).primaryIconTheme.color, + ), + const SizedBox(width: 16), + Text( + 'Copy Message', + style: StreamChatTheme.of(context).textTheme.headline, + ), + ], ), onTap: () async { await Clipboard.setData(ClipboardData(text: message.text)); @@ -266,12 +296,18 @@ class MessageActionsModal extends StatelessWidget { Widget _buildEditMessage(BuildContext context) { return ListTile( - title: Text( - 'Edit message', - style: Theme.of(context).textTheme.headline6, - ), - leading: StreamSvgIcon.edit( - color: StreamChatTheme.of(context).primaryIconTheme.color, + dense: true, + title: Row( + children: [ + StreamSvgIcon.edit( + color: StreamChatTheme.of(context).primaryIconTheme.color, + ), + const SizedBox(width: 16), + Text( + 'Edit Message', + style: StreamChatTheme.of(context).textTheme.headline, + ), + ], ), onTap: () async { Navigator.pop(context); @@ -283,12 +319,18 @@ class MessageActionsModal extends StatelessWidget { Widget _buildResendMessage(BuildContext context) { final isUpdateFailed = message.status == MessageSendingStatus.FAILED_UPDATE; return ListTile( - title: Text( - isUpdateFailed ? 'Resend edited message' : 'Resend', - style: Theme.of(context).textTheme.headline6, - ), - leading: StreamSvgIcon.circle_up( - color: StreamChatTheme.of(context).colorTheme.accentBlue, + dense: true, + title: Row( + children: [ + StreamSvgIcon.circle_up( + color: StreamChatTheme.of(context).colorTheme.accentBlue, + ), + const SizedBox(width: 16), + Text( + isUpdateFailed ? 'Resend Edited Message' : 'Resend', + style: StreamChatTheme.of(context).textTheme.headline, + ), + ], ), onTap: () { Navigator.pop(context); @@ -374,12 +416,18 @@ class MessageActionsModal extends StatelessWidget { Widget _buildThreadReplyButton(BuildContext context) { return ListTile( - title: Text( - 'Thread reply', - style: Theme.of(context).textTheme.headline6, - ), - leading: StreamSvgIcon.thread( - color: StreamChatTheme.of(context).primaryIconTheme.color, + dense: true, + title: Row( + children: [ + StreamSvgIcon.thread( + color: StreamChatTheme.of(context).primaryIconTheme.color, + ), + const SizedBox(width: 16), + Text( + 'Thread Reply', + style: StreamChatTheme.of(context).textTheme.headline, + ), + ], ), onTap: () { Navigator.pop(context); diff --git a/packages/flutter_widgets/lib/src/message_list_view.dart b/packages/flutter_widgets/lib/src/message_list_view.dart index 9f4cbc0d..cda0d64c 100644 --- a/packages/flutter_widgets/lib/src/message_list_view.dart +++ b/packages/flutter_widgets/lib/src/message_list_view.dart @@ -187,7 +187,6 @@ class MessageListView extends StatefulWidget { class _MessageListViewState extends State { ItemScrollController _scrollController; - bool _bottomWasVisible = false; Function _onThreadTap; bool _showScrollToBottom = false; ItemPositionsListener _itemPositionListener; @@ -220,6 +219,8 @@ class _MessageListViewState extends State { bool get _upToDate => streamChannel.channel.state.isUpToDate; + bool get _isThreadConversation => widget.parentMessage != null; + bool _topPaginationActive = false; bool _bottomPaginationActive = false; @@ -234,7 +235,7 @@ class _MessageListViewState extends State { @override Widget build(BuildContext context) { - final messagesStream = widget.parentMessage != null + final messagesStream = _isThreadConversation ? streamChannel.channel.state.threadsStream .where((threads) => threads.containsKey(widget.parentMessage.id)) .map((threads) => threads[widget.parentMessage.id]) @@ -324,7 +325,7 @@ class _MessageListViewState extends State { onInBetweenOfPage: () { _inBetweenList = true; }, - child: ScrollablePositionedList.builder( + child: ScrollablePositionedList.separated( key: ValueKey(initialIndex + initialAlignment), itemPositionsListener: _itemPositionListener, addAutomaticKeepAlives: true, @@ -333,9 +334,49 @@ class _MessageListViewState extends State { physics: widget.scrollPhysics, itemScrollController: _scrollController, reverse: true, - itemCount: messages.length + - 2 + - (widget.parentMessage != null ? 1 : 0), + itemCount: + messages.length + 2 + (_isThreadConversation ? 1 : 0), + separatorBuilder: (context, i) { + if (i == messages.length) return Offstage(); + if (i == messages.length + 2) return Offstage(); + if (i == messages.length + 1) return Offstage(); + if (i == 0) return SizedBox(height: 30); + final message = messages[i]; + final nextMessage = messages[i - 1]; + if (!Jiffy(message.createdAt.toLocal()).isSame( + nextMessage.createdAt.toLocal(), + Units.DAY, + )) { + final divider = widget.dateDividerBuilder != null + ? widget.dateDividerBuilder( + nextMessage.createdAt.toLocal(), + ) + : DateDivider( + dateTime: nextMessage.createdAt.toLocal(), + ); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: divider, + ); + } + final timeDiff = + Jiffy(nextMessage.createdAt.toLocal()).diff( + message.createdAt.toLocal(), + Units.MINUTE, + ); + + final isNextUserSame = + message.user.id == nextMessage.user?.id; + final isThread = message.replyCount > 0; + final isDeleted = message.isDeleted; + if (timeDiff >= 1 || + !isNextUserSame || + isThread || + isDeleted) { + return SizedBox(height: 8); + } + return SizedBox(height: 2); + }, itemBuilder: (context, i) { if (i == messages.length + 2) { if (widget.parentMessageBuilder != null) { @@ -383,7 +424,6 @@ class _MessageListViewState extends State { ); } final message = messages[i - 1]; - final nextMessage = (i - 1) > 0 ? messages[i - 2] : null; Widget messageWidget; @@ -419,27 +459,6 @@ class _MessageListViewState extends State { messageWidget = buildMessage(message, messages, i); } } - - if (nextMessage != null && - !Jiffy(message.createdAt.toLocal()).isSame( - nextMessage.createdAt.toLocal(), Units.DAY)) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - messageWidget, - Padding( - padding: const EdgeInsets.symmetric(vertical: 12.0), - child: widget.dateDividerBuilder != null - ? widget.dateDividerBuilder( - nextMessage.createdAt.toLocal()) - : DateDivider( - dateTime: nextMessage.createdAt.toLocal(), - ), - ), - ], - ); - } - return messageWidget; }, ), @@ -482,7 +501,7 @@ class _MessageListViewState extends State { Future _paginateData( StreamChannelState channel, QueryDirection direction) { - if (widget.parentMessage == null) { + if (!_isThreadConversation) { return channel.queryMessages(direction: direction); } else { return channel.getReplies(widget.parentMessage.id); @@ -672,14 +691,13 @@ class _MessageListViewState extends State { key: ValueKey('BOTTOM-MESSAGE'), onVisibilityChanged: (visibility) { final isVisible = visibility.visibleBounds != Rect.zero; - if (isVisible && !_bottomWasVisible) { + if (isVisible) { final channel = streamChannel.channel; if (_upToDate && channel.config?.readEvents == true && channel.state.unreadCount > 0) { streamChannel.channel.markRead(); } - _bottomWasVisible = !isVisible; } if (mounted) { setState(() => _showScrollToBottom = !isVisible); @@ -697,7 +715,12 @@ class _MessageListViewState extends State { return MessageWidget( showThreadReplyIndicator: false, showInChannelIndicator: false, - showReplyIndicator: false, + showReplyMessage: false, + showResendMessage: false, + showThreadReplyMessage: false, + showCopyMessage: false, + showDeleteMessage: false, + showEditMessage: false, message: message, reverse: isMyMessage, showUsername: !isMyMessage, @@ -707,10 +730,8 @@ class _MessageListViewState extends State { right: 8.0, bottom: 16.0, ), - showSendingIndicator: DisplayWidget.hide, + showSendingIndicator: false, onThreadTap: _onThreadTap, - showEditMessage: false, - showDeleteMessage: false, borderRadiusGeometry: BorderRadius.only( topLeft: Radius.circular(16), bottomLeft: Radius.circular(2), @@ -740,36 +761,73 @@ class _MessageListViewState extends State { final userId = StreamChat.of(context).user.id; final isMyMessage = message.user.id == userId; - final isNextUser = - index - 2 >= 0 && message.user.id == messages[index - 2]?.user?.id; + final nextMessage = index - 2 >= 0 ? messages[index - 2] : null; + final isNextUserSame = + nextMessage != null && message.user.id == nextMessage.user.id; + + num timeDiff = 0; + if (nextMessage != null) { + timeDiff = Jiffy(nextMessage.createdAt.toLocal()).diff( + message.createdAt.toLocal(), + Units.MINUTE, + ); + } final channel = streamChannel.channel; - final readList = channel.state?.read - ?.where((element) => element.user.id != userId) - ?.where((read) => - (read.lastRead.isAfter(message.createdAt) || - read.lastRead.isAtSameMomentAs(message.createdAt)) && - (index == 0 || - read.lastRead.isBefore(messages[index - 1].createdAt))) - ?.toList() ?? + final readList = channel.state?.read?.where((read) { + if (read.user.id == userId) return false; + return (read.lastRead.isAfter(message.createdAt) || + read.lastRead.isAtSameMomentAs(message.createdAt)) && + (index == 0 || + read.lastRead.isBefore(messages[index - 1].createdAt)); + })?.toList() ?? []; final allRead = readList.length >= (channel.memberCount ?? 0) - 1; + final hasFileAttachment = + message.attachments.any((it) => it.type == 'file'); final isThreadMessage = - widget.parentMessage != null || message?.showInChannel == true; + message?.parentId != null && message?.showInChannel == true; + + final hasReplies = message.replyCount > 0; + + final attachmentBorderRadius = hasFileAttachment ? 12.0 : 14.0; + + final showTimeStamp = message.createdAt != null && + (!isThreadMessage || _isThreadConversation) && + !hasReplies && + (timeDiff >= 1 || !isNextUserSame); + + final showUsername = !isMyMessage && + (!isThreadMessage || _isThreadConversation) && + !hasReplies && + (timeDiff >= 1 || !isNextUserSame); + + final showUserAvatar = isMyMessage + ? DisplayWidget.gone + : (timeDiff >= 1 || !isNextUserSame) + ? DisplayWidget.show + : DisplayWidget.hide; + + final showSendingIndicator = + isMyMessage && (index == 0 || timeDiff >= 1 || !isNextUserSame); + + bool showInChannelIndicator = !_isThreadConversation && isThreadMessage; + bool showThreadReplyIndicator = !_isThreadConversation && hasReplies; Widget child = MessageWidget( key: ValueKey('MESSAGE-${message.id}'), message: message, reverse: isMyMessage, showReactions: !message.isDeleted, - padding: EdgeInsets.only( - left: 8.0, - right: 8.0, - bottom: index == 0 ? 30 : (isNextUser ? 2 : 7), - top: 3, - ), + padding: const EdgeInsets.symmetric(horizontal: 8.0), + showInChannelIndicator: showInChannelIndicator, + showThreadReplyIndicator: showThreadReplyIndicator, + showUsername: showUsername, + showTimestamp: showTimeStamp, + showSendingIndicator: showSendingIndicator, + showUserAvatar: showUserAvatar, onQuotedMessageTap: (quotedMessageId) async { final scrollToIndex = () { final index = messages.indexWhere((m) => m.id == quotedMessageId); @@ -790,35 +848,26 @@ class _MessageListViewState extends State { }); } }, - showInChannelIndicator: widget.parentMessage == null, - showThreadReplyIndicator: widget.parentMessage == null, - showUsername: !isMyMessage && !isNextUser, - showSendingIndicator: isMyMessage && - (index == 0 || message.status != MessageSendingStatus.SENT) - ? DisplayWidget.show - : DisplayWidget.hide, - showTimestamp: !isNextUser || readList?.isNotEmpty == true, showEditMessage: isMyMessage, showDeleteMessage: isMyMessage, + showThreadReplyMessage: !isThreadMessage, borderSide: isMyMessage ? BorderSide.none : null, onThreadTap: _onThreadTap, onReplyTap: widget.onReplyTap, attachmentBorderRadiusGeometry: BorderRadius.only( - topLeft: Radius.circular(16), - bottomLeft: Radius.circular(!isNextUser ? 0 : 16), - topRight: Radius.circular(16), - bottomRight: Radius.circular(16), + topLeft: Radius.circular(attachmentBorderRadius), + bottomLeft: Radius.circular( + timeDiff >= 1 || !isNextUserSame ? 0 : attachmentBorderRadius), + topRight: Radius.circular(attachmentBorderRadius), + bottomRight: Radius.circular(attachmentBorderRadius), ), attachmentPadding: const EdgeInsets.all(2), borderRadiusGeometry: BorderRadius.only( topLeft: Radius.circular(16), - bottomLeft: Radius.circular(!isNextUser ? 0 : 16), + bottomLeft: Radius.circular(timeDiff >= 1 || !isNextUserSame ? 0 : 16), topRight: Radius.circular(16), bottomRight: Radius.circular(16), ), - showUserAvatar: isMyMessage - ? DisplayWidget.gone - : (isNextUser ? DisplayWidget.hide : DisplayWidget.show), messageTheme: isMyMessage ? StreamChatTheme.of(context).ownMessageTheme : StreamChatTheme.of(context).otherMessageTheme, @@ -827,7 +876,7 @@ class _MessageListViewState extends State { onShowMessage: widget.onShowMessage, ); - if (!isThreadMessage) { + if (!message.isDeleted && !message.isSystem && !message.isEphemeral) { child = Swipeable( onSwipeEnd: () => widget.onMessageSwiped(message), backgroundIcon: StreamSvgIcon.reply( @@ -890,7 +939,7 @@ class _MessageListViewState extends State { } }); - if (widget.parentMessage != null) { + if (_isThreadConversation) { streamChannel.getReplies(widget.parentMessage.id); } diff --git a/packages/flutter_widgets/lib/src/message_reactions_modal.dart b/packages/flutter_widgets/lib/src/message_reactions_modal.dart index 375f5e0c..5c24e047 100644 --- a/packages/flutter_widgets/lib/src/message_reactions_modal.dart +++ b/packages/flutter_widgets/lib/src/message_reactions_modal.dart @@ -116,7 +116,7 @@ class MessageReactionsModal extends StatelessWidget { showThreadReplyIndicator: false, showTimestamp: false, translateUserAvatar: false, - showSendingIndicator: DisplayWidget.gone, + showSendingIndicator: false, shape: messageShape, showInChannelIndicator: false, showReactionPickerIndicator: showReactions && @@ -127,7 +127,6 @@ class MessageReactionsModal extends StatelessWidget { ), ); }), - SizedBox(height: 8), if (message.latestReactions?.isNotEmpty == true) _buildReactionCard(context), ], @@ -152,27 +151,22 @@ class MessageReactionsModal extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: const EdgeInsets.all(16.0), - child: Text( + margin: EdgeInsets.zero, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( 'Message Reactions', - style: Theme.of(context).textTheme.headline6, + style: StreamChatTheme.of(context).textTheme.headlineBold, ), - ), - Flexible( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.only( - left: 18, - right: 18, - bottom: 26, - ), + const SizedBox(height: 16), + Flexible( + child: SingleChildScrollView( child: Wrap( spacing: 16, - runSpacing: 22, + runSpacing: 16, alignment: WrapAlignment.start, children: message.latestReactions .map((e) => _buildReaction( @@ -184,8 +178,8 @@ class MessageReactionsModal extends StatelessWidget { ), ), ), - ), - ], + ], + ), ), ), ); @@ -227,24 +221,28 @@ class MessageReactionsModal extends StatelessWidget { ), Positioned( child: Align( - alignment: Alignment.centerLeft, + alignment: reverse + ? Alignment.centerRight + : Alignment.centerLeft, child: ReactionBubble( reactions: [reaction], + flipTail: !reverse, borderColor: messageTheme.reactionsBorderColor, backgroundColor: messageTheme.reactionsBackgroundColor, highlightOwnReactions: false, ), ), - bottom: 4, + bottom: 6, left: isCurrentUser ? 0 : null, right: isCurrentUser ? 0 : null, ), ], ), + const SizedBox(height: 8), Text( reaction.user.name, - style: Theme.of(context).textTheme.subtitle2, + style: StreamChatTheme.of(context).textTheme.footnoteBold, textAlign: TextAlign.center, ), ], diff --git a/packages/flutter_widgets/lib/src/message_widget.dart b/packages/flutter_widgets/lib/src/message_widget.dart index 1200f38d..e9ea4873 100644 --- a/packages/flutter_widgets/lib/src/message_widget.dart +++ b/packages/flutter_widgets/lib/src/message_widget.dart @@ -1,7 +1,6 @@ import 'dart:math'; import 'dart:ui'; -import 'package:emojis/emoji.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -96,7 +95,7 @@ class MessageWidget extends StatefulWidget { final DisplayWidget showUserAvatar; /// It controls the display behaviour of the sending indicator - final DisplayWidget showSendingIndicator; + final bool showSendingIndicator; /// If true the widget will show the reactions final bool showReactions; @@ -106,9 +105,6 @@ class MessageWidget extends StatefulWidget { /// If true the widget will show the thread reply indicator final bool showThreadReplyIndicator; - /// If true the widget will show the reply indicator - final bool showReplyIndicator; - /// If true the widget will show the show in channel indicator final bool showInChannelIndicator; @@ -121,9 +117,6 @@ class MessageWidget extends StatefulWidget { /// Used in [MessageReactionsModal] and [MessageActionsModal] final bool showReactionPickerIndicator; - /// If true the widget will show the resendMessage indicator - final bool showResendMessage; - final List readList; final ShowMessageCallback onShowMessage; @@ -131,8 +124,14 @@ class MessageWidget extends StatefulWidget { /// If true show the users username next to the timestamp of the message final bool showUsername; final bool showTimestamp; - final bool showDeleteMessage; + + final bool showReplyMessage; + final bool showThreadReplyMessage; final bool showEditMessage; + final bool showCopyMessage; + final bool showDeleteMessage; + final bool showResendMessage; + final Map attachmentBuilders; /// Center user avatar with bottom of the message @@ -157,10 +156,9 @@ class MessageWidget extends StatefulWidget { this.onMentionTap, this.showReactionPickerIndicator = false, this.showUserAvatar = DisplayWidget.show, - this.showSendingIndicator = DisplayWidget.show, + this.showSendingIndicator = true, this.showThreadReplyIndicator = true, this.showInChannelIndicator = true, - this.showReplyIndicator = true, this.onReplyTap, this.onThreadTap, this.showUsername = true, @@ -168,6 +166,10 @@ class MessageWidget extends StatefulWidget { this.showReactions = true, this.showDeleteMessage = true, this.showEditMessage = true, + this.showReplyMessage = true, + this.showThreadReplyMessage = true, + this.showResendMessage = true, + this.showCopyMessage = true, this.onUserAvatarTap, this.onLinkTap, this.onMessageActions, @@ -175,7 +177,6 @@ class MessageWidget extends StatefulWidget { this.editMessageInputBuilder, this.textBuilder, Map customAttachmentBuilders, - this.showResendMessage = true, this.readList, this.padding, this.textPadding = const EdgeInsets.symmetric( @@ -239,18 +240,19 @@ class MessageWidget extends StatefulWidget { } class _MessageWidgetState extends State { - bool get showThreadReplyIndicator => - widget.showThreadReplyIndicator && widget.message.replyCount > 0; + bool get showThreadReplyIndicator => widget.showThreadReplyIndicator; + + bool get showSendingIndicator => widget.showSendingIndicator; + + bool get isDeleted => widget.message.isDeleted; bool get showUsername => widget.showUsername; - bool get showTimeStamp => - widget.message.createdAt != null && widget.showTimestamp; + bool get showTimeStamp => widget.showTimestamp; bool get isMessageRead => widget.readList?.isNotEmpty == true; - bool get showInChannel => - widget.showInChannelIndicator && widget.message?.showInChannel == true; + bool get showInChannel => widget.showInChannelIndicator; bool get hasQuotedMessage => widget.message?.quotedMessage != null; @@ -272,16 +274,15 @@ class _MessageWidgetState extends State { showThreadReplyIndicator || showUsername || showTimeStamp || - showInChannel; + showInChannel || + showSendingIndicator || + isDeleted; @override Widget build(BuildContext context) { final avatarWidth = widget.messageTheme.avatarTheme.constraints.maxWidth; var leftPadding = - widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 4.5; - - final isOnlyEmoji = - widget.message.text.characters.every((c) => Emoji.byChar(c) != null); + widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5; final hasFiles = widget.message.attachments?.any((element) => element.type == 'file') == @@ -330,7 +331,7 @@ class _MessageWidgetState extends State { child: PortalEntry( portal: Container( transform: - Matrix4.translationValues(-16, 2, 0), + Matrix4.translationValues(-12, 0, 0), child: _buildReactionIndicator(context), constraints: BoxConstraints(maxWidth: 22 * 6.0), @@ -372,6 +373,14 @@ class _MessageWidgetState extends State { : Card( clipBehavior: Clip.antiAlias, elevation: 0.0, + margin: EdgeInsets.symmetric( + horizontal: + widget.showUserAvatar == + DisplayWidget + .gone + ? 0 + : 4.0, + ), shape: widget.shape ?? RoundedRectangleBorder( side: isOnlyEmoji && @@ -395,7 +404,7 @@ class _MessageWidgetState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment - .start, + .end, mainAxisSize: MainAxisSize.min, children: [ @@ -423,10 +432,12 @@ class _MessageWidgetState extends State { widget.reverse ? pi : 0), child: CustomPaint( painter: ReactionBubblePainter( - widget.messageTheme - .reactionsBackgroundColor, - widget.messageTheme - .reactionsBorderColor, + StreamChatTheme.of(context) + .colorTheme + .white, + StreamChatTheme.of(context) + .colorTheme + .white, ), ), ), @@ -440,7 +451,11 @@ class _MessageWidgetState extends State { if (showBottomRow) SizedBox(height: 20.0), ], ), - if (showBottomRow) _buildBottomRow(leftPadding), + if (showBottomRow) + Padding( + padding: EdgeInsets.only(left: leftPadding), + child: _bottomRow, + ), if (isFailedState) Positioned( left: widget.reverse ? -3 : null, @@ -469,12 +484,12 @@ class _MessageWidgetState extends State { Widget _buildQuotedMessage() { final isMyMessage = widget.message.user.id == StreamChat.of(context).user.id; + final onTap = widget.message?.quotedMessage?.isDeleted != true && + widget.onQuotedMessageTap != null + ? () => widget.onQuotedMessageTap(widget.message.quotedMessageId) + : null; return QuotedMessageWidget( - onTap: () { - if (widget.onQuotedMessageTap != null) { - widget.onQuotedMessageTap(widget.message.quotedMessageId); - } - }, + onTap: onTap, message: widget.message.quotedMessage, messageTheme: isMyMessage ? StreamChatTheme.of(context).otherMessageTheme @@ -483,127 +498,117 @@ class _MessageWidgetState extends State { ); } - Widget _buildBottomRow(double leftPadding) { - final deleted = widget.message.isDeleted; - var children = []; - if (deleted) { - children.add( - Row( + Widget get _bottomRow { + if (isDeleted) { + return Transform( + transform: Matrix4.rotationY(widget.reverse ? pi : 0), + alignment: Alignment.center, + child: Row( mainAxisSize: MainAxisSize.min, children: [ StreamSvgIcon.eye( - color: - StreamChatTheme.of(context).colorTheme.black.withOpacity(0.5), + color: StreamChatTheme.of(context).colorTheme.grey, size: 16.0, ), SizedBox(width: 8.0), Text( 'Only visible to you', - style: StreamChatTheme.of(context).textTheme.footnote.copyWith( - color: StreamChatTheme.of(context) - .colorTheme - .black - .withOpacity(0.5)), + style: StreamChatTheme.of(context) + .textTheme + .footnote + .copyWith(color: StreamChatTheme.of(context).colorTheme.grey), ), ], ), ); - } else if (showInChannel) { - final onThreadTap = () async { - try { + } + + var children = []; + + final threadParticipants = widget.message.threadParticipants; + final showThreadParticipants = threadParticipants?.isNotEmpty == true; + final replyCount = widget.message.replyCount; + + var msg = 'Thread Reply'; + if (showThreadReplyIndicator && replyCount > 1) { + msg = '$replyCount Thread Replies'; + } + + final onThreadTap = () async { + try { + var message = widget.message; + if (showInChannel) { final channel = StreamChannel.of(context); - final message = await channel.getMessage(widget.message.parentId); - return widget.onThreadTap(message); - } catch (e, stk) { - print(e); - print(stk); - return null; + message = await channel.getMessage(widget.message.parentId); } - }; - children.add( + return widget.onThreadTap(message); + } catch (e, stk) { + print(e); + print(stk); + return null; + } + }; + + children.addAll([ + if (showSendingIndicator) _buildSendingIndicator(), + if (showInChannel || showThreadReplyIndicator) ...[ + if (showThreadParticipants) + SizedBox.fromSize( + size: Size((threadParticipants.length * 8.0) + 10, 16), + child: _buildThreadParticipantsIndicator(), + ), InkWell( onTap: widget.onThreadTap != null ? onThreadTap : null, - child: Text('Thread Reply', style: widget.messageTheme?.replies), + child: Text(msg, style: widget.messageTheme?.replies), ), - ); - } else { - final showSendingIndicator = - widget.showSendingIndicator == DisplayWidget.show; - final threadParticipants = widget.message.threadParticipants; - final showThreadParticipants = threadParticipants?.isNotEmpty == true; - final replyCount = widget.message.replyCount; - final msg = replyCount != 0 - ? '$replyCount ${replyCount > 1 ? 'Thread Replies' : 'Thread Reply'}' - : 'Thread Reply'; + ], + if (showUsername) + Text( + widget.message.user.name, + style: widget.messageTheme.replies.copyWith( + color: widget.messageTheme.createdAt.color, + ), + ), + if (showTimeStamp) + Text( + Jiffy(widget.message.createdAt.toLocal()).jm, + style: widget.messageTheme.createdAt, + ), + ]); - final onThreadTap = () async { - var message = widget.message; - return widget.onThreadTap(message); - }; - - children.addAll([ - if (showSendingIndicator) _buildSendingIndicator(), - if (showThreadReplyIndicator) ...[ - if (showThreadParticipants) - SizedBox.fromSize( - size: Size((threadParticipants.length * 8.0) + 10, 16), - child: _buildThreadParticipantsIndicator(), - ), - InkWell( - onTap: widget.onThreadTap != null ? onThreadTap : null, - child: Text(msg, style: widget.messageTheme?.replies), - ), - ], - if (showUsername) - Text( - widget.message.user.name, - style: widget.messageTheme.replies.copyWith( - color: widget.messageTheme.createdAt.color, - ), - ), - if (showTimeStamp) - Text( - Jiffy(widget.message.createdAt.toLocal()).jm, - style: widget.messageTheme.createdAt, - ), - ]); - } if (widget.reverse) children = children.reversed.toList(); - return Padding( - padding: EdgeInsets.only(left: leftPadding), - child: Flex( - direction: Axis.horizontal, - clipBehavior: Clip.none, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - if (!deleted && (showThreadReplyIndicator || showInChannel)) - Container( - margin: EdgeInsets.only( - bottom: widget.messageTheme.replies.fontSize / 2, - ), - child: CustomPaint( - size: const Size(16, 32), - painter: _ThreadReplyPainter( - context: context, - color: widget.messageTheme.messageBorderColor, - ), - ), + return Flex( + direction: Axis.horizontal, + clipBehavior: Clip.none, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + if (showThreadReplyIndicator || showInChannel) + Container( + margin: EdgeInsets.only( + bottom: widget.messageTheme.replies.fontSize / 2, ), - ...children.map( - (child) => Transform( - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - alignment: Alignment.center, - child: Container( - height: 16, - child: Center( - child: child, - ), + child: CustomPaint( + size: const Size(16, 32), + painter: _ThreadReplyPainter( + context: context, + color: widget.messageTheme.messageBorderColor, ), ), ), - ].insertBetween(const SizedBox(width: 8.0)), - ), + ...children.map( + (child) => Transform( + transform: Matrix4.rotationY(widget.reverse ? pi : 0), + alignment: Alignment.center, + child: Container( + height: 16, + child: Center( + child: child, + ), + ), + ), + ), + ].insertBetween(const SizedBox(width: 8.0)), ); } @@ -698,14 +703,15 @@ class _MessageWidgetState extends State { messageTheme: widget.messageTheme, messageShape: widget.shape ?? _getDefaultShape(context), reverse: widget.reverse, - showDeleteMessage: widget.showDeleteMessage, + showDeleteMessage: widget.showDeleteMessage || isDeleteFailed, message: widget.message, editMessageInputBuilder: widget.editMessageInputBuilder, onReplyTap: widget.onReplyTap, onThreadReplyTap: widget.onThreadTap, showResendMessage: widget.showResendMessage && (isSendFailed || isUpdateFailed), - showCopyMessage: !isFailedState && + showCopyMessage: widget.showCopyMessage && + !isFailedState && widget.message.text?.trim()?.isNotEmpty == true, showEditMessage: widget.showEditMessage && !isDeleteFailed && @@ -713,11 +719,12 @@ class _MessageWidgetState extends State { ?.any((element) => element.type == 'giphy') != true, showReactions: widget.showReactions, - showReply: widget.showReplyIndicator && + showReplyMessage: widget.showReplyMessage && !isFailedState && widget.onReplyTap != null, - showThreadReply: - widget.showThreadReplyIndicator && widget.onThreadTap != null, + showThreadReplyMessage: widget.showThreadReplyMessage && + !isFailedState && + widget.onThreadTap != null, ), ); }); @@ -754,9 +761,7 @@ class _MessageWidgetState extends State { side: widget.attachmentBorderSide ?? widget.borderSide ?? BorderSide( - color: Theme.of(context).brightness == Brightness.dark - ? StreamChatTheme.of(context).colorTheme.white.withAlpha(24) - : StreamChatTheme.of(context).colorTheme.black.withAlpha(24), + color: StreamChatTheme.of(context).colorTheme.greyGainsboro, ), borderRadius: widget.attachmentBorderRadiusGeometry ?? widget.borderRadiusGeometry ?? @@ -933,9 +938,7 @@ class _MessageWidgetState extends State { ); } - bool get isOnlyEmoji => - widget.message.text.characters.isNotEmpty && - widget.message.text.characters.every((c) => Emoji.byChar(c) != null); + bool get isOnlyEmoji => textIsOnlyEmoji(widget.message.text); Color _getBackgroundColor() { if (hasQuotedMessage) { diff --git a/packages/flutter_widgets/lib/src/quoted_message_widget.dart b/packages/flutter_widgets/lib/src/quoted_message_widget.dart index 7c5dd512..7621174f 100644 --- a/packages/flutter_widgets/lib/src/quoted_message_widget.dart +++ b/packages/flutter_widgets/lib/src/quoted_message_widget.dart @@ -1,18 +1,17 @@ import 'dart:math'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:emojis/emoji.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:video_player/video_player.dart'; import 'attachment_error.dart'; +import 'extension.dart'; import 'image_attachment.dart'; import 'message_text.dart'; import 'stream_chat_theme.dart'; import 'user_avatar.dart'; import 'utils.dart'; -import 'extension.dart'; typedef QuotedMessageAttachmentThumbnailBuilder = Widget Function( BuildContext, @@ -111,7 +110,7 @@ class QuotedMessageWidget extends StatelessWidget { return InkWell( onTap: onTap, child: Padding( - padding: const EdgeInsets.only(top: 8, bottom: 6, right: 4, left: 8), + padding: const EdgeInsets.only(top: 8, right: 4, left: 8), child: Row( crossAxisAlignment: CrossAxisAlignment.end, mainAxisSize: MainAxisSize.min, @@ -126,8 +125,7 @@ class QuotedMessageWidget extends StatelessWidget { } Widget _buildMessage(BuildContext context) { - final isOnlyEmoji = - message.text.characters.every((c) => Emoji.byChar(c) != null); + final isOnlyEmoji = textIsOnlyEmoji(message.text); var msg = _hasAttachments && !_containsText ? message.copyWith(text: message.attachments.last?.title ?? '') : message; diff --git a/packages/flutter_widgets/lib/src/reaction_bubble.dart b/packages/flutter_widgets/lib/src/reaction_bubble.dart index d8f8c623..9eee9997 100644 --- a/packages/flutter_widgets/lib/src/reaction_bubble.dart +++ b/packages/flutter_widgets/lib/src/reaction_bubble.dart @@ -27,7 +27,8 @@ class ReactionBubble extends StatelessWidget { @override Widget build(BuildContext context) { final reactionIcons = StreamChatTheme.of(context).reactionIcons; - final offset = reactions.length > 1 ? 16.0 : 2.0; + final totalReactions = reactions.length; + final offset = totalReactions > 1 ? 16.0 : 2.0; return Transform( transform: Matrix4.rotationY(reverse ? pi : 0), alignment: Alignment.center, @@ -37,7 +38,10 @@ class ReactionBubble extends StatelessWidget { Transform.translate( offset: Offset(reverse ? offset : -offset, 0), child: Container( - padding: const EdgeInsets.symmetric(vertical: 4), + padding: EdgeInsets.symmetric( + vertical: 4, + horizontal: totalReactions > 1 ? 4 : 0, + ), decoration: BoxDecoration( border: Border.all( color: borderColor, @@ -134,16 +138,11 @@ class ReactionBubble extends StatelessWidget { borderColor, ), ); - - if (!flipTail) { - return tail; - } else { - return Transform( - transform: Matrix4.rotationY(pi), - alignment: Alignment.center, - child: tail, - ); - } + return Transform( + transform: Matrix4.rotationY(flipTail ? 0 : pi), + alignment: Alignment.center, + child: tail, + ); } } diff --git a/packages/flutter_widgets/lib/src/reaction_picker.dart b/packages/flutter_widgets/lib/src/reaction_picker.dart index 9344b386..c5c42bd1 100644 --- a/packages/flutter_widgets/lib/src/reaction_picker.dart +++ b/packages/flutter_widgets/lib/src/reaction_picker.dart @@ -58,62 +58,65 @@ class _ReactionPickerState extends State return Transform.scale( scale: val, child: Material( - color: widget.messageTheme.reactionsBackgroundColor, + color: StreamChatTheme.of(context).colorTheme.white, clipBehavior: Clip.hardEdge, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(24), ), - 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; - var index = reactionIcons.indexOf(reactionIcon); + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + 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; + var index = reactionIcons.indexOf(reactionIcon); - return IconButton( - iconSize: 24, - icon: AnimatedBuilder( - animation: animations[index], - builder: (context, val) { - return Transform( - transform: Matrix4.identity() - ..scale(animations[index].value, - animations[index].value) - ..rotateZ(1.0 - animations[index].value), - child: StreamSvgIcon( - assetName: reactionIcon.assetName, - height: animations[index].value * 24.0, - width: animations[index].value * 24.0, - color: ownReactionIndex != -1 - ? StreamChatTheme.of(context) - .colorTheme - .accentBlue - : Theme.of(context) - .iconTheme - .color - .withOpacity(.5), - ), + return IconButton( + iconSize: 24, + icon: AnimatedBuilder( + animation: animations[index], + builder: (context, val) { + return Transform( + transform: Matrix4.identity() + ..scale(animations[index].value, + animations[index].value) + ..rotateZ(1.0 - animations[index].value), + child: StreamSvgIcon( + assetName: reactionIcon.assetName, + height: animations[index].value * 24.0, + width: animations[index].value * 24.0, + color: ownReactionIndex != -1 + ? StreamChatTheme.of(context) + .colorTheme + .accentBlue + : Theme.of(context) + .iconTheme + .color + .withOpacity(.5), + ), + ); + }), + onPressed: () { + if (ownReactionIndex != -1) { + removeReaction( + context, + widget.message.ownReactions[ownReactionIndex], ); - }), - onPressed: () { - if (ownReactionIndex != -1) { - removeReaction( - context, - widget.message.ownReactions[ownReactionIndex], - ); - } else { - sendReaction( - context, - reactionIcon.type, - ); - } - }, - ); - }).toList(), + } else { + sendReaction( + context, + reactionIcon.type, + ); + } + }, + ); + }).toList(), + ), ), ), ); diff --git a/packages/flutter_widgets/lib/src/stream_chat_theme.dart b/packages/flutter_widgets/lib/src/stream_chat_theme.dart index 439bd5ef..c6d2d72f 100644 --- a/packages/flutter_widgets/lib/src/stream_chat_theme.dart +++ b/packages/flutter_widgets/lib/src/stream_chat_theme.dart @@ -228,19 +228,9 @@ class StreamChatThemeData { inputBackground: colorTheme.white.withAlpha(12), ), ownMessageTheme: MessageTheme( - messageText: TextStyle( - fontSize: 14.5, - color: colorTheme.black, - ), - createdAt: TextStyle( - color: colorTheme.black.withOpacity(.5), - fontSize: 12, - ), - replies: TextStyle( - color: accentColor, - fontWeight: FontWeight.w600, - fontSize: 12, - ), + messageText: textTheme.body, + createdAt: textTheme.footnote.copyWith(color: colorTheme.grey), + replies: textTheme.footnoteBold.copyWith(color: accentColor), messageBackgroundColor: colorTheme.greyGainsboro, reactionsBackgroundColor: colorTheme.white, reactionsBorderColor: colorTheme.greyWhisper, @@ -259,19 +249,9 @@ class StreamChatThemeData { otherMessageTheme: MessageTheme( reactionsBackgroundColor: colorTheme.greyGainsboro, reactionsBorderColor: colorTheme.white, - messageText: TextStyle( - fontSize: 14.5, - color: colorTheme.black, - ), - createdAt: TextStyle( - color: colorTheme.black.withOpacity(.5), - fontSize: 12, - ), - replies: TextStyle( - color: accentColor, - fontWeight: FontWeight.w600, - fontSize: 12, - ), + messageText: textTheme.body, + createdAt: textTheme.footnote.copyWith(color: colorTheme.grey), + replies: textTheme.footnoteBold.copyWith(color: accentColor), messageLinks: TextStyle( color: accentColor, ), diff --git a/packages/flutter_widgets/lib/src/stream_svg_icon.dart b/packages/flutter_widgets/lib/src/stream_svg_icon.dart index 3e8cfae2..fcef384f 100644 --- a/packages/flutter_widgets/lib/src/stream_svg_icon.dart +++ b/packages/flutter_widgets/lib/src/stream_svg_icon.dart @@ -470,7 +470,19 @@ class StreamSvgIcon extends StatelessWidget { ); } - factory StreamSvgIcon.icon_SHARE({ + factory StreamSvgIcon.Icon_moon({ + double size, + Color color, + }) { + return StreamSvgIcon( + assetName: 'icon_moon.svg', + color: color, + width: size, + height: size, + ); + } + + factory StreamSvgIcon.Icon_SHARE({ double size, Color color, }) { diff --git a/packages/flutter_widgets/lib/src/thread_header.dart b/packages/flutter_widgets/lib/src/thread_header.dart index 8e165af7..eb748272 100644 --- a/packages/flutter_widgets/lib/src/thread_header.dart +++ b/packages/flutter_widgets/lib/src/thread_header.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'back_button.dart'; import 'channel_name.dart'; @@ -81,6 +82,7 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { elevation: 1, leading: showBackButton ? StreamBackButton( + cid: StreamChannel.of(context).channel.cid, onPressed: onBackPressed, showUnreads: true, ) diff --git a/packages/flutter_widgets/lib/src/unread_indicator.dart b/packages/flutter_widgets/lib/src/unread_indicator.dart index c9f42fad..cf115f3b 100644 --- a/packages/flutter_widgets/lib/src/unread_indicator.dart +++ b/packages/flutter_widgets/lib/src/unread_indicator.dart @@ -5,14 +5,22 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; class UnreadIndicator extends StatelessWidget { const UnreadIndicator({ Key key, + this.cid, }) : super(key: key); + /// Channel cid used to retrieve unread count + final String cid; + @override Widget build(BuildContext context) { final client = StreamChat.of(context).client; return StreamBuilder( - stream: client.state.totalUnreadCountStream, - initialData: client.state.totalUnreadCount, + stream: cid != null + ? client.state.channels[cid].state.unreadCountStream + : client.state.totalUnreadCountStream, + initialData: cid != null + ? client.state.channels[cid].state.unreadCount + : client.state.totalUnreadCount, builder: (context, snapshot) { if (!snapshot.hasData || snapshot.data == 0) { return SizedBox(); diff --git a/packages/flutter_widgets/lib/src/url_attachment.dart b/packages/flutter_widgets/lib/src/url_attachment.dart index 130b1109..b0584bce 100644 --- a/packages/flutter_widgets/lib/src/url_attachment.dart +++ b/packages/flutter_widgets/lib/src/url_attachment.dart @@ -24,10 +24,6 @@ class UrlAttachment extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (urlAttachment.imageUrl != null) - SizedBox( - height: 16.0, - ), if (urlAttachment.imageUrl != null) Container( clipBehavior: Clip.antiAliasWithSaveLayer, diff --git a/packages/flutter_widgets/lib/src/utils.dart b/packages/flutter_widgets/lib/src/utils.dart index f573b77b..be10263e 100644 --- a/packages/flutter_widgets/lib/src/utils.dart +++ b/packages/flutter_widgets/lib/src/utils.dart @@ -1,10 +1,10 @@ +import 'package:emojis/emoji.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'stream_svg_icon.dart'; - import '../stream_chat_flutter.dart'; +import 'stream_svg_icon.dart'; Future launchURL(BuildContext context, String url) async { if (await canLaunch(url)) { @@ -215,3 +215,16 @@ StreamSvgIcon getFileTypeImage(String type) { break; } } + +final _emojis = Emoji.all(); + +bool textIsOnlyEmoji(String text) { + return text.trim().characters.isNotEmpty && + text.trim().characters.every((c) => + _emojis.firstWhere( + (Emoji emoji) => emoji.char.contains(c), + orElse: () => null, + ) != + null) && + text.characters.length < 4; +} diff --git a/packages/flutter_widgets/lib/svgs/icon_moon.svg b/packages/flutter_widgets/lib/svgs/icon_moon.svg new file mode 100644 index 00000000..ebc3279c --- /dev/null +++ b/packages/flutter_widgets/lib/svgs/icon_moon.svg @@ -0,0 +1,3 @@ + + + diff --git a/packages/flutter_widgets/test/src/message_action_modal_test.dart b/packages/flutter_widgets/test/src/message_action_modal_test.dart index 0dcd8d87..3cb916d1 100644 --- a/packages/flutter_widgets/test/src/message_action_modal_test.dart +++ b/packages/flutter_widgets/test/src/message_action_modal_test.dart @@ -42,10 +42,10 @@ void main() { await tester.pump(Duration(milliseconds: 1000)); expect(find.byKey(Key('MessageWidget')), findsOneWidget); - expect(find.text('Thread reply'), findsOneWidget); - expect(find.text('Edit message'), findsOneWidget); - expect(find.text('Delete message'), findsOneWidget); - expect(find.text('Copy message'), findsOneWidget); + expect(find.text('Thread Reply'), findsOneWidget); + expect(find.text('Edit Message'), findsOneWidget); + expect(find.text('Delete Message'), findsOneWidget); + expect(find.text('Copy Message'), findsOneWidget); }, ); testWidgets( @@ -70,8 +70,8 @@ void main() { showEditMessage: false, showCopyMessage: false, showDeleteMessage: false, - showReply: false, - showThreadReply: false, + showReplyMessage: false, + showThreadReplyMessage: false, message: Message( text: 'test', user: User(