diff --git a/example/lib/advanced_options_page.dart b/example/lib/advanced_options_page.dart index 7555f286..2da11406 100644 --- a/example/lib/advanced_options_page.dart +++ b/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/example/lib/customize_message_widget.dart b/example/lib/customize_message_widget.dart index a3713b69..84e65eab 100644 --- a/example/lib/customize_message_widget.dart +++ b/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/example/lib/main.dart b/example/lib/main.dart index 25282335..98fa71a2 100644 --- a/example/lib/main.dart +++ b/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, ); } } @@ -166,113 +185,131 @@ class _HomePageState extends State { Drawer _buildDrawer(BuildContext context, User user) { return Drawer( - child: SafeArea( - child: Padding( - padding: EdgeInsets.only( - top: MediaQuery.of(context).viewPadding.top + 8, - ), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only( - bottom: 20.0, - left: 8, - ), - child: Row( - children: [ - UserAvatar( - user: user, - showOnlineStatus: false, - constraints: BoxConstraints.tight(Size.fromRadius(20)), - ), - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: Text( - user.name, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, + child: Container( + color: StreamChatTheme.of(context).colorTheme.white, + child: SafeArea( + child: Padding( + padding: EdgeInsets.only( + top: MediaQuery.of(context).viewPadding.top + 8, + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only( + bottom: 20.0, + left: 8, + ), + child: Row( + children: [ + UserAvatar( + user: user, + showOnlineStatus: false, + constraints: BoxConstraints.tight(Size.fromRadius(20)), + ), + Padding( + padding: const EdgeInsets.only(left: 16.0), + child: Text( + user.name, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), ), ), + ], + ), + ), + ListTile( + leading: StreamSvgIcon.penWrite( + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(.5), + ), + onTap: () { + Navigator.popAndPushNamed( + context, + Routes.NEW_CHAT, + ); + }, + title: Text( + 'New direct message', + style: TextStyle( + fontSize: 14.5, ), - ], - ), - ), - ListTile( - leading: StreamSvgIcon.penWrite( - color: StreamChatTheme.of(context) - .colorTheme - .black - .withOpacity(.5), - ), - onTap: () { - Navigator.popAndPushNamed( - context, - Routes.NEW_CHAT, - ); - }, - title: Text( - 'New direct message', - style: TextStyle( - fontSize: 14.5, ), ), - ), - ListTile( - leading: StreamSvgIcon.contacts( - color: StreamChatTheme.of(context) - .colorTheme - .black - .withOpacity(.5), - ), - onTap: () { - Navigator.popAndPushNamed( - context, - Routes.NEW_GROUP_CHAT, - ); - }, - title: Text( - 'New group', - style: TextStyle( - fontSize: 14.5, + ListTile( + leading: StreamSvgIcon.contacts( + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(.5), + ), + onTap: () { + Navigator.popAndPushNamed( + context, + Routes.NEW_GROUP_CHAT, + ); + }, + title: Text( + 'New group', + style: TextStyle( + fontSize: 14.5, + ), ), ), - ), - Expanded( - child: Container( - alignment: Alignment.bottomCenter, - child: ListTile( - onTap: () async { - Navigator.pop(context); + Expanded( + child: Container( + alignment: Alignment.bottomCenter, + child: ListTile( + onTap: () async { + Navigator.pop(context); - final secureStorage = FlutterSecureStorage(); - await secureStorage.deleteAll(); + final secureStorage = FlutterSecureStorage(); + await secureStorage.deleteAll(); - StreamChat.of(context).client.disconnect( - clearUser: true, + StreamChat.of(context).client.disconnect( + clearUser: true, + ); + + await Navigator.pushReplacementNamed( + context, + Routes.CHOOSE_USER, + ); + }, + leading: StreamSvgIcon.user( + color: StreamChatTheme.of(context) + .colorTheme + .black + .withOpacity(.5), + ), + title: Text( + 'Sign out', + style: TextStyle( + 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, ); - - await Navigator.pushReplacementNamed( - context, - Routes.CHOOSE_USER, - ); - }, - leading: StreamSvgIcon.user( - color: StreamChatTheme.of(context) - .colorTheme - .black - .withOpacity(.5), - ), - title: Text( - 'Sign out', - style: TextStyle( - fontSize: 14.5, + }, ), ), ), ), - ), - ], + ], + ), ), ), ), diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 539166ec..16bc8108 100644 --- a/example/pubspec.yaml +++ b/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/lib/src/back_button.dart b/lib/src/back_button.dart index 52adeade..9d787517 100644 --- a/lib/src/back_button.dart +++ b/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/lib/src/deleted_message.dart b/lib/src/deleted_message.dart index 5921df43..bd014770 100644 --- a/lib/src/deleted_message.dart +++ b/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/lib/src/image_footer.dart b/lib/src/image_footer.dart index 7ee9a128..e43eb3fb 100644 --- a/lib/src/image_footer.dart +++ b/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/lib/src/message_actions_modal.dart b/lib/src/message_actions_modal.dart index 4db2dd73..fa7b8da1 100644 --- a/lib/src/message_actions_modal.dart +++ b/lib/src/message_actions_modal.dart @@ -32,8 +32,8 @@ class MessageActionsModal extends StatefulWidget { final bool showCopyMessage; final bool showEditMessage; final bool showResendMessage; - final bool showReply; - final bool showThreadReply; + final bool showReplyMessage; + final bool showThreadReplyMessage; final bool showFlagButton; final bool reverse; final ShapeBorder messageShape; @@ -49,9 +49,9 @@ class MessageActionsModal extends StatefulWidget { this.onReplyTap, this.onThreadReplyTap, this.showCopyMessage = true, - this.showReply = true, + this.showReplyMessage = true, this.showResendMessage = true, - this.showThreadReply = true, + this.showThreadReplyMessage = true, this.showFlagButton = true, this.showUserAvatar = DisplayWidget.show, this.editMessageInputBuilder, @@ -148,8 +148,8 @@ class _MessageActionsModalState extends State { showReactions: false, showUsername: false, showThreadReplyIndicator: false, - showReplyIndicator: false, - showUserAvatar: widget.showUserAvatar, + showReplyMessage: false, + showUserAvatar: showUserAvatar, showTimestamp: false, translateUserAvatar: false, showReactionPickerIndicator: @@ -200,14 +200,14 @@ class _MessageActionsModalState extends State { .greyWhisper, context: context, tiles: [ - if (widget.showReply && + if (widget.showReplyMessage && (widget.message.status == MessageSendingStatus.SENT || widget.message.status == null) && widget.message.parentId == null) _buildReplyButton(context), - if (widget.showThreadReply && + if (widget.showThreadReplyMessage && (widget.message.status == MessageSendingStatus.SENT || widget.message.status == diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index bf730b45..bece4e7c 100644 --- a/lib/src/message_list_view.dart +++ b/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]) @@ -333,9 +334,8 @@ 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(); @@ -368,7 +368,11 @@ class _MessageListViewState extends State { final isNextUserSame = message.user.id == nextMessage.user?.id; final isThread = message.replyCount > 0; - if (timeDiff >= 1 || !isNextUserSame || isThread) { + final isDeleted = message.isDeleted; + if (timeDiff >= 1 || + !isNextUserSame || + isThread || + isDeleted) { return SizedBox(height: 8); } return SizedBox(height: 2); @@ -497,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); @@ -687,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); @@ -712,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, @@ -722,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), @@ -768,32 +774,60 @@ class _MessageListViewState extends State { } 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 isThreadMessage = - widget.parentMessage != null || message?.showInChannel == true; - final hasFileAttachment = message.attachments.any((it) => it.type == 'file'); + final isThreadMessage = + 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: 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); @@ -814,17 +848,9 @@ class _MessageListViewState extends State { }); } }, - showInChannelIndicator: widget.parentMessage == null, - showThreadReplyIndicator: widget.parentMessage == null, - showUsername: !isMyMessage && (timeDiff >= 1 || !isNextUserSame), - showSendingIndicator: - isMyMessage && (index == 0 || timeDiff >= 1 || !isNextUserSame) - ? DisplayWidget.show - : DisplayWidget.hide, - showTimestamp: - !isNextUserSame || readList?.isNotEmpty == true || timeDiff >= 1, showEditMessage: isMyMessage, showDeleteMessage: isMyMessage, + showThreadReplyMessage: !isThreadMessage, showFlagButton: !isMyMessage, borderSide: isMyMessage ? BorderSide.none : null, onThreadTap: _onThreadTap, @@ -843,11 +869,6 @@ class _MessageListViewState extends State { topRight: Radius.circular(16), bottomRight: Radius.circular(16), ), - showUserAvatar: isMyMessage - ? DisplayWidget.gone - : (timeDiff >= 1 || !isNextUserSame - ? DisplayWidget.show - : DisplayWidget.hide), messageTheme: isMyMessage ? StreamChatTheme.of(context).ownMessageTheme : StreamChatTheme.of(context).otherMessageTheme, @@ -856,7 +877,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( @@ -919,7 +940,7 @@ class _MessageListViewState extends State { } }); - if (widget.parentMessage != null) { + if (_isThreadConversation) { streamChannel.getReplies(widget.parentMessage.id); } diff --git a/lib/src/message_reactions_modal.dart b/lib/src/message_reactions_modal.dart index 702bb0ed..5c24e047 100644 --- a/lib/src/message_reactions_modal.dart +++ b/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 && diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index ebf57c94..8118af01 100644 --- a/lib/src/message_widget.dart +++ b/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 bool showFlagButton; final Map attachmentBuilders; @@ -158,10 +157,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, @@ -169,6 +167,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.showFlagButton = true, this.onUserAvatarTap, this.onLinkTap, @@ -177,7 +179,6 @@ class MessageWidget extends StatefulWidget { this.editMessageInputBuilder, this.textBuilder, Map customAttachmentBuilders, - this.showResendMessage = true, this.readList, this.padding, this.textPadding = const EdgeInsets.symmetric( @@ -241,18 +242,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; @@ -274,7 +276,9 @@ class _MessageWidgetState extends State { showThreadReplyIndicator || showUsername || showTimeStamp || - showInChannel; + showInChannel || + showSendingIndicator || + isDeleted; @override Widget build(BuildContext context) { @@ -282,9 +286,6 @@ class _MessageWidgetState extends State { var leftPadding = widget.showUserAvatar != DisplayWidget.gone ? avatarWidth + 8.5 : 0.5; - final isOnlyEmoji = - widget.message.text.characters.every((c) => Emoji.byChar(c) != null); - final hasFiles = widget.message.attachments?.any((element) => element.type == 'file') == true; @@ -452,7 +453,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, @@ -481,12 +486,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 @@ -495,12 +500,12 @@ 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( @@ -518,102 +523,94 @@ class _MessageWidgetState extends State { ], ), ); - } 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)), ); } @@ -709,14 +706,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 && @@ -724,11 +722,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, showFlagButton: widget.showFlagButton, ), ); @@ -943,9 +942,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/lib/src/quoted_message_widget.dart b/lib/src/quoted_message_widget.dart index c0439d46..7621174f 100644 --- a/lib/src/quoted_message_widget.dart +++ b/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, @@ -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/lib/src/stream_svg_icon.dart b/lib/src/stream_svg_icon.dart index e8af3436..0d705d31 100644 --- a/lib/src/stream_svg_icon.dart +++ b/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/lib/src/thread_header.dart b/lib/src/thread_header.dart index 8e165af7..eb748272 100644 --- a/lib/src/thread_header.dart +++ b/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/lib/src/unread_indicator.dart b/lib/src/unread_indicator.dart index c9f42fad..cf115f3b 100644 --- a/lib/src/unread_indicator.dart +++ b/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/lib/src/utils.dart b/lib/src/utils.dart index 4e55111f..27559f7a 100644 --- a/lib/src/utils.dart +++ b/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)) { @@ -289,3 +289,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/lib/svgs/icon_moon.svg b/lib/svgs/icon_moon.svg new file mode 100644 index 00000000..ebc3279c --- /dev/null +++ b/lib/svgs/icon_moon.svg @@ -0,0 +1,3 @@ + + + diff --git a/test/src/message_action_modal_test.dart b/test/src/message_action_modal_test.dart index 7da2095d..3cb916d1 100644 --- a/test/src/message_action_modal_test.dart +++ b/test/src/message_action_modal_test.dart @@ -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(