From 903dec9d1fdeb1b305a5f17e6164a943fba55d4a Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 21 Dec 2020 15:29:59 +0530 Subject: [PATCH 01/17] [Thread Header] Redesign wrt new ui Signed-off-by: Sahil Kumar --- lib/src/thread_header.dart | 79 +++++++++++++++++++++----------------- 1 file changed, 43 insertions(+), 36 deletions(-) diff --git a/lib/src/thread_header.dart b/lib/src/thread_header.dart index aa14cb61..cfb6b1f1 100644 --- a/lib/src/thread_header.dart +++ b/lib/src/thread_header.dart @@ -1,7 +1,9 @@ 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/src/stream_svg_icon.dart'; + +import 'back_button.dart'; +import 'channel_name.dart'; /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/thread_header.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/thread_header_paint.png) @@ -77,43 +79,48 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { return AppBar( automaticallyImplyLeading: false, elevation: 1, + leading: showBackButton + ? StreamBackButton( + onPressed: onBackPressed, + showUnreads: true, + ) + : SizedBox(), backgroundColor: StreamChatTheme.of(context).channelTheme.channelHeaderTheme.color, - actions: [ - Container( - child: showBackButton - ? AspectRatio( - aspectRatio: 1, - child: IconButton( - onPressed: onBackPressed ?? () => Navigator.pop(context), - icon: StreamSvgIcon.close( - size: 24, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white - : Colors.black, - ), - ), - ) - : SizedBox(), - ), - ], - centerTitle: false, - title: Text.rich( - TextSpan( - text: 'Thread', - children: [ - TextSpan( - text: - ' ${parent.replyCount} ${parent.replyCount == 1 ? 'reply' : 'replies'}', - style: StreamChatTheme.of(context) - .channelTheme - .channelHeaderTheme - .lastMessageAt, - ), - ], - ), - style: - StreamChatTheme.of(context).channelTheme.channelHeaderTheme.title, + centerTitle: true, + title: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Thread Reply', + style: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .title, + ), + SizedBox(height: 2), + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'with ', + style: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .lastMessageAt, + ), + ChannelName( + textStyle: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .lastMessageAt, + ), + ], + ), + ], ), ); } From a4071c117e2e9a2b5cd3b93aca24318ce2e6b207 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 21 Dec 2020 15:31:13 +0530 Subject: [PATCH 02/17] [Message Input] Change "sendAsDm" checkbox title Signed-off-by: Sahil Kumar --- lib/src/message_input.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index dec2b033..c46c899b 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -21,7 +21,6 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/user_avatar.dart'; import 'package:substring_highlight/substring_highlight.dart'; import 'package:video_compress/video_compress.dart'; -import 'package:photo_manager/photo_manager.dart'; import '../stream_chat_flutter.dart'; import 'stream_channel.dart'; @@ -302,7 +301,7 @@ class MessageInputState extends State { ), Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Text('Send also as direct message'), + child: Text('Also send as direct message'), ), ], ), From 4aa2a8946a8906dc5455b8992ea84b36af512e75 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 22 Dec 2020 17:45:08 +0530 Subject: [PATCH 03/17] feat: Added gridview for channel list view and changed implementation for image sharing --- lib/src/channel_image.dart | 6 ++ lib/src/channel_list_view.dart | 91 +++++++++++++++++---- lib/src/group_image.dart | 41 ++++++++-- lib/src/image_footer.dart | 143 ++++++++++++++++----------------- lib/src/user_avatar.dart | 3 + 5 files changed, 192 insertions(+), 92 deletions(-) diff --git a/lib/src/channel_image.dart b/lib/src/channel_image.dart index c3d76202..d79b2efa 100644 --- a/lib/src/channel_image.dart +++ b/lib/src/channel_image.dart @@ -52,6 +52,7 @@ class ChannelImage extends StatelessWidget { this.onTap, this.showOnlineStatus = true, this.borderRadius, + this.selected = false, }) : super(key: key); final BorderRadius borderRadius; @@ -67,6 +68,8 @@ class ChannelImage extends StatelessWidget { final bool showOnlineStatus; + final bool selected; + @override Widget build(BuildContext context) { final streamChat = StreamChat.of(context); @@ -99,6 +102,7 @@ class ChannelImage extends StatelessWidget { onTap(); } : null, + selected: selected, ); }); } else { @@ -111,12 +115,14 @@ class ChannelImage extends StatelessWidget { .toList(); return GroupImage( images: images, + borderRadius: borderRadius, constraints: constraints ?? StreamChatTheme.of(context) .channelPreviewTheme .avatarTheme .constraints, onTap: onTap, + selected: selected, ); } diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart index 5dbc9f90..d5278f5e 100644 --- a/lib/src/channel_list_view.dart +++ b/lib/src/channel_list_view.dart @@ -74,6 +74,8 @@ class ChannelListView extends StatefulWidget { this.onStartChatPressed, this.swipeToAction = false, this.pullToRefresh = true, + this.crossAxisCount = 1, + this.selectedChannels = const [], }) : super(key: key); /// The builder that will be used in case of error @@ -134,6 +136,11 @@ class ChannelListView extends StatefulWidget { /// Callback used in the default empty list widget final VoidCallback onStartChatPressed; + /// The number of children in the cross axis. + final int crossAxisCount; + + final List selectedChannels; + @override _ChannelListViewState createState() => _ChannelListViewState(); } @@ -259,22 +266,35 @@ class _ChannelListViewState extends State } if (channels.isNotEmpty) { - child = ListView.custom( - physics: AlwaysScrollableScrollPhysics(), - controller: _scrollController, - childrenDelegate: SliverChildBuilderDelegate( - (context, i) { - return _itemBuilder(context, i, channels); + if (widget.crossAxisCount > 1) { + child = GridView.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: widget.crossAxisCount), + itemCount: channels.length, + physics: AlwaysScrollableScrollPhysics(), + controller: _scrollController, + itemBuilder: (context, index) { + return _gridItemBuilder(context, index, channels); }, - childCount: (channels.length * 2) + 1, - findChildIndexCallback: (key) { - final ValueKey valueKey = key; - final index = channels.indexWhere( - (channel) => 'CHANNEL-${channel.id}' == valueKey.value); - return index != -1 ? (index * 2) : null; - }, - ), - ); + ); + } else { + child = ListView.custom( + physics: AlwaysScrollableScrollPhysics(), + controller: _scrollController, + childrenDelegate: SliverChildBuilderDelegate( + (context, i) { + return _itemBuilder(context, i, channels); + }, + childCount: (channels.length * 2) + 1, + findChildIndexCallback: (key) { + final ValueKey valueKey = key; + final index = channels.indexWhere( + (channel) => 'CHANNEL-${channel.id}' == valueKey.value); + return index != -1 ? (index * 2) : null; + }, + ), + ); + } } } @@ -581,6 +601,47 @@ class _ChannelListViewState extends State } } + Widget _gridItemBuilder(BuildContext context, int i, List channels) { + var channel = channels[i]; + + var selected = widget.selectedChannels.contains(channel); + + return Container( + key: ValueKey('CHANNEL-${channel.id}'), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ChannelImage( + channel: channel, + borderRadius: BorderRadius.circular(32), + selected: selected, + constraints: BoxConstraints.tightFor( + width: 64, + height: 64, + ), + onTap: () { + widget.onChannelTap(channel, null); + }, + ), + SizedBox(height: 7), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: StreamChannel( + child: ChannelName( + textStyle: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + channel: channel, + ), + ), + ], + ), + ); + } + Widget _buildQueryProgressIndicator( context, ChannelsBlocState channelsProvider, diff --git a/lib/src/group_image.dart b/lib/src/group_image.dart index 96e25f74..5f54377d 100644 --- a/lib/src/group_image.dart +++ b/lib/src/group_image.dart @@ -9,21 +9,33 @@ class GroupImage extends StatelessWidget { @required this.images, this.constraints, this.onTap, + this.borderRadius, + this.selected = false, + this.selectionColor = const Color(0xFF006CFF), + this.selectionThickness = 4, }) : super(key: key); final List images; final BoxConstraints constraints; final VoidCallback onTap; + final bool selected; + final BorderRadius borderRadius; + final Color selectionColor; + final double selectionThickness; @override Widget build(BuildContext context) { - return GestureDetector( + var avatar; + final streamChatTheme = StreamChatTheme.of(context); + + avatar = GestureDetector( onTap: onTap, child: ClipRRect( - borderRadius: StreamChatTheme.of(context) - .ownMessageTheme - .avatarTheme - .borderRadius, + borderRadius: borderRadius ?? + StreamChatTheme.of(context) + .ownMessageTheme + .avatarTheme + .borderRadius, child: Container( constraints: constraints ?? StreamChatTheme.of(context) @@ -91,5 +103,24 @@ class GroupImage extends StatelessWidget { ), ), ); + + if (selected) { + avatar = ClipRRect( + borderRadius: (borderRadius ?? + streamChatTheme.ownMessageTheme.avatarTheme.borderRadius) + + BorderRadius.circular(selectionThickness), + child: Container( + color: selectionColor, + height: 64.0, + width: 64.0, + child: Padding( + padding: EdgeInsets.all(selectionThickness), + child: avatar, + ), + ), + ); + } + + return avatar; } } diff --git a/lib/src/image_footer.dart b/lib/src/image_footer.dart index 3c37a66a..79c1f501 100644 --- a/lib/src/image_footer.dart +++ b/lib/src/image_footer.dart @@ -68,7 +68,7 @@ class _ImageFooterState extends State { String _userNameQuery; bool _isSearchActive = false; - Set _selectedUsers = {}; + List _selectedChannels = []; bool _loading = false; Timer _debounce; @@ -278,78 +278,86 @@ class _ImageFooterState extends State { ), clipBehavior: Clip.antiAlias, child: Scaffold( + resizeToAvoidBottomInset: false, body: UsersBloc( child: Column( children: [ _buildTextInputSection(modalSetState), + if (_userSearchMode) + SizedBox( + height: 22.0, + ), Expanded( - child: UserListView( - selectedUsers: _selectedUsers, - onUserTap: (user, _) { - _searchController.clear(); - if (!_selectedUsers.contains(user)) { - modalSetState(() { - _selectedUsers.add(user); - }); - } else { - modalSetState(() { - _selectedUsers.remove(user); - }); - } - }, - crossAxisCount: 4, - pagination: PaginationParams( - limit: 25, - ), - filter: { - if (_searchController.text.isNotEmpty) - 'name': { - r'$autocomplete': _userNameQuery, - }, - 'id': { - r'$ne': StreamChat.of(context).user.id, + child: ChannelsBloc( + child: ChannelListView( + selectedChannels: _selectedChannels, + onChannelTap: (user, _) { + _searchController.clear(); + if (!_selectedChannels.contains(user)) { + modalSetState(() { + _selectedChannels.add(user); + }); + } else { + modalSetState(() { + _selectedChannels.remove(user); + }); + } }, - }, - sort: [ - SortOption( - 'name', - direction: 1, + crossAxisCount: 4, + pagination: PaginationParams( + limit: 25, ), - ], - emptyBuilder: (_) { - return LayoutBuilder( - builder: (context, viewportConstraints) { - return SingleChildScrollView( - physics: AlwaysScrollableScrollPhysics(), - child: ConstrainedBox( - constraints: BoxConstraints( - minHeight: viewportConstraints.maxHeight, - ), - child: Center( - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(24), - child: StreamSvgIcon.search( - size: 96, - color: Colors.grey, + filter: { + if (_searchController.text.isNotEmpty) + 'name': { + r'$autocomplete': _userNameQuery, + }, + 'id': { + r'$ne': StreamChat.of(context).user.id, + }, + }, + sort: [ + SortOption( + 'name', + direction: 1, + ), + ], + emptyBuilder: (_) { + return LayoutBuilder( + builder: (context, viewportConstraints) { + return SingleChildScrollView( + physics: AlwaysScrollableScrollPhysics(), + child: ConstrainedBox( + constraints: BoxConstraints( + minHeight: + viewportConstraints.maxHeight, + ), + child: Center( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(24), + child: StreamSvgIcon.search( + size: 96, + color: Colors.grey, + ), ), - ), - Text( - 'No user matches these keywords...'), - ], + Text( + 'No user matches these keywords...'), + ], + ), ), ), - ), - ); - }, - ); - }, + ); + }, + ); + }, + ), ), ), - if (_selectedUsers.isNotEmpty) + if (_selectedChannels.isNotEmpty) _buildShareTextInputSection(modalSetState), - if (!_userSearchMode && _selectedUsers.isEmpty) + if (!_userSearchMode && _selectedChannels.isEmpty) Align( alignment: Alignment.bottomCenter, child: Container( @@ -614,25 +622,16 @@ class _ImageFooterState extends State { final client = StreamChat.of(context).client; - for (var user in _selectedUsers) { - var c = client.channel('messaging', extraData: { - 'members': [ - user.id, - StreamChat.of(context).user.id, - ], - }); - - await c.watch(); - + for (var channel in _selectedChannels) { final message = Message( text: text, attachments: [attachments[widget.currentPage]], ); - await c.sendMessage(message); + await channel.sendMessage(message); } - _selectedUsers.clear(); + _selectedChannels.clear(); Navigator.pop(context); } diff --git a/lib/src/user_avatar.dart b/lib/src/user_avatar.dart index 756252cc..e698d773 100644 --- a/lib/src/user_avatar.dart +++ b/lib/src/user_avatar.dart @@ -61,12 +61,15 @@ class UserAvatar extends StatelessWidget { : streamChatTheme.defaultUserImage(context, user), ), ); + if (selected) { avatar = ClipRRect( borderRadius: (borderRadius ?? streamChatTheme.ownMessageTheme.avatarTheme.borderRadius) + BorderRadius.circular(selectionThickness), child: Container( + constraints: constraints ?? + streamChatTheme.ownMessageTheme.avatarTheme.constraints, color: selectionColor, child: Padding( padding: EdgeInsets.all(selectionThickness), From 209c5a3c24766e9ee472870b98320c3e63753f6c Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 22 Dec 2020 21:45:14 +0530 Subject: [PATCH 04/17] Initial implementation Signed-off-by: Sahil Kumar --- example/lib/main.dart | 1 + lib/src/message_actions_modal.dart | 2 +- lib/src/message_list_view.dart | 34 +- lib/src/message_reactions_modal.dart | 2 +- lib/src/message_widget.dart | 535 +++++++++++++++------------ lib/src/reply_indicator.dart | 56 --- lib/src/stream_channel.dart | 13 + lib/src/stream_chat_theme.dart | 19 +- lib/src/utils.dart | 9 + lib/stream_chat_flutter.dart | 1 - 10 files changed, 359 insertions(+), 313 deletions(-) delete mode 100644 lib/src/reply_indicator.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index f0f1e59b..803df8e6 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -470,6 +470,7 @@ class ThreadPage extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( + backgroundColor: Color.fromRGBO(252, 252, 252, 1), appBar: ThreadHeader( parent: parent, ), diff --git a/lib/src/message_actions_modal.dart b/lib/src/message_actions_modal.dart index 1967cd2b..f4cd47c8 100644 --- a/lib/src/message_actions_modal.dart +++ b/lib/src/message_actions_modal.dart @@ -113,7 +113,7 @@ class MessageActionsModal extends StatelessWidget { messageTheme: messageTheme, showReactions: false, showUsername: false, - showReplyIndicator: false, + showThreadReplyIndicator: false, showUserAvatar: showUserAvatar, showTimestamp: false, translateUserAvatar: false, diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 5a850ad3..24d32d88 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -241,7 +241,7 @@ class _MessageListViewState extends State { builder: (context, snapshot) { if (!snapshot.hasData) { return Center( - child: CircularProgressIndicator(), + child: const CircularProgressIndicator(), ); } @@ -336,18 +336,25 @@ class _MessageListViewState extends State { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ buildParentMessage(widget.parentMessage), - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 32), - child: Container( - padding: const EdgeInsets.all(8), + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Color(0XFFF7F7F7), + Color(0XFFFCFCFC), + ], + ), + ), + child: Padding( + padding: const EdgeInsets.all(8.0), child: Text( - 'Start of thread', + '${widget.parentMessage.replyCount} ${widget.parentMessage.replyCount == 1 ? 'Reply' : 'Replies'}', textAlign: TextAlign.center, + style: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .lastMessageAt, ), - color: Theme.of(context) - .accentColor - .withAlpha(50), ), ), ], @@ -585,7 +592,7 @@ class _MessageListViewState extends State { if (!snapshot.data) { if (direction == QueryDirection.top) { return Container( - height: 50, + height: 52, width: double.infinity, ); } @@ -679,7 +686,8 @@ class _MessageListViewState extends State { final isMyMessage = message.user.id == StreamChat.of(context).user.id; return MessageWidget( - showReplyIndicator: false, + showThreadReplyIndicator: false, + showInChannelIndicator: false, message: message, reverse: isMyMessage, showUsername: !isMyMessage, @@ -748,6 +756,8 @@ class _MessageListViewState extends State { bottom: index == 0 ? 30 : (isNextUser ? 2 : 7), top: 3, ), + showInChannelIndicator: widget.parentMessage == null, + showThreadReplyIndicator: widget.parentMessage == null, showUsername: !isMyMessage && !isNextUser, showSendingIndicator: isMyMessage && (index == 0 || message.status != MessageSendingStatus.SENT) diff --git a/lib/src/message_reactions_modal.dart b/lib/src/message_reactions_modal.dart index db5d8710..e5043e62 100644 --- a/lib/src/message_reactions_modal.dart +++ b/lib/src/message_reactions_modal.dart @@ -105,7 +105,7 @@ class MessageReactionsModal extends StatelessWidget { showReactions: false, showUsername: false, showUserAvatar: showUserAvatar, - showReplyIndicator: false, + showThreadReplyIndicator: false, showTimestamp: false, translateUserAvatar: false, showSendingIndicator: DisplayWidget.gone, diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index c7c6acdb..4fe84311 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -99,8 +99,11 @@ class MessageWidget extends StatefulWidget { final bool allRead; - /// If true the widget will show the reply indicator - final bool showReplyIndicator; + /// If true the widget will show the thread reply indicator + final bool showThreadReplyIndicator; + + /// If true the widget will show the show in channel indicator + final bool showInChannelIndicator; /// The function called when tapping on UserAvatar final void Function(User) onUserAvatarTap; @@ -123,6 +126,7 @@ class MessageWidget extends StatefulWidget { /// Center user avatar with bottom of the message final bool translateUserAvatar; + /// MessageWidget({ Key key, @required this.message, @@ -139,7 +143,8 @@ class MessageWidget extends StatefulWidget { this.showReactionPickerIndicator = false, this.showUserAvatar = DisplayWidget.show, this.showSendingIndicator = DisplayWidget.show, - this.showReplyIndicator = true, + this.showThreadReplyIndicator = true, + this.showInChannelIndicator = true, this.onThreadTap, this.showUsername = true, this.showTimestamp = true, @@ -211,10 +216,23 @@ class MessageWidget extends StatefulWidget { } class _MessageWidgetState extends State { + bool get showThreadReplyIndicator => + widget.showThreadReplyIndicator && widget.message.replyCount > 0; + + bool get showUsername => widget.showUsername; + + bool get showTimeStamp => + widget.message.createdAt != null && widget.showTimestamp; + + bool get showReadList => widget.readList?.isNotEmpty == true; + + bool get showInChannel => + widget.showInChannelIndicator && widget.message?.showInChannel == true; + @override Widget build(BuildContext context) { var leftPadding = widget.showUserAvatar != DisplayWidget.gone - ? widget.messageTheme.avatarTheme.constraints.maxWidth + 16.0 + ? widget.messageTheme.avatarTheme.constraints.maxWidth + 14.5 : 6.0; final hasFiles = @@ -234,140 +252,151 @@ class _MessageWidgetState extends State { crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.end, + Stack( + alignment: AlignmentDirectional.bottomStart, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, - children: [ - if (widget.showUserAvatar == DisplayWidget.show) - _buildUserAvatar(), - SizedBox( - width: 6, - ), - if (widget.showUserAvatar == DisplayWidget.hide) - SizedBox( - width: widget.messageTheme.avatarTheme.constraints - .maxWidth + - 8, - ), - Flexible( - child: PortalEntry( - portal: Container( - transform: Matrix4.translationValues(-16, 2, 0), - child: _buildReactionIndicator(context), - constraints: BoxConstraints(maxWidth: 22 * 6.0), - ), - portalAnchor: Alignment(-1.0, -1.0), - childAnchor: Alignment(1, -1.0), - child: Stack( - clipBehavior: Clip.none, - children: [ - Padding( - padding: widget.showReactions - ? EdgeInsets.only( - top: widget.message.reactionCounts - ?.isNotEmpty == - true - ? 18 - : 0, - ) - : EdgeInsets.zero, - child: (widget.message.isDeleted && - widget.message.status != - MessageSendingStatus - .FAILED_DELETE) - ? Transform( - alignment: Alignment.center, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisSize: MainAxisSize.min, + children: [ + if (widget.showUserAvatar == DisplayWidget.show) + _buildUserAvatar(), + SizedBox(width: 6), + if (widget.showUserAvatar == DisplayWidget.hide) + SizedBox( + width: widget.messageTheme.avatarTheme + .constraints.maxWidth + + 8, + ), + Flexible( + child: PortalEntry( + portal: Container( + transform: + Matrix4.translationValues(-16, 2, 0), + child: _buildReactionIndicator(context), + constraints: + BoxConstraints(maxWidth: 22 * 6.0), + ), + portalAnchor: Alignment(-1.0, -1.0), + childAnchor: Alignment(1, -1.0), + child: Stack( + clipBehavior: Clip.none, + children: [ + Padding( + padding: widget.showReactions + ? EdgeInsets.only( + top: widget.message.reactionCounts + ?.isNotEmpty == + true + ? 18 + : 0, + ) + : EdgeInsets.zero, + child: (widget.message.isDeleted && + widget.message.status != + MessageSendingStatus + .FAILED_DELETE) + ? Transform( + alignment: Alignment.center, + transform: Matrix4.rotationY( + widget.reverse ? pi : 0), + child: DeletedMessage( + reverse: widget.reverse, + borderRadiusGeometry: + widget.borderRadiusGeometry, + borderSide: widget.borderSide, + shape: widget.shape, + messageTheme: + widget.messageTheme, + ), + ) + : Material( + clipBehavior: Clip.antiAlias, + shape: widget.shape ?? + RoundedRectangleBorder( + side: isOnlyEmoji + ? BorderSide.none + : widget.borderSide ?? + BorderSide( + color: Theme.of(context) + .brightness == + Brightness + .dark + ? Colors.white + .withAlpha( + 24) + : Colors.black + .withAlpha( + 24), + ), + borderRadius: widget + .borderRadiusGeometry ?? + BorderRadius.zero, + ), + color: _getBackgroundColor(), + child: Padding( + padding: EdgeInsets.all( + hasFiles ? 2.0 : 0.0), + child: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + mainAxisSize: + MainAxisSize.min, + children: [ + ..._parseAttachments( + context), + if (widget.message.text + .trim() + .isNotEmpty && + !isGiphy) + _buildTextBubble(context), + ], + ), + ), + ), + ), + if (widget.showReactionPickerIndicator) + Positioned( + right: 0, + top: -6, + child: Transform( transform: Matrix4.rotationY( widget.reverse ? pi : 0), - child: DeletedMessage( - reverse: widget.reverse, - borderRadiusGeometry: - widget.borderRadiusGeometry, - borderSide: widget.borderSide, - shape: widget.shape, - messageTheme: widget.messageTheme, - ), - ) - : Material( - clipBehavior: Clip.antiAlias, - shape: widget.shape ?? - RoundedRectangleBorder( - side: isOnlyEmoji - ? BorderSide.none - : widget.borderSide ?? - BorderSide( - color: Theme.of(context) - .brightness == - Brightness - .dark - ? Colors.white - .withAlpha(24) - : Colors.black - .withAlpha( - 24), - ), - borderRadius: widget - .borderRadiusGeometry ?? - BorderRadius.zero, - ), - color: _getBackgroundColor(), - child: Padding( - padding: EdgeInsets.all( - hasFiles ? 2.0 : 0.0), - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - ..._parseAttachments(context), - if (widget.message.text - .trim() - .isNotEmpty && - !isGiphy) - _buildTextBubble(context), - ], + child: CustomPaint( + painter: ReactionBubblePainter( + widget.messageTheme + .reactionsBackgroundColor, + widget.messageTheme + .reactionsBorderColor, ), ), ), - ), - if (widget.showReactionPickerIndicator) - Positioned( - right: 0, - top: -6, - child: Transform( - transform: Matrix4.rotationY( - widget.reverse ? pi : 0), - child: CustomPaint( - painter: ReactionBubblePainter( - widget.messageTheme - .reactionsBackgroundColor, - widget.messageTheme - .reactionsBorderColor, - ), ), - ), - ), - ], + ], + ), + ), ), - ), + ], ), + if (showThreadReplyIndicator || + showUsername || + showTimeStamp || + showInChannel) + SizedBox(height: 20.0), ], ), - if (widget.showReplyIndicator && - widget.message.replyCount > 0) - _buildReplyIndicator(leftPadding), + if (showThreadReplyIndicator || + showUsername || + showTimeStamp || + showInChannel) + _buildBottomRows(leftPadding) ], ), - if ((widget.message.createdAt != null && - widget.showTimestamp) || - widget.showUsername || - widget.readList?.isNotEmpty == true) - _buildBottomRow(leftPadding), ], ), ), @@ -376,6 +405,113 @@ class _MessageWidgetState extends State { ); } + Widget _buildBottomRows(double leftPadding) { + final deleted = widget.message.isDeleted; + var children = []; + if (deleted) { + children.add( + Row( + mainAxisSize: MainAxisSize.min, + children: [ + StreamSvgIcon.eye( + color: Colors.black.withOpacity(0.5), + size: 16.0, + ), + SizedBox(width: 8.0), + Text( + 'Only visible to you', + style: TextStyle( + color: Colors.black.withOpacity(0.5), + fontSize: 12.0, + ), + ), + ], + ), + ); + } else { + final showSendingIndicator = + widget.showSendingIndicator == DisplayWidget.show; + final replyCount = widget.message.replyCount; + final msg = showInChannel + ? 'Thread Reply' + : replyCount != 0 + ? '$replyCount ${replyCount > 1 ? 'Thread Replies' : 'Thread Reply'}' + : 'Thread Reply'; + + final onThreadTap = () async { + try { + var message = widget.message; + if (showInChannel && message.parentId != null) { + final channel = StreamChannel.of(context); + message = await channel.getMessage(widget.message.parentId); + } + return widget.onThreadTap(message); + } catch (e, stk) { + print(e); + print(stk); + return null; + } + }; + + children.addAll([ + if (showSendingIndicator) _buildSendingIndicator(), + if (showReadList) + SizedBox.fromSize( + size: Size((widget.readList.length * 10.0) + 10, 17), + child: Padding( + padding: const EdgeInsets.only(left: 4.0), + child: _buildReadIndicator(), + ), + ), + if (showThreadReplyIndicator || showInChannel) + 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: Row( + 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( + color: widget.messageTheme.replyThreadColor, + ), + ), + ), + ...children.map( + (child) => Transform( + transform: Matrix4.rotationY(widget.reverse ? pi : 0), + alignment: Alignment.center, + child: child, + ), + ), + ].insertBetween(const SizedBox(width: 8.0)), + ), + ); + } + Widget _buildUrlAttachment() { var urlAttachment = widget.message.attachments .firstWhere((element) => element.ogScrapeUrl != null); @@ -394,87 +530,6 @@ class _MessageWidgetState extends State { ); } - Padding _buildBottomRow(double leftPadding) { - return Padding( - padding: EdgeInsets.only( - left: leftPadding, - top: 2, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Transform( - alignment: Alignment.center, - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - child: RichText( - text: TextSpan( - style: widget.messageTheme.createdAt, - children: [ - if (widget.showUsername) - TextSpan( - text: widget.message.user.name, - style: TextStyle( - fontWeight: FontWeight.bold, - color: widget.messageTheme.createdAt.color - .withOpacity(1)), - ), - if (widget.message.createdAt != null && widget.showTimestamp) - TextSpan( - text: Jiffy(widget.message.createdAt.toLocal()) - .format(' HH:mm'), - ), - ], - ), - ), - ), - if (widget.showSendingIndicator == DisplayWidget.show) - _buildSendingIndicator(), - if (widget.readList?.isNotEmpty == true) - SizedBox.fromSize( - size: Size((widget.readList.length * 10.0) + 10, 17), - child: Transform( - alignment: Alignment.center, - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - child: Padding( - padding: const EdgeInsets.only(left: 4.0), - child: _buildReadIndicator(), - ), - ), - ), - if (widget.message.isDeleted) - Transform( - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - alignment: Alignment.center, - child: Padding( - padding: - const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - StreamSvgIcon.eye( - color: Colors.black.withOpacity(0.5), - size: 16.0, - ), - SizedBox( - width: 8.0, - ), - Text( - 'Only visible to you', - style: TextStyle( - color: Colors.black.withOpacity(0.5), - fontSize: 12.0, - ), - ), - ], - ), - ), - ), - ], - ), - ); - } - bool get isGiphy => widget.message.attachments?.any((element) => element.type == 'giphy') == true; @@ -565,7 +620,7 @@ class _MessageWidgetState extends State { true, showReactions: widget.showReactions, showReply: - widget.showReplyIndicator && widget.onThreadTap != null, + widget.showThreadReplyIndicator && widget.onThreadTap != null, ), ); }); @@ -715,38 +770,13 @@ class _MessageWidgetState extends State { return; } - Widget _buildReplyIndicator(double leftPadding) { - return Padding( - padding: EdgeInsets.only( - left: leftPadding, - ), - child: Transform( - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - alignment: Alignment.center, - child: ReplyIndicator( - message: widget.message, - reversed: widget.reverse, - messageTheme: widget.messageTheme, - onTap: widget.onThreadTap != null - ? () { - widget.onThreadTap(widget.message); - } - : null, - ), - ), - ); - } - Widget _buildSendingIndicator() { - return Padding( - padding: const EdgeInsets.only(right: 4.0), - child: Transform( - transform: Matrix4.rotationY(widget.reverse ? pi : 0), - alignment: Alignment.center, - child: SendingIndicator( - message: widget.message, - allRead: widget.allRead, - ), + return Container( + height: widget.messageTheme.createdAt.fontSize + 2, + width: widget.messageTheme.createdAt.fontSize + 2, + child: SendingIndicator( + message: widget.message, + allRead: widget.allRead, ), ); } @@ -958,3 +988,32 @@ class _MessageWidgetState extends State { } } } + +class _ThreadReplyPainter extends CustomPainter { + final Color color; + + const _ThreadReplyPainter({@required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color ?? Color(0XFFDBDBDB) + ..style = PaintingStyle.stroke + ..strokeWidth = 1 + ..strokeCap = StrokeCap.round; + + final path = Path() + ..moveTo(0, 0) + ..quadraticBezierTo(0, size.height * 0.38, 0, size.height * 0.50) + ..quadraticBezierTo( + 0, + size.height, + size.width, + size.height, + ); + canvas.drawPath(path, paint); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/lib/src/reply_indicator.dart b/lib/src/reply_indicator.dart deleted file mode 100644 index cdb1cd57..00000000 --- a/lib/src/reply_indicator.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:stream_chat/stream_chat.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; - -/// A reply button indicator -class ReplyIndicator extends StatelessWidget { - final Message message; - final VoidCallback onTap; - final bool reversed; - final MessageTheme messageTheme; - - const ReplyIndicator({ - Key key, - this.message, - this.onTap, - this.reversed = false, - this.messageTheme, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - var row = [ - Text( - 'Replies: ${message.replyCount}', - style: messageTheme?.replies, - ), - Transform( - transform: Matrix4.rotationY(reversed ? 0 : pi), - alignment: Alignment.center, - child: Icon( - Icons.subdirectory_arrow_left, - color: Theme.of(context).brightness == Brightness.dark - ? Colors.white12 - : Colors.black12, - ), - ), - ]; - - if (!reversed) { - row = row.reversed.toList(); - } - - return GestureDetector( - onTap: onTap, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: row, - ), - ), - ); - } -} diff --git a/lib/src/stream_channel.dart b/lib/src/stream_channel.dart index 001d98ff..d5a4b030 100644 --- a/lib/src/stream_channel.dart +++ b/lib/src/stream_channel.dart @@ -267,6 +267,19 @@ class StreamChannelState extends State { return state; } + /// + Future getMessage(String messageId) async { + var message = channel.state.messages.firstWhere( + (it) => it.id == messageId, + orElse: () => null, + ); + if (message == null) { + final response = await channel.getMessagesById([messageId]); + message = response.messages.first; + } + return message; + } + /// Reloads the channel with latest message Future reloadChannel() => queryAtMessage(before: 30); diff --git a/lib/src/stream_chat_theme.dart b/lib/src/stream_chat_theme.dart index 54b096d1..a0f552c4 100644 --- a/lib/src/stream_chat_theme.dart +++ b/lib/src/stream_chat_theme.dart @@ -191,6 +191,8 @@ class StreamChatThemeData { this.ownMessageTheme.messageBackgroundColor, avatarTheme: ownMessageTheme?.avatarTheme ?? this.ownMessageTheme.avatarTheme, + replyThreadColor: ownMessageTheme?.replyThreadColor ?? + this.ownMessageTheme.replyThreadColor, ) ?? this.ownMessageTheme, otherMessageTheme: otherMessageTheme?.copyWith( @@ -209,6 +211,8 @@ class StreamChatThemeData { this.otherMessageTheme.messageBackgroundColor, avatarTheme: otherMessageTheme?.avatarTheme ?? this.otherMessageTheme.avatarTheme, + replyThreadColor: ownMessageTheme?.replyThreadColor ?? + this.ownMessageTheme.replyThreadColor, ) ?? this.otherMessageTheme, reactionIcons: reactionIcons ?? this.reactionIcons, @@ -297,16 +301,17 @@ class StreamChatThemeData { color: isDark ? Colors.white.withOpacity(.5) : Colors.black.withOpacity(.5), - fontSize: 11, + fontSize: 12, ), replies: TextStyle( color: accentColor, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w600, fontSize: 12, ), messageBackgroundColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA), reactionsBackgroundColor: isDark ? Colors.black : Colors.white, reactionsBorderColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA), + replyThreadColor: isDark ? Color(0xff191919) : Color(0xffEAEAEA), avatarTheme: AvatarTheme( borderRadius: BorderRadius.circular(20), constraints: BoxConstraints.tightFor( @@ -330,17 +335,19 @@ class StreamChatThemeData { color: isDark ? Colors.white.withOpacity(.5) : Colors.black.withOpacity(.5), - fontSize: 11, + fontSize: 12, ), replies: TextStyle( color: accentColor, - fontWeight: FontWeight.bold, + fontWeight: FontWeight.w600, fontSize: 12, ), messageLinks: TextStyle( color: accentColor, ), messageBackgroundColor: isDark ? Colors.black : Colors.white, + replyThreadColor: + isDark ? Colors.white.withAlpha(24) : Colors.black.withAlpha(24), avatarTheme: AvatarTheme( borderRadius: BorderRadius.circular(20), constraints: BoxConstraints.tightFor( @@ -455,6 +462,7 @@ class MessageTheme { final Color messageBackgroundColor; final Color reactionsBackgroundColor; final Color reactionsBorderColor; + final Color replyThreadColor; final AvatarTheme avatarTheme; const MessageTheme({ @@ -465,6 +473,7 @@ class MessageTheme { this.messageBackgroundColor, this.reactionsBackgroundColor, this.reactionsBorderColor, + this.replyThreadColor, this.avatarTheme, this.createdAt, }); @@ -479,6 +488,7 @@ class MessageTheme { AvatarTheme avatarTheme, Color reactionsBackgroundColor, Color reactionsBorderColor, + Color replyThreadColor, }) => MessageTheme( messageText: messageText ?? this.messageText, @@ -492,6 +502,7 @@ class MessageTheme { reactionsBackgroundColor: reactionsBackgroundColor ?? this.reactionsBackgroundColor, reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor, + replyThreadColor: replyThreadColor ?? this.replyThreadColor, ); } diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 74b1da64..681a28aa 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -92,3 +92,12 @@ Future showConfirmationDialog( /// Get random png with initials String getRandomPicUrl(User user) => 'https://getstream.io/random_png/?id=${user.id}&name=${user.name}'; + +/// List extension +extension ListX on List { + /// Insert any item inBetween the list items + List insertBetween(T item) => expand((e) sync* { + yield item; + yield e; + }).skip(1).toList(growable: false); +} diff --git a/lib/stream_chat_flutter.dart b/lib/stream_chat_flutter.dart index 5dd46fa3..1cef26a6 100644 --- a/lib/stream_chat_flutter.dart +++ b/lib/stream_chat_flutter.dart @@ -21,7 +21,6 @@ export 'src/message_list_view.dart'; export 'src/message_text.dart'; export 'src/message_widget.dart'; export 'src/reaction_picker.dart'; -export 'src/reply_indicator.dart'; export 'src/sending_indicator.dart'; export 'src/stream_channel.dart'; export 'src/stream_chat.dart'; From 011b939c282596d6318934155cd05ce794d79fc0 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 23 Dec 2020 13:58:02 +0530 Subject: [PATCH 05/17] [Thread Header] Fix channelName overflow Signed-off-by: Sahil Kumar --- lib/src/thread_header.dart | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/src/thread_header.dart b/lib/src/thread_header.dart index cfb6b1f1..8e165af7 100644 --- a/lib/src/thread_header.dart +++ b/lib/src/thread_header.dart @@ -112,11 +112,13 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { .channelHeaderTheme .lastMessageAt, ), - ChannelName( - textStyle: StreamChatTheme.of(context) - .channelTheme - .channelHeaderTheme - .lastMessageAt, + Flexible( + child: ChannelName( + textStyle: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .lastMessageAt, + ), ), ], ), From c2633cecb253fa0c8dfca5abf8f303d487925a7a Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 23 Dec 2020 14:02:18 +0530 Subject: [PATCH 06/17] fix typo Signed-off-by: Sahil Kumar --- lib/src/message_widget.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index 4fe84311..c52e7f63 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -394,7 +394,7 @@ class _MessageWidgetState extends State { showUsername || showTimeStamp || showInChannel) - _buildBottomRows(leftPadding) + _buildBottomRow(leftPadding) ], ), ], @@ -405,7 +405,7 @@ class _MessageWidgetState extends State { ); } - Widget _buildBottomRows(double leftPadding) { + Widget _buildBottomRow(double leftPadding) { final deleted = widget.message.isDeleted; var children = []; if (deleted) { From 538029a5de21f168820de12edd4bc754d5be19c8 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 23 Dec 2020 14:48:23 +0530 Subject: [PATCH 07/17] [Stream Channel] Fix getReplies Signed-off-by: Sahil Kumar --- lib/src/stream_channel.dart | 59 +++++++++++++++++++++++++++++-------- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/lib/src/stream_channel.dart b/lib/src/stream_channel.dart index d5a4b030..be6cb4a5 100644 --- a/lib/src/stream_channel.dart +++ b/lib/src/stream_channel.dart @@ -134,6 +134,40 @@ class StreamChannelState extends State { return _queryBottomMessages(); } + //if (_queryMessageController.value == true || _paginationEnded) { + // return; + // } + // + // _queryMessageController.add(true); + // + // String firstId; + // if (widget.channel.state.threads.containsKey(parentId)) { + // final thread = widget.channel.state.threads[parentId]; + // + // if (thread != null && thread.isNotEmpty) { + // firstId = thread?.first?.id; + // } + // } + // + // final messageLimit = 50; + // return widget.channel + // .getReplies( + // parentId, + // PaginationParams( + // lessThan: firstId, + // limit: messageLimit, + // ), + // preferOffline: true, + // ) + // .then((res) { + // if (res.messages.isEmpty || res.messages.length < messageLimit) { + // _paginationEnded = true; + // } + // _queryMessageController.add(false); + // }).catchError((e, stack) { + // _queryMessageController.addError(e, stack); + // }); + /// Calls [channel.getReplies] updating [queryMessage] stream Future getReplies( String parentId, { @@ -143,23 +177,24 @@ class StreamChannelState extends State { if (_topPaginationEnded || _queryTopMessagesController.value) return; _queryTopMessagesController.add(true); - if (!channel.state.threads.containsKey(parentId)) { - return _queryTopMessagesController.add(false); + Message message; + if (channel.state.threads.containsKey(parentId)) { + final thread = channel.state.threads[parentId]; + if (thread.isNotEmpty) { + message = thread.first; + } } - final thread = channel.state.threads[parentId]; - - if (thread.isEmpty) return _queryTopMessagesController.add(false); - - final message = thread.first; - try { - final state = await queryBeforeMessage( - message.id, - limit: limit, + final response = await channel.getReplies( + parentId, + PaginationParams( + lessThan: message?.id, + limit: limit, + ), preferOffline: preferOffline, ); - if (state.messages.isEmpty || state.messages.length < limit) { + if (response.messages.isEmpty || response.messages.length < limit) { _topPaginationEnded = true; } _queryTopMessagesController.add(false); From 6b97879009eed22aa42474ced2f423cf3f4d8459 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 23 Dec 2020 14:53:34 +0530 Subject: [PATCH 08/17] cleanup Signed-off-by: Sahil Kumar --- lib/src/stream_channel.dart | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/lib/src/stream_channel.dart b/lib/src/stream_channel.dart index be6cb4a5..8b4b97ef 100644 --- a/lib/src/stream_channel.dart +++ b/lib/src/stream_channel.dart @@ -134,40 +134,6 @@ class StreamChannelState extends State { return _queryBottomMessages(); } - //if (_queryMessageController.value == true || _paginationEnded) { - // return; - // } - // - // _queryMessageController.add(true); - // - // String firstId; - // if (widget.channel.state.threads.containsKey(parentId)) { - // final thread = widget.channel.state.threads[parentId]; - // - // if (thread != null && thread.isNotEmpty) { - // firstId = thread?.first?.id; - // } - // } - // - // final messageLimit = 50; - // return widget.channel - // .getReplies( - // parentId, - // PaginationParams( - // lessThan: firstId, - // limit: messageLimit, - // ), - // preferOffline: true, - // ) - // .then((res) { - // if (res.messages.isEmpty || res.messages.length < messageLimit) { - // _paginationEnded = true; - // } - // _queryMessageController.add(false); - // }).catchError((e, stack) { - // _queryMessageController.addError(e, stack); - // }); - /// Calls [channel.getReplies] updating [queryMessage] stream Future getReplies( String parentId, { From beea7e1c673686398fe4f1b31078a4b05be12073 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 23 Dec 2020 14:57:03 +0530 Subject: [PATCH 09/17] [Message Actions Modal] Hide showInChannel Indicator Signed-off-by: Sahil Kumar --- lib/src/message_actions_modal.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/message_actions_modal.dart b/lib/src/message_actions_modal.dart index f4cd47c8..8013a8b3 100644 --- a/lib/src/message_actions_modal.dart +++ b/lib/src/message_actions_modal.dart @@ -118,6 +118,7 @@ class MessageActionsModal extends StatelessWidget { showTimestamp: false, translateUserAvatar: false, showReactionPickerIndicator: true, + showInChannelIndicator: false, showSendingIndicator: DisplayWidget.gone, shape: messageShape, ), From 7efb6ca3dd25d07d12ba2f09e12dae36796840f1 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 23 Dec 2020 15:13:17 +0530 Subject: [PATCH 10/17] [MessageListView] Change parentMessage gradient alignment Signed-off-by: Sahil Kumar --- lib/src/message_list_view.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 24d32d88..a856db2b 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -339,6 +339,8 @@ class _MessageListViewState extends State { Container( decoration: BoxDecoration( gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, colors: [ Color(0XFFF7F7F7), Color(0XFFFCFCFC), From bb490849521ecaeba839486d883b938f2efc4b1b Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 23 Dec 2020 15:30:41 +0530 Subject: [PATCH 11/17] Hide timeStamp and sendingIndicator for a threaded reply message Signed-off-by: Sahil Kumar --- lib/src/message_widget.dart | 38 +++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index c52e7f63..9d81d679 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -428,23 +428,11 @@ class _MessageWidgetState extends State { ], ), ); - } else { - final showSendingIndicator = - widget.showSendingIndicator == DisplayWidget.show; - final replyCount = widget.message.replyCount; - final msg = showInChannel - ? 'Thread Reply' - : replyCount != 0 - ? '$replyCount ${replyCount > 1 ? 'Thread Replies' : 'Thread Reply'}' - : 'Thread Reply'; - + } else if (showInChannel) { final onThreadTap = () async { try { - var message = widget.message; - if (showInChannel && message.parentId != null) { - final channel = StreamChannel.of(context); - message = await channel.getMessage(widget.message.parentId); - } + final channel = StreamChannel.of(context); + final message = await channel.getMessage(widget.message.parentId); return widget.onThreadTap(message); } catch (e, stk) { print(e); @@ -452,6 +440,24 @@ class _MessageWidgetState extends State { return null; } }; + children.add( + InkWell( + onTap: widget.onThreadTap != null ? onThreadTap : null, + child: Text('Thread Reply', style: widget.messageTheme?.replies), + ), + ); + } else { + final showSendingIndicator = + widget.showSendingIndicator == DisplayWidget.show; + final replyCount = widget.message.replyCount; + final msg = replyCount != 0 + ? '$replyCount ${replyCount > 1 ? 'Thread Replies' : 'Thread Reply'}' + : 'Thread Reply'; + + final onThreadTap = () async { + var message = widget.message; + return widget.onThreadTap(message); + }; children.addAll([ if (showSendingIndicator) _buildSendingIndicator(), @@ -463,7 +469,7 @@ class _MessageWidgetState extends State { child: _buildReadIndicator(), ), ), - if (showThreadReplyIndicator || showInChannel) + if (showThreadReplyIndicator) InkWell( onTap: widget.onThreadTap != null ? onThreadTap : null, child: Text(msg, style: widget.messageTheme?.replies), From d0a9ee7fae30e27ec2e134b35e94563aaad3d485 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Wed, 23 Dec 2020 16:53:04 +0530 Subject: [PATCH 12/17] fix: Corrected shimmer --- lib/src/channel_list_view.dart | 125 +++++++++++++++++++++------------ 1 file changed, 80 insertions(+), 45 deletions(-) diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart index d5278f5e..66ec09f7 100644 --- a/lib/src/channel_list_view.dart +++ b/lib/src/channel_list_view.dart @@ -312,11 +312,13 @@ class _ChannelListViewState extends State children: List.generate( 25, (i) { - if (i % 2 != 0) { - if (widget.separatorBuilder != null) { - return widget.separatorBuilder(context, i); + if (widget.crossAxisCount == 1) { + if (i % 2 != 0) { + if (widget.separatorBuilder != null) { + return widget.separatorBuilder(context, i); + } + return _separatorBuilder(context, i); } - return _separatorBuilder(context, i); } return _buildLoadingItem(); }, @@ -325,63 +327,96 @@ class _ChannelListViewState extends State } Shimmer _buildLoadingItem() { - return Shimmer.fromColors( - baseColor: Color(0xffE5E5E5), - highlightColor: Color(0xffffffff), - child: ListTile( - leading: Container( - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - constraints: BoxConstraints.tightFor( - height: 40, - width: 40, - ), + if (widget.crossAxisCount > 1) { + return Shimmer.fromColors( + baseColor: Color(0xffE5E5E5), + highlightColor: Color(0xffffffff), + child: Column( + children: [ + SizedBox( + height: 4.0, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + for (int i = 0; i < widget.crossAxisCount; i++) + Container( + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + constraints: BoxConstraints.tightFor( + height: 70, + width: 70, + ), + ), + ], + ), + SizedBox( + height: 16.0, + ), + ], ), - title: Align( - alignment: Alignment.centerLeft, - child: Container( + ); + } else { + return Shimmer.fromColors( + baseColor: Color(0xffE5E5E5), + highlightColor: Color(0xffffffff), + child: ListTile( + leading: Container( decoration: BoxDecoration( color: Colors.white, - borderRadius: BorderRadius.circular(11), + shape: BoxShape.circle, ), constraints: BoxConstraints.tightFor( - height: 16, - width: 82, + height: 40, + width: 40, ), ), - ), - subtitle: Row( - children: [ - Align( - alignment: Alignment.centerLeft, - child: Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(11), - ), - constraints: BoxConstraints.tightFor( - height: 16, - width: 238, - ), - ), - ), - Container( - margin: const EdgeInsets.only(left: 16), + title: Align( + alignment: Alignment.centerLeft, + child: Container( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(11), ), constraints: BoxConstraints.tightFor( height: 16, - width: 42, + width: 82, ), ), - ], + ), + subtitle: Row( + children: [ + Align( + alignment: Alignment.centerLeft, + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(11), + ), + constraints: BoxConstraints.tightFor( + height: 16, + width: 238, + ), + ), + ), + Container( + margin: const EdgeInsets.only(left: 16), + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(11), + ), + constraints: BoxConstraints.tightFor( + height: 16, + width: 42, + ), + ), + ], + ), ), - ), - ); + ); + } } Widget _buildErrorWidget( From 25342266a06dc1eeaa404992eb732dd5705c4ed4 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 25 Dec 2020 23:36:16 +0530 Subject: [PATCH 13/17] fix: Corrected keyboard --- lib/src/image_footer.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/src/image_footer.dart b/lib/src/image_footer.dart index 79c1f501..b1496de3 100644 --- a/lib/src/image_footer.dart +++ b/lib/src/image_footer.dart @@ -64,6 +64,7 @@ class _ImageFooterState extends State { bool _userSearchMode = false; TextEditingController _searchController; TextEditingController _messageController = TextEditingController(); + FocusNode _messageFocusNode = FocusNode(); String _userNameQuery; bool _isSearchActive = false; @@ -91,6 +92,7 @@ class _ImageFooterState extends State { void initState() { super.initState(); _searchController = TextEditingController()..addListener(_userNameListener); + _messageFocusNode.addListener(() {setState(() {});}); } @override @@ -258,7 +260,7 @@ class _ImageFooterState extends State { ); } - Widget _buildShareModal(context) { + void _buildShareModal(context) { showDialog( context: context, builder: (context) { @@ -266,7 +268,7 @@ class _ImageFooterState extends State { modalSetStateCallback = modalSetState; return Padding( padding: EdgeInsets.only( - top: _userSearchMode + top: _userSearchMode || _messageFocusNode.hasFocus ? 16.0 : MediaQuery.of(context).size.height / 2, left: 8.0, @@ -278,7 +280,6 @@ class _ImageFooterState extends State { ), clipBehavior: Clip.antiAlias, child: Scaffold( - resizeToAvoidBottomInset: false, body: UsersBloc( child: Column( children: [ @@ -552,6 +553,7 @@ class _ImageFooterState extends State { padding: const EdgeInsets.only(left: 8.0), child: TextField( controller: _messageController, + focusNode: _messageFocusNode, onChanged: (val) { modalSetState(() {}); }, From 0f36fdcf4f89a821cc0964edbdf8418f8cefb455 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 25 Dec 2020 23:36:35 +0530 Subject: [PATCH 14/17] fmt --- lib/src/image_footer.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/image_footer.dart b/lib/src/image_footer.dart index b1496de3..c071bb30 100644 --- a/lib/src/image_footer.dart +++ b/lib/src/image_footer.dart @@ -92,7 +92,9 @@ class _ImageFooterState extends State { void initState() { super.initState(); _searchController = TextEditingController()..addListener(_userNameListener); - _messageFocusNode.addListener(() {setState(() {});}); + _messageFocusNode.addListener(() { + setState(() {}); + }); } @override From 38d3140ab10c49327b591578a5673ba6eb5bdb6e Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 28 Dec 2020 23:30:35 +0530 Subject: [PATCH 15/17] [ChannelListView] Convert listView.custom into listView.separated Signed-off-by: Sahil Kumar --- lib/src/channel_list_view.dart | 38 ++++++++++++---------------------- 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart index 66ec09f7..5efa1e29 100644 --- a/lib/src/channel_list_view.dart +++ b/lib/src/channel_list_view.dart @@ -278,21 +278,19 @@ class _ChannelListViewState extends State }, ); } else { - child = ListView.custom( + child = ListView.separated( physics: AlwaysScrollableScrollPhysics(), - controller: _scrollController, - childrenDelegate: SliverChildBuilderDelegate( - (context, i) { - return _itemBuilder(context, i, channels); - }, - childCount: (channels.length * 2) + 1, - findChildIndexCallback: (key) { - final ValueKey valueKey = key; - final index = channels.indexWhere( - (channel) => 'CHANNEL-${channel.id}' == valueKey.value); - return index != -1 ? (index * 2) : null; - }, - ), + itemCount: + channels.isNotEmpty ? channels.length + 1 : channels.length, + separatorBuilder: (_, index) { + if (widget.separatorBuilder != null) { + return widget.separatorBuilder(context, index); + } + return _separatorBuilder(context, index); + }, + itemBuilder: (context, index) { + return _listItemBuilder(context, index, channels); + }, ); } } @@ -483,20 +481,10 @@ class _ChannelListViewState extends State ); } - Widget _itemBuilder(context, int i, List channels) { - if (i % 2 != 0) { - if (widget.separatorBuilder != null) { - return widget.separatorBuilder(context, i); - } - return _separatorBuilder(context, i); - } - - i = i ~/ 2; - + Widget _listItemBuilder(BuildContext context, int i, List channels) { final channelsProvider = ChannelsBloc.of(context); if (i < channels.length) { final channel = channels[i]; - ChannelTapCallback onTap; if (widget.onChannelTap != null) { onTap = widget.onChannelTap; From 38e3d541bbd410b492cb4e9e4e07bc7f035f7e06 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 28 Dec 2020 23:31:02 +0530 Subject: [PATCH 16/17] [Channel Image] Fix selection state Signed-off-by: Sahil Kumar --- lib/src/channel_image.dart | 41 ++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/lib/src/channel_image.dart b/lib/src/channel_image.dart index d79b2efa..ad3e930a 100644 --- a/lib/src/channel_image.dart +++ b/lib/src/channel_image.dart @@ -53,6 +53,8 @@ class ChannelImage extends StatelessWidget { this.showOnlineStatus = true, this.borderRadius, this.selected = false, + this.selectionColor = const Color(0xFF006CFF), + this.selectionThickness = 4, }) : super(key: key); final BorderRadius borderRadius; @@ -70,6 +72,10 @@ class ChannelImage extends StatelessWidget { final bool selected; + final Color selectionColor; + + final double selectionThickness; + @override Widget build(BuildContext context) { final streamChat = StreamChat.of(context); @@ -97,12 +103,10 @@ class ChannelImage extends StatelessWidget { .channelPreviewTheme .avatarTheme .constraints, - onTap: onTap != null - ? (_) { - onTap(); - } - : null, + onTap: onTap != null ? (_) => onTap() : null, selected: selected, + selectionColor: selectionColor, + selectionThickness: selectionThickness, ); }); } else { @@ -123,10 +127,12 @@ class ChannelImage extends StatelessWidget { .constraints, onTap: onTap, selected: selected, + selectionColor: selectionColor, + selectionThickness: selectionThickness, ); } - return ClipRRect( + Widget child = ClipRRect( borderRadius: borderRadius ?? StreamChatTheme.of(context) .channelPreviewTheme @@ -175,6 +181,29 @@ class ChannelImage extends StatelessWidget { ), ), ); + if (selected) { + child = ClipRRect( + borderRadius: (borderRadius ?? + StreamChatTheme.of(context) + .ownMessageTheme + .avatarTheme + .borderRadius) + + BorderRadius.circular(selectionThickness), + child: Container( + constraints: constraints ?? + StreamChatTheme.of(context) + .ownMessageTheme + .avatarTheme + .constraints, + color: selectionColor, + child: Padding( + padding: EdgeInsets.all(selectionThickness), + child: child, + ), + ), + ); + } + return child; }); } } From 27a83e77918279ee4d2f26c8b9c6a68842e69421 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 28 Dec 2020 23:32:57 +0530 Subject: [PATCH 17/17] Minor refactoring Signed-off-by: Sahil Kumar --- lib/src/image_footer.dart | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/lib/src/image_footer.dart b/lib/src/image_footer.dart index c071bb30..00b38a90 100644 --- a/lib/src/image_footer.dart +++ b/lib/src/image_footer.dart @@ -16,8 +16,6 @@ import 'package:stream_chat_flutter/src/stream_chat.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'stream_channel.dart'; - class ImageFooter extends StatefulWidget { /// Callback to call when pressing the back button. /// By default it calls [Navigator.pop] @@ -66,8 +64,7 @@ class _ImageFooterState extends State { TextEditingController _messageController = TextEditingController(); FocusNode _messageFocusNode = FocusNode(); - String _userNameQuery; - bool _isSearchActive = false; + String _channelNameQuery; List _selectedChannels = []; bool _loading = false; @@ -81,8 +78,7 @@ class _ImageFooterState extends State { _debounce = Timer(const Duration(milliseconds: 350), () { if (mounted && modalSetStateCallback != null) { modalSetStateCallback(() { - _userNameQuery = _searchController.text; - _isSearchActive = _userNameQuery.isNotEmpty; + _channelNameQuery = _searchController.text; }); } }); @@ -294,15 +290,15 @@ class _ImageFooterState extends State { child: ChannelsBloc( child: ChannelListView( selectedChannels: _selectedChannels, - onChannelTap: (user, _) { + onChannelTap: (channel, _) { _searchController.clear(); - if (!_selectedChannels.contains(user)) { + if (!_selectedChannels.contains(channel)) { modalSetState(() { - _selectedChannels.add(user); + _selectedChannels.add(channel); }); } else { modalSetState(() { - _selectedChannels.remove(user); + _selectedChannels.remove(channel); }); } }, @@ -313,7 +309,7 @@ class _ImageFooterState extends State { filter: { if (_searchController.text.isNotEmpty) 'name': { - r'$autocomplete': _userNameQuery, + r'$autocomplete': _channelNameQuery, }, 'id': { r'$ne': StreamChat.of(context).user.id, @@ -346,7 +342,7 @@ class _ImageFooterState extends State { ), ), Text( - 'No user matches these keywords...'), + 'No chat matches these keywords...'), ], ), ), @@ -624,8 +620,6 @@ class _ImageFooterState extends State { _messageController.clear(); - final client = StreamChat.of(context).client; - for (var channel in _selectedChannels) { final message = Message( text: text,