diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 0088abfa..a94eaaea 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -4,25 +4,27 @@ - `MessageListView` now allows more better control over spacing after messages using `spacingWidgetBuilder`. - `StreamChannel` can now fetch messages around a message ID with the `queryAroundMessage` call. +- Added `MessageListView.keyboardDismissBehavior` property. 🐞 Fixed - [[#766]]`AttachmentActionsModal` now has customisation options for actions. - Fixed `MessageWidget` null errors associated with `channel.memberCount`. - Fixed adding attachments on web. +- [[#767]](https://github.com/GetStream/stream-chat-flutter/issues/767): Fix `MessageInput` focus behaviour when sending messages. ## 3.2.0 -- Updated Dart SDK constraints to `>=2.14.0 <3.0.0` +- Updated Dart SDK constraints to `>=2.14.0 <3.0.0`. - Updated `stream_chat_flutter_core` dependency to [`3.2.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog). 🐞 Fixed -- Fixed message highlight animation alignment in `MessageListView` +- Fixed message highlight animation alignment in `MessageListView`. - [[#491]](https://github.com/GetStream/stream-chat-flutter/issues/491): Fix `MediaListView` showing media in wrong order. - Fixed `MessageListView` initialIndex not working in some cases. - Improved `MessageListView` rendering in case of reordering. -- Fix image thumbnail generation when using Stream CDN +- Fix image thumbnail generation when using Stream CDN. ✅ Added diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/positioned_list.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/positioned_list.dart index 2ce9adb5..c77b668c 100644 --- a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/positioned_list.dart +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/positioned_list.dart @@ -45,6 +45,7 @@ class PositionedList extends StatefulWidget { this.addSemanticIndexes = true, this.addRepaintBoundaries = true, this.addAutomaticKeepAlives = true, + this.keyboardDismissBehavior, }) : assert((positionedIndex == 0) || (positionedIndex < itemCount), 'positionedIndex cannot be 0 and must be smaller than itemCount'), super(key: key); @@ -134,6 +135,10 @@ class PositionedList extends StatefulWidget { /// See [SliverChildBuilderDelegate.addAutomaticKeepAlives]. final bool addAutomaticKeepAlives; + /// [ScrollViewKeyboardDismissBehavior] the defines how this [PositionedList] will + /// dismiss the keyboard automatically. + final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior; + @override State createState() => _PositionedListState(); } @@ -173,6 +178,7 @@ class _PositionedListState extends State { anchor: widget.alignment, center: _centerKey, controller: scrollController, + keyboardDismissBehavior: widget.keyboardDismissBehavior, scrollDirection: widget.scrollDirection, reverse: widget.reverse, cacheExtent: widget.cacheExtent, diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scroll_view.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scroll_view.dart index a0b499c0..1aff0df3 100644 --- a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scroll_view.dart +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scroll_view.dart @@ -28,9 +28,12 @@ class UnboundedCustomScrollView extends CustomScrollView { List slivers = const [], int? semanticChildCount, DragStartBehavior dragStartBehavior = DragStartBehavior.start, + ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior, }) : _anchor = anchor, super( key: key, + keyboardDismissBehavior: keyboardDismissBehavior ?? + ScrollViewKeyboardDismissBehavior.manual, scrollDirection: scrollDirection, reverse: reverse, controller: controller, diff --git a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart index 765a90a0..f3203ff6 100644 --- a/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart +++ b/packages/stream_chat_flutter/lib/scrollable_positioned_list/src/scrollable_positioned_list.dart @@ -52,6 +52,7 @@ class ScrollablePositionedList extends StatefulWidget { this.addRepaintBoundaries = true, this.minCacheExtent, this.findChildIndexCallback, + this.keyboardDismissBehavior, }) : itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?, separatorBuilder = null, super(key: key); @@ -77,6 +78,7 @@ class ScrollablePositionedList extends StatefulWidget { this.addRepaintBoundaries = true, this.minCacheExtent, this.findChildIndexCallback, + this.keyboardDismissBehavior, }) : assert(separatorBuilder != null, 'seperatorBuilder cannot be null'), itemPositionsNotifier = itemPositionsListener as ItemPositionsNotifier?, super(key: key); @@ -92,6 +94,10 @@ class ScrollablePositionedList extends StatefulWidget { /// index of the child element with that associated key, or null if not found. final ChildIndexGetter? findChildIndexCallback; + /// [ScrollViewKeyboardDismissBehavior] the defines how this [PositionedList] will + /// dismiss the keyboard automatically. + final ScrollViewKeyboardDismissBehavior? keyboardDismissBehavior; + /// Number of items the [itemBuilder] can produce. final int itemCount; @@ -344,6 +350,7 @@ class _ScrollablePositionedListState extends State child: NotificationListener( onNotification: (_) => _isTransitioning, child: PositionedList( + keyboardDismissBehavior: widget.keyboardDismissBehavior, itemBuilder: widget.itemBuilder, separatorBuilder: widget.separatorBuilder, itemCount: widget.itemCount, @@ -374,6 +381,8 @@ class _ScrollablePositionedListState extends State child: NotificationListener( onNotification: (_) => false, child: PositionedList( + keyboardDismissBehavior: + widget.keyboardDismissBehavior, itemBuilder: widget.itemBuilder, separatorBuilder: widget.separatorBuilder, itemCount: widget.itemCount, diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 08fc2a7b..a08c799b 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -204,6 +204,7 @@ class MessageInput extends StatefulWidget { this.commandButtonBuilder, this.customOverlays = const [], this.mentionAllAppUsers = false, + this.shouldKeepFocusAfterMessage, }) : assert( initialMessage == null || editMessage == null, "Can't provide both `initialMessage` and `editMessage`", @@ -322,6 +323,8 @@ class MessageInput extends StatefulWidget { /// Defaults to false. final bool mentionAllAppUsers; + final bool? shouldKeepFocusAfterMessage; + @override MessageInputState createState() => MessageInputState(); @@ -1759,7 +1762,9 @@ class MessageInputState extends State { return; } - final shouldUnfocus = _commandEnabled; + var shouldKeepFocus = widget.shouldKeepFocusAfterMessage; + + shouldKeepFocus ??= !_commandEnabled; if (_commandEnabled) { text = '${'/${_chosenCommand!.name} '}$text'; @@ -1822,8 +1827,10 @@ class MessageInputState extends State { sendingFuture = channel.updateMessage(message); } - if (!shouldUnfocus) { + if (shouldKeepFocus) { FocusScope.of(context).requestFocus(_focusNode); + } else { + FocusScope.of(context).unfocus(); } final resp = await sendingFuture; diff --git a/packages/stream_chat_flutter/lib/src/message_list_view.dart b/packages/stream_chat_flutter/lib/src/message_list_view.dart index 94824ef4..c61ccfed 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -211,9 +211,14 @@ class MessageListView extends StatefulWidget { this.reverse = true, this.paginationLimit = 20, this.paginationLoadingIndicatorBuilder, + this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.onDrag, this.spacingWidgetBuilder, }) : super(key: key); + /// [ScrollViewKeyboardDismissBehavior] the defines how this [PositionedList] will + /// dismiss the keyboard automatically. + final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior; + /// Function used to build a custom message widget final MessageBuilder? messageBuilder; @@ -490,9 +495,6 @@ class _MessageListViewState extends State { childAnchor: Alignment.topCenter, message: statusString, child: LazyLoadScrollView( - onPageScrollStart: () { - FocusScope.of(context).unfocus(); - }, onStartOfPage: () async { _inBetweenList = false; if (!_upToDate) { @@ -518,6 +520,7 @@ class _MessageListViewState extends State { key: (initialIndex != 0 && initialAlignment != 0) ? ValueKey('$initialIndex-$initialAlignment') : null, + keyboardDismissBehavior: widget.keyboardDismissBehavior, itemPositionsListener: _itemPositionListener, initialScrollIndex: initialIndex, initialAlignment: initialAlignment, diff --git a/packages/stream_chat_flutter_core/test/channel_list_core_test.dart b/packages/stream_chat_flutter_core/test/channel_list_core_test.dart index 8407be67..88711647 100644 --- a/packages/stream_chat_flutter_core/test/channel_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/channel_list_core_test.dart @@ -476,7 +476,7 @@ void main() { _stateSetter?.call(() => limit = 6); final updatedChannels = _generateChannels(mockClient, count: limit); - final updatedPagination = pagination.copyWith(limit: limit); + final updatedPagination = PaginationParams(limit: limit); when(() => mockClient.queryChannels( filter: any(named: 'filter'), sort: any(named: 'sort'), diff --git a/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart b/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart index 475c6c23..d699faa0 100644 --- a/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/message_search_list_core_test.dart @@ -518,7 +518,7 @@ void main() { _stateSetter?.call(() => limit = 6); final updatedMessageResponseList = _generateMessages(count: limit); - final updatedPagination = pagination.copyWith(limit: limit); + final updatedPagination = PaginationParams(limit: limit); when(() => mockClient.search( testFilter, query: any(named: 'query'), diff --git a/packages/stream_chat_flutter_core/test/stream_channel_test.dart b/packages/stream_chat_flutter_core/test/stream_channel_test.dart index 08763f9d..de536cb5 100644 --- a/packages/stream_chat_flutter_core/test/stream_channel_test.dart +++ b/packages/stream_chat_flutter_core/test/stream_channel_test.dart @@ -189,9 +189,7 @@ void main() { membersPagination: any(named: 'membersPagination'), watchersPagination: any(named: 'watchersPagination'), preferOffline: any(named: 'preferOffline'), - )).called( - 2, // Fetching After messages + Fetching Before messages, - ); + )).called(1); }, ); @@ -214,14 +212,10 @@ void main() { child: const Offstage(key: childKey), ); - final beforePagination = PaginationParams( - lessThan: initialMessageId, - limit: 20, - ); - - final afterPagination = PaginationParams( - greaterThanOrEqual: initialMessageId, - limit: 20, + final paginationParams = PaginationParams( + idAround: initialMessageId, + after: 20, + before: 20, ); when(() => mockChannel.initialized).thenAnswer((_) async => true); @@ -232,17 +226,7 @@ void main() { state: any(named: 'state'), watch: any(named: 'watch'), presence: any(named: 'presence'), - messagesPagination: beforePagination, - membersPagination: any(named: 'membersPagination'), - watchersPagination: any(named: 'watchersPagination'), - preferOffline: any(named: 'preferOffline'), - )).thenAnswer((_) async => ChannelState(messages: messages)); - - when(() => mockChannel.query( - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - messagesPagination: afterPagination, + messagesPagination: paginationParams, membersPagination: any(named: 'membersPagination'), watchersPagination: any(named: 'watchersPagination'), preferOffline: any(named: 'preferOffline'), @@ -267,17 +251,7 @@ void main() { state: any(named: 'state'), watch: any(named: 'watch'), presence: any(named: 'presence'), - messagesPagination: beforePagination, - membersPagination: any(named: 'membersPagination'), - watchersPagination: any(named: 'watchersPagination'), - preferOffline: any(named: 'preferOffline'), - )).called(1); - - verify(() => mockChannel.query( - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - messagesPagination: afterPagination, + messagesPagination: paginationParams, membersPagination: any(named: 'membersPagination'), watchersPagination: any(named: 'watchersPagination'), preferOffline: any(named: 'preferOffline'), @@ -285,29 +259,15 @@ void main() { _stateSetter?.call(() => initialMessageId = 'testInitialMessageId2'); - final updatedBeforePagination = beforePagination.copyWith( - lessThan: initialMessageId, - ); - - final updatedAfterPagination = afterPagination.copyWith( - greaterThanOrEqual: initialMessageId, + final updatedPaginationParams = paginationParams.copyWith( + idAround: initialMessageId, ); when(() => mockChannel.query( state: any(named: 'state'), watch: any(named: 'watch'), presence: any(named: 'presence'), - messagesPagination: updatedBeforePagination, - membersPagination: any(named: 'membersPagination'), - watchersPagination: any(named: 'watchersPagination'), - preferOffline: any(named: 'preferOffline'), - )).thenAnswer((_) async => ChannelState(messages: messages)); - - when(() => mockChannel.query( - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - messagesPagination: updatedAfterPagination, + messagesPagination: updatedPaginationParams, membersPagination: any(named: 'membersPagination'), watchersPagination: any(named: 'watchersPagination'), preferOffline: any(named: 'preferOffline'), @@ -319,17 +279,7 @@ void main() { state: any(named: 'state'), watch: any(named: 'watch'), presence: any(named: 'presence'), - messagesPagination: updatedBeforePagination, - membersPagination: any(named: 'membersPagination'), - watchersPagination: any(named: 'watchersPagination'), - preferOffline: any(named: 'preferOffline'), - )).called(1); - - verify(() => mockChannel.query( - state: any(named: 'state'), - watch: any(named: 'watch'), - presence: any(named: 'presence'), - messagesPagination: updatedAfterPagination, + messagesPagination: updatedPaginationParams, membersPagination: any(named: 'membersPagination'), watchersPagination: any(named: 'watchersPagination'), preferOffline: any(named: 'preferOffline'), diff --git a/packages/stream_chat_flutter_core/test/user_list_core_test.dart b/packages/stream_chat_flutter_core/test/user_list_core_test.dart index c916ce17..27f4cbe8 100644 --- a/packages/stream_chat_flutter_core/test/user_list_core_test.dart +++ b/packages/stream_chat_flutter_core/test/user_list_core_test.dart @@ -496,7 +496,7 @@ void main() { _stateSetter?.call(() => limit = 6); final updatedUsers = _generateUsers(count: limit); - final updatedPagination = pagination.copyWith(limit: limit); + final updatedPagination = PaginationParams(limit: limit); when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'),