From 390062f1c20879183578be66a59a7f6ce5521cdb Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 2 Dec 2020 19:30:24 +0530 Subject: [PATCH 01/74] feat: 2 way pagination Signed-off-by: Sahil Kumar --- example/lib/main.dart | 230 +++++++++++++++-------------- example/lib/routes/app_routes.dart | 8 +- lib/src/message_list_view.dart | 19 ++- lib/src/stream_channel.dart | 98 +++++++++--- pubspec.yaml | 5 +- 5 files changed, 219 insertions(+), 141 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 4b529e23..f1667139 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -294,130 +294,136 @@ class _ChannelListPageState extends State { @override Widget build(BuildContext context) { final user = StreamChat.of(context).user; - return ChannelsBloc( - child: MessageSearchBloc( - child: Column( - children: [ - SearchTextField( - controller: _controller, - showCloseButton: _isSearchActive, - ), - Expanded( - child: AnimatedSwitcher( - duration: const Duration(milliseconds: 350), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onPanDown: (_) => FocusScope.of(context).unfocus(), - child: _isSearchActive - ? MessageSearchListView( - messageQuery: _channelQuery, - filters: { - 'members': { - r'$in': [user.id] - } - }, - sortOptions: [ - SortOption( - 'created_at', - direction: SortOption.ASC, - ), - ], - paginationParams: PaginationParams(limit: 20), - onItemTap: (message) {}, - ) - : ChannelListView( - onStartChatPressed: () { - Navigator.pushNamed(context, Routes.NEW_CHAT); - }, - swipeToAction: true, - filter: { - 'members': { - r'$in': [user.id], + return WillPopScope( + onWillPop: () async { + if (_isSearchActive) { + _controller.clear(); + setState(() => _isSearchActive = false); + return false; + } + return true; + }, + child: ChannelsBloc( + child: MessageSearchBloc( + child: Column( + children: [ + SearchTextField( + controller: _controller, + showCloseButton: _isSearchActive, + ), + Expanded( + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 350), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onPanDown: (_) => FocusScope.of(context).unfocus(), + child: _isSearchActive + ? MessageSearchListView( + messageQuery: _channelQuery, + filters: { + 'members': { + r'$in': [user.id] + } }, - }, - options: { - 'presence': true, - }, - pagination: PaginationParams( - limit: 20, + sortOptions: [ + SortOption( + 'created_at', + direction: SortOption.ASC, + ), + ], + paginationParams: PaginationParams(limit: 20), + onItemTap: (messageResponse) async { + final client = StreamChat.of(context).client; + final message = messageResponse.message; + final channel = Channel.fromState( + client, + ChannelState( + channel: messageResponse.channel, + messages: [message], + ), + ); + await Future.wait([ + channel.query( + messagesPagination: PaginationParams( + lessThan: message.id, + limit: 25, + ), + preferOffline: true, + ), + channel.query( + messagesPagination: PaginationParams( + greaterThan: message.id, + limit: 25, + ), + preferOffline: true, + ), + ]); + final messages = channel.state.messages; + final totalMessages = messages.length; + final messageIndex = messages + .indexWhere((e) => e.id == message.id); + final initialIndex = totalMessages - messageIndex; + final bool isFirstMessage = messageIndex == 0; + Navigator.pushNamed( + context, + Routes.CHANNEL_PAGE, + arguments: ChannelPageArgs( + channel: channel, + initialScrollIndex: initialIndex, + initialAlignment: isFirstMessage ? 0 : 0.5, + ), + ); + }, + ) + : ChannelListView( + onStartChatPressed: () { + Navigator.pushNamed(context, Routes.NEW_CHAT); + }, + swipeToAction: true, + filter: { + 'members': { + r'$in': [user.id], + }, + }, + options: { + 'presence': true, + }, + pagination: PaginationParams( + limit: 20, + ), + channelWidget: ChannelPage(), ), - channelWidget: ChannelPage(), - ), + ), ), ), - ), - ], + ], + ), ), ), ); } } -class ChannelQuerySearchResultPage extends StatelessWidget { - final Stream> searchResultStream; +class ChannelPageArgs { + final Channel channel; + final int initialScrollIndex; + final double initialAlignment; - const ChannelQuerySearchResultPage({ - Key key, - @required this.searchResultStream, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return StreamBuilder>( - initialData: const [], - stream: searchResultStream, - builder: (context, snapshot) { - final result = snapshot.data; - return Column( - children: [ - if (result.isNotEmpty) - Container( - width: double.maxFinite, - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.centerLeft, - end: Alignment.centerRight, - colors: [ - Colors.black.withOpacity(0.02), - Colors.white.withOpacity(0.05), - ], - stops: [0, 1], - ), - ), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: 8, - horizontal: 8, - ), - child: Text( - '${result.length} results', - style: TextStyle( - color: Colors.black.withOpacity(0.5), - ), - ), - ), - ), - Expanded( - child: ListView.builder( - itemCount: result.length, - itemBuilder: (context, index) { - return ListTile( - leading: UserAvatar(), - title: Text(result[index].toJson().toString()), - ); - }, - ), - ), - ], - ); - }, - ); - } + const ChannelPageArgs({ + this.channel, + this.initialScrollIndex = 0, + this.initialAlignment = 0, + }); } class ChannelPage extends StatelessWidget { + final int initialScrollIndex; + final double initialAlignment; + const ChannelPage({ Key key, + this.initialScrollIndex = 0, + this.initialAlignment = 0, }) : super(key: key); @override @@ -433,6 +439,8 @@ class ChannelPage extends StatelessWidget { child: Stack( children: [ MessageListView( + initialScrollIndex: initialScrollIndex, + initialAlignment: initialAlignment, threadBuilder: (_, parentMessage) { return ThreadPage( parent: parentMessage, @@ -467,10 +475,14 @@ class ChannelPage extends StatelessWidget { class ThreadPage extends StatelessWidget { final Message parent; + final int initialScrollIndex; + final double initialAlignment; ThreadPage({ Key key, this.parent, + this.initialScrollIndex = 0, + this.initialAlignment = 0, }) : super(key: key); @override @@ -484,6 +496,8 @@ class ThreadPage extends StatelessWidget { Expanded( child: MessageListView( parentMessage: parent, + initialScrollIndex: initialScrollIndex, + initialAlignment: initialAlignment, ), ), if (parent.type != 'deleted') diff --git a/example/lib/routes/app_routes.dart b/example/lib/routes/app_routes.dart index 6b0529e1..8ee907b8 100644 --- a/example/lib/routes/app_routes.dart +++ b/example/lib/routes/app_routes.dart @@ -34,9 +34,13 @@ class AppRoutes { return MaterialPageRoute( settings: const RouteSettings(name: Routes.CHANNEL_PAGE), builder: (_) { + final arg = args as ChannelPageArgs; return StreamChannel( - channel: args as Channel, - child: ChannelPage(), + channel: arg.channel, + child: ChannelPage( + initialScrollIndex: arg.initialScrollIndex, + initialAlignment: arg.initialAlignment, + ), ); }); case Routes.NEW_CHAT: diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 3feda8ce..f9e40bf7 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -480,8 +480,8 @@ class _MessageListViewState extends State { } else { streamChannel.getReplies(widget.parentMessage.id); } + _topWasVisible = !topIsVisible; } - _topWasVisible = topIsVisible; }, ); } @@ -515,14 +515,21 @@ class _MessageListViewState extends State { key: ValueKey('BOTTOM-MESSAGE'), onVisibilityChanged: (visibility) { final isVisible = visibility.visibleBounds != Rect.zero; - if (isVisible && - !_bottomWasVisible && - streamChannel.channel.config?.readEvents == true) { - if (streamChannel.channel.state.unreadCount > 0) { + if (isVisible && !_bottomWasVisible) { + if (widget.parentMessage == null) { + streamChannel.queryMessages(direction: QueryDirection.bottom); + } else { + streamChannel.getReplies( + widget.parentMessage.id, + direction: QueryDirection.bottom, + ); + } + if (streamChannel.channel.config?.readEvents == true && + streamChannel.channel.state.unreadCount > 0) { streamChannel.channel.markRead(); } + _bottomWasVisible = !isVisible; } - _bottomWasVisible = isVisible; if (mounted) { setState(() { _showScrollToBottom = !isVisible; diff --git a/lib/src/stream_channel.dart b/lib/src/stream_channel.dart index 6fafa070..5dbfd103 100644 --- a/lib/src/stream_channel.dart +++ b/lib/src/stream_channel.dart @@ -4,6 +4,8 @@ import 'package:flutter/material.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/stream_chat.dart'; +enum QueryDirection { top, bottom } + /// Widget used to provide information about the channel to the widget tree /// /// Use [StreamChannel.of] to get the current [StreamChannelState] instance. @@ -52,34 +54,57 @@ class StreamChannelState extends State { /// The stream notifying the state of queryMessage call Stream get queryMessage => _queryMessageController.stream; - bool _paginationEnded = false; + bool _topPaginationEnded = false; + bool _bottomPaginationEnded = false; /// Calls [channel.query] updating [queryMessage] stream - void queryMessages() { - if (_queryMessageController.value == true || _paginationEnded) { + void queryMessages({QueryDirection direction = QueryDirection.top}) { + if (_queryMessageController.value == true || + (_topPaginationEnded && _bottomPaginationEnded)) { return; } _queryMessageController.add(true); - String firstId; - if (channel.state.messages.isNotEmpty) { - firstId = channel.state.messages.first.id; - } + String id; + PaginationParams params; - final messageLimit = 50; + final messageLimit = 25; + + if (channel.state.messages.isNotEmpty) { + switch (direction) { + case QueryDirection.top: + id = channel.state.messages.first.id; + params = PaginationParams( + lessThan: id, + limit: messageLimit, + ); + break; + case QueryDirection.bottom: + id = channel.state.messages.last.id; + params = PaginationParams( + greaterThan: id, + limit: messageLimit, + ); + break; + } + } widget.channel .query( - messagesPagination: PaginationParams( - lessThan: firstId, - limit: messageLimit, - ), + messagesPagination: params, preferOffline: true, ) .then((res) { if (res.messages.isEmpty || res.messages.length < messageLimit) { - _paginationEnded = true; + switch (direction) { + case QueryDirection.top: + _topPaginationEnded = true; + break; + case QueryDirection.bottom: + _bottomPaginationEnded = true; + break; + } } _queryMessageController.add(false); }).catchError((e, stack) { @@ -90,35 +115,60 @@ class StreamChannelState extends State { } /// Calls [channel.getReplies] updating [queryMessage] stream - Future getReplies(String parentId) async { - if (_queryMessageController.value == true || _paginationEnded) { + Future getReplies( + String parentId, { + QueryDirection direction = QueryDirection.top, + }) async { + if (_queryMessageController.value == true || + (_topPaginationEnded && _bottomPaginationEnded)) { return; } _queryMessageController.add(true); - String firstId; + String id; + PaginationParams params; + + final messageLimit = 50; + if (widget.channel.state.threads.containsKey(parentId)) { final thread = widget.channel.state.threads[parentId]; - if (thread != null && thread.isNotEmpty) { - firstId = thread?.first?.id; + switch (direction) { + case QueryDirection.top: + id = thread?.first?.id; + params = PaginationParams( + lessThan: id, + limit: messageLimit, + ); + break; + case QueryDirection.bottom: + id = thread?.last?.id; + params = PaginationParams( + greaterThan: id, + limit: messageLimit, + ); + break; + } } } - final messageLimit = 50; return widget.channel .getReplies( parentId, - PaginationParams( - lessThan: firstId, - limit: messageLimit, - ), + params, preferOffline: true, ) .then((res) { if (res.messages.isEmpty || res.messages.length < messageLimit) { - _paginationEnded = true; + switch (direction) { + case QueryDirection.top: + _topPaginationEnded = true; + break; + case QueryDirection.bottom: + _bottomPaginationEnded = true; + break; + } } _queryMessageController.add(false); }).catchError((e, stack) { diff --git a/pubspec.yaml b/pubspec.yaml index 75195a30..1d2a299e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,7 +28,10 @@ dependencies: file_picker: ^2.0.12 image_picker: ^0.6.7+2 flutter_keyboard_visibility: ^3.3.0 - stream_chat: ^0.2.14 + stream_chat: + git: + url: https://github.com/GetStream/stream-chat-dart + ref: two-way-pagination mime: ^0.9.6+3 video_compress: ^2.1.1 visibility_detector: ^0.1.5 From 546fdaf4874658697f13df4f1e5bd91c706ce39b Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 2 Dec 2020 19:30:55 +0530 Subject: [PATCH 02/74] [LazyLoadScrollView] : Add support of onStartOfPage Signed-off-by: Sahil Kumar --- lib/src/lazy_load_scroll_view.dart | 49 ++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/lib/src/lazy_load_scroll_view.dart b/lib/src/lazy_load_scroll_view.dart index e83fc777..5ca13d07 100644 --- a/lib/src/lazy_load_scroll_view.dart +++ b/lib/src/lazy_load_scroll_view.dart @@ -1,18 +1,18 @@ import 'package:flutter/widgets.dart'; -enum LoadingStatus { LOADING, STABLE } +enum _LoadingStatus { LOADING, STABLE } -/// Signature for EndOfPageListeners -typedef EndOfPageListenerCallback = void Function(); - -/// A widget that wraps a [Widget] and will trigger [onEndOfPage] when it -/// reaches the bottom of the list +/// A widget that wraps a [Widget] and will trigger [onEndOfPage]/[onStartOfPage] when it +/// reaches the bottom/start of the list class LazyLoadScrollView extends StatefulWidget { /// The [Widget] that this widget watches for changes on final Widget child; + /// Called when the [child] reaches the start of the list + final VoidCallback onStartOfPage; + /// Called when the [child] reaches the end of the list - final EndOfPageListenerCallback onEndOfPage; + final VoidCallback onEndOfPage; /// The offset to take into account when triggering [onEndOfPage] in pixels final int scrollOffset; @@ -20,14 +20,15 @@ class LazyLoadScrollView extends StatefulWidget { /// Used to determine if loading of new data has finished. You should use set this if you aren't using a FutureBuilder or StreamBuilder final bool isLoading; + /// Initiates a LazyLoadScrollView widget LazyLoadScrollView({ Key key, @required this.child, - @required this.onEndOfPage, + this.onStartOfPage, + this.onEndOfPage, this.isLoading = false, this.scrollOffset = 100, - }) : assert(onEndOfPage != null), - assert(child != null), + }) : assert(child != null), super(key: key); @override @@ -35,13 +36,13 @@ class LazyLoadScrollView extends StatefulWidget { } class _LazyLoadScrollViewState extends State { - LoadingStatus _loadMoreStatus = LoadingStatus.STABLE; + _LoadingStatus _loadMoreStatus = _LoadingStatus.STABLE; @override void didUpdateWidget(LazyLoadScrollView oldWidget) { super.didUpdateWidget(oldWidget); if (!widget.isLoading) { - _loadMoreStatus = LoadingStatus.STABLE; + _loadMoreStatus = _LoadingStatus.STABLE; } } @@ -59,21 +60,37 @@ class _LazyLoadScrollViewState extends State { notification.metrics.maxScrollExtent - notification.metrics.pixels <= widget.scrollOffset) { if (_loadMoreStatus != null && - _loadMoreStatus == LoadingStatus.STABLE) { - _loadMoreStatus = LoadingStatus.LOADING; + _loadMoreStatus == _LoadingStatus.STABLE) { + _loadMoreStatus = _LoadingStatus.LOADING; widget.onEndOfPage(); } } + if (notification.metrics.minScrollExtent < notification.metrics.pixels && + notification.metrics.pixels - notification.metrics.minScrollExtent <= + widget.scrollOffset) { + if (_loadMoreStatus != null && + _loadMoreStatus == _LoadingStatus.STABLE) { + _loadMoreStatus = _LoadingStatus.LOADING; + widget.onStartOfPage(); + } + } return true; } if (notification is OverscrollNotification) { if (notification.overscroll > 0) { if (_loadMoreStatus != null && - _loadMoreStatus == LoadingStatus.STABLE) { - _loadMoreStatus = LoadingStatus.LOADING; + _loadMoreStatus == _LoadingStatus.STABLE) { + _loadMoreStatus = _LoadingStatus.LOADING; widget.onEndOfPage(); } } + if (notification.overscroll < 0) { + if (_loadMoreStatus != null && + _loadMoreStatus == _LoadingStatus.STABLE) { + _loadMoreStatus = _LoadingStatus.LOADING; + widget.onStartOfPage(); + } + } return true; } return false; From 759fe5510ee96c089b596bb5ed70710f9b1999dd Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 3 Dec 2020 14:28:35 +0530 Subject: [PATCH 03/74] [MessageSearchListView] : Minor improvements. Signed-off-by: Sahil Kumar --- lib/src/message_search_list_view.dart | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/lib/src/message_search_list_view.dart b/lib/src/message_search_list_view.dart index 8b346570..087ae839 100644 --- a/lib/src/message_search_list_view.dart +++ b/lib/src/message_search_list_view.dart @@ -14,6 +14,10 @@ typedef MessageSearchItemTapCallback = void Function(GetMessageResponse); typedef MessageSearchItemBuilder = Widget Function( BuildContext, GetMessageResponse); +/// Builder used when [MessageSearchListView] is empty +typedef EmptyMessageSearchBuilder = Widget Function( + BuildContext context, String searchQuery); + /// /// It shows the list of searched messages. /// @@ -85,7 +89,7 @@ class MessageSearchListView extends StatefulWidget { final MessageSearchItemTapCallback onItemTap; /// The builder used when the channel list is empty. - final WidgetBuilder emptyBuilder; + final EmptyMessageSearchBuilder emptyBuilder; /// The builder that will be used in case of error final Widget Function(Error error) errorBuilder; @@ -250,11 +254,10 @@ class _MessageSearchListViewState extends State { final items = snapshot.data; - if (items.isEmpty && widget.emptyBuilder != null) { - return widget.emptyBuilder(context); - } - - if (items.isEmpty && widget.emptyBuilder == null) { + if (items.isEmpty) { + if (widget.emptyBuilder != null) { + return widget.emptyBuilder(context, widget.messageQuery); + } return LayoutBuilder( builder: (context, viewportConstraints) { return SingleChildScrollView( From 4373888ae63fb2d7168b7fd895980d6b1f2b5b4a Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 3 Dec 2020 10:49:41 +0100 Subject: [PATCH 04/74] jump scrollview on new messages --- lib/src/message_list_view.dart | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index f9e40bf7..803ce456 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -164,6 +164,7 @@ class _MessageListViewState extends State { Function _onThreadTap; bool _showScrollToBottom = false; ItemPositionsListener _itemPositionListener; + int messageListLength; @override Widget build(BuildContext context) { @@ -203,6 +204,22 @@ class _MessageListViewState extends State { ); } + final newMessagesListLength = messages.length; + + if (_itemPositionListener.itemPositions.value?.isNotEmpty == true && + messageListLength != null) { + final first = _itemPositionListener.itemPositions.value.first; + final diff = newMessagesListLength - messageListLength; + if (diff > 0) { + _scrollController.jumpTo( + index: first.index + diff, + alignment: first.itemLeadingEdge, + ); + } + } + + messageListLength = newMessagesListLength; + return Stack( alignment: Alignment.center, children: [ From f367e6b7a8e6273c2bfa8e6652c2c34b106dc825 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 3 Dec 2020 15:46:58 +0530 Subject: [PATCH 05/74] minor changes Signed-off-by: Sahil Kumar --- lib/src/message_list_view.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 803ce456..88e33ddc 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -164,7 +164,7 @@ class _MessageListViewState extends State { Function _onThreadTap; bool _showScrollToBottom = false; ItemPositionsListener _itemPositionListener; - int messageListLength; + int _messageListLength; @override Widget build(BuildContext context) { @@ -207,9 +207,9 @@ class _MessageListViewState extends State { final newMessagesListLength = messages.length; if (_itemPositionListener.itemPositions.value?.isNotEmpty == true && - messageListLength != null) { + _messageListLength != null) { final first = _itemPositionListener.itemPositions.value.first; - final diff = newMessagesListLength - messageListLength; + final diff = newMessagesListLength - _messageListLength; if (diff > 0) { _scrollController.jumpTo( index: first.index + diff, @@ -218,7 +218,7 @@ class _MessageListViewState extends State { } } - messageListLength = newMessagesListLength; + _messageListLength = newMessagesListLength; return Stack( alignment: Alignment.center, From 5cd9653c34093087f891e1c8729136d61ef1e679 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 3 Dec 2020 17:28:38 +0530 Subject: [PATCH 06/74] [MessageListView] : Use notification listener to implement pagination. Signed-off-by: Sahil Kumar --- lib/src/lazy_load_scroll_view.dart | 2 +- lib/src/message_list_view.dart | 230 ++++++++++++++--------------- 2 files changed, 114 insertions(+), 118 deletions(-) diff --git a/lib/src/lazy_load_scroll_view.dart b/lib/src/lazy_load_scroll_view.dart index 5ca13d07..5cfeea61 100644 --- a/lib/src/lazy_load_scroll_view.dart +++ b/lib/src/lazy_load_scroll_view.dart @@ -15,7 +15,7 @@ class LazyLoadScrollView extends StatefulWidget { final VoidCallback onEndOfPage; /// The offset to take into account when triggering [onEndOfPage] in pixels - final int scrollOffset; + final double scrollOffset; /// Used to determine if loading of new data has finished. You should use set this if you aren't using a FutureBuilder or StreamBuilder final bool isLoading; diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 88e33ddc..cbe09a6f 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:stream_chat/stream_chat.dart'; +import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart'; import 'package:stream_chat_flutter/src/message_widget.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/system_message.dart'; @@ -160,7 +161,6 @@ class MessageListView extends StatefulWidget { class _MessageListViewState extends State { ItemScrollController _scrollController; bool _bottomWasVisible = false; - bool _topWasVisible = false; Function _onThreadTap; bool _showScrollToBottom = false; ItemPositionsListener _itemPositionListener; @@ -223,110 +223,121 @@ class _MessageListViewState extends State { return Stack( alignment: Alignment.center, children: [ - ScrollablePositionedList.builder( - itemPositionsListener: _itemPositionListener, - addAutomaticKeepAlives: true, - key: Key('messageListView'), - initialScrollIndex: widget.initialScrollIndex, - initialAlignment: widget.initialAlignment, - physics: widget.scrollPhysics, - itemScrollController: _scrollController, - reverse: true, - itemCount: messages.length + - 1 + - (widget.parentMessage != null ? 1 : 0), - itemBuilder: (context, i) { - if (i == messages.length + 1) { - if (widget.parentMessageBuilder != null) { - return widget.parentMessageBuilder( + LazyLoadScrollView( + onStartOfPage: () => _paginateData( + streamChannel, + QueryDirection.bottom, + ), + onEndOfPage: () => _paginateData( + streamChannel, + QueryDirection.top, + ), + child: ScrollablePositionedList.builder( + itemPositionsListener: _itemPositionListener, + addAutomaticKeepAlives: true, + key: Key('messageListView'), + initialScrollIndex: widget.initialScrollIndex, + initialAlignment: widget.initialAlignment, + physics: widget.scrollPhysics, + itemScrollController: _scrollController, + reverse: true, + itemCount: messages.length + + 1 + + (widget.parentMessage != null ? 1 : 0), + itemBuilder: (context, i) { + if (i == messages.length + 1) { + if (widget.parentMessageBuilder != null) { + return widget.parentMessageBuilder( + context, + widget.parentMessage, + ); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + buildParentMessage(widget.parentMessage), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 32), + child: Container( + padding: const EdgeInsets.all(8), + child: Text( + 'Start of thread', + textAlign: TextAlign.center, + ), + color: + Theme.of(context).accentColor.withAlpha(50), + ), + ), + ], + ); + } + } + + if (i == messages.length) { + return _buildLoadingIndicator(streamChannel); + } + final message = messages[i]; + final nextMessage = i > 0 ? messages[i - 1] : null; + + Widget messageWidget; + + if (i == 0) { + messageWidget = _buildBottomMessage( context, - widget.parentMessage, + message, + messages, + streamChannel, + ); + } else if (i == messages.length - 1) { + messageWidget = _buildTopMessage( + context, + message, + messages, + streamChannel, ); } else { + if (widget.messageBuilder != null) { + messageWidget = Builder( + key: ValueKey('MESSAGE-${message.id}'), + builder: (_) => widget.messageBuilder( + context, + MessageDetails( + context, + message, + messages, + i, + ), + messages), + ); + } else { + messageWidget = buildMessage(message, messages, i); + } + } + + if (nextMessage != null && + !Jiffy(message.createdAt.toLocal()).isSame( + nextMessage.createdAt.toLocal(), Units.DAY)) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - buildParentMessage(widget.parentMessage), + messageWidget, Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: Container( - padding: const EdgeInsets.all(8), - child: Text( - 'Start of thread', - textAlign: TextAlign.center, - ), - color: - Theme.of(context).accentColor.withAlpha(50), - ), + padding: const EdgeInsets.symmetric(vertical: 12.0), + child: widget.dateDividerBuilder != null + ? widget.dateDividerBuilder( + nextMessage.createdAt.toLocal()) + : DateDivider( + dateTime: nextMessage.createdAt.toLocal(), + ), ), ], ); } - } - if (i == messages.length) { - return _buildLoadingIndicator(streamChannel); - } - final message = messages[i]; - final nextMessage = i > 0 ? messages[i - 1] : null; - - Widget messageWidget; - - if (i == 0) { - messageWidget = _buildBottomMessage( - context, - message, - messages, - streamChannel, - ); - } else if (i == messages.length - 1) { - messageWidget = _buildTopMessage( - context, - message, - messages, - streamChannel, - ); - } else { - if (widget.messageBuilder != null) { - messageWidget = Builder( - key: ValueKey('MESSAGE-${message.id}'), - builder: (_) => widget.messageBuilder( - context, - MessageDetails( - context, - message, - messages, - i, - ), - messages), - ); - } else { - messageWidget = buildMessage(message, messages, i); - } - } - - if (nextMessage != null && - !Jiffy(message.createdAt.toLocal()) - .isSame(nextMessage.createdAt.toLocal(), Units.DAY)) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - messageWidget, - Padding( - padding: const EdgeInsets.symmetric(vertical: 12.0), - child: widget.dateDividerBuilder != null - ? widget.dateDividerBuilder( - nextMessage.createdAt.toLocal()) - : DateDivider( - dateTime: nextMessage.createdAt.toLocal(), - ), - ), - ], - ); - } - - return messageWidget; - }, + return messageWidget; + }, + ), ), if (widget.showScrollToBottom && _showScrollToBottom) _buildScrollToBottom(), @@ -365,6 +376,14 @@ class _MessageListViewState extends State { }); } + void _paginateData(StreamChannelState channel, QueryDirection direction) { + if (widget.parentMessage == null) { + channel.queryMessages(direction: direction); + } else { + channel.getReplies(widget.parentMessage.id, direction: direction); + } + } + ItemPosition _getTopElement(Iterable values) { return values .where((ItemPosition position) => position.itemLeadingEdge < 0.9) @@ -485,22 +504,7 @@ class _MessageListViewState extends State { } else { messageWidget = buildMessage(message, messages, messages.length - 1); } - - return VisibilityDetector( - key: ValueKey('TOP-MESSAGE'), - child: messageWidget, - onVisibilityChanged: (visibility) { - final topIsVisible = visibility.visibleBounds != Rect.zero; - if (topIsVisible && !_topWasVisible) { - if (widget.parentMessage == null) { - streamChannel.queryMessages(); - } else { - streamChannel.getReplies(widget.parentMessage.id); - } - _topWasVisible = !topIsVisible; - } - }, - ); + return messageWidget; } Widget _buildBottomMessage( @@ -533,14 +537,6 @@ class _MessageListViewState extends State { onVisibilityChanged: (visibility) { final isVisible = visibility.visibleBounds != Rect.zero; if (isVisible && !_bottomWasVisible) { - if (widget.parentMessage == null) { - streamChannel.queryMessages(direction: QueryDirection.bottom); - } else { - streamChannel.getReplies( - widget.parentMessage.id, - direction: QueryDirection.bottom, - ); - } if (streamChannel.channel.config?.readEvents == true && streamChannel.channel.state.unreadCount > 0) { streamChannel.channel.markRead(); From 16ed0c5bb46541877d624298e82ae176a1947ffc Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 4 Dec 2020 16:29:43 +0530 Subject: [PATCH 07/74] feat: Added chat info page --- lib/src/channel_header.dart | 23 ++- lib/src/chat_info_screen.dart | 319 +++++++++++++++++++++++++++++++++ lib/src/stream_svg_icon.dart | 36 ++++ lib/svgs/Icon_group.svg | 3 + lib/svgs/Icon_notification.svg | 6 + 5 files changed, 386 insertions(+), 1 deletion(-) create mode 100644 lib/src/chat_info_screen.dart create mode 100644 lib/svgs/Icon_group.svg create mode 100644 lib/svgs/Icon_notification.svg diff --git a/lib/src/channel_header.dart b/lib/src/channel_header.dart index ebaaf20f..09833ad3 100644 --- a/lib/src/channel_header.dart +++ b/lib/src/channel_header.dart @@ -5,8 +5,10 @@ import 'package:stream_chat_flutter/src/channel_info.dart'; import 'package:stream_chat_flutter/src/channel_name.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import '../stream_chat_flutter.dart'; import './channel_name.dart'; import 'channel_image.dart'; +import 'chat_info_screen.dart'; import 'stream_channel.dart'; /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_header.png) @@ -97,7 +99,26 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { padding: const EdgeInsets.only(right: 10.0), child: Center( child: ChannelImage( - onTap: onImageTap, + onTap: onImageTap ?? + () { + var currentUser = StreamChat.of(context).user; + var otherUser = channel.state.members.firstWhere( + (element) => element.user.id != currentUser.id); + + if (channel.memberCount == 2) { + if (otherUser != null) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: ChatInfoScreen( + user: otherUser.user, + ), + ))); + } + } + }, ), ), ), diff --git a/lib/src/chat_info_screen.dart b/lib/src/chat_info_screen.dart new file mode 100644 index 00000000..18efdc1b --- /dev/null +++ b/lib/src/chat_info_screen.dart @@ -0,0 +1,319 @@ +import 'package:emojis/emojis.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; + +import '../stream_chat_flutter.dart'; + +/// Detail screen for a 1:1 chat correspondence +class ChatInfoScreen extends StatefulWidget { + /// User in consideration + final User user; + + const ChatInfoScreen({Key key, this.user}) : super(key: key); + + @override + _ChatInfoScreenState createState() => _ChatInfoScreenState(); +} + +class _ChatInfoScreenState extends State { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Color(0xFFe6e6e6), + body: ListView( + children: [ + _buildUserHeader(), + SizedBox( + height: 8.0, + ), + _buildOptionListTiles(), + SizedBox( + height: 8.0, + ), + _buildDeleteListTile(), + ], + ), + ); + } + + Widget _buildUserHeader() { + return Material( + color: Colors.white, + child: SafeArea( + child: Stack( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: UserAvatar( + user: widget.user, + constraints: BoxConstraints( + maxWidth: 72.0, + maxHeight: 72.0, + ), + borderRadius: BorderRadius.circular(36.0), + ), + ), + SizedBox(height: 15.0), + Text( + widget.user.name, + style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold), + ), + SizedBox(height: 7.0), + Text('Online for 5 minutes'), + SizedBox(height: 15.0), + _OptionListTile( + title: '@user', + trailing: Text(widget.user.name), + onTap: () {}, + ), + ], + ), + Positioned( + top: 21, + left: 16, + child: InkWell( + child: StreamSvgIcon.left(), + onTap: () { + Navigator.of(context).pop(); + }, + ), + ), + ], + ), + ), + ); + } + + Widget _buildOptionListTiles() { + var channel = StreamChannel.of(context); + + return Column( + children: [ + _OptionListTile( + title: 'Notifications', + leading: StreamSvgIcon.Icon_notification( + size: 24.0, + color: Colors.black.withOpacity(0.5), + ), + trailing: CupertinoSwitch( + value: true, + onChanged: (val) {}, + ), + onTap: () {}, + ), + StreamBuilder( + stream: StreamChannel.of(context).channel.isMutedStream, + builder: (context, snapshot) { + return _OptionListTile( + title: 'Mute user', + leading: StreamSvgIcon.mute( + size: 23.0, + color: Colors.black.withOpacity(0.5), + ), + trailing: snapshot.data == null + ? CircularProgressIndicator() + : CupertinoSwitch( + value: snapshot.data, + onChanged: (val) { + if (snapshot.data) { + channel.channel.unmute(); + } else { + channel.channel.mute(); + } + }, + ), + onTap: () {}, + ); + }), + _OptionListTile( + title: 'Block User', + leading: StreamSvgIcon.Icon_user_delete( + size: 24.0, + color: Colors.black.withOpacity(0.5), + ), + trailing: CupertinoSwitch( + value: true, + onChanged: (val) {}, + ), + onTap: () {}, + ), + _OptionListTile( + title: '615 Photos & Videos', + leading: StreamSvgIcon.pictures( + size: 24.0, + color: Colors.black.withOpacity(0.5), + ), + trailing: StreamSvgIcon.right(), + onTap: () {}, + ), + _OptionListTile( + title: '8 Files', + leading: StreamSvgIcon.files( + size: 24.0, + color: Colors.black.withOpacity(0.5), + ), + trailing: StreamSvgIcon.right(), + onTap: () {}, + ), + _OptionListTile( + title: '2 Shared groups', + leading: StreamSvgIcon.Icon_group( + size: 24.0, + color: Colors.black.withOpacity(0.5), + ), + trailing: StreamSvgIcon.right(), + onTap: () {}, + ), + ], + ); + } + + Widget _buildDeleteListTile() { + return _OptionListTile( + title: 'Delete', + leading: StreamSvgIcon.delete( + color: Colors.red, + size: 20.0, + ), + onTap: () { + _showDeleteDialog(); + }, + titleColor: Colors.red, + ); + } + + void _showDeleteDialog() { + var channel = StreamChannel.of(context).channel; + + showModalBottomSheet( + backgroundColor: Colors.white, + context: context, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16.0), + topRight: Radius.circular(16.0), + )), + builder: (context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 26.0, + ), + StreamSvgIcon.delete( + color: Colors.red, + ), + SizedBox( + height: 26.0, + ), + Text( + 'Delete Conversation', + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0), + ), + SizedBox( + height: 7.0, + ), + Text('Are you sure you want to delete this conversation?'), + SizedBox( + height: 36.0, + ), + Container( + color: Color(0xffe6e6e6), + height: 1.0, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + FlatButton( + child: Text( + 'CANCEL', + style: TextStyle( + color: Colors.black.withOpacity(0.5), + fontWeight: FontWeight.w400), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + FlatButton( + child: Text( + 'DELETE', + style: TextStyle( + color: Colors.red, fontWeight: FontWeight.w400), + ), + onPressed: () { + channel.delete().then((value) { + Navigator.pop(context); + Navigator.pop(context); + Navigator.pop(context); + }); + }, + ), + ], + ), + ], + ); + }); + } +} + +class _OptionListTile extends StatelessWidget { + final String title; + final StreamSvgIcon leading; + final Widget trailing; + final VoidCallback onTap; + final Color titleColor; + + _OptionListTile({ + this.title, + this.leading, + this.trailing, + this.onTap, + this.titleColor, + }); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Container( + color: Color(0xffe6e6e6), + height: 2.0, + ), + Material( + color: Colors.white, + child: InkWell( + onTap: onTap, + child: Row( + children: [ + if (leading != null) + Padding( + padding: const EdgeInsets.all(22.0), + child: leading, + ), + if (leading == null) + SizedBox( + width: 16.0, + ), + Expanded( + child: Text( + title, + style: + TextStyle(fontWeight: FontWeight.w600, color: titleColor), + )), + if (trailing != null) + Padding( + padding: const EdgeInsets.all(16.0), + child: trailing, + ), + ], + ), + ), + ), + ], + ); + } +} diff --git a/lib/src/stream_svg_icon.dart b/lib/src/stream_svg_icon.dart index 42fed46e..98ce0f8e 100644 --- a/lib/src/stream_svg_icon.dart +++ b/lib/src/stream_svg_icon.dart @@ -721,4 +721,40 @@ class StreamSvgIcon extends StatelessWidget { height: size, ); } + + factory StreamSvgIcon.Icon_group({ + double size, + Color color, + }) { + return StreamSvgIcon( + assetName: 'Icon_group.svg', + color: color, + width: size, + height: size, + ); + } + + factory StreamSvgIcon.Icon_notification({ + double size, + Color color, + }) { + return StreamSvgIcon( + assetName: 'Icon_notification.svg', + color: color, + width: size, + height: size, + ); + } + + factory StreamSvgIcon.Icon_user_delete({ + double size, + Color color, + }) { + return StreamSvgIcon( + assetName: 'Icon_user_delete.svg', + color: color, + width: size, + height: size, + ); + } } diff --git a/lib/svgs/Icon_group.svg b/lib/svgs/Icon_group.svg new file mode 100644 index 00000000..6db40129 --- /dev/null +++ b/lib/svgs/Icon_group.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/svgs/Icon_notification.svg b/lib/svgs/Icon_notification.svg new file mode 100644 index 00000000..213f33c0 --- /dev/null +++ b/lib/svgs/Icon_notification.svg @@ -0,0 +1,6 @@ + + + + + + From 75262dbd4995a28e37940dfa8ed4ef6b223f5c50 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Mon, 7 Dec 2020 20:30:54 +0530 Subject: [PATCH 08/74] feat: Added shared groups, removed notifications, fixed listtile --- lib/src/chat_info_screen.dart | 185 +++++++++++++++++++++++++--------- 1 file changed, 138 insertions(+), 47 deletions(-) diff --git a/lib/src/chat_info_screen.dart b/lib/src/chat_info_screen.dart index 18efdc1b..8310d1b9 100644 --- a/lib/src/chat_info_screen.dart +++ b/lib/src/chat_info_screen.dart @@ -92,18 +92,18 @@ class _ChatInfoScreenState extends State { return Column( children: [ - _OptionListTile( - title: 'Notifications', - leading: StreamSvgIcon.Icon_notification( - size: 24.0, - color: Colors.black.withOpacity(0.5), - ), - trailing: CupertinoSwitch( - value: true, - onChanged: (val) {}, - ), - onTap: () {}, - ), + // _OptionListTile( + // title: 'Notifications', + // leading: StreamSvgIcon.Icon_notification( + // size: 24.0, + // color: Colors.black.withOpacity(0.5), + // ), + // trailing: CupertinoSwitch( + // value: true, + // onChanged: (val) {}, + // ), + // onTap: () {}, + // ), StreamBuilder( stream: StreamChannel.of(context).channel.isMutedStream, builder: (context, snapshot) { @@ -143,7 +143,7 @@ class _ChatInfoScreenState extends State { _OptionListTile( title: '615 Photos & Videos', leading: StreamSvgIcon.pictures( - size: 24.0, + size: 32.0, color: Colors.black.withOpacity(0.5), ), trailing: StreamSvgIcon.right(), @@ -152,21 +152,36 @@ class _ChatInfoScreenState extends State { _OptionListTile( title: '8 Files', leading: StreamSvgIcon.files( - size: 24.0, - color: Colors.black.withOpacity(0.5), - ), - trailing: StreamSvgIcon.right(), - onTap: () {}, - ), - _OptionListTile( - title: '2 Shared groups', - leading: StreamSvgIcon.Icon_group( - size: 24.0, + size: 32.0, color: Colors.black.withOpacity(0.5), ), trailing: StreamSvgIcon.right(), onTap: () {}, ), + StreamBuilder>( + stream: StreamChat.of(context).client.queryChannels( + filter: { + 'members': [StreamChat.of(context).user.id, widget.user.id], + }, + ), + builder: (context, snapshot) { + return _OptionListTile( + title: + '${snapshot.data == null ? '0' : snapshot.data.length} Shared groups', + leading: StreamSvgIcon.Icon_group( + size: 24.0, + color: Colors.black.withOpacity(0.5), + ), + trailing: StreamSvgIcon.right(), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => _SharedGroupsScreen( + StreamChat.of(context).user, widget.user))); + }, + ); + }), ], ); } @@ -285,31 +300,34 @@ class _OptionListTile extends StatelessWidget { ), Material( color: Colors.white, - child: InkWell( - onTap: onTap, - child: Row( - children: [ - if (leading != null) - Padding( - padding: const EdgeInsets.all(22.0), - child: leading, + child: Container( + height: 56.0, + child: InkWell( + onTap: onTap, + child: Row( + children: [ + if (leading != null) + Expanded( + child: Center(child: leading), + ), + if (leading == null) + SizedBox( + width: 16.0, + ), + Expanded( + flex: 4, + child: Text( + title, + style: TextStyle( + fontWeight: FontWeight.w600, color: titleColor), + )), + Expanded( + child: Center( + child: trailing ?? Container(), + ), ), - if (leading == null) - SizedBox( - width: 16.0, - ), - Expanded( - child: Text( - title, - style: - TextStyle(fontWeight: FontWeight.w600, color: titleColor), - )), - if (trailing != null) - Padding( - padding: const EdgeInsets.all(16.0), - child: trailing, - ), - ], + ], + ), ), ), ), @@ -317,3 +335,76 @@ class _OptionListTile extends StatelessWidget { ); } } + +class _SharedGroupsScreen extends StatefulWidget { + final User mainUser; + final User otherUser; + + _SharedGroupsScreen(this.mainUser, this.otherUser); + + @override + __SharedGroupsScreenState createState() => __SharedGroupsScreenState(); +} + +class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { + @override + Widget build(BuildContext context) { + var chat = StreamChat.of(context); + + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + brightness: Theme.of(context).brightness, + elevation: 1, + centerTitle: true, + title: Text( + 'Shared Groups', + style: TextStyle(color: Colors.black, fontSize: 16.0), + ), + leading: Center( + child: InkWell( + onTap: () { + Navigator.of(context).pop(); + }, + child: Container( + child: StreamSvgIcon.left( + color: Colors.black, + size: 24.0, + ), + width: 24.0, + height: 24.0, + ), + ), + ), + backgroundColor: StreamChatTheme.of(context).primaryColor, + ), + body: StreamBuilder>( + stream: chat.client.queryChannels( + filter: { + 'members': [widget.mainUser.id, widget.otherUser.id], + }, + ), + builder: (context, snapshot) { + if (snapshot.data == null) { + return Center( + child: CircularProgressIndicator(), + ); + } + + return ListView.builder( + itemCount: snapshot.data.length, + itemBuilder: (context, position) { + return StreamChannel( + channel: snapshot.data[position], + child: ChannelPreview( + channel: snapshot.data[position], + onTap: (val) {}, + ), + ); + }, + ); + }, + ), + ); + } +} From 3394126adfbb1df2d441cfd9cdac2058b16b738b Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 8 Dec 2020 17:21:20 +0530 Subject: [PATCH 09/74] fix: Fixed message input --- lib/src/message_input.dart | 47 ++++++++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 4e30afe1..1d220eaf 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -354,7 +354,7 @@ class MessageInputState extends State { child: Container( clipBehavior: Clip.antiAlias, decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20.0), + borderRadius: BorderRadius.circular(24.0), border: Border.all( color: Colors.grey, ), @@ -404,26 +404,45 @@ class MessageInputState extends State { child: Chip( backgroundColor: StreamChatTheme.of(context).accentColor, - label: Text( - _chosenCommand?.name ?? "", - style: TextStyle(color: Colors.white), - ), - avatar: StreamSvgIcon.lightning( - color: Colors.white, + padding: EdgeInsets.zero, + labelPadding: + EdgeInsets.symmetric(horizontal: 9.0), + label: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + StreamSvgIcon.lightning( + color: Colors.white, + size: 16.0, + ), + Text( + _chosenCommand?.name?.toUpperCase() ?? "", + style: TextStyle( + color: Colors.white, fontSize: 12.0), + ), + ], ), ), ) : null, suffixIcon: _commandEnabled - ? IconButton( - icon: Icon(Icons.cancel_outlined), - onPressed: () { + ? InkWell( + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0), + child: StreamSvgIcon.close_small(), + ), + onTap: () { setState(() { _commandEnabled = false; }); }, ) : null, + suffixIconConstraints: BoxConstraints( + maxHeight: 24.0, + maxWidth: 40.0, + ), ), textCapitalization: TextCapitalization.sentences, ), @@ -1784,7 +1803,7 @@ class MessageInputState extends State { Widget _buildIdleSendButton(BuildContext context) { return Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(8.0) + EdgeInsets.only(bottom: 3.0), child: Center( child: InkWell( onTap: () { @@ -1793,6 +1812,8 @@ class MessageInputState extends State { child: StreamSvgIcon( assetName: _getIdleSendIcon(), color: Colors.grey, + height: 24.0, + width: 24.0, ), )), ); @@ -1801,7 +1822,7 @@ class MessageInputState extends State { Widget _buildSendButton(BuildContext context) { return Center( child: Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.all(8.0) + EdgeInsets.only(bottom: 3.0), child: InkWell( onTap: () { sendMessage(); @@ -1809,6 +1830,8 @@ class MessageInputState extends State { child: StreamSvgIcon( assetName: _getSendIcon(), color: StreamChatTheme.of(context).accentColor, + height: 24.0, + width: 24.0, ), ), ), From 08c7b281983082c4adec0caf12c13a331166c0a4 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 8 Dec 2020 17:36:21 +0530 Subject: [PATCH 10/74] fix: Fixed command picker --- lib/src/message_input.dart | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 1d220eaf..2eb5b706 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -1461,16 +1461,31 @@ class MessageInputState extends State { padding: const EdgeInsets.only(left: 4.0, right: 8.0, top: 8.0, bottom: 8.0), child: StreamSvgIcon.lightning( - color: Color(0xFF000000).withAlpha(128), + color: _commandsOverlay != null + ? StreamChatTheme.of(context).accentColor + : Color(0xFF000000).withAlpha(128), ), ), - onTap: () { + onTap: () async { + if (_openFilePickerSection) { + setState(() { + _animateContainer = false; + _openFilePickerSection = false; + _filePickerSize = _kMinMediaPickerSize; + }); + await Future.delayed(Duration(milliseconds: 300)); + } + if (_commandsOverlay == null) { - _commandsOverlay = _buildCommandsOverlayEntry(); - Overlay.of(context).insert(_commandsOverlay); + setState(() { + _commandsOverlay = _buildCommandsOverlayEntry(); + Overlay.of(context).insert(_commandsOverlay); + }); } else { - _commandsOverlay?.remove(); - _commandsOverlay = null; + setState(() { + _commandsOverlay?.remove(); + _commandsOverlay = null; + }); } }, ); From b9a1f813ac1affc12c90cbd68f7f295b54b4deff Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 8 Dec 2020 19:25:04 +0530 Subject: [PATCH 11/74] fix: Fixed file title --- lib/src/message_input.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 2eb5b706..43bfec97 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -1701,7 +1701,7 @@ class MessageInputState extends State { localUri: file.path != null ? Uri.parse(file.path) : null, type: attachmentType, extraData: extraDataMap.isNotEmpty ? extraDataMap : null, - title: file.name ?? 'File', + title: mimeType.type == 'file' ? file.name : null, ), ); From c6a97751316bdebcc05f1a6c19535a3aa0315c8d Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 8 Dec 2020 20:00:30 +0530 Subject: [PATCH 12/74] feat: Added dummy pages for files and media --- lib/src/chat_info_screen.dart | 78 ++++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/lib/src/chat_info_screen.dart b/lib/src/chat_info_screen.dart index 8310d1b9..92ee4446 100644 --- a/lib/src/chat_info_screen.dart +++ b/lib/src/chat_info_screen.dart @@ -147,7 +147,10 @@ class _ChatInfoScreenState extends State { color: Colors.black.withOpacity(0.5), ), trailing: StreamSvgIcon.right(), - onTap: () {}, + onTap: () { + Navigator.push(context, + MaterialPageRoute(builder: (context) => _MediaDisplayScreen())); + }, ), _OptionListTile( title: '8 Files', @@ -156,7 +159,10 @@ class _ChatInfoScreenState extends State { color: Colors.black.withOpacity(0.5), ), trailing: StreamSvgIcon.right(), - onTap: () {}, + onTap: () { + Navigator.push(context, + MaterialPageRoute(builder: (context) => _FileDisplayScreen())); + }, ), StreamBuilder>( stream: StreamChat.of(context).client.queryChannels( @@ -408,3 +414,71 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { ); } } + +class _MediaDisplayScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + brightness: Theme.of(context).brightness, + elevation: 1, + centerTitle: true, + title: Text( + 'Photos & Videos', + style: TextStyle(color: Colors.black, fontSize: 16.0), + ), + leading: Center( + child: InkWell( + onTap: () { + Navigator.of(context).pop(); + }, + child: Container( + child: StreamSvgIcon.left( + color: Colors.black, + size: 24.0, + ), + width: 24.0, + height: 24.0, + ), + ), + ), + backgroundColor: StreamChatTheme.of(context).primaryColor, + ), + ); + } +} + +class _FileDisplayScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + brightness: Theme.of(context).brightness, + elevation: 1, + centerTitle: true, + title: Text( + 'Files', + style: TextStyle(color: Colors.black, fontSize: 16.0), + ), + leading: Center( + child: InkWell( + onTap: () { + Navigator.of(context).pop(); + }, + child: Container( + child: StreamSvgIcon.left( + color: Colors.black, + size: 24.0, + ), + width: 24.0, + height: 24.0, + ), + ), + ), + backgroundColor: StreamChatTheme.of(context).primaryColor, + ), + ); + } +} From 5cef917e511538342e855e0d22d1a4d2f4d79077 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Wed, 9 Dec 2020 13:35:28 +0530 Subject: [PATCH 13/74] feat: Added implementation for last seen --- lib/src/chat_info_screen.dart | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/lib/src/chat_info_screen.dart b/lib/src/chat_info_screen.dart index 92ee4446..9ef80c9e 100644 --- a/lib/src/chat_info_screen.dart +++ b/lib/src/chat_info_screen.dart @@ -1,6 +1,7 @@ import 'package:emojis/emojis.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:jiffy/jiffy.dart'; import '../stream_chat_flutter.dart'; @@ -62,7 +63,7 @@ class _ChatInfoScreenState extends State { style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold), ), SizedBox(height: 7.0), - Text('Online for 5 minutes'), + _buildConnectedTitleState(), SizedBox(height: 15.0), _OptionListTile( title: '@user', @@ -279,6 +280,31 @@ class _ChatInfoScreenState extends State { ); }); } + + Widget _buildConnectedTitleState() { + var alternativeWidget; + + final otherMember = widget.user; + + if (otherMember != null) { + if (otherMember.online) { + alternativeWidget = Text( + 'Online', + style: StreamChatTheme.of(context) + .channelTheme + .channelHeaderTheme + .lastMessageAt, + ); + } else { + alternativeWidget = Text( + 'Last seen ${Jiffy(otherMember.lastActive).fromNow()}', + //style: textStyle, + ); + } + } + + return alternativeWidget; + } } class _OptionListTile extends StatelessWidget { From fe541892d9ee8dacbd42818544ba301c5629cba4 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 9 Dec 2020 12:03:01 +0100 Subject: [PATCH 14/74] fix .type on null bug --- lib/src/message_input.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 43bfec97..96439be9 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -818,7 +818,7 @@ class MessageInputState extends State { Widget _buildPickerSection() { var _attachmentContainsFile = - _attachments.any((element) => element.attachment.type == 'file'); + _attachments.any((element) => element.attachment?.type == 'file'); switch (_filePickerIndex) { case 0: From aa09fd380ef08eebe48986dd920121e9fb8e856f Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Wed, 9 Dec 2020 16:56:26 +0530 Subject: [PATCH 15/74] fix: UI fixes --- lib/src/chat_info_screen.dart | 95 +++++++++++++++++++++++++++++++---- 1 file changed, 84 insertions(+), 11 deletions(-) diff --git a/lib/src/chat_info_screen.dart b/lib/src/chat_info_screen.dart index 9ef80c9e..cbda9d9b 100644 --- a/lib/src/chat_info_screen.dart +++ b/lib/src/chat_info_screen.dart @@ -67,7 +67,11 @@ class _ChatInfoScreenState extends State { SizedBox(height: 15.0), _OptionListTile( title: '@user', - trailing: Text(widget.user.name), + trailing: Text( + widget.user.name, + style: TextStyle( + color: Colors.black.withOpacity(0.5), fontSize: 16.0), + ), onTap: () {}, ), ], @@ -198,7 +202,7 @@ class _ChatInfoScreenState extends State { title: 'Delete', leading: StreamSvgIcon.delete( color: Colors.red, - size: 20.0, + size: 24.0, ), onTap: () { _showDeleteDialog(); @@ -290,15 +294,12 @@ class _ChatInfoScreenState extends State { if (otherMember.online) { alternativeWidget = Text( 'Online', - style: StreamChatTheme.of(context) - .channelTheme - .channelHeaderTheme - .lastMessageAt, + style: TextStyle(color: Colors.black.withOpacity(0.5)), ); } else { alternativeWidget = Text( 'Last seen ${Jiffy(otherMember.lastActive).fromNow()}', - //style: textStyle, + style: TextStyle(color: Colors.black.withOpacity(0.5)), ); } } @@ -428,10 +429,7 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { itemBuilder: (context, position) { return StreamChannel( channel: snapshot.data[position], - child: ChannelPreview( - channel: snapshot.data[position], - onTap: (val) {}, - ), + child: _buildListTile(snapshot.data[position]), ); }, ); @@ -439,6 +437,81 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { ), ); } + + Widget _buildListTile(Channel channel) { + var extraData = channel.extraData; + var members = channel.state.members; + + var textStyle = TextStyle(fontSize: 14.0, fontWeight: FontWeight.bold); + + return Container( + height: 64.0, + child: LayoutBuilder(builder: (context, constraints) { + String title; + if (extraData['name'] == null) { + final otherMembers = members.where( + (member) => member.userId != StreamChat.of(context).user.id); + if (otherMembers.isNotEmpty) { + final maxWidth = constraints.maxWidth; + final maxChars = maxWidth / textStyle.fontSize; + var currentChars = 0; + final currentMembers = []; + otherMembers.forEach((element) { + final newLength = currentChars + element.user.name.length; + if (newLength < maxChars) { + currentChars = newLength; + currentMembers.add(element); + } + }); + + final exceedingMembers = + otherMembers.length - currentMembers.length; + title = + '${currentMembers.map((e) => e.user.name).join(', ')} ${exceedingMembers > 0 ? '+ $exceedingMembers' : ''}'; + } else { + title = 'No title'; + } + } else { + title = extraData['name']; + } + + return Column( + children: [ + Expanded( + child: Row( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: ChannelImage( + channel: channel, + constraints: + BoxConstraints(maxWidth: 40.0, maxHeight: 40.0), + ), + ), + Expanded( + child: Text( + title, + style: textStyle, + )), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + '${channel.memberCount} members', + style: TextStyle(color: Colors.black.withOpacity(0.5)), + ), + ) + ], + ), + ), + Container( + height: 1.0, + color: Color(0xffe6e6e6), + ), + ], + ); + }), + ); + } } class _MediaDisplayScreen extends StatelessWidget { From 9f56730409cb49063518a239019adbeae2df5c54 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Wed, 9 Dec 2020 17:13:59 +0530 Subject: [PATCH 16/74] fix: UI fixes --- lib/src/chat_info_screen.dart | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/src/chat_info_screen.dart b/lib/src/chat_info_screen.dart index cbda9d9b..c961b0e0 100644 --- a/lib/src/chat_info_screen.dart +++ b/lib/src/chat_info_screen.dart @@ -55,6 +55,7 @@ class _ChatInfoScreenState extends State { maxHeight: 72.0, ), borderRadius: BorderRadius.circular(36.0), + showOnlineStatus: false, ), ), SizedBox(height: 15.0), @@ -304,7 +305,28 @@ class _ChatInfoScreenState extends State { } } - return alternativeWidget; + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (widget.user.online) + Material( + type: MaterialType.circle, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + constraints: BoxConstraints.tightFor( + width: 28, + height: 12, + ), + child: Material( + shape: CircleBorder(), + color: Color(0xff20E070), + ), + ), + color: Colors.white, + ), + alternativeWidget, + ], + ); } } From 839582d72b4b905a77d2a3570bc39322ead00869 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Wed, 9 Dec 2020 17:16:48 +0530 Subject: [PATCH 17/74] fix: UI fixes --- lib/src/chat_info_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/chat_info_screen.dart b/lib/src/chat_info_screen.dart index c961b0e0..aad1cd05 100644 --- a/lib/src/chat_info_screen.dart +++ b/lib/src/chat_info_screen.dart @@ -58,7 +58,7 @@ class _ChatInfoScreenState extends State { showOnlineStatus: false, ), ), - SizedBox(height: 15.0), + //SizedBox(height: 4.0), Text( widget.user.name, style: TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold), From ca6a90c97f3ada6d9bdcdf24cb6fe57d05a37436 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Wed, 9 Dec 2020 17:47:24 +0530 Subject: [PATCH 18/74] fix: Fixed colors of message attachments --- lib/src/message_input.dart | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 96439be9..b19211ba 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -683,14 +683,10 @@ class MessageInputState extends State { Color _getIconColor(int index) { switch (index) { case 0: - return _attachmentContainsFile && _attachments.isNotEmpty - ? Colors.black.withOpacity(0.2) - : Colors.black.withOpacity(0.5); + return _attachments.isEmpty ? StreamChatTheme.of(context).accentColor : (!_attachmentContainsFile ? StreamChatTheme.of(context).accentColor : Colors.black.withOpacity(0.2)); break; case 1: - return !_attachmentContainsFile && _attachments.isNotEmpty - ? Colors.black.withOpacity(0.2) - : Colors.black.withOpacity(0.5); + return _attachmentContainsFile ? StreamChatTheme.of(context).accentColor : (_attachments.isEmpty ? Colors.black.withOpacity(0.5) : Colors.black.withOpacity(0.2)); break; case 2: return _attachmentContainsFile && _attachments.isNotEmpty From 79b1401700a3d5cd2ec1090f46d3b34f1e4c93ae Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Wed, 9 Dec 2020 17:54:13 +0530 Subject: [PATCH 19/74] fix: Fixed padding --- lib/src/chat_info_screen.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/src/chat_info_screen.dart b/lib/src/chat_info_screen.dart index aad1cd05..3881e4a3 100644 --- a/lib/src/chat_info_screen.dart +++ b/lib/src/chat_info_screen.dart @@ -68,10 +68,13 @@ class _ChatInfoScreenState extends State { SizedBox(height: 15.0), _OptionListTile( title: '@user', - trailing: Text( - widget.user.name, - style: TextStyle( - color: Colors.black.withOpacity(0.5), fontSize: 16.0), + trailing: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + widget.user.name, + style: TextStyle( + color: Colors.black.withOpacity(0.5), fontSize: 16.0), + ), ), onTap: () {}, ), From 39a2eeea32407f72074271d80dccfc6c06bc36c0 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 10 Dec 2020 13:40:01 +0530 Subject: [PATCH 20/74] fix: File fix --- lib/src/message_input.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index b19211ba..e21ab4b1 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -1697,7 +1697,7 @@ class MessageInputState extends State { localUri: file.path != null ? Uri.parse(file.path) : null, type: attachmentType, extraData: extraDataMap.isNotEmpty ? extraDataMap : null, - title: mimeType.type == 'file' ? file.name : null, + title: file.name, ), ); From 328bc4d1bcf02b56c63706f7e19991cc424849db Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 10 Dec 2020 14:42:58 +0530 Subject: [PATCH 21/74] fix: Removed block button --- lib/src/chat_info_screen.dart | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/lib/src/chat_info_screen.dart b/lib/src/chat_info_screen.dart index 3881e4a3..434b0021 100644 --- a/lib/src/chat_info_screen.dart +++ b/lib/src/chat_info_screen.dart @@ -137,18 +137,24 @@ class _ChatInfoScreenState extends State { onTap: () {}, ); }), - _OptionListTile( - title: 'Block User', - leading: StreamSvgIcon.Icon_user_delete( - size: 24.0, - color: Colors.black.withOpacity(0.5), - ), - trailing: CupertinoSwitch( - value: true, - onChanged: (val) {}, - ), - onTap: () {}, - ), + // _OptionListTile( + // title: 'Block User', + // leading: StreamSvgIcon.Icon_user_delete( + // size: 24.0, + // color: Colors.black.withOpacity(0.5), + // ), + // trailing: CupertinoSwitch( + // value: widget.user.banned, + // onChanged: (val) { + // if (widget.user.banned) { + // channel.channel.shadowBan(widget.user.id, {}); + // } else { + // channel.channel.unbanUser(widget.user.id); + // } + // }, + // ), + // onTap: () {}, + // ), _OptionListTile( title: '615 Photos & Videos', leading: StreamSvgIcon.pictures( From e0e98299cae1fa51b1b88d4fb54d7e6f7d945643 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 10 Dec 2020 15:06:32 +0530 Subject: [PATCH 22/74] fix: Filter and alignment fix --- lib/src/chat_info_screen.dart | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/lib/src/chat_info_screen.dart b/lib/src/chat_info_screen.dart index 434b0021..e56e812c 100644 --- a/lib/src/chat_info_screen.dart +++ b/lib/src/chat_info_screen.dart @@ -182,7 +182,18 @@ class _ChatInfoScreenState extends State { StreamBuilder>( stream: StreamChat.of(context).client.queryChannels( filter: { - 'members': [StreamChat.of(context).user.id, widget.user.id], + r'$and': [ + { + 'members': { + r'$in': [widget.user.id], + }, + }, + { + 'members': { + r'$in': [StreamChat.of(context).user.id], + }, + } + ], }, ), builder: (context, snapshot) { @@ -386,8 +397,13 @@ class _OptionListTile extends StatelessWidget { fontWeight: FontWeight.w600, color: titleColor), )), Expanded( - child: Center( - child: trailing ?? Container(), + flex: 2, + child: Padding( + padding: const EdgeInsets.only(right: 16.0), + child: Align( + alignment: Alignment.centerRight, + child: trailing ?? Container(), + ), ), ), ], From d97f7ecebffe821fce9edca057ad1ec368a21378 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 10 Dec 2020 15:18:14 +0530 Subject: [PATCH 23/74] fix: Filter and alignment fix --- lib/src/chat_info_screen.dart | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/src/chat_info_screen.dart b/lib/src/chat_info_screen.dart index e56e812c..f9ee70cd 100644 --- a/lib/src/chat_info_screen.dart +++ b/lib/src/chat_info_screen.dart @@ -461,7 +461,18 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { body: StreamBuilder>( stream: chat.client.queryChannels( filter: { - 'members': [widget.mainUser.id, widget.otherUser.id], + r'$and': [ + { + 'members': { + r'$in': [widget.otherUser.id], + }, + }, + { + 'members': { + r'$in': [widget.mainUser.id], + }, + } + ], }, ), builder: (context, snapshot) { From df2d82231502f32551612c81ef095a447ac7c71e Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 10 Dec 2020 11:42:01 +0100 Subject: [PATCH 24/74] add ontap to channelpreview --- lib/src/channel_header.dart | 29 ++++++++++++++++------------- lib/src/channel_preview.dart | 25 ++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/lib/src/channel_header.dart b/lib/src/channel_header.dart index 09833ad3..96ed9769 100644 --- a/lib/src/channel_header.dart +++ b/lib/src/channel_header.dart @@ -101,21 +101,24 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { child: ChannelImage( onTap: onImageTap ?? () { - var currentUser = StreamChat.of(context).user; - var otherUser = channel.state.members.firstWhere( - (element) => element.user.id != currentUser.id); - - if (channel.memberCount == 2) { + if (channel.memberCount == 2 && channel.isDistinct) { + final currentUser = StreamChat.of(context).user; + final otherUser = channel.state.members.firstWhere( + (element) => element.user.id != currentUser.id, + orElse: () => null, + ); if (otherUser != null) { Navigator.push( - context, - MaterialPageRoute( - builder: (context) => StreamChannel( - channel: channel, - child: ChatInfoScreen( - user: otherUser.user, - ), - ))); + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: ChatInfoScreen( + user: otherUser.user, + ), + ), + ), + ); } } }, diff --git a/lib/src/channel_preview.dart b/lib/src/channel_preview.dart index 26f68812..5f58e254 100644 --- a/lib/src/channel_preview.dart +++ b/lib/src/channel_preview.dart @@ -7,6 +7,7 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import '../stream_chat_flutter.dart'; import 'channel_name.dart'; import 'channel_unread_indicator.dart'; +import 'chat_info_screen.dart'; /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/screenshots/channel_preview_paint.png) @@ -60,7 +61,29 @@ class ChannelPreview extends StatelessWidget { } }, leading: ChannelImage( - onTap: onImageTap, + onTap: onImageTap ?? + () { + if (channel.memberCount == 2 && channel.isDistinct) { + final currentUser = StreamChat.of(context).user; + final otherUser = channel.state.members.firstWhere( + (element) => element.user.id != currentUser.id, + orElse: () => null, + ); + if (otherUser != null) { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StreamChannel( + channel: channel, + child: ChatInfoScreen( + user: otherUser.user, + ), + ), + ), + ); + } + } + }, ), title: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, From b85ed04170019289b8d1aa06bc4ddcde5ec12935 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 10 Dec 2020 12:29:51 +0100 Subject: [PATCH 25/74] update 1:1 chatinfo ui --- lib/src/chat_info_screen.dart | 56 +++++++++++------------------------ 1 file changed, 18 insertions(+), 38 deletions(-) diff --git a/lib/src/chat_info_screen.dart b/lib/src/chat_info_screen.dart index f9ee70cd..0ba300ee 100644 --- a/lib/src/chat_info_screen.dart +++ b/lib/src/chat_info_screen.dart @@ -67,7 +67,7 @@ class _ChatInfoScreenState extends State { _buildConnectedTitleState(), SizedBox(height: 15.0), _OptionListTile( - title: '@user', + title: '@${widget.user.id}', trailing: Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text( @@ -156,7 +156,7 @@ class _ChatInfoScreenState extends State { // onTap: () {}, // ), _OptionListTile( - title: '615 Photos & Videos', + title: 'Photos & Videos', leading: StreamSvgIcon.pictures( size: 32.0, color: Colors.black.withOpacity(0.5), @@ -168,7 +168,7 @@ class _ChatInfoScreenState extends State { }, ), _OptionListTile( - title: '8 Files', + title: 'Files', leading: StreamSvgIcon.files( size: 32.0, color: Colors.black.withOpacity(0.5), @@ -179,41 +179,21 @@ class _ChatInfoScreenState extends State { MaterialPageRoute(builder: (context) => _FileDisplayScreen())); }, ), - StreamBuilder>( - stream: StreamChat.of(context).client.queryChannels( - filter: { - r'$and': [ - { - 'members': { - r'$in': [widget.user.id], - }, - }, - { - 'members': { - r'$in': [StreamChat.of(context).user.id], - }, - } - ], - }, - ), - builder: (context, snapshot) { - return _OptionListTile( - title: - '${snapshot.data == null ? '0' : snapshot.data.length} Shared groups', - leading: StreamSvgIcon.Icon_group( - size: 24.0, - color: Colors.black.withOpacity(0.5), - ), - trailing: StreamSvgIcon.right(), - onTap: () { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => _SharedGroupsScreen( - StreamChat.of(context).user, widget.user))); - }, - ); - }), + _OptionListTile( + title: 'Shared groups', + leading: StreamSvgIcon.Icon_group( + size: 24.0, + color: Colors.black.withOpacity(0.5), + ), + trailing: StreamSvgIcon.right(), + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => _SharedGroupsScreen( + StreamChat.of(context).user, widget.user))); + }, + ), ], ); } From 0534488b9480c9ff3503b7d6bc83cdc61c8d0c2b Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Thu, 10 Dec 2020 14:42:15 +0100 Subject: [PATCH 26/74] fix delete conversation logic --- lib/src/channel_bottom_sheet.dart | 72 +++++++++++------------ lib/src/channel_header.dart | 8 ++- lib/src/channel_list_view.dart | 55 +++++++----------- lib/src/chat_info_screen.dart | 97 ++++++++----------------------- lib/src/utils.dart | 93 +++++++++++++++++++++-------- 5 files changed, 158 insertions(+), 167 deletions(-) diff --git a/lib/src/channel_bottom_sheet.dart b/lib/src/channel_bottom_sheet.dart index 48ce8887..2d16bf6d 100644 --- a/lib/src/channel_bottom_sheet.dart +++ b/lib/src/channel_bottom_sheet.dart @@ -69,40 +69,26 @@ class ChannelBottomSheet extends StatelessWidget { ), ), Divider(), - StreamBuilder( - stream: channel.isMutedStream, - initialData: channel.isMuted, - builder: (context, snapshot) { - return ListTile( - leading: StreamSvgIcon.mute( - size: 22, - color: StreamChatTheme.of(context).primaryIconTheme.color, - ), - title: Text('Mute ${channel.isGroup ? 'group' : 'user'}'), - trailing: Switch( - onChanged: (bool muted) async { - if (muted) { - await channel.mute(); - } else { - await channel.unmute(); - } - }, - value: snapshot.data, - ), - ); - }), - Divider(), if (channel.isGroup && !channel.isDistinct) ListTile( leading: StreamSvgIcon.userRemove( - size: 22, - color: Colors.black, + size: 24, + color: Color(0xff7A7A7A), + ), + title: Text( + 'Leave Group', + style: TextStyle(fontWeight: FontWeight.bold), ), - title: Text('Leave Group'), onTap: () async { final confirm = await showConfirmationDialog( context, - 'Do you want to leave the group?', + title: 'Leave Group', + okText: 'LEAVE', + question: 'Are you sure you want to leave this group?', + cancelText: 'CANCEL', + icon: StreamSvgIcon.userRemove( + color: Colors.red, + ), ); if (confirm == true) { await channel @@ -111,11 +97,17 @@ class ChannelBottomSheet extends StatelessWidget { } }, ), - if (!channel.isGroup && !channel.isDistinct) + if ([ + 'admin', + 'owner', + ].contains(channel.state.members + .firstWhere((m) => m.userId == channel.client.state.user.id, + orElse: () => null) + ?.role)) ListTile( - leading: Icon( - Icons.delete_outline, + leading: StreamSvgIcon.delete( color: Color(0xFFFF3742), + size: 24, ), title: Text( 'Delete chat', @@ -124,14 +116,22 @@ class ChannelBottomSheet extends StatelessWidget { ), ), onTap: () async { - final confirm = await showConfirmationDialog( + final res = await showConfirmationDialog( context, - 'Do you want to delete the chat?', + title: 'Delete Conversation', + okText: 'DELETE', + question: + 'Are you sure you want to delete this conversation?', + cancelText: 'CANCEL', + icon: StreamSvgIcon.delete( + color: Colors.red, + ), ); - if (confirm == true) { - await channel - .removeMembers([StreamChat.of(context).user.id]); - Navigator.pop(context); + var channel = StreamChannel.of(context).channel; + if (res == true) { + await channel.delete().then((value) { + Navigator.pop(context); + }); } }, ), diff --git a/lib/src/channel_header.dart b/lib/src/channel_header.dart index 96ed9769..65f63cc4 100644 --- a/lib/src/channel_header.dart +++ b/lib/src/channel_header.dart @@ -100,7 +100,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { child: Center( child: ChannelImage( onTap: onImageTap ?? - () { + () async { if (channel.memberCount == 2 && channel.isDistinct) { final currentUser = StreamChat.of(context).user; final otherUser = channel.state.members.firstWhere( @@ -108,7 +108,7 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { orElse: () => null, ); if (otherUser != null) { - Navigator.push( + final pop = await Navigator.push( context, MaterialPageRoute( builder: (context) => StreamChannel( @@ -119,6 +119,10 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { ), ), ); + + if (pop == true) { + Navigator.pop(context); + } } } }, diff --git a/lib/src/channel_list_view.dart b/lib/src/channel_list_view.dart index 03284c6d..5dbc9f90 100644 --- a/lib/src/channel_list_view.dart +++ b/lib/src/channel_list_view.dart @@ -524,44 +524,33 @@ class _ChannelListViewState extends State ); }, ), - IconSlideAction( - color: backgroundColor, - iconWidget: StreamSvgIcon.mute(), - onTap: () async { - if (!channel.isMuted) { - await channel.mute(); - } else { - await channel.unmute(); - } - }, - ), - if (channel.isGroup && !channel.isDistinct) + if ([ + 'admin', + 'owner', + ].contains(channel.state.members + .firstWhere( + (m) => m.userId == channel.client.state.user.id, + orElse: () => null) + ?.role)) IconSlideAction( color: backgroundColor, - iconWidget: StreamSvgIcon.userRemove(), + iconWidget: StreamSvgIcon.delete( + color: Color(0xFFFF3742), + ), onTap: () async { - final confirm = await showConfirmationDialog( + final res = await showConfirmationDialog( context, - 'Do you want to leave the group?', + title: 'Delete Conversation', + okText: 'DELETE', + question: + 'Are you sure you want to delete this conversation?', + cancelText: 'CANCEL', + icon: StreamSvgIcon.delete( + color: Color(0xFFFF3742), + ), ); - if (confirm == true) { - await channel - .removeMembers([StreamChat.of(context).user.id]); - } - }, - ), - if (!channel.isGroup && !channel.isDistinct) - IconSlideAction( - color: backgroundColor, - icon: Icons.delete_outline, - onTap: () async { - final confirm = await showConfirmationDialog( - context, - 'Do you want to delete the chat?', - ); - if (confirm == true) { - await channel - .removeMembers([StreamChat.of(context).user.id]); + if (res == true) { + await channel.delete(); } }, ), diff --git a/lib/src/chat_info_screen.dart b/lib/src/chat_info_screen.dart index 0ba300ee..0ec62c36 100644 --- a/lib/src/chat_info_screen.dart +++ b/lib/src/chat_info_screen.dart @@ -19,6 +19,7 @@ class ChatInfoScreen extends StatefulWidget { class _ChatInfoScreenState extends State { @override Widget build(BuildContext context) { + final channel = StreamChannel.of(context).channel; return Scaffold( backgroundColor: Color(0xFFe6e6e6), body: ListView( @@ -31,7 +32,14 @@ class _ChatInfoScreenState extends State { SizedBox( height: 8.0, ), - _buildDeleteListTile(), + if ([ + 'admin', + 'owner', + ].contains(channel.state.members + .firstWhere((m) => m.userId == channel.client.state.user.id, + orElse: () => null) + ?.role)) + _buildDeleteListTile(), ], ), ); @@ -212,78 +220,23 @@ class _ChatInfoScreenState extends State { ); } - void _showDeleteDialog() { + void _showDeleteDialog() async { + final res = await showConfirmationDialog( + context, + title: 'Delete Conversation', + okText: 'DELETE', + question: 'Are you sure you want to delete this conversation?', + cancelText: 'CANCEL', + icon: StreamSvgIcon.delete( + color: Colors.red, + ), + ); var channel = StreamChannel.of(context).channel; - - showModalBottomSheet( - backgroundColor: Colors.white, - context: context, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.only( - topLeft: Radius.circular(16.0), - topRight: Radius.circular(16.0), - )), - builder: (context) { - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - SizedBox( - height: 26.0, - ), - StreamSvgIcon.delete( - color: Colors.red, - ), - SizedBox( - height: 26.0, - ), - Text( - 'Delete Conversation', - style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0), - ), - SizedBox( - height: 7.0, - ), - Text('Are you sure you want to delete this conversation?'), - SizedBox( - height: 36.0, - ), - Container( - color: Color(0xffe6e6e6), - height: 1.0, - ), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - FlatButton( - child: Text( - 'CANCEL', - style: TextStyle( - color: Colors.black.withOpacity(0.5), - fontWeight: FontWeight.w400), - ), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - FlatButton( - child: Text( - 'DELETE', - style: TextStyle( - color: Colors.red, fontWeight: FontWeight.w400), - ), - onPressed: () { - channel.delete().then((value) { - Navigator.pop(context); - Navigator.pop(context); - Navigator.pop(context); - }); - }, - ), - ], - ), - ], - ); - }); + if (res == true) { + await channel.delete().then((value) { + Navigator.pop(context); + }); + } } Widget _buildConnectedTitleState() { diff --git a/lib/src/utils.dart b/lib/src/utils.dart index 20e3ce1f..74b1da64 100644 --- a/lib/src/utils.dart +++ b/lib/src/utils.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:url_launcher/url_launcher.dart'; +import '../stream_chat_flutter.dart'; + Future launchURL(BuildContext context, String url) async { if (await canLaunch(url)) { await launch(url); @@ -15,33 +17,76 @@ Future launchURL(BuildContext context, String url) async { } Future showConfirmationDialog( - BuildContext context, + BuildContext context, { + String title, + Widget icon, String question, -) { - return showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text(question), - actions: [ - FlatButton( - child: Text('Ok'), - onPressed: () => Navigator.pop( - context, - true, + String okText, + String cancelText, +}) { + return showModalBottomSheet( + backgroundColor: Colors.white, + context: context, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.only( + topLeft: Radius.circular(16.0), + topRight: Radius.circular(16.0), + )), + builder: (context) { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + SizedBox( + height: 26.0, ), - ), - FlatButton( - child: Text('Cancel'), - onPressed: () => Navigator.pop( - context, - false, + if (icon != null) icon, + SizedBox( + height: 26.0, ), - ), - ], - ); - }, - ); + Text( + title, + style: TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0), + ), + SizedBox( + height: 7.0, + ), + Text(question), + SizedBox( + height: 36.0, + ), + Container( + color: Color(0xffe6e6e6), + height: 1.0, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + FlatButton( + child: Text( + cancelText, + style: TextStyle( + color: Colors.black.withOpacity(0.5), + fontWeight: FontWeight.w400), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + FlatButton( + child: Text( + okText, + style: TextStyle( + color: Colors.red, fontWeight: FontWeight.w400), + ), + onPressed: () { + Navigator.pop(context, true); + }, + ), + ], + ), + ], + ); + }); } /// Get random png with initials From 54cb49eac198142eec3d239f480e768f4264c47b Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Thu, 10 Dec 2020 19:52:32 +0530 Subject: [PATCH 27/74] feat: Added file previews --- lib/src/file_attachment.dart | 46 +++++++++++++++++++++++++++++++++--- lib/src/media_utils.dart | 17 +++++++++++++ lib/src/message_input.dart | 26 +++++++++++++++----- 3 files changed, 80 insertions(+), 9 deletions(-) create mode 100644 lib/src/media_utils.dart diff --git a/lib/src/file_attachment.dart b/lib/src/file_attachment.dart index fff13bbe..cef966c5 100644 --- a/lib/src/file_attachment.dart +++ b/lib/src/file_attachment.dart @@ -1,18 +1,28 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:file_picker/file_picker.dart'; 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 'package:stream_chat_flutter/src/utils.dart'; +import 'media_utils.dart'; + +enum FileAttachmentType { local, online } class FileAttachment extends StatelessWidget { final Attachment attachment; final Size size; final Widget trailing; + final FileAttachmentType attachmentType; + final PlatformFile file; const FileAttachment({ Key key, @required this.attachment, this.size, this.trailing, + this.attachmentType = FileAttachmentType.online, + this.file, }) : super(key: key); @override @@ -32,7 +42,7 @@ class FileAttachment extends StatelessWidget { child: Row( children: [ Container( - child: _getFileTypeImage(attachment.extraData['mime_type']), + child: _getFileTypeImage(), height: 40.0, width: 33.33, margin: EdgeInsets.all(8.0), @@ -117,8 +127,38 @@ class FileAttachment extends StatelessWidget { ); } - StreamSvgIcon _getFileTypeImage(String type) { - switch (type) { + Widget _getFileTypeImage() { + if ((MediaUtils.getMimeType(attachment.title).type == 'image')) { + switch (attachmentType) { + case FileAttachmentType.local: + return Image.memory( + file.bytes, + fit: BoxFit.cover, + ); + break; + case FileAttachmentType.online: + return CachedNetworkImage( + imageUrl: attachment.imageUrl ?? + attachment.assetUrl ?? + attachment.thumbUrl, + fit: BoxFit.cover, + progressIndicatorBuilder: (context, _, progress) { + return Center( + child: Container( + width: 20.0, + height: 20.0, + child: CircularProgressIndicator( + backgroundColor: StreamChatTheme.of(context).accentColor, + ), + ), + ); + }, + ); + break; + } + } + + switch (attachment.extraData['mime_type']) { case '7z': return StreamSvgIcon.filetype_7z(); break; diff --git a/lib/src/media_utils.dart b/lib/src/media_utils.dart new file mode 100644 index 00000000..e91a47d6 --- /dev/null +++ b/lib/src/media_utils.dart @@ -0,0 +1,17 @@ +import 'package:http_parser/http_parser.dart' as httpParser; +import 'package:mime/mime.dart'; + +class MediaUtils { + static httpParser.MediaType getMimeType(String filename) { + httpParser.MediaType mimeType; + if (filename != null) { + if (filename.toLowerCase().endsWith('heic')) { + mimeType = httpParser.MediaType.parse('image/heic'); + } else { + mimeType = httpParser.MediaType.parse(lookupMimeType(filename)); + } + } + + return mimeType; + } +} diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index e21ab4b1..870c1ed9 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -683,10 +683,18 @@ class MessageInputState extends State { Color _getIconColor(int index) { switch (index) { case 0: - return _attachments.isEmpty ? StreamChatTheme.of(context).accentColor : (!_attachmentContainsFile ? StreamChatTheme.of(context).accentColor : Colors.black.withOpacity(0.2)); + return _attachments.isEmpty + ? StreamChatTheme.of(context).accentColor + : (!_attachmentContainsFile + ? StreamChatTheme.of(context).accentColor + : Colors.black.withOpacity(0.2)); break; case 1: - return _attachmentContainsFile ? StreamChatTheme.of(context).accentColor : (_attachments.isEmpty ? Colors.black.withOpacity(0.5) : Colors.black.withOpacity(0.2)); + return _attachmentContainsFile + ? StreamChatTheme.of(context).accentColor + : (_attachments.isEmpty + ? Colors.black.withOpacity(0.5) + : Colors.black.withOpacity(0.2)); break; case 2: return _attachmentContainsFile && _attachments.isNotEmpty @@ -1268,6 +1276,8 @@ class MessageInputState extends State { clipBehavior: Clip.antiAlias, child: FileAttachment( attachment: e.attachment, + attachmentType: FileAttachmentType.local, + file: e.file, size: Size( MediaQuery.of(context).size.width * 0.55, MediaQuery.of(context).size.height * 0.3, @@ -1676,12 +1686,16 @@ class MessageInputState extends State { final mimeType = _getMimeType(file.path.split('/').last); - if (mimeType.type == 'video' || mimeType.type == 'image') { - attachmentType = mimeType.type; - } - Map extraDataMap = {}; + if (camera) { + if (mimeType.type == 'video' || mimeType.type == 'image') { + attachmentType = mimeType.type; + } + } else { + attachmentType = 'file'; + } + if (mimeType?.subtype != null) { extraDataMap['mime_type'] = mimeType.subtype.toLowerCase(); } From 5302b16115e7af9d272a3f3605c65468e5b67152 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 11 Dec 2020 13:04:12 +0530 Subject: [PATCH 28/74] fix: keyboard fix --- lib/src/message_input.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 870c1ed9..dd9e2718 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -326,14 +326,17 @@ class MessageInputState extends State { return AnimatedCrossFade( crossFadeState: _actionsShrunk ? CrossFadeState.showFirst : CrossFadeState.showSecond, - firstChild: IconButton( - onPressed: () { + firstChild: InkWell( + onTap: () { setState(() { _actionsShrunk = false; }); }, - icon: StreamSvgIcon.emptyCircleLeft( - color: StreamChatTheme.of(context).accentColor, + child: Padding( + padding: const EdgeInsets.all(8.0) + EdgeInsets.only(bottom: 3.0), + child: StreamSvgIcon.emptyCircleLeft( + color: StreamChatTheme.of(context).accentColor, + ), ), ), secondChild: Row( From bcdc034a2d7f9f1e9c3de93d3cc1e435bf75c2cd Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 11 Dec 2020 09:40:33 +0100 Subject: [PATCH 29/74] fix medialistview null bug --- lib/src/media_list_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/media_list_view.dart b/lib/src/media_list_view.dart index 507238da..5f52e9b5 100644 --- a/lib/src/media_list_view.dart +++ b/lib/src/media_list_view.dart @@ -180,7 +180,7 @@ class MediaThumbnailProvider extends ImageProvider { MediaThumbnailProvider key, DecoderCallback decode) async { assert(key == this); final bytes = await media.thumbData; - if (bytes.isEmpty) return null; + if (bytes?.isNotEmpty != true) return null; return await decode(bytes); } From 9fd79a3e00055e4b3671e4a55fa94cfe56aac36c Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 11 Dec 2020 10:15:07 +0100 Subject: [PATCH 30/74] fix urlattachment title --- lib/src/url_attachment.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/url_attachment.dart b/lib/src/url_attachment.dart index d735ad67..ad314455 100644 --- a/lib/src/url_attachment.dart +++ b/lib/src/url_attachment.dart @@ -78,8 +78,9 @@ class UrlAttachment extends StatelessWidget { children: [ if (urlAttachment.title != null) Text( - urlAttachment.title, + urlAttachment.title.trim(), maxLines: 1, + overflow: TextOverflow.ellipsis, style: TextStyle( fontWeight: FontWeight.w700, fontSize: 12.0, From 4b122a7e5460f16ee671fad205caa505c135e533 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 11 Dec 2020 17:01:00 +0530 Subject: [PATCH 31/74] feat: Added new picker implementation --- lib/src/message_input.dart | 47 +++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 16 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index dd9e2718..b6a74f11 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -839,22 +839,37 @@ class MessageInputState extends State { } if (snapshot.data) { - return IgnorePointer( - ignoring: _attachmentContainsFile, - child: MediaListView( - selectedIds: _attachments.map((e) => e.id).toList(), - onSelect: (media) async { - if (!_attachments - .any((element) => element.id == media.id)) { - _addAttachment(media); - } else { - setState(() { - _attachments - .removeWhere((element) => element.id == media.id); - }); - } - }, - ), + if (_attachmentContainsFile) { + return Container( + color: Color(0xfff2f2f2), + child: InkWell( + onTap: () { + pickFile(DefaultAttachmentTypes.file); + }, + child: Text( + 'Add more files', + style: TextStyle( + color: StreamChatTheme.of(context).accentColor, + fontWeight: FontWeight.bold, + ), + ), + ), + alignment: Alignment.center, + ); + } + return MediaListView( + selectedIds: _attachments.map((e) => e.id).toList(), + onSelect: (media) async { + if (!_attachments + .any((element) => element.id == media.id)) { + _addAttachment(media); + } else { + setState(() { + _attachments + .removeWhere((element) => element.id == media.id); + }); + } + }, ); } From edeb38a2553dac733d48135c919ad2e3cbfd86bb Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 11 Dec 2020 17:10:12 +0530 Subject: [PATCH 32/74] fix: Fixed file clip bug --- lib/src/message_input.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index b6a74f11..befed883 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -362,6 +362,7 @@ class MessageInputState extends State { color: Colors.grey, ), ), + padding: EdgeInsets.all(6.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ From 102e6ce4d09dcc1485c9274a32b5a1a6a226db7c Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 11 Dec 2020 17:11:19 +0530 Subject: [PATCH 33/74] fix: Fixed file clip bug --- lib/src/message_input.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index befed883..e60c6187 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -362,7 +362,7 @@ class MessageInputState extends State { color: Colors.grey, ), ), - padding: EdgeInsets.all(6.0), + padding: _attachments.isEmpty ? null : EdgeInsets.all(6.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ From 18781201e37d28d09fcdce0c24bd8569a1ab53fc Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 11 Dec 2020 18:18:35 +0530 Subject: [PATCH 34/74] feat: Added video thumbnails and playback for attachments --- lib/src/file_attachment.dart | 101 +++++++++++++++++++++++++++++------ 1 file changed, 85 insertions(+), 16 deletions(-) diff --git a/lib/src/file_attachment.dart b/lib/src/file_attachment.dart index cef966c5..95e628f0 100644 --- a/lib/src/file_attachment.dart +++ b/lib/src/file_attachment.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:cached_network_image/cached_network_image.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; @@ -5,11 +7,13 @@ 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 'package:stream_chat_flutter/src/utils.dart'; +import 'package:video_compress/video_compress.dart'; +import 'package:video_player/video_player.dart'; import 'media_utils.dart'; enum FileAttachmentType { local, online } -class FileAttachment extends StatelessWidget { +class FileAttachment extends StatefulWidget { final Attachment attachment; final Size size; final Widget trailing; @@ -25,17 +29,44 @@ class FileAttachment extends StatelessWidget { this.file, }) : super(key: key); + @override + _FileAttachmentState createState() => _FileAttachmentState(); +} + +class _FileAttachmentState extends State { + VideoPlayerController _controller; + Future _initializeVideoPlayerFuture; + + @override + void initState() { + super.initState(); + if (MediaUtils.getMimeType(widget.attachment.title).type == 'video') { + if (widget.attachmentType == FileAttachmentType.online) { + _controller = VideoPlayerController.network( + widget.attachment.assetUrl, + ); + } else { + _controller = VideoPlayerController.file( + File.fromRawPath(widget.file.bytes), + ); + } + + _initializeVideoPlayerFuture = _controller.initialize(); + } + } + @override Widget build(BuildContext context) { return Material( child: Container( - width: size?.width ?? 100, + width: widget.size?.width ?? 100, height: 56.0, - margin: trailing != null ? EdgeInsets.only(top: 4.0) : null, + margin: widget.trailing != null ? EdgeInsets.only(top: 4.0) : null, decoration: BoxDecoration( color: Colors.white, - borderRadius: trailing != null ? BorderRadius.circular(16.0) : null, - border: trailing != null + borderRadius: + widget.trailing != null ? BorderRadius.circular(16.0) : null, + border: widget.trailing != null ? Border.fromBorderSide(BorderSide(color: Color(0xFFE6E6E6))) : null, ), @@ -56,7 +87,7 @@ class FileAttachment extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - attachment?.title ?? 'File', + widget.attachment?.title ?? 'File', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 14.0, @@ -68,7 +99,7 @@ class FileAttachment extends StatelessWidget { height: 3.0, ), Text( - '${attachment.extraData['file_size'] ?? 'N/A'} bytes', + '${widget.attachment.extraData['file_size'] ?? 'N/A'} bytes', style: TextStyle( color: Colors.black.withOpacity(0.5), fontSize: 14.0, @@ -79,13 +110,13 @@ class FileAttachment extends StatelessWidget { ), Column( children: [ - trailing ?? + widget.trailing ?? IconButton( icon: StreamSvgIcon.cloud_download( color: Colors.black, ), onPressed: () { - launchURL(context, attachment.assetUrl); + launchURL(context, widget.attachment.assetUrl); }, ), ], @@ -128,19 +159,19 @@ class FileAttachment extends StatelessWidget { } Widget _getFileTypeImage() { - if ((MediaUtils.getMimeType(attachment.title).type == 'image')) { - switch (attachmentType) { + if ((MediaUtils.getMimeType(widget.attachment.title).type == 'image')) { + switch (widget.attachmentType) { case FileAttachmentType.local: return Image.memory( - file.bytes, + widget.file.bytes, fit: BoxFit.cover, ); break; case FileAttachmentType.online: return CachedNetworkImage( - imageUrl: attachment.imageUrl ?? - attachment.assetUrl ?? - attachment.thumbUrl, + imageUrl: widget.attachment.imageUrl ?? + widget.attachment.assetUrl ?? + widget.attachment.thumbUrl, fit: BoxFit.cover, progressIndicatorBuilder: (context, _, progress) { return Center( @@ -158,7 +189,45 @@ class FileAttachment extends StatelessWidget { } } - switch (attachment.extraData['mime_type']) { + if ((MediaUtils.getMimeType(widget.attachment.title).type == 'video')) { + switch (widget.attachmentType) { + case FileAttachmentType.local: + return FutureBuilder( + future: VideoCompress.getFileThumbnail(widget.file.path), + builder: (context, snapshot) { + if (!snapshot.hasData) { + return Image.asset( + 'images/placeholder.png', + package: 'stream_chat_flutter', + ); + } + + return Image.file( + snapshot.data, + fit: BoxFit.cover, + ); + }, + ); + break; + case FileAttachmentType.online: + return FutureBuilder( + future: _initializeVideoPlayerFuture, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + return AspectRatio( + aspectRatio: _controller.value.aspectRatio, + child: VideoPlayer(_controller), + ); + } else { + return Center(child: CircularProgressIndicator()); + } + }, + ); + break; + } + } + + switch (widget.attachment.extraData['mime_type']) { case '7z': return StreamSvgIcon.filetype_7z(); break; From c487ce92d0fd1f458f757e445811c8fd14760c18 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Fri, 11 Dec 2020 19:54:02 +0530 Subject: [PATCH 35/74] feat: Added file size in proper format --- lib/src/file_attachment.dart | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/lib/src/file_attachment.dart b/lib/src/file_attachment.dart index 95e628f0..4bba7779 100644 --- a/lib/src/file_attachment.dart +++ b/lib/src/file_attachment.dart @@ -99,7 +99,7 @@ class _FileAttachmentState extends State { height: 3.0, ), Text( - '${widget.attachment.extraData['file_size'] ?? 'N/A'} bytes', + '${_getSizeText(widget.attachment.extraData['file_size'])}', style: TextStyle( color: Colors.black.withOpacity(0.5), fontSize: 14.0, @@ -284,4 +284,18 @@ class _FileAttachmentState extends State { break; } } + + String _getSizeText(int bytes) { + if (bytes == null) { + return 'Size N/A'; + } + + if (bytes <= 1000) { + return '${bytes} bytes'; + } else if (bytes <= 100000) { + return '${(bytes / 1000).toStringAsFixed(2)} KB'; + } else { + return '${(bytes / 1000000).toStringAsFixed(2)} MB'; + } + } } From bdd94693717ca31c660c95845734877630921227 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 11 Dec 2020 15:09:25 +0100 Subject: [PATCH 36/74] use expanded gesturedetector --- lib/src/message_input.dart | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index e60c6187..542f63e7 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -841,12 +841,13 @@ class MessageInputState extends State { if (snapshot.data) { if (_attachmentContainsFile) { - return Container( - color: Color(0xfff2f2f2), - child: InkWell( - onTap: () { - pickFile(DefaultAttachmentTypes.file); - }, + return GestureDetector( + onTap: () { + pickFile(DefaultAttachmentTypes.file); + }, + child: Container( + constraints: BoxConstraints.expand(), + color: Color(0xfff2f2f2), child: Text( 'Add more files', style: TextStyle( @@ -854,8 +855,8 @@ class MessageInputState extends State { fontWeight: FontWeight.bold, ), ), + alignment: Alignment.center, ), - alignment: Alignment.center, ); } return MediaListView( From 255f9c1608a10d6b0e5619fdff70f864f7128f91 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 14 Dec 2020 10:02:14 +0100 Subject: [PATCH 37/74] add enforce_unique: true in send reaction --- lib/src/reaction_picker.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/reaction_picker.dart b/lib/src/reaction_picker.dart index ff23b8a3..b019ee79 100644 --- a/lib/src/reaction_picker.dart +++ b/lib/src/reaction_picker.dart @@ -136,7 +136,9 @@ class _ReactionPickerState extends State void sendReaction(BuildContext context, String reactionType) { StreamChannel.of(context) .channel - .sendReaction(widget.message, reactionType); + .sendReaction(widget.message, reactionType, extraData: { + 'enforce_unique': true, + }); pop(); } From 984d3ec104c614d975c0536a61f0d71815b898e8 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 14 Dec 2020 10:03:54 +0100 Subject: [PATCH 38/74] don't show copy message for empty messages --- lib/src/message_widget.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/message_widget.dart b/lib/src/message_widget.dart index f761f756..c7c6acdb 100644 --- a/lib/src/message_widget.dart +++ b/lib/src/message_widget.dart @@ -558,6 +558,7 @@ class _MessageWidgetState extends State { message: widget.message, editMessageInputBuilder: widget.editMessageInputBuilder, onThreadTap: widget.onThreadTap, + showCopyMessage: widget.message.text?.trim()?.isNotEmpty == true, showEditMessage: widget.showEditMessage && widget.message.attachments ?.any((element) => element.type == 'giphy') != From faafb21481a92eeb7630343c07cff568a27e54ac Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 14 Dec 2020 10:35:33 +0100 Subject: [PATCH 39/74] trigger ci --- example/lib/main.dart | 2 +- example/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 4b529e23..3fb47b66 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -57,7 +57,7 @@ class MyApp extends StatelessWidget { debugShowCheckedModeBanner: false, theme: ThemeData.light(), darkTheme: ThemeData.dark(), - //TODO change to system once dark theme is implemented + //TODO change to system once dark theme is implemented themeMode: ThemeMode.light, onGenerateRoute: AppRoutes.generateRoute, initialRoute: diff --git a/example/pubspec.yaml b/example/pubspec.yaml index c5bfa14f..5459debc 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.88+90 +version: 1.0.89+91 environment: sdk: ">=2.2.2 <3.0.0" From 2903cae4e84f3742c43f68d6107f9b443325b958 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 14 Dec 2020 11:47:27 +0100 Subject: [PATCH 40/74] trigger ci --- example/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 5459debc..2e0c0ae3 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.89+91 +version: 1.0.90+93 environment: sdk: ">=2.2.2 <3.0.0" From 5e7e5aaac03c0821309565c839d394596cb15bcb Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Tue, 15 Dec 2020 12:18:16 +0100 Subject: [PATCH 41/74] fix new chat screen flow --- example/ios/fastlane/report.xml | 22 ++------ example/lib/new_chat_screen.dart | 87 ++++++++++++++++++++------------ example/pubspec.yaml | 2 +- lib/src/message_input.dart | 31 +++++++----- lib/src/message_list_view.dart | 8 +-- lib/src/user_list_view.dart | 2 +- 6 files changed, 85 insertions(+), 67 deletions(-) diff --git a/example/ios/fastlane/report.xml b/example/ios/fastlane/report.xml index d6fdf2d7..17688944 100644 --- a/example/ios/fastlane/report.xml +++ b/example/ios/fastlane/report.xml @@ -5,39 +5,27 @@ - + - + - + - + - - - - - - - - - - - - - + diff --git a/example/lib/new_chat_screen.dart b/example/lib/new_chat_screen.dart index 5fbfbf07..a2ce9ebf 100644 --- a/example/lib/new_chat_screen.dart +++ b/example/lib/new_chat_screen.dart @@ -37,6 +37,8 @@ class _NewChatScreenState extends State { bool _showUserList = true; + bool _channelExisted = false; + void _userNameListener() { if (_debounce?.isActive ?? false) _debounce.cancel(); _debounce = Timer(const Duration(milliseconds: 350), () { @@ -56,11 +58,6 @@ class _NewChatScreenState extends State { _searchFocusNode.addListener(() async { if (_searchFocusNode.hasFocus && !_showUserList) { - if (channel.extraData['draft'] == true) { - await channel.stopWatching(); - channel.dispose(); - channel.client.state.channels.remove(channel.cid); - } setState(() { _showUserList = true; }); @@ -71,19 +68,38 @@ class _NewChatScreenState extends State { if (_messageInputFocusNode.hasFocus && _selectedUsers.isNotEmpty) { final chatState = StreamChat.of(context); - channel = chatState.client.channel( - 'messaging', - extraData: { + final res = await chatState.client.queryChannels( + options: { + 'state': false, + 'watch': false, + }, + filter: { 'members': [ ..._selectedUsers.map((e) => e.id), chatState.user.id, ], - 'draft': true, + 'distinct': true, }, - ); + messageLimit: 0, + paginationParams: PaginationParams( + limit: 1, + ), + ).first; - if (!chatState.client.state.channels.containsKey(channel.cid)) { + final _channelExisted = res.length == 1; + if (_channelExisted) { + channel = res.first; await channel.watch(); + } else { + channel = chatState.client.channel( + 'messaging', + extraData: { + 'members': [ + ..._selectedUsers.map((e) => e.id), + chatState.user.id, + ], + }, + ); } setState(() { @@ -134,6 +150,7 @@ class _NewChatScreenState extends State { return GestureDetector( onTap: () { _chipInputTextFieldState.removeItem(user); + _searchFocusNode.requestFocus(); }, child: Stack( alignment: AlignmentDirectional.centerStart, @@ -311,18 +328,38 @@ class _NewChatScreenState extends State { ), ), ) - : MessageListView(), + : FutureBuilder( + future: channel.initialized, + builder: (context, snapshot) { + if (snapshot.data == true) { + return MessageListView(); + } + + return Center( + child: Text( + 'No chats here yet...', + style: TextStyle( + fontSize: 12, + color: Colors.black.withOpacity(.5), + ), + ), + ); + }, + ), ), MessageInput( focusNode: _messageInputFocusNode, + preMessageSending: (message) async { + await channel.watch(); + return message; + }, onMessageSent: (m) { - if (!m.isEphemeral) { - _updateChannelAndNavigate(context); - } else { - channel.on('message.new').first.then((_) { - _updateChannelAndNavigate(context); - }); - } + Navigator.pushNamedAndRemoveUntil( + context, + Routes.CHANNEL_PAGE, + ModalRoute.withName(Routes.HOME), + arguments: channel, + ); }, ), ], @@ -330,16 +367,4 @@ class _NewChatScreenState extends State { ), ); } - - void _updateChannelAndNavigate(BuildContext context) { - channel.update({ - 'draft': false, - }); - Navigator.pushNamedAndRemoveUntil( - context, - Routes.CHANNEL_PAGE, - ModalRoute.withName(Routes.HOME), - arguments: channel, - ); - } } diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 2e0c0ae3..f9c71355 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.90+93 +version: 1.0.91+94 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 542f63e7..447d49d9 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -343,7 +343,10 @@ class MessageInputState extends State { mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ if (!widget.disableAttachments) _buildAttachmentButton(), - if (widget.editMessage == null) _buildCommandButton(), + if (widget.editMessage == null && + StreamChannel.of(context).channel?.config?.commands?.isNotEmpty == + true) + _buildCommandButton(), ], ), duration: Duration(milliseconds: 300), @@ -543,11 +546,12 @@ class MessageInputState extends State { void _checkCommands(String s, BuildContext context) { if (s.startsWith('/')) { var matchedCommandsList = StreamChannel.of(context) - .channel - .config - .commands - .where((element) => element.name == s.substring(1)) - .toList(); + .channel + .config + ?.commands + ?.where((element) => element.name == s.substring(1)) + ?.toList() ?? + []; if (matchedCommandsList.length == 1) { _chosenCommand = matchedCommandsList[0]; @@ -568,11 +572,12 @@ class MessageInputState extends State { OverlayEntry _buildCommandsOverlayEntry() { final text = textEditingController.text.trimLeft(); final commands = StreamChannel.of(context) - .channel - .config - .commands - .where((c) => c.name.contains(text.replaceFirst('/', ''))) - .toList(); + .channel + .config + ?.commands + ?.where((c) => c.name.contains(text.replaceFirst('/', ''))) + ?.toList() ?? + []; RenderBox renderBox = context.findRenderObject(); final size = renderBox.size; @@ -1927,8 +1932,6 @@ class MessageInputState extends State { _mentionsOverlay?.remove(); _mentionsOverlay = null; - final channel = StreamChannel.of(context).channel; - Future sendingFuture; Message message; if (widget.editMessage != null) { @@ -1953,6 +1956,8 @@ class MessageInputState extends State { message = await widget.preMessageSending(message); } + final channel = StreamChannel.of(context).channel; + if (widget.editMessage == null || widget.editMessage.status == MessageSendingStatus.FAILED) { sendingFuture = channel.sendMessage(message); diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 430e0b85..5eaccc6a 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -173,15 +173,15 @@ class _MessageListViewState extends State { ? streamChannel.channel.state.threadsStream .where((threads) => threads.containsKey(widget.parentMessage.id)) .map((threads) => threads[widget.parentMessage.id]) - : streamChannel.channel.state.messagesStream; + : streamChannel.channel.state?.messagesStream; return StreamBuilder>( - stream: messagesStream.map((messages) => messages - .where((e) => + stream: messagesStream?.map((messages) => messages + ?.where((e) => !e.isDeleted || (e.isDeleted && e.user.id == streamChannel.channel.client.state.user.id)) - .toList()), + ?.toList()), builder: (context, snapshot) { if (!snapshot.hasData) { return Center( diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index b57e2d55..8ea0a72c 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -190,7 +190,7 @@ class _UserListViewState extends State } final groupedUsers = >{}; for (var e in temp) { - final alphabet = e.name[0]; + final alphabet = e.name[0]?.toUpperCase(); groupedUsers[alphabet] = [...groupedUsers[alphabet] ?? [], e]; } final items = []; From f85cf6bb16ff6d7353d9d463f4f55240429b3af0 Mon Sep 17 00:00:00 2001 From: Deven Joshi Date: Tue, 15 Dec 2020 20:18:13 +0530 Subject: [PATCH 42/74] qa fix for file list --- lib/src/message_input.dart | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 447d49d9..dec2b033 100644 --- a/lib/src/message_input.dart +++ b/lib/src/message_input.dart @@ -1287,10 +1287,11 @@ class MessageInputState extends State { children: [ if (_attachments.any((e) => e.attachment?.type == 'file')) LimitedBox( - maxHeight: 73.0, + maxHeight: 136.0, child: ListView( - scrollDirection: Axis.horizontal, - children: _attachments + reverse: true, + shrinkWrap: true, + children: _attachments.reversed .where((e) => e.attachment?.type == 'file') .map( (e) => Padding( @@ -1304,8 +1305,8 @@ class MessageInputState extends State { attachmentType: FileAttachmentType.local, file: e.file, size: Size( - MediaQuery.of(context).size.width * 0.55, - MediaQuery.of(context).size.height * 0.3, + MediaQuery.of(context).size.width * 0.65, + 56.0, ), trailing: Padding( padding: const EdgeInsets.all(8.0), From 7495acee3ee5d6c9fa3e7589ac3f7f45d518e86b Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 16 Dec 2020 16:56:09 +0530 Subject: [PATCH 43/74] changes Signed-off-by: Sahil Kumar --- example/lib/choose_user_page.dart | 4 +- example/lib/group_chat_details_screen.dart | 2 +- example/lib/main.dart | 47 +-- example/lib/new_chat_screen.dart | 2 +- example/lib/routes/app_routes.dart | 4 +- lib/src/lazy_load_scroll_view.dart | 37 ++- lib/src/message_input.dart | 10 +- lib/src/message_list_view.dart | 359 +++++++++++++-------- lib/src/stream_channel.dart | 351 ++++++++++++-------- lib/src/user_list_view.dart | 4 +- pubspec.yaml | 7 +- 11 files changed, 504 insertions(+), 323 deletions(-) diff --git a/example/lib/choose_user_page.dart b/example/lib/choose_user_page.dart index a64013b9..73b12425 100644 --- a/example/lib/choose_user_page.dart +++ b/example/lib/choose_user_page.dart @@ -13,13 +13,13 @@ import 'routes/routes.dart'; const kStreamApiKey = 'STREAM_API_KEY'; const kStreamUserId = 'STREAM_USER_ID'; const kStreamToken = 'STREAM_TOKEN'; -const kDefaultStreamApiKey = 'uj7qrdbfrzvg'; +const kDefaultStreamApiKey = 's2dxdhpxd94g'; class ChooseUserPage extends StatelessWidget { @override Widget build(BuildContext context) { final users = { - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidmlzaGFsIn0.lCz-idDgaZ-xszjnuB_hTfeIOhTFmJtTB2fEjhwrcCI': + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidmlzaGFsIn0._JHWzo92fpTWZMZriJHXqOng6ShYVmWrdaIaPwEPKBg': User( id: 'vishal', extraData: { diff --git a/example/lib/group_chat_details_screen.dart b/example/lib/group_chat_details_screen.dart index d759a82e..c6d4331d 100644 --- a/example/lib/group_chat_details_screen.dart +++ b/example/lib/group_chat_details_screen.dart @@ -133,7 +133,7 @@ class _GroupChatDetailsScreenState extends State { context, Routes.CHANNEL_PAGE, ModalRoute.withName(Routes.HOME), - arguments: channel, + arguments: ChannelPageArgs(channel: channel), ); }, ), diff --git a/example/lib/main.dart b/example/lib/main.dart index f1667139..10f6d5b3 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -337,40 +337,14 @@ class _ChannelListPageState extends State { final message = messageResponse.message; final channel = Channel.fromState( client, - ChannelState( - channel: messageResponse.channel, - messages: [message], - ), + ChannelState(channel: messageResponse.channel), ); - await Future.wait([ - channel.query( - messagesPagination: PaginationParams( - lessThan: message.id, - limit: 25, - ), - preferOffline: true, - ), - channel.query( - messagesPagination: PaginationParams( - greaterThan: message.id, - limit: 25, - ), - preferOffline: true, - ), - ]); - final messages = channel.state.messages; - final totalMessages = messages.length; - final messageIndex = messages - .indexWhere((e) => e.id == message.id); - final initialIndex = totalMessages - messageIndex; - final bool isFirstMessage = messageIndex == 0; Navigator.pushNamed( context, Routes.CHANNEL_PAGE, arguments: ChannelPageArgs( channel: channel, - initialScrollIndex: initialIndex, - initialAlignment: isFirstMessage ? 0 : 0.5, + initialMessage: message, ), ); }, @@ -406,24 +380,24 @@ class _ChannelListPageState extends State { class ChannelPageArgs { final Channel channel; - final int initialScrollIndex; - final double initialAlignment; + final Message initialMessage; const ChannelPageArgs({ this.channel, - this.initialScrollIndex = 0, - this.initialAlignment = 0, + this.initialMessage, }); } class ChannelPage extends StatelessWidget { final int initialScrollIndex; final double initialAlignment; + final bool highlightInitialMessage; const ChannelPage({ Key key, - this.initialScrollIndex = 0, - this.initialAlignment = 0, + this.initialScrollIndex, + this.initialAlignment, + this.highlightInitialMessage = false, }) : super(key: key); @override @@ -441,6 +415,7 @@ class ChannelPage extends StatelessWidget { MessageListView( initialScrollIndex: initialScrollIndex, initialAlignment: initialAlignment, + highlightInitialMessage: highlightInitialMessage, threadBuilder: (_, parentMessage) { return ThreadPage( parent: parentMessage, @@ -481,8 +456,8 @@ class ThreadPage extends StatelessWidget { ThreadPage({ Key key, this.parent, - this.initialScrollIndex = 0, - this.initialAlignment = 0, + this.initialScrollIndex, + this.initialAlignment, }) : super(key: key); @override diff --git a/example/lib/new_chat_screen.dart b/example/lib/new_chat_screen.dart index 5fbfbf07..94c7f866 100644 --- a/example/lib/new_chat_screen.dart +++ b/example/lib/new_chat_screen.dart @@ -339,7 +339,7 @@ class _NewChatScreenState extends State { context, Routes.CHANNEL_PAGE, ModalRoute.withName(Routes.HOME), - arguments: channel, + arguments: ChannelPageArgs(channel: channel), ); } } diff --git a/example/lib/routes/app_routes.dart b/example/lib/routes/app_routes.dart index 8ee907b8..8082245f 100644 --- a/example/lib/routes/app_routes.dart +++ b/example/lib/routes/app_routes.dart @@ -37,9 +37,9 @@ class AppRoutes { final arg = args as ChannelPageArgs; return StreamChannel( channel: arg.channel, + initialMessageId: arg.initialMessage?.id, child: ChannelPage( - initialScrollIndex: arg.initialScrollIndex, - initialAlignment: arg.initialAlignment, + highlightInitialMessage: arg.initialMessage != null, ), ); }); diff --git a/lib/src/lazy_load_scroll_view.dart b/lib/src/lazy_load_scroll_view.dart index 5cfeea61..a3c344aa 100644 --- a/lib/src/lazy_load_scroll_view.dart +++ b/lib/src/lazy_load_scroll_view.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; enum _LoadingStatus { LOADING, STABLE } @@ -9,10 +10,10 @@ class LazyLoadScrollView extends StatefulWidget { final Widget child; /// Called when the [child] reaches the start of the list - final VoidCallback onStartOfPage; + final AsyncCallback onStartOfPage; /// Called when the [child] reaches the end of the list - final VoidCallback onEndOfPage; + final AsyncCallback onEndOfPage; /// The offset to take into account when triggering [onEndOfPage] in pixels final double scrollOffset; @@ -38,14 +39,6 @@ class LazyLoadScrollView extends StatefulWidget { class _LazyLoadScrollViewState extends State { _LoadingStatus _loadMoreStatus = _LoadingStatus.STABLE; - @override - void didUpdateWidget(LazyLoadScrollView oldWidget) { - super.didUpdateWidget(oldWidget); - if (!widget.isLoading) { - _loadMoreStatus = _LoadingStatus.STABLE; - } - } - @override Widget build(BuildContext context) { return NotificationListener( @@ -62,7 +55,11 @@ class _LazyLoadScrollViewState extends State { if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) { _loadMoreStatus = _LoadingStatus.LOADING; - widget.onEndOfPage(); + if (widget.onEndOfPage != null) { + widget.onEndOfPage().whenComplete(() { + _loadMoreStatus = _LoadingStatus.STABLE; + }); + } } } if (notification.metrics.minScrollExtent < notification.metrics.pixels && @@ -71,7 +68,11 @@ class _LazyLoadScrollViewState extends State { if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) { _loadMoreStatus = _LoadingStatus.LOADING; - widget.onStartOfPage(); + if (widget.onStartOfPage != null) { + widget.onStartOfPage().whenComplete(() { + _loadMoreStatus = _LoadingStatus.STABLE; + }); + } } } return true; @@ -81,14 +82,22 @@ class _LazyLoadScrollViewState extends State { if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) { _loadMoreStatus = _LoadingStatus.LOADING; - widget.onEndOfPage(); + if (widget.onEndOfPage != null) { + widget.onEndOfPage().whenComplete(() { + _loadMoreStatus = _LoadingStatus.STABLE; + }); + } } } if (notification.overscroll < 0) { if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) { _loadMoreStatus = _LoadingStatus.LOADING; - widget.onStartOfPage(); + if (widget.onStartOfPage != null) { + widget.onStartOfPage().whenComplete(() { + _loadMoreStatus = _LoadingStatus.STABLE; + }); + } } } return true; diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 4e30afe1..e38b9af3 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'; @@ -436,6 +435,7 @@ class MessageInputState extends State { } Timer _debounce; + void _onChanged(BuildContext context, String s) { if (_debounce?.isActive == true) _debounce.cancel(); _debounce = Timer( @@ -1859,7 +1859,8 @@ class MessageInputState extends State { _mentionsOverlay?.remove(); _mentionsOverlay = null; - final channel = StreamChannel.of(context).channel; + final streamChannel = StreamChannel.of(context); + final channel = streamChannel.channel; Future sendingFuture; Message message; @@ -1885,6 +1886,10 @@ class MessageInputState extends State { message = await widget.preMessageSending(message); } + if (!channel.state.isUpToDate) { + await streamChannel.reloadChannel(); + } + if (widget.editMessage == null || widget.editMessage.status == MessageSendingStatus.FAILED) { sendingFuture = channel.sendMessage(message); @@ -1970,6 +1975,7 @@ class MessageInputState extends State { } bool _initialized = false; + @override void didChangeDependencies() { if (widget.editMessage != null && !_initialized) { diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 5efdb36a..042e20b9 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -1,8 +1,10 @@ import 'dart:async'; import 'dart:math'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; +import 'package:rxdart/rxdart.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_flutter/src/lazy_load_scroll_view.dart'; @@ -109,10 +111,11 @@ class MessageListView extends StatefulWidget { this.onThreadTap, this.dateDividerBuilder, this.scrollPhysics = const AlwaysScrollableScrollPhysics(), - this.initialScrollIndex = 0, - this.initialAlignment = 0, + this.initialScrollIndex, + this.initialAlignment, this.scrollController, this.itemPositionListener, + this.highlightInitialMessage = false, }) : super(key: key); /// Function used to build a custom message widget @@ -154,6 +157,11 @@ class MessageListView extends StatefulWidget { /// The ScrollPhysics used by the ListView final ScrollPhysics scrollPhysics; + /// If true the list will highlight the initialMessage if there is any. + /// + /// Also See [StreamChannel] + final bool highlightInitialMessage; + @override _MessageListViewState createState() => _MessageListViewState(); } @@ -166,6 +174,51 @@ class _MessageListViewState extends State { ItemPositionsListener _itemPositionListener; int _messageListLength; + int get _initialIndex { + if (widget.initialScrollIndex != null) return widget.initialScrollIndex; + final streamChannel = StreamChannel.of(context); + if (streamChannel.initialMessageId != null) { + final messages = streamChannel.channel.state.messages; + final totalMessages = messages.length; + final messageIndex = messages.indexWhere((e) { + return e.id == streamChannel.initialMessageId; + }); + return totalMessages - messageIndex - 1; + } + return 0; + } + + double get _initialAlignment { + if (widget.initialAlignment != null) return widget.initialAlignment; + final streamChannel = StreamChannel.of(context); + if (streamChannel.initialMessageId != null) { + final messages = streamChannel.channel.state.messages; + final messageIndex = messages.indexWhere((e) { + return e.id == streamChannel.initialMessageId; + }); + final isFirstMessage = messageIndex == 0; + return isFirstMessage ? 0 : 0.5; + } + return 0; + } + + bool _isInitialMessage(String id) { + final streamChannel = StreamChannel.of(context); + return streamChannel.initialMessageId == id; + } + + bool get _upToDate => StreamChannel.of(context).channel.state.isUpToDate; + + bool _topPaginationActive = false; + bool _bottomPaginationActive = false; + + bool get _paginationActive => _topPaginationActive || _bottomPaginationActive; + + int initialIndex; + double initialAlignment; + + List messages = []; + @override Widget build(BuildContext context) { final streamChannel = StreamChannel.of(context); @@ -176,6 +229,11 @@ class _MessageListViewState extends State { .map((threads) => threads[widget.parentMessage.id]) : streamChannel.channel.state.messagesStream; + if (!_paginationActive && !_upToDate) { + initialIndex = _initialIndex; + initialAlignment = _initialAlignment; + } + return StreamBuilder>( stream: messagesStream.map((messages) => messages .where((e) => @@ -190,32 +248,40 @@ class _MessageListViewState extends State { ); } - final messages = snapshot.data?.reversed?.toList() ?? []; + final messageList = snapshot.data?.reversed?.toList() ?? []; - if (messages.isEmpty) { - return Center( - child: Text( - 'No chats here yet...', - style: TextStyle( - fontSize: 12, - color: Colors.black.withOpacity(.5), + if (messageList.isEmpty) { + if (_upToDate) { + return Center( + child: Text( + 'No chats here yet...', + style: TextStyle( + fontSize: 12, + color: Colors.black.withOpacity(.5), + ), ), - ), - ); + ); + } + } else { + messages = messageList; } final newMessagesListLength = messages.length; - if (_itemPositionListener.itemPositions.value?.isNotEmpty == true && - _messageListLength != null) { - final first = _itemPositionListener.itemPositions.value.first; - final diff = newMessagesListLength - _messageListLength; - if (diff > 0) { - _scrollController.jumpTo( - index: first.index + diff, - alignment: first.itemLeadingEdge, - ); + if (_bottomPaginationActive) { + if (_itemPositionListener.itemPositions.value?.isNotEmpty == true && + _messageListLength != null) { + final first = _itemPositionListener.itemPositions.value.first; + final diff = newMessagesListLength - _messageListLength; + if (diff > 0) { + initialIndex = first.index + diff; + initialAlignment = first.itemLeadingEdge; + } } + } else if (!_topPaginationActive && _upToDate) { + // Reset the index in-case we send any new message + initialIndex = 0; + initialAlignment = 0; } _messageListLength = newMessagesListLength; @@ -224,28 +290,32 @@ class _MessageListViewState extends State { alignment: Alignment.center, children: [ LazyLoadScrollView( - onStartOfPage: () => _paginateData( - streamChannel, - QueryDirection.bottom, - ), - onEndOfPage: () => _paginateData( - streamChannel, - QueryDirection.top, - ), + onStartOfPage: () async { + if (!_upToDate) { + _topPaginationActive = false; + _bottomPaginationActive = true; + _paginateData(streamChannel, QueryDirection.bottom); + } + }, + onEndOfPage: () async { + _topPaginationActive = true; + _bottomPaginationActive = false; + _paginateData(streamChannel, QueryDirection.top); + }, child: ScrollablePositionedList.builder( + key: ValueKey(initialIndex + initialAlignment), itemPositionsListener: _itemPositionListener, addAutomaticKeepAlives: true, - key: Key('messageListView'), - initialScrollIndex: widget.initialScrollIndex, - initialAlignment: widget.initialAlignment, + initialScrollIndex: initialIndex ?? 0, + initialAlignment: initialAlignment ?? 0, physics: widget.scrollPhysics, itemScrollController: _scrollController, reverse: true, itemCount: messages.length + - 1 + + 2 + (widget.parentMessage != null ? 1 : 0), itemBuilder: (context, i) { - if (i == messages.length + 1) { + if (i == messages.length + 2) { if (widget.parentMessageBuilder != null) { return widget.parentMessageBuilder( context, @@ -273,16 +343,24 @@ class _MessageListViewState extends State { ); } } - - if (i == messages.length) { - return _buildLoadingIndicator(streamChannel); + if (i == messages.length + 1) { + return _buildLoadingIndicator( + streamChannel, + QueryDirection.top, + ); } - final message = messages[i]; - final nextMessage = i > 0 ? messages[i - 1] : null; + if (i == 0) { + return _buildLoadingIndicator( + streamChannel, + QueryDirection.bottom, + ); + } + final message = messages[i - 1]; + final nextMessage = (i - 1) > 0 ? messages[i - 2] : null; Widget messageWidget; - if (i == 0) { + if (i == 1) { messageWidget = _buildBottomMessage( context, message, @@ -339,8 +417,7 @@ class _MessageListViewState extends State { }, ), ), - if (widget.showScrollToBottom && _showScrollToBottom) - _buildScrollToBottom(), + if (widget.showScrollToBottom) _buildScrollToBottom(), Positioned( top: 20.0, child: ValueListenableBuilder>( @@ -380,7 +457,7 @@ class _MessageListViewState extends State { if (widget.parentMessage == null) { channel.queryMessages(direction: direction); } else { - channel.getReplies(widget.parentMessage.id, direction: direction); + channel.getReplies(widget.parentMessage.id); } } @@ -393,91 +470,117 @@ class _MessageListViewState extends State { Widget _buildScrollToBottom() { final streamChannel = StreamChannel.of(context); - return Positioned( - bottom: 8, - right: 8, - width: 40, - height: 40, - child: Stack( - clipBehavior: Clip.none, - children: [ - FloatingActionButton( - backgroundColor: Colors.white, - child: StreamSvgIcon.down( - color: Colors.black, - ), - onPressed: () { - setState(() { - _showScrollToBottom = false; - }); - _scrollController.scrollTo( - index: 0, - duration: Duration(seconds: 1), - curve: Curves.easeInOut, - ); - }, - ), - if (streamChannel.channel.state.members.any((Member e) => - e.userId == streamChannel.channel.client.state.user.id)) - StreamBuilder( - stream: streamChannel.channel.state.unreadCountStream, - initialData: streamChannel.channel.state.unreadCount, - builder: (context, snapshot) { - if (!snapshot.hasData || snapshot.data <= 0) { - return Offstage(); + return StreamBuilder>( + stream: Rx.combineLatest2( + streamChannel.channel.state.isUpToDateStream, + streamChannel.channel.state.unreadCountStream, + (bool isUpToDate, int unreadCount) => Tuple2(isUpToDate, unreadCount), + ), + builder: (_, snapshot) { + if (snapshot.hasError) { + return Offstage(); + } else if (!snapshot.hasData) { + return Offstage(); + } + final isUpToDate = snapshot.data.item1; + final showScrollToBottom = !isUpToDate || _showScrollToBottom; + if (!showScrollToBottom) { + return Offstage(); + } + final unreadCount = snapshot.data.item2; + final showUnreadCount = unreadCount > 0 && + streamChannel.channel.state.members.any( + (e) => e.userId == streamChannel.channel.client.state.user.id); + return Positioned( + bottom: 8, + right: 8, + width: 40, + height: 40, + child: Stack( + clipBehavior: Clip.none, + children: [ + FloatingActionButton( + backgroundColor: Colors.white, + child: StreamSvgIcon.down( + color: Colors.black, + ), + onPressed: () { + if (!_upToDate) { + _bottomPaginationActive = false; + _topPaginationActive = false; + streamChannel.reloadChannel(); + } else { + setState(() => _showScrollToBottom = false); + _scrollController.scrollTo( + index: 0, + duration: Duration(seconds: 1), + curve: Curves.easeInOut, + ); } - return Positioned( - width: 20, - height: 20, - left: 10, - top: -10, - child: CircleAvatar( - child: Padding( - padding: const EdgeInsets.all(3.0), - child: Text( - snapshot.data.toString(), - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.bold, - ), + }, + ), + if (showUnreadCount) + Positioned( + width: 20, + height: 20, + left: 10, + top: -10, + child: CircleAvatar( + child: Padding( + padding: const EdgeInsets.all(3.0), + child: Text( + snapshot.data.toString(), + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.bold, ), ), ), - ); - }), - ], - ), + ), + ), + ], + ), + ); + }, ); } - Container _buildLoadingIndicator(StreamChannelState streamChannel) { - return Container( - key: Key('LOADING-INDICATOR'), - height: 50, - width: double.infinity, - child: StreamBuilder( - stream: streamChannel.queryMessage, - initialData: false, - builder: (context, snapshot) { - if (snapshot.hasError) { - return Container( - color: Color(0xffd0021B).withAlpha(26), - child: Center( - child: Text('Error loading messages'), - ), - ); - } - if (!snapshot.data) { - return SizedBox(); - } - return Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: CircularProgressIndicator(), + Widget _buildLoadingIndicator( + StreamChannelState streamChannel, + QueryDirection direction, + ) { + final stream = direction == QueryDirection.top + ? streamChannel.queryTopMessages + : streamChannel.queryBottomMessages; + return StreamBuilder( + key: Key('LOADING-INDICATOR'), + stream: stream, + initialData: false, + builder: (context, snapshot) { + if (snapshot.hasError) { + return Container( + color: Color(0xffd0021B).withAlpha(26), + child: Center( + child: Text('Error loading messages'), ), ); - }), - ); + } + if (!snapshot.data) { + if (direction == QueryDirection.top) { + return Container( + height: 50, + width: double.infinity, + ); + } + return Offstage(); + } + return Center( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: const CupertinoActivityIndicator(), + ), + ); + }); } Widget _buildTopMessage( @@ -544,9 +647,7 @@ class _MessageListViewState extends State { _bottomWasVisible = !isVisible; } if (mounted) { - setState(() { - _showScrollToBottom = !isVisible; - }); + setState(() => _showScrollToBottom = !isVisible); } }, child: messageWidget, @@ -602,7 +703,7 @@ class _MessageListViewState extends State { final userId = StreamChat.of(context).user.id; final isMyMessage = message.user.id == userId; final isNextUser = - index - 1 >= 0 && message.user.id == messages[index - 1]?.user?.id; + index - 2 >= 0 && message.user.id == messages[index - 2]?.user?.id; final channel = StreamChannel.of(context).channel; final readList = channel.state?.read @@ -673,24 +774,12 @@ class _MessageListViewState extends State { _messageNewListener = streamChannel.channel.on(EventType.messageNew).listen((event) { - final firstElementInViewport = - _itemPositionListener.itemPositions.value.first; if (event.message.user.id == streamChannel.channel.client.state.user.id) { WidgetsBinding.instance.addPostFrameCallback((_) { _scrollController.jumpTo( index: 0, ); }); - } else { - if (firstElementInViewport.index != 0) { - _scrollController.jumpTo( - index: firstElementInViewport.index + 1, - alignment: firstElementInViewport.itemLeadingEdge, - ); - } - } - if (firstElementInViewport.index == 0) { - streamChannel.channel.markRead(); } }); diff --git a/lib/src/stream_channel.dart b/lib/src/stream_channel.dart index 5dbfd103..0ca87b96 100644 --- a/lib/src/stream_channel.dart +++ b/lib/src/stream_channel.dart @@ -10,19 +10,23 @@ enum QueryDirection { top, bottom } /// /// Use [StreamChannel.of] to get the current [StreamChannelState] instance. class StreamChannel extends StatefulWidget { - StreamChannel({ + const StreamChannel({ Key key, @required this.child, @required this.channel, this.showLoading = true, - }) : super( - key: key, - ); + this.initialMessageId, + }) : assert(child != null), + assert(channel != null), + super(key: key); final Widget child; final Channel channel; final bool showLoading; + /// If passed the channel will load from this particular message. + final String initialMessageId; + /// Use this method to get the current [StreamChannelState] instance static StreamChannelState of(BuildContext context) { StreamChannelState streamChannelState; @@ -45,135 +49,123 @@ class StreamChannelState extends State { /// Current channel Channel get channel => widget.channel; + /// InitialMessageId + String get initialMessageId => widget.initialMessageId; + /// Current channel state stream Stream get channelStateStream => widget.channel.state.channelStateStream; - final BehaviorSubject _queryMessageController = BehaviorSubject(); + final _queryTopMessagesController = BehaviorSubject.seeded(false); + final _queryBottomMessagesController = BehaviorSubject.seeded(false); - /// The stream notifying the state of queryMessage call - Stream get queryMessage => _queryMessageController.stream; + /// The stream notifying the state of [_queryTopMessages] call + Stream get queryTopMessages => _queryTopMessagesController.stream; + + /// The stream notifying the state of [_queryBottomMessages] call + Stream get queryBottomMessages => _queryBottomMessagesController.stream; bool _topPaginationEnded = false; bool _bottomPaginationEnded = false; - /// Calls [channel.query] updating [queryMessage] stream - void queryMessages({QueryDirection direction = QueryDirection.top}) { - if (_queryMessageController.value == true || - (_topPaginationEnded && _bottomPaginationEnded)) { + Future _queryTopMessages({ + int limit = 20, + bool preferOffline = false, + }) async { + if (_topPaginationEnded || _queryTopMessagesController?.value == true) { return; } + _queryTopMessagesController.add(true); - _queryMessageController.add(true); - - String id; - PaginationParams params; - - final messageLimit = 25; - - if (channel.state.messages.isNotEmpty) { - switch (direction) { - case QueryDirection.top: - id = channel.state.messages.first.id; - params = PaginationParams( - lessThan: id, - limit: messageLimit, - ); - break; - case QueryDirection.bottom: - id = channel.state.messages.last.id; - params = PaginationParams( - greaterThan: id, - limit: messageLimit, - ); - break; - } + if (channel.state.messages.isEmpty) { + return _queryTopMessagesController.add(false); } - widget.channel - .query( - messagesPagination: params, - preferOffline: true, - ) - .then((res) { - if (res.messages.isEmpty || res.messages.length < messageLimit) { - switch (direction) { - case QueryDirection.top: - _topPaginationEnded = true; - break; - case QueryDirection.bottom: - _bottomPaginationEnded = true; - break; - } + final oldestMessage = channel.state.messages.first; + + try { + final state = await queryBeforeMessage( + oldestMessage.id, + limit: limit, + preferOffline: preferOffline, + ); + if (state.messages.isEmpty || state.messages.length < limit) { + _topPaginationEnded = true; } - _queryMessageController.add(false); - }).catchError((e, stack) { - if (!_queryMessageController.isClosed) { - _queryMessageController.addError(e, stack); + _queryTopMessagesController.add(false); + } catch (e, stk) { + _queryTopMessagesController.addError(e, stk); + } + } + + Future _queryBottomMessages({ + int limit = 20, + bool preferOffline = false, + }) async { + if (_bottomPaginationEnded || + _queryBottomMessagesController?.value == true || + channel?.state?.isUpToDate == true) return; + _queryBottomMessagesController.add(true); + + if (channel.state.messages.isEmpty) { + return _queryBottomMessagesController.add(false); + } + + final recentMessage = channel.state.messages.last; + + try { + final state = await queryAfterMessage( + recentMessage.id, + limit: limit, + preferOffline: preferOffline, + ); + if (state.messages.isEmpty || state.messages.length < limit) { + _bottomPaginationEnded = true; } - }); + _queryBottomMessagesController.add(false); + } catch (e, stk) { + _queryBottomMessagesController.addError(e, stk); + } + } + + /// Calls [channel.query] updating [queryMessage] stream + Future queryMessages({QueryDirection direction = QueryDirection.top}) { + if (direction == QueryDirection.top) return _queryTopMessages(); + return _queryBottomMessages(); } /// Calls [channel.getReplies] updating [queryMessage] stream Future getReplies( String parentId, { - QueryDirection direction = QueryDirection.top, + int limit = 50, + bool preferOffline = false, }) async { - if (_queryMessageController.value == true || - (_topPaginationEnded && _bottomPaginationEnded)) { - return; + if (_topPaginationEnded || _queryTopMessagesController.value) return; + _queryTopMessagesController.add(true); + + if (!channel.state.threads.containsKey(parentId)) { + return _queryTopMessagesController.add(false); } - _queryMessageController.add(true); + final thread = channel.state.threads[parentId]; - String id; - PaginationParams params; + if (thread.isEmpty) return _queryTopMessagesController.add(false); - final messageLimit = 50; + final message = thread.first; - if (widget.channel.state.threads.containsKey(parentId)) { - final thread = widget.channel.state.threads[parentId]; - if (thread != null && thread.isNotEmpty) { - switch (direction) { - case QueryDirection.top: - id = thread?.first?.id; - params = PaginationParams( - lessThan: id, - limit: messageLimit, - ); - break; - case QueryDirection.bottom: - id = thread?.last?.id; - params = PaginationParams( - greaterThan: id, - limit: messageLimit, - ); - break; - } + try { + final state = await queryBeforeMessage( + message.id, + limit: limit, + preferOffline: preferOffline, + ); + if (state.messages.isEmpty || state.messages.length < limit) { + _topPaginationEnded = true; } + _queryTopMessagesController.add(false); + } catch (e, stk) { + _queryTopMessagesController.addError(e, stk); } - - return widget.channel - .getReplies( - parentId, - params, - preferOffline: true, - ) - .then((res) { - if (res.messages.isEmpty || res.messages.length < messageLimit) { - switch (direction) { - case QueryDirection.top: - _topPaginationEnded = true; - break; - case QueryDirection.bottom: - _bottomPaginationEnded = true; - break; - } - } - _queryMessageController.add(false); - }).catchError((e, stack) { - _queryMessageController.addError(e, stack); - }); } /// Query the channel members and watchers @@ -190,41 +182,148 @@ class StreamChannelState extends State { ); } + /// Loads channel at specific message + Future loadChannelAtMessage( + String messageId, { + int before = 20, + int after = 20, + bool preferOffline = false, + }) { + return queryAtMessage( + messageId: messageId, + before: before, + after: after, + preferOffline: preferOffline, + ); + } + + /// + Future queryAtMessage({ + String messageId, + int before = 20, + int after = 20, + bool preferOffline = false, + }) async { + if (channel.state == null) return; + channel.state.isUpToDate = false; + channel.state.truncate(); + + if (messageId == null) { + await channel.query( + messagesPagination: PaginationParams( + limit: before, + ), + preferOffline: preferOffline, + ); + channel.state.isUpToDate = true; + return; + } + + return Future.wait([ + queryBeforeMessage( + messageId, + limit: before, + preferOffline: preferOffline, + ), + queryAfterMessage( + messageId, + limit: after, + preferOffline: preferOffline, + ), + ]); + } + + /// + Future queryBeforeMessage( + String messageId, { + int limit = 20, + bool preferOffline = false, + }) { + return channel.query( + messagesPagination: PaginationParams( + lessThan: messageId, + limit: limit, + ), + preferOffline: preferOffline, + ); + } + + /// + Future queryAfterMessage( + String messageId, { + int limit = 20, + bool preferOffline = false, + }) async { + final state = await channel.query( + messagesPagination: PaginationParams( + greaterThanOrEqual: messageId, + limit: limit, + ), + preferOffline: preferOffline, + ); + if (state.messages.isEmpty || state.messages.length < limit) { + channel.state.isUpToDate = true; + } + return state; + } + + /// Reloads the channel with latest message + Future reloadChannel() => queryAtMessage(before: 30); + + List> _futures; + + Future get _loadChannelAtMessage async { + try { + await loadChannelAtMessage(initialMessageId); + return true; + } catch (e, stk) { + print('Error: $e\nStack: $stk'); + rethrow; + } + } + + @override + void initState() { + super.initState(); + _futures = [widget.channel.initialized]; + if (initialMessageId != null) { + _futures.add(_loadChannelAtMessage); + } + } + @override void dispose() { - _queryMessageController.close(); + _queryTopMessagesController.close(); + _queryBottomMessagesController.close(); super.dispose(); } @override Widget build(BuildContext context) { - if (widget.channel == null) { - return Center( - child: CircularProgressIndicator(), - ); - } - return FutureBuilder( - future: widget.channel.initialized, - initialData: widget.channel.state != null, + Widget child = FutureBuilder>( + future: Future.wait(_futures), + initialData: [ + channel.state != null, + if (initialMessageId != null) false, + ], builder: (context, snapshot) { - if (widget.showLoading && (!snapshot.hasData || !snapshot.data)) { - return Container( - height: 30, - child: Center( - child: CircularProgressIndicator(), - ), + final initialized = snapshot.data[0]; + final dataLoaded = initialMessageId == null ? true : snapshot.data[1]; + if (widget.showLoading && (!initialized || !dataLoaded)) { + return Center( + child: CircularProgressIndicator(), ); } else if (snapshot.hasError) { - return Container( - height: 30, - child: Center( - child: Text(snapshot.error), - ), + return Center( + child: Text(snapshot.error), ); - } else { - return widget.child; } + return widget.child; }, ); + if (initialMessageId != null) { + child = Material(child: child); + } + return child; } } diff --git a/lib/src/user_list_view.dart b/lib/src/user_list_view.dart index b57e2d55..928ea39e 100644 --- a/lib/src/user_list_view.dart +++ b/lib/src/user_list_view.dart @@ -339,7 +339,9 @@ class _UserListViewState extends State ); return LazyLoadScrollView( - onEndOfPage: () => _listenUserPagination(usersBlocState), + onEndOfPage: () async { + return _listenUserPagination(usersBlocState); + }, child: child, ); }, diff --git a/pubspec.yaml b/pubspec.yaml index a226f9c3..55f2bc65 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,9 +29,10 @@ dependencies: image_picker: ^0.6.7+2 flutter_keyboard_visibility: ^3.3.0 stream_chat: - git: - url: https://github.com/GetStream/stream-chat-dart - ref: two-way-pagination + path: ../stream-chat-dart +# git: +# url: https://github.com/GetStream/stream-chat-dart +# ref: two-way-pagination mime: ^0.9.6+3 video_compress: ^2.1.1 visibility_detector: ^0.1.5 From 3f300bdfa020c7900cd06e771819a590e4c87afb Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 16 Dec 2020 16:57:43 +0530 Subject: [PATCH 44/74] ref git url in pubspec Signed-off-by: Sahil Kumar --- pubspec.yaml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pubspec.yaml b/pubspec.yaml index 55f2bc65..a226f9c3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,10 +29,9 @@ dependencies: image_picker: ^0.6.7+2 flutter_keyboard_visibility: ^3.3.0 stream_chat: - path: ../stream-chat-dart -# git: -# url: https://github.com/GetStream/stream-chat-dart -# ref: two-way-pagination + git: + url: https://github.com/GetStream/stream-chat-dart + ref: two-way-pagination mime: ^0.9.6+3 video_compress: ^2.1.1 visibility_detector: ^0.1.5 From 8b4ff142bab22c8f214a16a22a03c8de318f4fed Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 16 Dec 2020 17:06:09 +0530 Subject: [PATCH 45/74] Merge conflicts Signed-off-by: Sahil Kumar --- lib/src/message_input.dart | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/src/message_input.dart b/lib/src/message_input.dart index 447d49d9..dd7d90ca 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'; @@ -462,6 +461,7 @@ class MessageInputState extends State { } Timer _debounce; + void _onChanged(BuildContext context, String s) { if (_debounce?.isActive == true) _debounce.cancel(); _debounce = Timer( @@ -1956,7 +1956,11 @@ class MessageInputState extends State { message = await widget.preMessageSending(message); } - final channel = StreamChannel.of(context).channel; + final streamChannel = StreamChannel.of(context); + final channel = streamChannel.channel; + if (!channel.state.isUpToDate) { + await streamChannel.reloadChannel(); + } if (widget.editMessage == null || widget.editMessage.status == MessageSendingStatus.FAILED) { @@ -2043,6 +2047,7 @@ class MessageInputState extends State { } bool _initialized = false; + @override void didChangeDependencies() { if (widget.editMessage != null && !_initialized) { From 12105a3120017dc02c7758d93e12e3b3be993fff Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Wed, 16 Dec 2020 19:03:03 +0530 Subject: [PATCH 46/74] Merge fixes Signed-off-by: Sahil Kumar --- example/lib/new_chat_screen.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/lib/new_chat_screen.dart b/example/lib/new_chat_screen.dart index a2ce9ebf..54371c3b 100644 --- a/example/lib/new_chat_screen.dart +++ b/example/lib/new_chat_screen.dart @@ -358,7 +358,7 @@ class _NewChatScreenState extends State { context, Routes.CHANNEL_PAGE, ModalRoute.withName(Routes.HOME), - arguments: channel, + arguments: ChannelPageArgs(channel: channel), ); }, ), From c81111a1e20959529faadbd8c247b86824aa802d Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 16 Dec 2020 15:42:49 +0100 Subject: [PATCH 47/74] fix online status in new chat page --- lib/src/user_item.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/user_item.dart b/lib/src/user_item.dart index f12e3e61..4c1af658 100644 --- a/lib/src/user_item.dart +++ b/lib/src/user_item.dart @@ -90,6 +90,8 @@ class UserItem extends StatelessWidget { } Widget _buildLastActive(context) { - return Text('Last online ${Jiffy(user.lastActive).fromNow()}'); + return user.online == true + ? Text('Online') + : Text('Last online ${Jiffy(user.lastActive).fromNow()}'); } } From 336580761510f39d472a67e27f7576f20d4ed44e Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 16 Dec 2020 15:45:07 +0100 Subject: [PATCH 48/74] fix unread count indicator --- lib/src/message_list_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 12adaf3e..4f40feb1 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -529,7 +529,7 @@ class _MessageListViewState extends State { child: Padding( padding: const EdgeInsets.all(3.0), child: Text( - snapshot.data.toString(), + '$unreadCount', style: TextStyle( fontSize: 11, fontWeight: FontWeight.bold, From 4a165249a1918621cb07a8c394b35c80d638d80b Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 16 Dec 2020 15:47:35 +0100 Subject: [PATCH 49/74] revert api key --- example/lib/choose_user_page.dart | 2 +- example/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/lib/choose_user_page.dart b/example/lib/choose_user_page.dart index 73b12425..dde87376 100644 --- a/example/lib/choose_user_page.dart +++ b/example/lib/choose_user_page.dart @@ -13,7 +13,7 @@ import 'routes/routes.dart'; const kStreamApiKey = 'STREAM_API_KEY'; const kStreamUserId = 'STREAM_USER_ID'; const kStreamToken = 'STREAM_TOKEN'; -const kDefaultStreamApiKey = 's2dxdhpxd94g'; +const kDefaultStreamApiKey = 'uj7qrdbfrzvg'; class ChooseUserPage extends StatelessWidget { @override diff --git a/example/pubspec.yaml b/example/pubspec.yaml index f9c71355..ef06f719 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.91+94 +version: 1.0.92+95 environment: sdk: ">=2.2.2 <3.0.0" From 7fd98111d23dd14625bb52ad7301e3a7413212bd Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Wed, 16 Dec 2020 16:00:29 +0100 Subject: [PATCH 50/74] revert api key --- example/lib/choose_user_page.dart | 2 +- example/pubspec.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/example/lib/choose_user_page.dart b/example/lib/choose_user_page.dart index dde87376..a64013b9 100644 --- a/example/lib/choose_user_page.dart +++ b/example/lib/choose_user_page.dart @@ -19,7 +19,7 @@ class ChooseUserPage extends StatelessWidget { @override Widget build(BuildContext context) { final users = { - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidmlzaGFsIn0._JHWzo92fpTWZMZriJHXqOng6ShYVmWrdaIaPwEPKBg': + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoidmlzaGFsIn0.lCz-idDgaZ-xszjnuB_hTfeIOhTFmJtTB2fEjhwrcCI': User( id: 'vishal', extraData: { diff --git a/example/pubspec.yaml b/example/pubspec.yaml index ef06f719..cec1b886 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.92+95 +version: 1.0.93+96 environment: sdk: ">=2.2.2 <3.0.0" From e17a3c30b790ee493430a51527ed43de528de88a Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 17 Dec 2020 14:42:46 +0530 Subject: [PATCH 51/74] [LazyLoadScrollView] Fix callback logic Signed-off-by: Sahil Kumar --- lib/src/lazy_load_scroll_view.dart | 89 ++++++++++++++++-------------- lib/src/message_list_view.dart | 11 ++-- 2 files changed, 54 insertions(+), 46 deletions(-) diff --git a/lib/src/lazy_load_scroll_view.dart b/lib/src/lazy_load_scroll_view.dart index a3c344aa..99eeb861 100644 --- a/lib/src/lazy_load_scroll_view.dart +++ b/lib/src/lazy_load_scroll_view.dart @@ -15,7 +15,7 @@ class LazyLoadScrollView extends StatefulWidget { /// Called when the [child] reaches the end of the list final AsyncCallback onEndOfPage; - /// The offset to take into account when triggering [onEndOfPage] in pixels + /// The offset to take into account when triggering [onEndOfPage]/[onStartOfPage] in pixels final double scrollOffset; /// Used to determine if loading of new data has finished. You should use set this if you aren't using a FutureBuilder or StreamBuilder @@ -38,6 +38,7 @@ class LazyLoadScrollView extends StatefulWidget { class _LazyLoadScrollViewState extends State { _LoadingStatus _loadMoreStatus = _LoadingStatus.STABLE; + double _scrollPosition = 0.0; @override Widget build(BuildContext context) { @@ -49,59 +50,65 @@ class _LazyLoadScrollViewState extends State { bool _onNotification(Notification notification) { if (notification is ScrollUpdateNotification) { - if (notification.metrics.maxScrollExtent > notification.metrics.pixels && - notification.metrics.maxScrollExtent - notification.metrics.pixels <= - widget.scrollOffset) { - if (_loadMoreStatus != null && - _loadMoreStatus == _LoadingStatus.STABLE) { - _loadMoreStatus = _LoadingStatus.LOADING; - if (widget.onEndOfPage != null) { - widget.onEndOfPage().whenComplete(() { - _loadMoreStatus = _LoadingStatus.STABLE; - }); - } + final pixels = notification.metrics.pixels; + final extentBefore = notification.metrics.extentBefore; + final extentAfter = notification.metrics.extentAfter; + final scrollOffset = widget.scrollOffset; + + final scrollingDown = _scrollPosition < pixels; + + if (scrollOffset == null || scrollOffset == 0) { + if (extentAfter == 0) { + _onEndOfPage(); + } + if (extentBefore == 0) { + _onStartOfPage(); } - } - if (notification.metrics.minScrollExtent < notification.metrics.pixels && - notification.metrics.pixels - notification.metrics.minScrollExtent <= - widget.scrollOffset) { - if (_loadMoreStatus != null && - _loadMoreStatus == _LoadingStatus.STABLE) { - _loadMoreStatus = _LoadingStatus.LOADING; - if (widget.onStartOfPage != null) { - widget.onStartOfPage().whenComplete(() { - _loadMoreStatus = _LoadingStatus.STABLE; - }); + } else { + if (scrollingDown) { + if (extentAfter <= scrollOffset) { + _onEndOfPage(); + } + } else { + if (extentBefore <= scrollOffset) { + _onStartOfPage(); } } } + _scrollPosition = pixels; return true; } if (notification is OverscrollNotification) { if (notification.overscroll > 0) { - if (_loadMoreStatus != null && - _loadMoreStatus == _LoadingStatus.STABLE) { - _loadMoreStatus = _LoadingStatus.LOADING; - if (widget.onEndOfPage != null) { - widget.onEndOfPage().whenComplete(() { - _loadMoreStatus = _LoadingStatus.STABLE; - }); - } - } + _onEndOfPage(); } if (notification.overscroll < 0) { - if (_loadMoreStatus != null && - _loadMoreStatus == _LoadingStatus.STABLE) { - _loadMoreStatus = _LoadingStatus.LOADING; - if (widget.onStartOfPage != null) { - widget.onStartOfPage().whenComplete(() { - _loadMoreStatus = _LoadingStatus.STABLE; - }); - } - } + _onStartOfPage(); } return true; } return false; } + + void _onEndOfPage() { + if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) { + _loadMoreStatus = _LoadingStatus.LOADING; + if (widget.onEndOfPage != null) { + widget.onEndOfPage().whenComplete(() { + _loadMoreStatus = _LoadingStatus.STABLE; + }); + } + } + } + + void _onStartOfPage() { + if (_loadMoreStatus != null && _loadMoreStatus == _LoadingStatus.STABLE) { + _loadMoreStatus = _LoadingStatus.LOADING; + if (widget.onStartOfPage != null) { + widget.onStartOfPage().whenComplete(() { + _loadMoreStatus = _LoadingStatus.STABLE; + }); + } + } + } } diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 4f40feb1..d8a9d416 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -294,13 +294,13 @@ class _MessageListViewState extends State { if (!_upToDate) { _topPaginationActive = false; _bottomPaginationActive = true; - _paginateData(streamChannel, QueryDirection.bottom); + return _paginateData(streamChannel, QueryDirection.bottom); } }, onEndOfPage: () async { _topPaginationActive = true; _bottomPaginationActive = false; - _paginateData(streamChannel, QueryDirection.top); + return _paginateData(streamChannel, QueryDirection.top); }, child: ScrollablePositionedList.builder( key: ValueKey(initialIndex + initialAlignment), @@ -453,11 +453,12 @@ class _MessageListViewState extends State { }); } - void _paginateData(StreamChannelState channel, QueryDirection direction) { + Future _paginateData( + StreamChannelState channel, QueryDirection direction) { if (widget.parentMessage == null) { - channel.queryMessages(direction: direction); + return channel.queryMessages(direction: direction); } else { - channel.getReplies(widget.parentMessage.id); + return channel.getReplies(widget.parentMessage.id); } } From 84aa3fbb87df9acb858fa52aabbddcbfc5b84d2d Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 17 Dec 2020 17:10:45 +0530 Subject: [PATCH 52/74] Use localChannel if present instead of creating a new Signed-off-by: Sahil Kumar --- example/lib/main.dart | 7 +- lib/src/message_list_view.dart | 396 +++++++++++++++++---------------- lib/src/stream_channel.dart | 21 +- 3 files changed, 225 insertions(+), 199 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index ca7006b0..1c61dde2 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -335,10 +335,11 @@ class _ChannelListPageState extends State { onItemTap: (messageResponse) async { final client = StreamChat.of(context).client; final message = messageResponse.message; - final channel = Channel.fromState( - client, - ChannelState(channel: messageResponse.channel), + final channel = client.channel( + messageResponse.channel.type, + id: messageResponse.channel.id, ); + await channel.watch(); Navigator.pushNamed( context, Routes.CHANNEL_PAGE, diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index d8a9d416..ca184d6b 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -183,7 +183,7 @@ class _MessageListViewState extends State { final messageIndex = messages.indexWhere((e) { return e.id == streamChannel.initialMessageId; }); - return totalMessages - messageIndex - 1; + return totalMessages - messageIndex; } return 0; } @@ -234,223 +234,235 @@ class _MessageListViewState extends State { initialAlignment = _initialAlignment; } - return StreamBuilder>( - stream: messagesStream?.map((messages) => messages - ?.where((e) => - !e.isDeleted || - (e.isDeleted && - e.user.id == streamChannel.channel.client.state.user.id)) - ?.toList()), - builder: (context, snapshot) { - if (!snapshot.hasData) { - return Center( - child: CircularProgressIndicator(), - ); - } - - final messageList = snapshot.data?.reversed?.toList() ?? []; - - if (messageList.isEmpty) { - if (_upToDate) { + return WillPopScope( + onWillPop: () async { + if (!_upToDate) { + await streamChannel.reloadChannel(); + } + return true; + }, + child: StreamBuilder>( + stream: messagesStream?.map((messages) => messages + ?.where((e) => + !e.isDeleted || + (e.isDeleted && + e.user.id == streamChannel.channel.client.state.user.id)) + ?.toList()), + builder: (context, snapshot) { + if (!snapshot.hasData) { return Center( - child: Text( - 'No chats here yet...', - style: TextStyle( - fontSize: 12, - color: Colors.black.withOpacity(.5), - ), - ), + child: CircularProgressIndicator(), ); } - } else { - messages = messageList; - } - final newMessagesListLength = messages.length; + final messageList = snapshot.data?.reversed?.toList() ?? []; - if (_bottomPaginationActive) { - if (_itemPositionListener.itemPositions.value?.isNotEmpty == true && - _messageListLength != null) { - final first = _itemPositionListener.itemPositions.value.first; - final diff = newMessagesListLength - _messageListLength; - if (diff > 0) { - initialIndex = first.index + diff; - initialAlignment = first.itemLeadingEdge; + if (messageList.isEmpty) { + if (_upToDate) { + return Center( + child: Text( + 'No chats here yet...', + style: TextStyle( + fontSize: 12, + color: Colors.black.withOpacity(.5), + ), + ), + ); } + } else { + messages = messageList; } - } else if (!_topPaginationActive && _upToDate) { - // Reset the index in-case we send any new message - initialIndex = 0; - initialAlignment = 0; - } - _messageListLength = newMessagesListLength; + final newMessagesListLength = messages.length; - return Stack( - alignment: Alignment.center, - children: [ - LazyLoadScrollView( - onStartOfPage: () async { - if (!_upToDate) { - _topPaginationActive = false; - _bottomPaginationActive = true; - return _paginateData(streamChannel, QueryDirection.bottom); - } - }, - onEndOfPage: () async { - _topPaginationActive = true; - _bottomPaginationActive = false; - return _paginateData(streamChannel, QueryDirection.top); - }, - child: ScrollablePositionedList.builder( - key: ValueKey(initialIndex + initialAlignment), - itemPositionsListener: _itemPositionListener, - addAutomaticKeepAlives: true, - initialScrollIndex: initialIndex ?? 0, - initialAlignment: initialAlignment ?? 0, - physics: widget.scrollPhysics, - itemScrollController: _scrollController, - reverse: true, - itemCount: messages.length + - 2 + - (widget.parentMessage != null ? 1 : 0), - itemBuilder: (context, i) { - if (i == messages.length + 2) { - if (widget.parentMessageBuilder != null) { - return widget.parentMessageBuilder( + if (_bottomPaginationActive) { + if (_itemPositionListener.itemPositions.value?.isNotEmpty == + true && + _messageListLength != null) { + final first = _itemPositionListener.itemPositions.value.first; + final diff = newMessagesListLength - _messageListLength; + if (diff > 0) { + initialIndex = first.index + diff; + initialAlignment = first.itemLeadingEdge; + } + } + } else if (!_topPaginationActive && _upToDate) { + // Reset the index in-case we send any new message + initialIndex = 0; + initialAlignment = 0; + } + + _messageListLength = newMessagesListLength; + + return Stack( + alignment: Alignment.center, + children: [ + LazyLoadScrollView( + onStartOfPage: () async { + if (!_upToDate) { + _topPaginationActive = false; + _bottomPaginationActive = true; + return _paginateData( + streamChannel, QueryDirection.bottom); + } + }, + onEndOfPage: () async { + _topPaginationActive = true; + _bottomPaginationActive = false; + return _paginateData(streamChannel, QueryDirection.top); + }, + child: ScrollablePositionedList.builder( + key: ValueKey(initialIndex + initialAlignment), + itemPositionsListener: _itemPositionListener, + addAutomaticKeepAlives: true, + initialScrollIndex: initialIndex ?? 0, + initialAlignment: initialAlignment ?? 0, + physics: widget.scrollPhysics, + itemScrollController: _scrollController, + reverse: true, + itemCount: messages.length + + 2 + + (widget.parentMessage != null ? 1 : 0), + itemBuilder: (context, i) { + if (i == messages.length + 2) { + if (widget.parentMessageBuilder != null) { + return widget.parentMessageBuilder( + context, + widget.parentMessage, + ); + } else { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + buildParentMessage(widget.parentMessage), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 32), + child: Container( + padding: const EdgeInsets.all(8), + child: Text( + 'Start of thread', + textAlign: TextAlign.center, + ), + color: Theme.of(context) + .accentColor + .withAlpha(50), + ), + ), + ], + ); + } + } + if (i == messages.length + 1) { + return _buildLoadingIndicator( + streamChannel, + QueryDirection.top, + ); + } + if (i == 0) { + return _buildLoadingIndicator( + streamChannel, + QueryDirection.bottom, + ); + } + final message = messages[i - 1]; + final nextMessage = (i - 1) > 0 ? messages[i - 2] : null; + + Widget messageWidget; + + if (i == 1) { + messageWidget = _buildBottomMessage( context, - widget.parentMessage, + message, + messages, + streamChannel, + ); + } else if (i == messages.length - 1) { + messageWidget = _buildTopMessage( + context, + message, + messages, + streamChannel, ); } else { + if (widget.messageBuilder != null) { + messageWidget = Builder( + key: ValueKey('MESSAGE-${message.id}'), + builder: (_) => widget.messageBuilder( + context, + MessageDetails( + context, + message, + messages, + i, + ), + messages), + ); + } else { + messageWidget = buildMessage(message, messages, i); + } + } + + if (nextMessage != null && + !Jiffy(message.createdAt.toLocal()).isSame( + nextMessage.createdAt.toLocal(), Units.DAY)) { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - buildParentMessage(widget.parentMessage), + messageWidget, Padding( padding: - const EdgeInsets.symmetric(horizontal: 32), - child: Container( - padding: const EdgeInsets.all(8), - child: Text( - 'Start of thread', - textAlign: TextAlign.center, - ), - color: - Theme.of(context).accentColor.withAlpha(50), - ), + const EdgeInsets.symmetric(vertical: 12.0), + child: widget.dateDividerBuilder != null + ? widget.dateDividerBuilder( + nextMessage.createdAt.toLocal()) + : DateDivider( + dateTime: nextMessage.createdAt.toLocal(), + ), ), ], ); } - } - if (i == messages.length + 1) { - return _buildLoadingIndicator( - streamChannel, - QueryDirection.top, - ); - } - if (i == 0) { - return _buildLoadingIndicator( - streamChannel, - QueryDirection.bottom, - ); - } - final message = messages[i - 1]; - final nextMessage = (i - 1) > 0 ? messages[i - 2] : null; - Widget messageWidget; - - if (i == 1) { - messageWidget = _buildBottomMessage( - context, - message, - messages, - streamChannel, - ); - } else if (i == messages.length - 1) { - messageWidget = _buildTopMessage( - context, - message, - messages, - streamChannel, - ); - } else { - if (widget.messageBuilder != null) { - messageWidget = Builder( - key: ValueKey('MESSAGE-${message.id}'), - builder: (_) => widget.messageBuilder( - context, - MessageDetails( - context, - message, - messages, - i, - ), - messages), - ); - } else { - messageWidget = buildMessage(message, messages, i); + return messageWidget; + }, + ), + ), + if (widget.showScrollToBottom) _buildScrollToBottom(), + Positioned( + top: 20.0, + child: ValueListenableBuilder>( + valueListenable: _itemPositionListener.itemPositions, + builder: (context, values, _) { + final items = _itemPositionListener.itemPositions?.value; + if (items.isEmpty || messages.isEmpty) { + return SizedBox(); } - } - if (nextMessage != null && - !Jiffy(message.createdAt.toLocal()).isSame( - nextMessage.createdAt.toLocal(), Units.DAY)) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - messageWidget, - Padding( - padding: const EdgeInsets.symmetric(vertical: 12.0), - child: widget.dateDividerBuilder != null - ? widget.dateDividerBuilder( - nextMessage.createdAt.toLocal()) - : DateDivider( - dateTime: nextMessage.createdAt.toLocal(), - ), - ), - ], - ); - } + var index = _getTopElement(values).index; - return messageWidget; - }, + if (index > messages.length) { + return SizedBox(); + } + + if (index == messages.length) { + index = max(index - 1, 0); + } + + return widget.dateDividerBuilder != null + ? widget.dateDividerBuilder( + messages[index].createdAt.toLocal(), + ) + : DateDivider( + dateTime: messages[index].createdAt.toLocal(), + ); + }, + ), ), - ), - if (widget.showScrollToBottom) _buildScrollToBottom(), - Positioned( - top: 20.0, - child: ValueListenableBuilder>( - valueListenable: _itemPositionListener.itemPositions, - builder: (context, values, _) { - final items = _itemPositionListener.itemPositions?.value; - if (items.isEmpty || messages.isEmpty) { - return SizedBox(); - } - - var index = _getTopElement(values).index; - - if (index > messages.length) { - return SizedBox(); - } - - if (index == messages.length) { - index = max(index - 1, 0); - } - - return widget.dateDividerBuilder != null - ? widget.dateDividerBuilder( - messages[index].createdAt.toLocal(), - ) - : DateDivider( - dateTime: messages[index].createdAt.toLocal(), - ); - }, - ), - ), - ], - ); - }); + ], + ); + }), + ); } Future _paginateData( diff --git a/lib/src/stream_channel.dart b/lib/src/stream_channel.dart index 0ca87b96..001d98ff 100644 --- a/lib/src/stream_channel.dart +++ b/lib/src/stream_channel.dart @@ -307,16 +307,29 @@ class StreamChannelState extends State { if (initialMessageId != null) false, ], builder: (context, snapshot) { + if (snapshot.hasError) { + if (snapshot.error is Error) { + print((snapshot.error as Error).stackTrace); + } + var message = snapshot.error.toString(); + if (snapshot.error is DioError) { + final dioError = snapshot.error as DioError; + if (dioError.type == DioErrorType.RESPONSE) { + message = dioError.message; + } else { + message = 'Check your connection and retry'; + } + } + return Center( + child: Text(message), + ); + } final initialized = snapshot.data[0]; final dataLoaded = initialMessageId == null ? true : snapshot.data[1]; if (widget.showLoading && (!initialized || !dataLoaded)) { return Center( child: CircularProgressIndicator(), ); - } else if (snapshot.hasError) { - return Center( - child: Text(snapshot.error), - ); } return widget.child; }, From 49377b46a21def9076b6de5cd1caee78de2014cf Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 17 Dec 2020 23:15:56 +0530 Subject: [PATCH 53/74] fix initialIndex bug Signed-off-by: Sahil Kumar --- example/lib/main.dart | 4 +- lib/src/message_list_view.dart | 78 ++++++++++++++++++++-------------- 2 files changed, 50 insertions(+), 32 deletions(-) diff --git a/example/lib/main.dart b/example/lib/main.dart index 1c61dde2..9f41efd5 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -339,7 +339,9 @@ class _ChannelListPageState extends State { messageResponse.channel.type, id: messageResponse.channel.id, ); - await channel.watch(); + if (channel.state == null) { + await channel.watch(); + } Navigator.pushNamed( context, Routes.CHANNEL_PAGE, diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index ca184d6b..254af757 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -183,22 +183,15 @@ class _MessageListViewState extends State { final messageIndex = messages.indexWhere((e) { return e.id == streamChannel.initialMessageId; }); - return totalMessages - messageIndex; + final index = totalMessages - messageIndex; + if (index != 0) return index - 1; + return index; } return 0; } double get _initialAlignment { if (widget.initialAlignment != null) return widget.initialAlignment; - final streamChannel = StreamChannel.of(context); - if (streamChannel.initialMessageId != null) { - final messages = streamChannel.channel.state.messages; - final messageIndex = messages.indexWhere((e) { - return e.id == streamChannel.initialMessageId; - }); - final isFirstMessage = messageIndex == 0; - return isFirstMessage ? 0 : 0.5; - } return 0; } @@ -212,13 +205,13 @@ class _MessageListViewState extends State { bool _topPaginationActive = false; bool _bottomPaginationActive = false; - bool get _paginationActive => _topPaginationActive || _bottomPaginationActive; - int initialIndex; double initialAlignment; List messages = []; + bool initialMessageHighlightComplete = false; + @override Widget build(BuildContext context) { final streamChannel = StreamChannel.of(context); @@ -229,11 +222,6 @@ class _MessageListViewState extends State { .map((threads) => threads[widget.parentMessage.id]) : streamChannel.channel.state?.messagesStream; - if (!_paginationActive && !_upToDate) { - initialIndex = _initialIndex; - initialAlignment = _initialAlignment; - } - return WillPopScope( onWillPop: () async { if (!_upToDate) { @@ -275,21 +263,22 @@ class _MessageListViewState extends State { final newMessagesListLength = messages.length; - if (_bottomPaginationActive) { - if (_itemPositionListener.itemPositions.value?.isNotEmpty == - true && - _messageListLength != null) { - final first = _itemPositionListener.itemPositions.value.first; - final diff = newMessagesListLength - _messageListLength; - if (diff > 0) { - initialIndex = first.index + diff; - initialAlignment = first.itemLeadingEdge; + if (_messageListLength != null) { + if (_bottomPaginationActive) { + if (_itemPositionListener.itemPositions.value?.isNotEmpty == + true) { + final first = _itemPositionListener.itemPositions.value.first; + final diff = newMessagesListLength - _messageListLength; + if (diff > 0) { + initialIndex = first.index + diff; + initialAlignment = first.itemLeadingEdge; + } } + } else if (!_topPaginationActive && _upToDate) { + // Reset the index in-case we send any new message + initialIndex = 0; + initialAlignment = 0; } - } else if (!_topPaginationActive && _upToDate) { - // Reset the index in-case we send any new message - initialIndex = 0; - initialAlignment = 0; } _messageListLength = newMessagesListLength; @@ -731,7 +720,7 @@ class _MessageListViewState extends State { final allRead = readList.length >= (channel.memberCount ?? 0) - 1; - return MessageWidget( + Widget child = MessageWidget( key: ValueKey('MESSAGE-${message.id}'), message: message, reverse: isMyMessage, @@ -773,6 +762,30 @@ class _MessageListViewState extends State { readList: readList, allRead: allRead, ); + + if (!initialMessageHighlightComplete && + widget.highlightInitialMessage && + _isInitialMessage(message.id)) { + final accentColor = Theme.of(context).accentColor; + child = TweenAnimationBuilder( + tween: ColorTween( + begin: accentColor.withOpacity(0.7), + end: Colors.transparent, + ), + duration: const Duration(seconds: 2), + child: child, + onEnd: () { + initialMessageHighlightComplete = true; + }, + builder: (_, color, child) { + return Container( + color: color, + child: child, + ); + }, + ); + } + return child; } StreamSubscription _messageNewListener; @@ -785,6 +798,9 @@ class _MessageListViewState extends State { final streamChannel = StreamChannel.of(context); + initialIndex = _initialIndex; + initialAlignment = _initialAlignment; + _messageNewListener = streamChannel.channel.on(EventType.messageNew).listen((event) { if (event.message.user.id == streamChannel.channel.client.state.user.id) { From 8ee4b585b8d80e3096e1f3c7b68d7f8c706136f4 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 18 Dec 2020 10:00:10 +0100 Subject: [PATCH 54/74] mark read only if the channel is up to date --- lib/src/message_list_view.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 254af757..6d1e61f2 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -511,6 +511,7 @@ class _MessageListViewState extends State { _bottomPaginationActive = false; _topPaginationActive = false; streamChannel.reloadChannel(); + streamChannel.channel.markRead(); } else { setState(() => _showScrollToBottom = false); _scrollController.scrollTo( @@ -642,7 +643,8 @@ class _MessageListViewState extends State { onVisibilityChanged: (visibility) { final isVisible = visibility.visibleBounds != Rect.zero; if (isVisible && !_bottomWasVisible) { - if (streamChannel.channel.config?.readEvents == true && + if (streamChannel.channel.state.isUpToDate && + streamChannel.channel.config?.readEvents == true && streamChannel.channel.state.unreadCount > 0) { streamChannel.channel.markRead(); } From 89d1d5f562a4ccaa9955ea9d1694cde9d5cc7748 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 18 Dec 2020 11:54:47 +0100 Subject: [PATCH 55/74] update llc dependency --- example/ios/Flutter/.last_build_id | 2 +- example/lib/main.dart | 12 ++++++++---- example/pubspec.yaml | 2 +- pubspec.yaml | 2 +- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/example/ios/Flutter/.last_build_id b/example/ios/Flutter/.last_build_id index 20c7c514..e72273c3 100644 --- a/example/ios/Flutter/.last_build_id +++ b/example/ios/Flutter/.last_build_id @@ -1 +1 @@ -c3e639ccf9b069e37a7d1345194e6b99 \ No newline at end of file +13a41b8138c4868054e44b5158f3bdd6 \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart index 3fb47b66..4c37c57a 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -24,7 +24,7 @@ void main() async { final client = Client( apiKey ?? kDefaultStreamApiKey, - logLevel: Level.INFO, + logLevel: Level.SEVERE, showLocalNotification: (!kIsWeb && Platform.isAndroid) ? showLocalNotification : null, persistenceEnabled: true, @@ -213,12 +213,16 @@ class _HomePageState extends State { alignment: Alignment.bottomCenter, child: ListTile( onTap: () async { - await StreamChat.of(context).client.disconnect(); + Navigator.pop(context); final secureStorage = FlutterSecureStorage(); await secureStorage.deleteAll(); - Navigator.pop(context); - Navigator.pushReplacementNamed( + + StreamChat.of(context).client.disconnect( + clearUser: true, + ); + + await Navigator.pushReplacementNamed( context, Routes.CHOOSE_USER, ); diff --git a/example/pubspec.yaml b/example/pubspec.yaml index f9c71355..ef06f719 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.91+94 +version: 1.0.92+95 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index b41f58c6..47139390 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,7 +28,7 @@ dependencies: file_picker: ^2.0.12 image_picker: ^0.6.7+2 flutter_keyboard_visibility: ^3.3.0 - stream_chat: ^0.2.14 + stream_chat: ^0.2.18 mime: ^0.9.6+3 video_compress: ^2.1.1 visibility_detector: ^0.1.5 From 7cfb1752fba709ba808f3c7361024dc9f2aca961 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 18 Dec 2020 17:11:46 +0530 Subject: [PATCH 56/74] fix newMessage jump bug Signed-off-by: Sahil Kumar --- lib/src/lazy_load_scroll_view.dart | 38 ++++++++++++++++++++++++++++-- lib/src/message_list_view.dart | 22 ++++++++++++++--- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/lib/src/lazy_load_scroll_view.dart b/lib/src/lazy_load_scroll_view.dart index 99eeb861..2bc82a55 100644 --- a/lib/src/lazy_load_scroll_view.dart +++ b/lib/src/lazy_load_scroll_view.dart @@ -15,6 +15,15 @@ class LazyLoadScrollView extends StatefulWidget { /// Called when the [child] reaches the end of the list final AsyncCallback onEndOfPage; + /// Called when the list scrolling starts + final VoidCallback onPageScrollStart; + + /// Called when the list scrolling ends + final VoidCallback onPageScrollEnd; + + /// Called every time the [child] is in-between the list + final VoidCallback onInBetweenOfPage; + /// The offset to take into account when triggering [onEndOfPage]/[onStartOfPage] in pixels final double scrollOffset; @@ -27,6 +36,9 @@ class LazyLoadScrollView extends StatefulWidget { @required this.child, this.onStartOfPage, this.onEndOfPage, + this.onPageScrollStart, + this.onPageScrollEnd, + this.onInBetweenOfPage, this.isLoading = false, this.scrollOffset = 100, }) : assert(child != null), @@ -49,12 +61,34 @@ class _LazyLoadScrollViewState extends State { } bool _onNotification(Notification notification) { + if (notification is ScrollStartNotification) { + if (widget.onPageScrollStart != null) { + widget.onPageScrollStart(); + return true; + } + } + if (notification is ScrollEndNotification) { + if (widget.onPageScrollEnd != null) { + widget.onPageScrollEnd(); + return true; + } + } if (notification is ScrollUpdateNotification) { final pixels = notification.metrics.pixels; - final extentBefore = notification.metrics.extentBefore; - final extentAfter = notification.metrics.extentAfter; + final maxScrollExtent = notification.metrics.maxScrollExtent; + final minScrollExtent = notification.metrics.minScrollExtent; final scrollOffset = widget.scrollOffset; + if (pixels > (minScrollExtent + scrollOffset) && + pixels < (maxScrollExtent - scrollOffset)) { + if (widget.onInBetweenOfPage != null) { + widget.onInBetweenOfPage(); + return true; + } + } + + final extentBefore = notification.metrics.extentBefore; + final extentAfter = notification.metrics.extentAfter; final scrollingDown = _scrollPosition < pixels; if (scrollOffset == null || scrollOffset == 0) { diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 254af757..f4ab3103 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -212,6 +212,8 @@ class _MessageListViewState extends State { bool initialMessageHighlightComplete = false; + bool _inBetweenList = false; + @override Widget build(BuildContext context) { final streamChannel = StreamChannel.of(context); @@ -264,7 +266,7 @@ class _MessageListViewState extends State { final newMessagesListLength = messages.length; if (_messageListLength != null) { - if (_bottomPaginationActive) { + if (_bottomPaginationActive || (_inBetweenList && _upToDate)) { if (_itemPositionListener.itemPositions.value?.isNotEmpty == true) { final first = _itemPositionListener.itemPositions.value.first; @@ -288,17 +290,27 @@ class _MessageListViewState extends State { children: [ LazyLoadScrollView( onStartOfPage: () async { + _inBetweenList = false; if (!_upToDate) { _topPaginationActive = false; _bottomPaginationActive = true; return _paginateData( - streamChannel, QueryDirection.bottom); + streamChannel, + QueryDirection.bottom, + ); } }, onEndOfPage: () async { + _inBetweenList = false; _topPaginationActive = true; _bottomPaginationActive = false; - return _paginateData(streamChannel, QueryDirection.top); + return _paginateData( + streamChannel, + QueryDirection.top, + ); + }, + onInBetweenOfPage: () { + _inBetweenList = true; }, child: ScrollablePositionedList.builder( key: ValueKey(initialIndex + initialAlignment), @@ -803,6 +815,10 @@ class _MessageListViewState extends State { _messageNewListener = streamChannel.channel.on(EventType.messageNew).listen((event) { + if (_upToDate) { + _bottomPaginationActive = false; + _topPaginationActive = false; + } if (event.message.user.id == streamChannel.channel.client.state.user.id) { WidgetsBinding.instance.addPostFrameCallback((_) { _scrollController.jumpTo( From 9e3248d4c9132cb52ec8e590185d444100b4675f Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 18 Dec 2020 17:13:55 +0530 Subject: [PATCH 57/74] minor refactor Signed-off-by: Sahil Kumar --- lib/src/message_list_view.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index 56d246bf..b56add20 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -655,9 +655,10 @@ class _MessageListViewState extends State { onVisibilityChanged: (visibility) { final isVisible = visibility.visibleBounds != Rect.zero; if (isVisible && !_bottomWasVisible) { - if (streamChannel.channel.state.isUpToDate && - streamChannel.channel.config?.readEvents == true && - streamChannel.channel.state.unreadCount > 0) { + final channel = streamChannel.channel; + if (_upToDate && + channel.config?.readEvents == true && + channel.state.unreadCount > 0) { streamChannel.channel.markRead(); } _bottomWasVisible = !isVisible; From 966548132304084135e7f2d2c320be4097eb61bb Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Fri, 18 Dec 2020 19:55:27 +0530 Subject: [PATCH 58/74] Change cupertinoActivitYIndicator to cicularProgressIndicator Signed-off-by: Sahil Kumar --- lib/src/message_list_view.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index b56add20..e2807775 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -592,7 +592,7 @@ class _MessageListViewState extends State { return Center( child: Padding( padding: const EdgeInsets.all(8.0), - child: const CupertinoActivityIndicator(), + child: const CircularProgressIndicator(), ), ); }); From 14aebadabf1edc2676b364cd8f4bea7cf762bafa Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 18 Dec 2020 15:42:31 +0100 Subject: [PATCH 59/74] fix markread --- example/ios/Flutter/.last_build_id | 2 +- example/ios/Podfile.lock | 4 ++-- example/lib/main.dart | 2 +- example/pubspec.yaml | 2 +- lib/src/message_list_view.dart | 4 +++- 5 files changed, 8 insertions(+), 6 deletions(-) diff --git a/example/ios/Flutter/.last_build_id b/example/ios/Flutter/.last_build_id index e72273c3..f57e1e1f 100644 --- a/example/ios/Flutter/.last_build_id +++ b/example/ios/Flutter/.last_build_id @@ -1 +1 @@ -13a41b8138c4868054e44b5158f3bdd6 \ No newline at end of file +c770358a10272b4ee1cdc2eb432445f6 \ No newline at end of file diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index bf8c0e1a..f266b656 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -40,7 +40,7 @@ PODS: - Firebase/Messaging (6.33.0): - Firebase/CoreOnly - FirebaseMessaging (~> 4.7.0) - - firebase_core (0.5.2-1): + - firebase_core (0.5.3): - Firebase/CoreOnly (~> 6.33.0) - Flutter - firebase_messaging (7.0.3): @@ -258,7 +258,7 @@ SPEC CHECKSUMS: esys_flutter_share: 403498dab005b36ce1f8d7aff377e81f0621b0b4 file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1 Firebase: 8db6f2d1b2c5e2984efba4949a145875a8f65fe5 - firebase_core: 7423d688a1c6f2f2d859d64ae26991be39989781 + firebase_core: 5d6a02f3d85acd5f8321c2d6d62877626a670659 firebase_messaging: 0aea2cd5885b65e19ede58ee3507f485c992cc75 FirebaseCore: d889d9e12535b7f36ac8bfbf1713a0836a3012cd FirebaseCoreDiagnostics: 770ac5958e1372ce67959ae4b4f31d8e127c3ac1 diff --git a/example/lib/main.dart b/example/lib/main.dart index c8bb34f5..f0f1e59b 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -24,7 +24,7 @@ void main() async { final client = Client( apiKey ?? kDefaultStreamApiKey, - logLevel: Level.SEVERE, + logLevel: Level.INFO, showLocalNotification: (!kIsWeb && Platform.isAndroid) ? showLocalNotification : null, persistenceEnabled: true, diff --git a/example/pubspec.yaml b/example/pubspec.yaml index ef06f719..cec1b886 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.92+95 +version: 1.0.93+96 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index e2807775..df99687b 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -519,11 +519,13 @@ class _MessageListViewState extends State { color: Colors.black, ), onPressed: () { + if (unreadCount > 0) { + streamChannel.channel.markRead(); + } if (!_upToDate) { _bottomPaginationActive = false; _topPaginationActive = false; streamChannel.reloadChannel(); - streamChannel.channel.markRead(); } else { setState(() => _showScrollToBottom = false); _scrollController.scrollTo( From e3b268fb4552cf3901384782f825f84c5523bfb9 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 18 Dec 2020 15:53:14 +0100 Subject: [PATCH 60/74] fix padding --- lib/src/message_list_view.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/message_list_view.dart b/lib/src/message_list_view.dart index df99687b..5a850ad3 100644 --- a/lib/src/message_list_view.dart +++ b/lib/src/message_list_view.dart @@ -745,7 +745,8 @@ class _MessageListViewState extends State { padding: EdgeInsets.only( left: 8.0, right: 8.0, - bottom: index == 0 ? 30 : (isNextUser ? 5 : 10), + bottom: index == 0 ? 30 : (isNextUser ? 2 : 7), + top: 3, ), showUsername: !isMyMessage && !isNextUser, showSendingIndicator: isMyMessage && From 74781fd24f6da2fc0f9868a6587394ccb2b5cc08 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Fri, 18 Dec 2020 15:54:13 +0100 Subject: [PATCH 61/74] fix message search date --- lib/src/message_search_item.dart | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/src/message_search_item.dart b/lib/src/message_search_item.dart index 3e786b9a..35b31aa8 100644 --- a/lib/src/message_search_item.dart +++ b/lib/src/message_search_item.dart @@ -77,16 +77,16 @@ class MessageSearchItem extends StatelessWidget { } Widget _buildDate(BuildContext context, Message message) { - final lastUpdatedAt = message.updatedAt; + final createdAt = message.createdAt; String stringDate; final now = DateTime.now(); - if (now.year != lastUpdatedAt.year || - now.month != lastUpdatedAt.month || - now.day != lastUpdatedAt.day) { - stringDate = Jiffy(lastUpdatedAt.toLocal()).format('dd/MM/yyyy'); + if (now.year != createdAt.year || + now.month != createdAt.month || + now.day != createdAt.day) { + stringDate = Jiffy(createdAt.toLocal()).format('dd/MM/yyyy'); } else { - stringDate = Jiffy(lastUpdatedAt.toLocal()).format('HH:mm'); + stringDate = Jiffy(createdAt.toLocal()).format('HH:mm'); } return Text( From 903dec9d1fdeb1b305a5f17e6164a943fba55d4a Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Mon, 21 Dec 2020 15:29:59 +0530 Subject: [PATCH 62/74] [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 63/74] [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 abf218595e3d31d8220d72784d43ddba0b26b27f Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 21 Dec 2020 11:43:15 +0100 Subject: [PATCH 64/74] update llc dependency --- example/ios/Flutter/.last_build_id | 2 +- example/lib/new_chat_screen.dart | 2 +- example/pubspec.yaml | 2 +- lib/src/channels_bloc.dart | 10 +++++----- lib/src/chat_info_screen.dart | 4 ++-- pubspec.yaml | 5 +---- 6 files changed, 11 insertions(+), 14 deletions(-) diff --git a/example/ios/Flutter/.last_build_id b/example/ios/Flutter/.last_build_id index f57e1e1f..e72273c3 100644 --- a/example/ios/Flutter/.last_build_id +++ b/example/ios/Flutter/.last_build_id @@ -1 +1 @@ -c770358a10272b4ee1cdc2eb432445f6 \ No newline at end of file +13a41b8138c4868054e44b5158f3bdd6 \ No newline at end of file diff --git a/example/lib/new_chat_screen.dart b/example/lib/new_chat_screen.dart index 54371c3b..f29b1483 100644 --- a/example/lib/new_chat_screen.dart +++ b/example/lib/new_chat_screen.dart @@ -84,7 +84,7 @@ class _NewChatScreenState extends State { paginationParams: PaginationParams( limit: 1, ), - ).first; + ); final _channelExisted = res.length == 1; if (_channelExisted) { diff --git a/example/pubspec.yaml b/example/pubspec.yaml index cec1b886..175cdaeb 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,6 +1,6 @@ name: example description: A new Flutter project. -version: 1.0.93+96 +version: 1.0.94+97 environment: sdk: ">=2.2.2 <3.0.0" diff --git a/lib/src/channels_bloc.dart b/lib/src/channels_bloc.dart index 474d9599..15167317 100644 --- a/lib/src/channels_bloc.dart +++ b/lib/src/channels_bloc.dart @@ -85,7 +85,7 @@ class ChannelsBlocState extends State paginationParams.offset == null || paginationParams.offset == 0; final oldChannels = List.from(channels ?? []); - client + await client .queryChannels( filter: filter, sort: sortOptions, @@ -93,19 +93,19 @@ class ChannelsBlocState extends State paginationParams: paginationParams, onlyOffline: onlyOffline, ) - .listen((channels) { + .then((channels) { if (clear) { _channelsController.add(channels); } else { final l = oldChannels + channels; _channelsController.add(l); } - }, onDone: () { - _queryChannelsLoadingController.sink.add(false); - }, onError: (err, stackTrace) { + }).catchError((err, stackTrace) { print(err); print(stackTrace); _queryChannelsLoadingController.addError(err, stackTrace); + }).whenComplete(() { + _queryChannelsLoadingController.sink.add(false); }); } catch (err, stackTrace) { _queryChannelsLoadingController.addError(err, stackTrace); diff --git a/lib/src/chat_info_screen.dart b/lib/src/chat_info_screen.dart index 0ec62c36..8d291f38 100644 --- a/lib/src/chat_info_screen.dart +++ b/lib/src/chat_info_screen.dart @@ -391,8 +391,8 @@ class __SharedGroupsScreenState extends State<_SharedGroupsScreen> { ), backgroundColor: StreamChatTheme.of(context).primaryColor, ), - body: StreamBuilder>( - stream: chat.client.queryChannels( + body: FutureBuilder>( + future: chat.client.queryChannels( filter: { r'$and': [ { diff --git a/pubspec.yaml b/pubspec.yaml index a226f9c3..12374291 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -28,10 +28,7 @@ dependencies: file_picker: ^2.0.12 image_picker: ^0.6.7+2 flutter_keyboard_visibility: ^3.3.0 - stream_chat: - git: - url: https://github.com/GetStream/stream-chat-dart - ref: two-way-pagination + stream_chat: ^0.2.20 mime: ^0.9.6+3 video_compress: ^2.1.1 visibility_detector: ^0.1.5 From 3c35a800a88a5f28d980aeb21a105cfea40f15b7 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 21 Dec 2020 12:14:31 +0100 Subject: [PATCH 65/74] fix loading --- lib/src/channels_bloc.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/src/channels_bloc.dart b/lib/src/channels_bloc.dart index 15167317..0bae351c 100644 --- a/lib/src/channels_bloc.dart +++ b/lib/src/channels_bloc.dart @@ -100,12 +100,11 @@ class ChannelsBlocState extends State final l = oldChannels + channels; _channelsController.add(l); } + _queryChannelsLoadingController.sink.add(false); }).catchError((err, stackTrace) { print(err); print(stackTrace); _queryChannelsLoadingController.addError(err, stackTrace); - }).whenComplete(() { - _queryChannelsLoadingController.sink.add(false); }); } catch (err, stackTrace) { _queryChannelsLoadingController.addError(err, stackTrace); From b0093dc065bea3809d3fa797a327b78460811b54 Mon Sep 17 00:00:00 2001 From: Salvatore Giordano Date: Mon, 21 Dec 2020 12:21:35 +0100 Subject: [PATCH 66/74] remove unnecessary .catchError --- lib/src/channels_bloc.dart | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/src/channels_bloc.dart b/lib/src/channels_bloc.dart index 0bae351c..9b6e55bb 100644 --- a/lib/src/channels_bloc.dart +++ b/lib/src/channels_bloc.dart @@ -101,12 +101,10 @@ class ChannelsBlocState extends State _channelsController.add(l); } _queryChannelsLoadingController.sink.add(false); - }).catchError((err, stackTrace) { - print(err); - print(stackTrace); - _queryChannelsLoadingController.addError(err, stackTrace); }); } catch (err, stackTrace) { + print(err); + print(stackTrace); _queryChannelsLoadingController.addError(err, stackTrace); } } From 209c5a3c24766e9ee472870b98320c3e63753f6c Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Tue, 22 Dec 2020 21:45:14 +0530 Subject: [PATCH 67/74] 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 68/74] [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 69/74] 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 70/74] [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 71/74] 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 72/74] [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 73/74] [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 74/74] 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),