diff --git a/.github/workflows/dart_code_metrics.yaml b/.github/workflows/dart_code_metrics.yaml index 0ae85b88..667ac2dd 100644 --- a/.github/workflows/dart_code_metrics.yaml +++ b/.github/workflows/dart_code_metrics.yaml @@ -33,7 +33,7 @@ jobs: flutter-version: ${{ env.flutter_version }} - name: "Install Tools" - run: flutter pub global activate melos 1.0.0-dev.6 + run: flutter pub global activate melos 1.0.0-dev.10 - name: "Bootstrap Workspace" run: melos bootstrap diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index 65913a18..b91dcb85 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -31,7 +31,7 @@ jobs: flutter-version: ${{ env.flutter_version }} - name: "Install Tools" run: | - flutter pub global activate melos 1.0.0-dev.6 + flutter pub global activate melos 1.0.0-dev.10 - name: "Bootstrap Workspace" run: melos bootstrap - name: "Dart Analyze" diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 33a95b03..b807cb61 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -2,8 +2,15 @@ ✅ Added +- Extra properties added to `PaginationParams` to aid in fetching messages. - Added hard delete functionality. +🐞 Fixed + +- `closeConnection()` now uses `normalClosure` status when closing websocket. +- Fixed local unread count indicator increasing for thread replies +- Fixed user presence indicator not updating correctly + ## 3.2.0 🐞 Fixed @@ -726,4 +733,4 @@ ## 0.0.2 -- first beta version +- first beta version \ No newline at end of file diff --git a/packages/stream_chat/example/lib/main.dart b/packages/stream_chat/example/lib/main.dart index d2c5dcea..629f4a32 100644 --- a/packages/stream_chat/example/lib/main.dart +++ b/packages/stream_chat/example/lib/main.dart @@ -233,10 +233,10 @@ class _MessageViewState extends State { ), ), ), - ) + ), ], ), - ) + ), ], ); } diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index ab4f17c3..482427d6 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -1860,10 +1860,13 @@ class ChannelClientState { (m) => m.user.id == message.user?.id, ) != null; + final isThreadMessage = message.parentId != null; + return !message.silent && !message.shadowed && message.user?.id != userId && - !userIsMuted; + !userIsMuted && + !isThreadMessage; } /// Update threads with updated information about messages. diff --git a/packages/stream_chat/lib/src/core/api/requests.dart b/packages/stream_chat/lib/src/core/api/requests.dart index 14995b10..6f00b374 100644 --- a/packages/stream_chat/lib/src/core/api/requests.dart +++ b/packages/stream_chat/lib/src/core/api/requests.dart @@ -60,8 +60,11 @@ class PaginationParams extends Equatable { /// ``` const PaginationParams({ this.limit = 10, + this.before = 10, + this.after = 10, this.offset, this.next, + this.idAround, this.greaterThan, this.greaterThanOrEqual, this.lessThan, @@ -78,12 +81,22 @@ class PaginationParams extends Equatable { /// The amount of items requested from the APIs. final int limit; + /// The amount of items requested before message ID from the APIs. + final int before; + + /// The amount of items requested after message ID from the APIs. + final int after; + /// The offset of requesting items. final int? offset; /// A key used to paginate. final String? next; + /// Message ID to fetch messages around + @JsonKey(name: 'id_around') + final String? idAround; + /// Filter on ids greater than the given value. @JsonKey(name: 'id_gt') final String? greaterThan; @@ -106,7 +119,10 @@ class PaginationParams extends Equatable { /// Creates a copy of [PaginationParams] with specified attributes overridden. PaginationParams copyWith({ int? limit, + int? before, + int? after, int? offset, + String? idAround, String? next, String? greaterThan, String? greaterThanOrEqual, @@ -115,7 +131,10 @@ class PaginationParams extends Equatable { }) => PaginationParams( limit: limit ?? this.limit, + before: before ?? this.before, + after: limit ?? this.after, offset: offset ?? this.offset, + idAround: idAround ?? this.idAround, next: next ?? this.next, greaterThan: greaterThan ?? this.greaterThan, greaterThanOrEqual: greaterThanOrEqual ?? this.greaterThanOrEqual, @@ -126,8 +145,11 @@ class PaginationParams extends Equatable { @override List get props => [ limit, + before, + after, offset, next, + idAround, greaterThan, greaterThanOrEqual, lessThan, diff --git a/packages/stream_chat/lib/src/core/api/requests.g.dart b/packages/stream_chat/lib/src/core/api/requests.g.dart index ef995997..7d45ee86 100644 --- a/packages/stream_chat/lib/src/core/api/requests.g.dart +++ b/packages/stream_chat/lib/src/core/api/requests.g.dart @@ -21,8 +21,11 @@ Map _$SortOptionToJson(SortOption instance) => PaginationParams _$PaginationParamsFromJson(Map json) => PaginationParams( limit: json['limit'] as int? ?? 10, + before: json['before'] as int? ?? 10, + after: json['after'] as int? ?? 10, offset: json['offset'] as int?, next: json['next'] as String?, + idAround: json['id_around'] as String?, greaterThan: json['id_gt'] as String?, greaterThanOrEqual: json['id_gte'] as String?, lessThan: json['id_lt'] as String?, @@ -32,6 +35,8 @@ PaginationParams _$PaginationParamsFromJson(Map json) => Map _$PaginationParamsToJson(PaginationParams instance) { final val = { 'limit': instance.limit, + 'before': instance.before, + 'after': instance.after, }; void writeNotNull(String key, dynamic value) { @@ -42,6 +47,7 @@ Map _$PaginationParamsToJson(PaginationParams instance) { writeNotNull('offset', instance.offset); writeNotNull('next', instance.next); + writeNotNull('id_around', instance.idAround); writeNotNull('id_gt', instance.greaterThan); writeNotNull('id_gte', instance.greaterThanOrEqual); writeNotNull('id_lt', instance.lessThan); diff --git a/packages/stream_chat/lib/src/core/models/read.dart b/packages/stream_chat/lib/src/core/models/read.dart index 812fc6f7..0ad1164c 100644 --- a/packages/stream_chat/lib/src/core/models/read.dart +++ b/packages/stream_chat/lib/src/core/models/read.dart @@ -1,3 +1,4 @@ +import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/user.dart'; @@ -5,7 +6,7 @@ part 'read.g.dart'; /// The class that defines a read event @JsonSerializable() -class Read { +class Read extends Equatable { /// Constructor used for json serialization Read({ required this.lastRead, @@ -39,4 +40,11 @@ class Read { user: user ?? this.user, unreadMessages: unreadMessages ?? this.unreadMessages, ); + + @override + List get props => [ + lastRead, + user, + unreadMessages, + ]; } diff --git a/packages/stream_chat/lib/src/core/models/user.dart b/packages/stream_chat/lib/src/core/models/user.dart index 44df4399..7aa7c51c 100644 --- a/packages/stream_chat/lib/src/core/models/user.dart +++ b/packages/stream_chat/lib/src/core/models/user.dart @@ -179,5 +179,14 @@ class User extends Equatable { ); @override - List get props => [id, role]; + List get props => [ + id, + role, + lastActive, + online, + extraData, + banned, + teams, + language, + ]; } diff --git a/packages/stream_chat/lib/src/ws/websocket.dart b/packages/stream_chat/lib/src/ws/websocket.dart index 05382fb7..a95eae70 100644 --- a/packages/stream_chat/lib/src/ws/websocket.dart +++ b/packages/stream_chat/lib/src/ws/websocket.dart @@ -121,7 +121,8 @@ class WebSocket with TimerHelper { _logger?.info('Closing connection with $baseUrl'); if (_webSocketChannel != null) { _unsubscribeFromWebSocketChannel(); - _webSocketChannel?.sink.close(status.goingAway); + _webSocketChannel?.sink + .close(_manuallyClosed ? status.normalClosure : status.goingAway); _webSocketChannel = null; } } diff --git a/packages/stream_chat/test/src/core/models/reaction_test.dart b/packages/stream_chat/test/src/core/models/reaction_test.dart index fbe493ef..285d8a46 100644 --- a/packages/stream_chat/test/src/core/models/reaction_test.dart +++ b/packages/stream_chat/test/src/core/models/reaction_test.dart @@ -70,13 +70,19 @@ void main() { expect( newReaction.extraData, {'updated_at': '2020-01-28T22:17:31.108742Z'}); + final newUserCreateTime = DateTime.now(); + newReaction = reaction.copyWith( type: 'lol', createdAt: DateTime.parse('2021-01-28T22:17:31.108742Z'), extraData: {}, messageId: 'test', score: 2, - user: User(id: 'test'), + user: User( + id: 'test', + createdAt: newUserCreateTime, + updatedAt: newUserCreateTime, + ), userId: 'test', ); @@ -88,12 +94,21 @@ void main() { expect(newReaction.extraData, {}); expect(newReaction.messageId, 'test'); expect(newReaction.score, 2); - expect(newReaction.user, User(id: 'test')); + expect( + newReaction.user, + User( + id: 'test', + createdAt: newUserCreateTime, + updatedAt: newUserCreateTime, + ), + ); expect(newReaction.userId, 'test'); }); test('merge', () { final reaction = Reaction.fromJson(jsonFixture('reaction.json')); + final newUserCreateTime = DateTime.now(); + final newReaction = reaction.merge( Reaction( type: 'lol', @@ -101,7 +116,11 @@ void main() { extraData: {}, messageId: 'test', score: 2, - user: User(id: 'test'), + user: User( + id: 'test', + createdAt: newUserCreateTime, + updatedAt: newUserCreateTime, + ), userId: 'test', ), ); @@ -114,7 +133,14 @@ void main() { expect(newReaction.extraData, {}); expect(newReaction.messageId, 'test'); expect(newReaction.score, 2); - expect(newReaction.user, User(id: 'test')); + expect( + newReaction.user, + User( + id: 'test', + createdAt: newUserCreateTime, + updatedAt: newUserCreateTime, + ), + ); expect(newReaction.userId, 'test'); }); }); diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 00f8412b..e4047b1e 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,15 +1,33 @@ +## Upcoming + +✅ Added + +- `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. +- Fixed user presence indicator not updating correctly. +- Do not use `withData: true` in `FilePicker` calls. +- Fixed read indicator not updating correctly in specific situations. + ## 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 @@ -860,4 +878,4 @@ The property showVideoFullScreen was added mainly because of this issue brianega ## 0.0.1 -- First release +- First release \ No newline at end of file diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart index 423bd7db..19d5d1ae 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart @@ -64,11 +64,12 @@ class MyApp extends StatelessWidget { ), ), messageListViewTheme: const MessageListViewThemeData( - backgroundColor: Colors.grey, - backgroundImage: DecorationImage( - image: AssetImage('assets/background_doodle.png'), - fit: BoxFit.cover, - )), + backgroundColor: Colors.grey, + backgroundImage: DecorationImage( + image: AssetImage('assets/background_doodle.png'), + fit: BoxFit.cover, + ), + ), otherMessageTheme: MessageThemeData( messageBackgroundColor: colorTheme.textHighEmphasis, messageTextStyle: TextStyle( 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/attachment_actions_modal.dart b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart index f49561aa..0c10ae46 100644 --- a/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/attachment_actions_modal.dart @@ -24,6 +24,11 @@ class AttachmentActionsModal extends StatelessWidget { this.onShowMessage, this.imageDownloader, this.fileDownloader, + this.showReply = true, + this.showShowInChat = true, + this.showSave = true, + this.showDelete = true, + this.customActions = const [], }) : super(key: key); /// The message containing the attachments @@ -41,6 +46,49 @@ class AttachmentActionsModal extends StatelessWidget { /// Callback to provide download files final AttachmentDownloader? fileDownloader; + /// Show reply option + final bool showReply; + + /// Show show in chat option + final bool showShowInChat; + + /// Show save option + final bool showSave; + + /// Show delete option + final bool showDelete; + + /// List of custom actions + final List customActions; + + /// Creates a copy of [MessageWidget] with specified attributes overridden. + AttachmentActionsModal copyWith({ + Key? key, + int? currentIndex, + Message? message, + VoidCallback? onShowMessage, + AttachmentDownloader? imageDownloader, + AttachmentDownloader? fileDownloader, + bool? showReply, + bool? showShowInChat, + bool? showSave, + bool? showDelete, + List? customActions, + }) => + AttachmentActionsModal( + key: key ?? this.key, + currentIndex: currentIndex ?? this.currentIndex, + message: message ?? this.message, + onShowMessage: onShowMessage ?? this.onShowMessage, + imageDownloader: imageDownloader ?? this.imageDownloader, + fileDownloader: fileDownloader ?? this.fileDownloader, + showReply: showReply ?? this.showReply, + showShowInChat: showShowInChat ?? this.showShowInChat, + showSave: showSave ?? this.showSave, + showDelete: showDelete ?? this.showDelete, + customActions: customActions ?? this.customActions, + ); + @override Widget build(BuildContext context) => GestureDetector( behavior: HitTestBehavior.translucent, @@ -67,82 +115,86 @@ class AttachmentActionsModal extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ - _buildButton( - context, - context.translations.replyLabel, - StreamSvgIcon.iconCurveLineLeftUp( - size: 24, - color: theme.colorTheme.textLowEmphasis, + if (showReply) + _buildButton( + context, + context.translations.replyLabel, + StreamSvgIcon.iconCurveLineLeftUp( + size: 24, + color: theme.colorTheme.textLowEmphasis, + ), + () { + Navigator.pop(context, ReturnActionType.reply); + }, ), - () { - Navigator.pop(context, ReturnActionType.reply); - }, - ), - _buildButton( - context, - context.translations.showInChatLabel, - StreamSvgIcon.eye( - size: 24, - color: theme.colorTheme.textHighEmphasis, + if (showShowInChat) + _buildButton( + context, + context.translations.showInChatLabel, + StreamSvgIcon.eye( + size: 24, + color: theme.colorTheme.textHighEmphasis, + ), + onShowMessage, ), - onShowMessage, - ), - _buildButton( - context, - message.attachments[currentIndex].type == 'video' - ? context.translations.saveVideoLabel - : context.translations.saveImageLabel, - StreamSvgIcon.iconSave( - size: 24, - color: theme.colorTheme.textLowEmphasis, + if (showSave) + _buildButton( + context, + message.attachments[currentIndex].type == 'video' + ? context.translations.saveVideoLabel + : context.translations.saveImageLabel, + StreamSvgIcon.iconSave( + size: 24, + color: theme.colorTheme.textLowEmphasis, + ), + () { + final attachment = message.attachments[currentIndex]; + final isImage = attachment.type == 'image'; + final Future Function( + Attachment, { + void Function(int, int) progressCallback, + }) saveFile = fileDownloader ?? _downloadAttachment; + final Future Function( + Attachment, { + void Function(int, int) progressCallback, + }) saveImage = imageDownloader ?? _downloadAttachment; + final downloader = isImage ? saveImage : saveFile; + + final progressNotifier = + ValueNotifier<_DownloadProgress?>( + _DownloadProgress.initial(), + ); + + downloader( + attachment, + progressCallback: (received, total) { + progressNotifier.value = _DownloadProgress( + total, + received, + ); + }, + ).catchError((e, stk) { + progressNotifier.value = null; + }); + + // Closing attachment actions modal before opening + // attachment download dialog + Navigator.pop(context); + + showDialog( + barrierDismissible: false, + context: context, + barrierColor: theme.colorTheme.overlay, + builder: (context) => _buildDownloadProgressDialog( + context, + progressNotifier, + ), + ); + }, ), - () { - final attachment = message.attachments[currentIndex]; - final isImage = attachment.type == 'image'; - final Future Function( - Attachment, { - void Function(int, int) progressCallback, - }) saveFile = fileDownloader ?? _downloadAttachment; - final Future Function( - Attachment, { - void Function(int, int) progressCallback, - }) saveImage = imageDownloader ?? _downloadAttachment; - final downloader = isImage ? saveImage : saveFile; - - final progressNotifier = - ValueNotifier<_DownloadProgress?>( - _DownloadProgress.initial(), - ); - - downloader( - attachment, - progressCallback: (received, total) { - progressNotifier.value = _DownloadProgress( - total, - received, - ); - }, - ).catchError((e, stk) { - progressNotifier.value = null; - }); - - // Closing attachment actions modal before opening - // attachment download dialog - Navigator.pop(context); - - showDialog( - barrierDismissible: false, - context: context, - barrierColor: theme.colorTheme.overlay, - builder: (context) => _buildDownloadProgressDialog( - context, - progressNotifier, - ), - ); - }, - ), if (StreamChat.of(context).currentUser?.id == - message.user?.id) + message.user?.id && + showDelete) _buildButton( context, context.translations.deleteLabel.capitalize(), @@ -171,6 +223,16 @@ class AttachmentActionsModal extends StatelessWidget { }, color: theme.colorTheme.accentError, ), + ...customActions + .map( + (e) => _buildButton( + context, + e.actionTitle, + e.icon, + e.onTap, + ), + ) + .toList(), ] .map((e) => Align( alignment: Alignment.centerRight, @@ -193,7 +255,7 @@ class AttachmentActionsModal extends StatelessWidget { Widget _buildButton( context, String title, - StreamSvgIcon icon, + Widget icon, VoidCallback? onTap, { Color? color, Key? key, @@ -331,3 +393,22 @@ class _DownloadProgress { int get toPercentage => (received * 100) ~/ total; } + +/// Class for custom attachment action +class AttachmentAction { + /// Constructor for custom attachment action + AttachmentAction({ + required this.actionTitle, + required this.icon, + required this.onTap, + }); + + /// Title for the attachment action + String actionTitle; + + /// Icon for the attachment action + Widget icon; + + /// Callback for when the action is tapped + VoidCallback onTap; +} diff --git a/packages/stream_chat_flutter/lib/src/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel_info.dart index 6bd61dad..62bfd1de 100644 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel_info.dart @@ -59,9 +59,10 @@ class ChannelInfo extends StatelessWidget { final memberCount = channel.memberCount; if (memberCount != null && memberCount > 2) { var text = context.translations.membersCountText(memberCount); - final watcherCount = channel.state?.watcherCount ?? 0; - if (watcherCount > 0) { - text += ' ${context.translations.watchersCountText(watcherCount)}'; + final onlineCount = + members?.where((m) => m.user?.online == true).length ?? 0; + if (onlineCount > 0) { + text += ', ${context.translations.watchersCountText(onlineCount)}'; } alternativeWidget = Text( text, diff --git a/packages/stream_chat_flutter/lib/src/channel_preview.dart b/packages/stream_chat_flutter/lib/src/channel_preview.dart index 6e3e5363..de038c82 100644 --- a/packages/stream_chat_flutter/lib/src/channel_preview.dart +++ b/packages/stream_chat_flutter/lib/src/channel_preview.dart @@ -126,16 +126,26 @@ class ChannelPreview extends StatelessWidget { streamChatState.currentUser?.id) { return Padding( padding: const EdgeInsets.only(right: 4), - child: SendingIndicator( - message: lastMessage!, - size: channelPreviewTheme.indicatorIconSize, - isMessageRead: channel.state!.read - .where((element) => - element.user.id != - channel.client.state.currentUser!.id) - .where((element) => element.lastRead - .isAfter(lastMessage.createdAt)) - .isNotEmpty, + child: BetterStreamBuilder>( + stream: channel.state?.readStream, + initialData: channel.state?.read, + builder: (context, data) { + final readList = data.where((it) => + it.user.id != + channel.client.state.currentUser?.id && + (it.lastRead + .isAfter(lastMessage!.createdAt) || + it.lastRead.isAtSameMomentAs( + lastMessage.createdAt, + ))); + final isMessageRead = readList.length >= + (channel.memberCount ?? 0) - 1; + return SendingIndicator( + message: lastMessage!, + size: channelPreviewTheme.indicatorIconSize, + isMessageRead: isMessageRead, + ); + }, ), ); } diff --git a/packages/stream_chat_flutter/lib/src/extension.dart b/packages/stream_chat_flutter/lib/src/extension.dart index 92590de1..73956da6 100644 --- a/packages/stream_chat_flutter/lib/src/extension.dart +++ b/packages/stream_chat_flutter/lib/src/extension.dart @@ -1,6 +1,7 @@ import 'package:characters/characters.dart'; import 'package:diacritic/diacritic.dart'; import 'package:file_picker/file_picker.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/emoji/emoji.dart'; import 'package:stream_chat_flutter/src/localization/translations.dart'; @@ -46,7 +47,7 @@ extension IterableX on Iterable { extension PlatformFileX on PlatformFile { /// Converts the [PlatformFile] into [AttachmentFile] AttachmentFile get toAttachmentFile => AttachmentFile( - path: path, + path: kIsWeb ? null : path, name: name, bytes: bytes, size: size, diff --git a/packages/stream_chat_flutter/lib/src/full_screen_media.dart b/packages/stream_chat_flutter/lib/src/full_screen_media.dart index 0c47383c..6547a93d 100644 --- a/packages/stream_chat_flutter/lib/src/full_screen_media.dart +++ b/packages/stream_chat_flutter/lib/src/full_screen_media.dart @@ -33,6 +33,7 @@ class FullScreenMedia extends StatefulWidget { this.startIndex = 0, String? userName, this.onShowMessage, + this.attachmentActionsModalBuilder, }) : userName = userName ?? '', super(key: key); @@ -51,6 +52,11 @@ class FullScreenMedia extends StatefulWidget { /// Callback for when show message is tapped final ShowMessageCallback? onShowMessage; + /// Widget builder for attachment actions modal + /// [defaultActionsModal] is the default [AttachmentActionsModal] config + /// Use [defaultActionsModal.copyWith] to easily customize it + final AttachmentActionsBuilder? attachmentActionsModalBuilder; + @override _FullScreenMediaState createState() => _FullScreenMediaState(); } @@ -196,6 +202,8 @@ class _FullScreenMediaState extends State StreamChannel.of(context).channel, ); }, + attachmentActionsModalBuilder: + widget.attachmentActionsModalBuilder, ), if (!widget.message.isEphemeral) GalleryFooter( diff --git a/packages/stream_chat_flutter/lib/src/gallery_footer.dart b/packages/stream_chat_flutter/lib/src/gallery_footer.dart index dcfa0e29..0acd9b02 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_footer.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_footer.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:cached_network_image/cached_network_image.dart'; -import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; diff --git a/packages/stream_chat_flutter/lib/src/gallery_header.dart b/packages/stream_chat_flutter/lib/src/gallery_header.dart index fc5f67b4..2325af69 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_header.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_header.dart @@ -6,6 +6,15 @@ import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; +/// Widget builder for attachment actions modal +/// [defaultActionsModal] is the default [AttachmentActionsModal] config +/// Use [defaultActionsModal.copyWith] to easily customize it +typedef AttachmentActionsBuilder = Widget Function( + BuildContext context, + Attachment attachment, + AttachmentActionsModal defaultActionsModal, +); + /// Header/AppBar widget for media display screen class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { /// Creates a channel header @@ -21,6 +30,7 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { this.userName = '', this.sentAt = '', this.backgroundColor, + this.attachmentActionsModalBuilder, }) : preferredSize = const Size.fromHeight(kToolbarHeight), super(key: key); @@ -55,6 +65,11 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { /// The background color of this [GalleryHeader]. final Color? backgroundColor; + /// Widget builder for attachment actions modal + /// [defaultActionsModal] is the default [AttachmentActionsModal] config + /// Use [defaultActionsModal.copyWith] to easily customize it + final AttachmentActionsBuilder? attachmentActionsModalBuilder; + @override Widget build(BuildContext context) { final galleryHeaderThemeData = GalleryHeaderTheme.of(context); @@ -123,17 +138,26 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { final galleryHeaderThemeData = StreamChatTheme.of(context).galleryHeaderTheme; + final defaultModal = AttachmentActionsModal( + message: message, + currentIndex: currentIndex, + onShowMessage: onShowMessage, + ); + + final effectiveModal = attachmentActionsModalBuilder?.call( + context, + message.attachments[currentIndex], + defaultModal, + ) ?? + defaultModal; + final result = await showDialog( useRootNavigator: false, context: context, barrierColor: galleryHeaderThemeData.bottomSheetBarrierColor, builder: (context) => StreamChannel( channel: channel, - child: AttachmentActionsModal( - message: message, - currentIndex: currentIndex, - onShowMessage: onShowMessage, - ), + child: effectiveModal, ), ); diff --git a/packages/stream_chat_flutter/lib/src/group_avatar.dart b/packages/stream_chat_flutter/lib/src/group_avatar.dart index 4a16279e..bf599c63 100644 --- a/packages/stream_chat_flutter/lib/src/group_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/group_avatar.dart @@ -81,6 +81,7 @@ class GroupAvatar extends StatelessWidget { ), initialData: member, builder: (context, member) => UserAvatar( + showOnlineStatus: false, user: member.user!, borderRadius: BorderRadius.zero, ), @@ -118,6 +119,7 @@ class GroupAvatar extends StatelessWidget { ), initialData: member, builder: (context, member) => UserAvatar( + showOnlineStatus: false, user: member.user!, borderRadius: BorderRadius.zero, ), diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 08fc2a7b..022963e9 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,10 @@ class MessageInput extends StatefulWidget { /// Defaults to false. final bool mentionAllAppUsers; + /// Defines if the [MessageInput] loses focuses after a message is sent. + /// The default behaviour keeps focus until a command is enabled. + final bool? shouldKeepFocusAfterMessage; + @override MessageInputState createState() => MessageInputState(); @@ -1640,7 +1645,6 @@ class MessageInputState extends State { } final res = await FilePicker.platform.pickFiles( type: type, - withData: true, ); if (res?.files.isNotEmpty == true) { file = res!.files.single.toAttachmentFile; @@ -1759,7 +1763,9 @@ class MessageInputState extends State { return; } - final shouldUnfocus = _commandEnabled; + var shouldKeepFocus = widget.shouldKeepFocusAfterMessage; + + shouldKeepFocus ??= !_commandEnabled; if (_commandEnabled) { text = '${'/${_chosenCommand!.name} '}$text'; @@ -1822,8 +1828,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 5424a1c2..c61ccfed 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -58,6 +58,46 @@ typedef OnMessageTap = void Function(Message); /// Callback on reply tapped typedef ReplyTapCallback = void Function(Message); +/// Spacing Types (These are properties of a message to help inform the decision +/// of how much space / which widget to build after it) +enum SpacingType { + /// Message is a thread + thread, + + /// There is a >1s time diff between current and last message + timeDiff, + + /// Next message is by a different user + otherUser, + + /// Message is deleted + deleted, + + /// No other conditions are valid, default spacing (This will likely be the + /// only rule in the list provided) + defaultSpacing, +} + +/// Builder for building certain spacing after widgets. +/// This spacing can be in form of any widgets you like. +/// A List of [SpacingType] is provided to help inform the decision of +/// what to build after the message. +/// +/// As an example: +/// MessageListView( +/// spacingWidgetBuilder: (context, list) { +/// if(list.contains(SpacingType.defaultSpacing)) { +/// return SizedBox(height: 2.0,); +/// } else { +/// return SizedBox(height: 8.0,); +/// } +/// }, +/// ), +typedef SpacingWidgetBuilder = Widget Function( + BuildContext context, + List spacingTypes, +); + /// Class for message details // ignore: prefer-match-file-name class MessageDetails { @@ -171,8 +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; @@ -289,6 +335,12 @@ class MessageListView extends StatefulWidget { /// Builder used to build the loading indicator shown while paginating. final WidgetBuilder? paginationLoadingIndicatorBuilder; + /// This allows a user to customise the space after a message + /// A List of [SpacingType] is provided to provide more data about the + /// type of message (thread, difference in time between current and last + /// message, default spacing, etc) + final SpacingWidgetBuilder? spacingWidgetBuilder; + @override _MessageListViewState createState() => _MessageListViewState(); } @@ -443,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) { @@ -471,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, @@ -564,17 +614,38 @@ class _MessageListViewState extends State { Units.MINUTE, ); + final spacingRules = []; + final isNextUserSame = message.user!.id == nextMessage.user?.id; final isThread = message.replyCount! > 0; final isDeleted = message.isDeleted; - if (timeDiff >= 1 || - !isNextUserSame || - isThread || - isDeleted) { - return const SizedBox(height: 8); + final hasTimeDiff = timeDiff >= 1; + + if (hasTimeDiff) { + spacingRules.add(SpacingType.timeDiff); } - return const SizedBox(height: 2); + + if (!isNextUserSame) { + spacingRules.add(SpacingType.otherUser); + } + + if (isThread) { + spacingRules.add(SpacingType.thread); + } + + if (isDeleted) { + spacingRules.add(SpacingType.deleted); + } + + if (spacingRules.isNotEmpty) { + return widget.spacingWidgetBuilder + ?.call(context, spacingRules) ?? + const SizedBox(height: 8); + } + return widget.spacingWidgetBuilder + ?.call(context, [SpacingType.defaultSpacing]) ?? + const SizedBox(height: 2); }, itemBuilder: (context, i) { if (i == itemCount - 1) { diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index f6ae52c6..b82d4c31 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -97,14 +97,20 @@ class MessageWidget extends StatefulWidget { this.deletedBottomRowBuilder, this.onReturnAction, this.customAttachmentBuilders, - this.readList, this.padding, this.textPadding = const EdgeInsets.symmetric( horizontal: 16, vertical: 8, ), this.attachmentPadding = EdgeInsets.zero, - this.allRead = false, + @Deprecated(''' + allRead is now deprecated and it will be removed in future releases. + The MessageWidget now listens for read events on its own. + ''') this.allRead = false, + @Deprecated(''' + readList is now deprecated and it will be removed in future releases. + The MessageWidget now listens for read events on its own. + ''') this.readList, this.onQuotedMessageTap, this.customActions = const [], this.onAttachmentTap, @@ -558,8 +564,6 @@ class _MessageWidgetState extends State bool get showTimeStamp => widget.showTimestamp; - bool get isMessageRead => widget.readList?.isNotEmpty == true; - bool get showInChannel => widget.showInChannelIndicator; bool get hasQuotedMessage => widget.message.quotedMessage != null; @@ -1230,6 +1234,7 @@ class _MessageWidgetState extends State Widget _buildSendingIndicator() { final style = widget.messageTheme.createdAtStyle; final message = widget.message; + final memberCount = StreamChannel.of(context).channel.memberCount ?? 0; if (hasNonUrlAttachments && (message.status == MessageSendingStatus.sending || @@ -1252,27 +1257,40 @@ class _MessageWidgetState extends State ); } - Widget child = SendingIndicator( - message: message, - isMessageRead: isMessageRead, - size: style!.fontSize, + final channel = StreamChannel.of(context).channel; + + return BetterStreamBuilder>( + stream: channel.state?.readStream, + initialData: channel.state?.read, + builder: (context, data) { + final readList = data.where((it) => + it.user.id != _streamChat.currentUser?.id && + (it.lastRead.isAfter(message.createdAt) || + it.lastRead.isAtSameMomentAs(message.createdAt))); + final isMessageRead = readList.length >= (channel.memberCount ?? 0) - 1; + Widget child = SendingIndicator( + message: message, + isMessageRead: isMessageRead, + size: style!.fontSize, + ); + if (isMessageRead) { + child = Row( + children: [ + if (memberCount > 2) + Text( + readList.length.toString(), + style: style.copyWith( + color: _streamChatTheme.colorTheme.accentPrimary, + ), + ), + const SizedBox(width: 2), + child, + ], + ); + } + return child; + }, ); - if (isMessageRead) { - child = Row( - children: [ - if (StreamChannel.of(context).channel.memberCount! > 2) - Text( - widget.readList!.length.toString(), - style: style.copyWith( - color: _streamChatTheme.colorTheme.accentPrimary, - ), - ), - const SizedBox(width: 2), - child, - ], - ); - } - return child; } Widget _buildUserAvatar() => Transform.translate( diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 0447fc68..cd0d93a0 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -2,6 +2,7 @@ export 'package:jiffy/jiffy.dart'; export 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; export 'src/attachment/attachment.dart'; +export 'src/attachment_actions_modal.dart'; export 'src/back_button.dart'; export 'src/channel_avatar.dart'; export 'src/channel_header.dart'; diff --git a/packages/stream_chat_flutter_core/example/lib/main.dart b/packages/stream_chat_flutter_core/example/lib/main.dart index 407da315..4d3b2f55 100644 --- a/packages/stream_chat_flutter_core/example/lib/main.dart +++ b/packages/stream_chat_flutter_core/example/lib/main.dart @@ -83,9 +83,12 @@ class HomeScreen extends StatelessWidget { channelListController: channelListController, filter: Filter.and([ Filter.equal('type', 'messaging'), - Filter.in_('members', [ - StreamChatCore.of(context).currentUser!.id, - ]) + Filter.in_( + 'members', + [ + StreamChatCore.of(context).currentUser!.id, + ], + ) ]), emptyBuilder: (BuildContext context) => const Center( child: Text('Looks like you are not in any channels'), @@ -318,10 +321,10 @@ class _MessageScreenState extends State { ), ), ), - ) + ), ], ), - ) + ), ], ), ), diff --git a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart index 8117a26e..633c3137 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart @@ -227,13 +227,13 @@ class StreamChannelState extends State { preferOffline: preferOffline, ); - Future> _queryAtMessage({ + Future _queryAtMessage({ String? messageId, int before = 20, int after = 20, bool preferOffline = false, }) async { - if (channel.state == null) return []; + if (channel.state == null) return null; channel.state!.isUpToDate = false; channel.state!.truncate(); @@ -245,23 +245,33 @@ class StreamChannelState extends State { preferOffline: preferOffline, ); channel.state!.isUpToDate = true; - return []; + return null; } - return Future.wait([ - queryBeforeMessage( - messageId, - limit: before, - preferOffline: preferOffline, - ), - queryAfterMessage( - messageId, - limit: after, - preferOffline: preferOffline, - ), - ]); + return queryAroundMessage( + messageId, + before: before, + after: after, + preferOffline: preferOffline, + ); } + /// + Future queryAroundMessage( + String messageId, { + int before = 20, + int after = 20, + bool preferOffline = false, + }) => + channel.query( + messagesPagination: PaginationParams( + idAround: messageId, + before: before, + after: after, + ), + preferOffline: preferOffline, + ); + /// Future queryBeforeMessage( String messageId, { 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'), diff --git a/packages/stream_chat_localizations/example/lib/add_new_lang.dart b/packages/stream_chat_localizations/example/lib/add_new_lang.dart index 28ad50fb..6fa7689b 100644 --- a/packages/stream_chat_localizations/example/lib/add_new_lang.dart +++ b/packages/stream_chat_localizations/example/lib/add_new_lang.dart @@ -374,8 +374,10 @@ class NnStreamChatLocalizations extends GlobalStreamChatLocalizations { String get youText => 'You'; @override - String galleryPaginationText( - {required int currentPage, required int totalPages}) => + String galleryPaginationText({ + required int currentPage, + required int totalPages, + }) => '$currentPage of $totalPages'; @override diff --git a/packages/stream_chat_persistence/example/lib/main.dart b/packages/stream_chat_persistence/example/lib/main.dart index e187da56..551a3ae9 100644 --- a/packages/stream_chat_persistence/example/lib/main.dart +++ b/packages/stream_chat_persistence/example/lib/main.dart @@ -242,7 +242,7 @@ class _MessageViewState extends State { ), ), ), - ) + ), ], ), ) diff --git a/packages/stream_chat_persistence/test/src/dao/user_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/user_dao_test.dart index fb095e36..f5a40dbc 100644 --- a/packages/stream_chat_persistence/test/src/dao/user_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/user_dao_test.dart @@ -45,7 +45,6 @@ void main() { role: 'testRole', createdAt: DateTime.now(), updatedAt: DateTime.now(), - lastActive: DateTime.now(), online: math.Random().nextBool(), banned: math.Random().nextBool(), );