diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 90211d07..9c925f88 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -15,10 +15,14 @@ - Added `Filter.contains` and `Filter.empty` - Added support for `next`, `previous` value pagination in `client.search` , [read more.](https://getstream.io/chat/docs/other-rest/search/#pagination) +- `Attachment` class now has a `fileSize` and `mimeType` property. Setting a `file` will also set the `file_size` + , `mime_type` key on `extraData`, so `attachment.fileSize`, `attachment.mimetype` and `attachment.extraData['file_size']` + , `attachment.extraData['mime_type]` is same respectively. 🐞 Fixed - [[#659]](https://github.com/GetStream/stream-chat-flutter/issues/659) Fixed unread count not updating correctly. +- Fix `Filter.empty()` json encoding. ## 2.2.1 diff --git a/packages/stream_chat/lib/src/core/models/attachment.dart b/packages/stream_chat/lib/src/core/models/attachment.dart index 8973aee2..0ef9db7a 100644 --- a/packages/stream_chat/lib/src/core/models/attachment.dart +++ b/packages/stream_chat/lib/src/core/models/attachment.dart @@ -33,13 +33,20 @@ class Attachment extends Equatable { this.authorIcon, this.assetUrl, List? actions, - this.extraData = const {}, + Map extraData = const {}, this.file, UploadState? uploadState, }) : id = id ?? const Uuid().v4(), title = title ?? file?.name, localUri = file?.path != null ? Uri.parse(file!.path!) : null, - actions = actions ?? [] { + actions = actions ?? [], + // For backwards compatibility, + // set 'file_size', 'mime_type' in [extraData]. + extraData = { + ...extraData, + if (file?.size != null) 'file_size': file?.size, + if (file?.mimeType != null) 'mime_type': file?.mimeType?.mimeType, + } { this.uploadState = uploadState ?? ((assetUrl != null || imageUrl != null) ? const UploadState.success() @@ -121,6 +128,18 @@ class Attachment extends Equatable { /// This is created locally for uniquely identifying a attachment. final String id; + /// Shortcut for file size. + /// + /// {@macro fileSize} + @JsonKey(ignore: true) + int? get fileSize => extraData['file_size'] as int?; + + /// Shortcut for file mimeType. + /// + /// {@macro mimeType} + @JsonKey(ignore: true) + String? get mimeType => extraData['mime_type'] as String?; + /// Known top level fields. /// Useful for [Serializer] methods. static const topLevelFields = [ diff --git a/packages/stream_chat/lib/src/core/models/attachment_file.dart b/packages/stream_chat/lib/src/core/models/attachment_file.dart index 7bff5806..c6983b74 100644 --- a/packages/stream_chat/lib/src/core/models/attachment_file.dart +++ b/packages/stream_chat/lib/src/core/models/attachment_file.dart @@ -2,6 +2,7 @@ import 'dart:typed_data'; import 'package:dio/dio.dart' show MultipartFile; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:http_parser/http_parser.dart'; import 'package:meta/meta.dart'; import 'package:stream_chat/src/core/platform_detector/platform_detector.dart'; import 'package:stream_chat/src/core/util/extension.dart'; @@ -65,7 +66,7 @@ class AttachmentFile { AttachmentFile({ required this.size, this.path, - this.name, + String? name, this.bytes, }) : assert( path != null || bytes != null, @@ -74,7 +75,12 @@ class AttachmentFile { assert( !CurrentPlatform.isWeb || bytes != null, 'File by path is not supported in web, Please provide bytes', - ); + ), + assert( + name?.contains('.') ?? true, + 'Invalid file name, should also contain file extension', + ), + _name = name; /// Create a new instance from a json factory AttachmentFile.fromJson(Map json) => @@ -87,8 +93,10 @@ class AttachmentFile { /// ``` final String? path; + final String? _name; + /// File name including its extension. - final String? name; + String? get name => _name ?? path?.split('/').last; /// Byte data for this file. Particularly useful if you want to manipulate /// its data or easily upload to somewhere else. @@ -101,26 +109,26 @@ class AttachmentFile { /// File extension for this file. String? get extension => name?.split('.').last; + /// The mime type of this file. + MediaType? get mimeType => name?.mimeType; + /// Serialize to json Map toJson() => _$AttachmentFileToJson(this); /// Converts this into a [MultipartFile] Future toMultipartFile() async { - final filename = path?.split('/').last ?? name; - final mimeType = filename?.mimeType; - - late MultipartFile multiPartFile; + MultipartFile multiPartFile; if (CurrentPlatform.isWeb) { multiPartFile = MultipartFile.fromBytes( bytes!, - filename: filename, + filename: name, contentType: mimeType, ); } else { multiPartFile = await MultipartFile.fromFile( path!, - filename: filename, + filename: name, contentType: mimeType, ); } diff --git a/packages/stream_chat/lib/src/core/models/filter.dart b/packages/stream_chat/lib/src/core/models/filter.dart index d4536eab..ca494bdb 100644 --- a/packages/stream_chat/lib/src/core/models/filter.dart +++ b/packages/stream_chat/lib/src/core/models/filter.dart @@ -102,6 +102,12 @@ class Filter extends Equatable { this.key, }) : operator = operator.rawValue; + /// An empty filter + const Filter.empty() + : value = const {}, + operator = null, + key = null; + /// Combines the provided filters and matches the values /// matched by all filters. factory Filter.and(List filters) => @@ -172,9 +178,6 @@ class Filter extends Equatable { String? key, }) = Filter.__; - /// An empty filter - factory Filter.empty() => const Filter.raw(value: {}); - /// Creates a custom [Filter] from a raw map value /// /// ```dart diff --git a/packages/stream_chat/test/src/core/models/attachment_test.dart b/packages/stream_chat/test/src/core/models/attachment_test.dart index 37716656..4e08f7f2 100644 --- a/packages/stream_chat/test/src/core/models/attachment_test.dart +++ b/packages/stream_chat/test/src/core/models/attachment_test.dart @@ -1,5 +1,6 @@ import 'package:stream_chat/src/core/models/action.dart'; import 'package:stream_chat/src/core/models/attachment.dart'; +import 'package:stream_chat/src/core/models/attachment_file.dart'; import 'package:test/test.dart'; import '../../utils.dart'; @@ -41,5 +42,59 @@ void main() { }, ); }); + + test('fileName, mimeType property and extraData manipulation', () { + final file = AttachmentFile(size: 3, path: 'myfolder/myfile.txt'); + final attachment = Attachment(file: file); + + expect(attachment.fileSize, 3); + expect(attachment.mimeType, 'text/plain'); + expect(attachment.toJson(), { + 'title': 'myfile.txt', + 'actions': [], + 'file_size': 3, + 'mime_type': 'text/plain' + }); + expect(Attachment.fromJson(attachment.toJson()).toJson(), { + 'title': 'myfile.txt', + 'actions': [], + 'file_size': 3, + 'mime_type': 'text/plain' + }); + + // Setting the size and mimeType using extraData should work fine + var newAttachment = Attachment( + extraData: const { + 'file_size': 6, + 'mime_type': 'application/pdf', + }, + ); + + expect(newAttachment.extraData['file_size'], 6); + expect(newAttachment.extraData['mime_type'], 'application/pdf'); + expect(newAttachment.fileSize, 6); + expect(newAttachment.mimeType, 'application/pdf'); + + // switching a new file should update size and mimeType + final fileTwo = AttachmentFile(size: 12, path: 'myfolder/fileTwo.pdf'); + newAttachment = attachment.copyWith(file: fileTwo); + + expect(newAttachment.extraData['file_size'], 12); + expect(newAttachment.extraData['mime_type'], 'application/pdf'); + expect(newAttachment.fileSize, 12); + expect(newAttachment.mimeType, 'application/pdf'); + + // if file is available, should override size and mimeType. + final fileThree = AttachmentFile(size: 9, path: 'myfolder/fileThree.png'); + newAttachment = attachment.copyWith(file: fileThree, extraData: { + 'file_size': 88, + 'mime_type': 'application/pdf', + }); + + expect(newAttachment.extraData['file_size'], 9); + expect(newAttachment.extraData['mime_type'], 'image/png'); + expect(newAttachment.fileSize, 9); + expect(newAttachment.mimeType, 'image/png'); + }); }); } diff --git a/packages/stream_chat/test/src/core/models/filter_test.dart b/packages/stream_chat/test/src/core/models/filter_test.dart index 6e3dbf64..e396c2b3 100644 --- a/packages/stream_chat/test/src/core/models/filter_test.dart +++ b/packages/stream_chat/test/src/core/models/filter_test.dart @@ -139,7 +139,7 @@ void main() { }); test('empty', () { - final filter = Filter.empty(); + const filter = Filter.empty(); expect(filter.value, {}); }); @@ -226,6 +226,12 @@ void main() { json.encode(value), ); }); + + test('empty', () { + const filter = Filter.empty(); + final encoded = json.encode(filter); + expect(encoded, '{}'); + }); }); test('groupedFilter', () { diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index a8facbdd..4fa15914 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,19 +1,55 @@ +## Upcoming + +🐞 Fixed + +- [[#668]](https://github.com/GetStream/stream-chat-flutter/issues/668): Fix `MessageInput` rendering errors in case + there are no actions available to show. +- [[#349]](https://github.com/GetStream/stream-chat-flutter/issues/349): Fix `MessageInput` attachment render overflow error. + +🔄 Changed + +- Animation curves changed from default `Curves.linear` to `Curves.easeOut` and `Curves.easeIn` for attachment controls. + ## 2.2.1 -🛑️ Breaking Changes from `2.2.1` +⚠️ Deprecated -- `MessageSearchListView` paginationParams property is now non-nullable with a default value. +- `MessageSearchListView` `paginationParams` property is now deprecated in favor of `limit`. ```dart + // previous paginationParams = const PaginationParams(limit: 30) + + // new + limit = 30 ``` -- `UserListView` pagination property is now non-nullable with a default value. +- `UserListView` `pagination` property is now deprecated in favor of `limit`. ```dart + // previous pagination = const PaginationParams(limit: 30) + + // new + limit = 30 + ``` +- `ChannelListView` `pagination` property is now deprecated in favor of `limit`. + ```dart + // previous + pagination = const PaginationParams(limit: 30) + + // new + limit = 30 + ``` + +🔄 Changed + +- `UserListViewCore` filter property now has a default value. + ```dart + filter = const Filter.empty() ``` 🐞 Fixed - Fixed `MessageSearchListView` pagination. +- Fixed `MessageWidget` attachment tap callbacks. ## 2.2.1 diff --git a/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart index a7d4d2e3..df134250 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_title.dart @@ -19,42 +19,36 @@ class AttachmentTitle extends StatelessWidget { final Attachment attachment; @override - Widget build(BuildContext context) => GestureDetector( - onTap: () { - if (attachment.titleLink != null) { - launchURL(context, attachment.titleLink); - } - }, - child: Padding( - padding: const EdgeInsets.all(8), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (attachment.title != null) - Text( - attachment.title!, - overflow: TextOverflow.ellipsis, - style: messageTheme.messageTextStyle?.copyWith( - color: StreamChatTheme.of(context).colorTheme.accentPrimary, - fontWeight: FontWeight.bold, - ), + Widget build(BuildContext context) { + final normalizedTitleLink = attachment.titleLink?.replaceFirst( + RegExp(r'https?://(www\.)?'), + '', + ); + return GestureDetector( + onTap: () { + final titleLink = attachment.titleLink; + if (titleLink != null) launchURL(context, titleLink); + }, + child: Padding( + padding: const EdgeInsets.all(8), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (attachment.title != null) + Text( + attachment.title!, + overflow: TextOverflow.ellipsis, + style: messageTheme.messageTextStyle?.copyWith( + color: StreamChatTheme.of(context).colorTheme.accentPrimary, + fontWeight: FontWeight.bold, ), - if (attachment.titleLink != null || - attachment.ogScrapeUrl != null) - Text( - Uri.parse(attachment.titleLink ?? attachment.ogScrapeUrl!) - .authority - .split('.') - .reversed - .take(2) - .toList() - .reversed - .join('.'), - style: messageTheme.messageTextStyle, - ), - ], - ), + ), + if (normalizedTitleLink != null) + Text(normalizedTitleLink, style: messageTheme.messageTextStyle), + ], ), - ); + ), + ); + } } diff --git a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart index f57e1ee6..6045539f 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart @@ -258,7 +258,8 @@ class FileAttachment extends AttachmentWidget { visualDensity: VisualDensity.compact, splashRadius: 16, onPressed: () { - launchURL(context, attachment.assetUrl); + final assetUrl = attachment.assetUrl; + if (assetUrl != null) launchURL(context, assetUrl); }, ); } diff --git a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart index 7eb2606e..e165fc6d 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart @@ -98,7 +98,13 @@ class GiphyAttachment extends AttachmentWidget { Padding( padding: const EdgeInsets.all(2), child: GestureDetector( - onTap: () => onAttachmentTap ?? _onImageTap(context), + onTap: () { + if (onAttachmentTap != null) { + onAttachmentTap?.call(); + } else { + _onImageTap(context); + } + }, child: CachedNetworkImage( height: size?.height, width: size?.width, @@ -253,21 +259,12 @@ class GiphyAttachment extends AttachmentWidget { Widget _buildSentAttachment(BuildContext context, String imageUrl) => SizedBox( child: GestureDetector( - onTap: () async { - final res = - await Navigator.push(context, MaterialPageRoute(builder: (_) { - final channel = StreamChannel.of(context).channel; - return StreamChannel( - channel: channel, - child: FullScreenMedia( - mediaAttachments: [attachment], - userName: message.user?.name, - message: message, - onShowMessage: onShowMessage, - ), - ); - })); - if (res != null) onReturnAction!(res); + onTap: () { + if (onAttachmentTap != null) { + onAttachmentTap?.call(); + } else { + _onImageTap(context); + } }, child: Stack( children: [ diff --git a/packages/stream_chat_flutter/lib/src/channel_list_view.dart b/packages/stream_chat_flutter/lib/src/channel_list_view.dart index 35fc6d73..0f06e269 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -59,7 +59,7 @@ typedef ViewInfoCallback = void Function(Channel); /// Modify it to change the widget appearance. class ChannelListView extends StatefulWidget { /// Instantiate a new ChannelListView - const ChannelListView({ + ChannelListView({ Key? key, this.filter, this.sort, @@ -68,9 +68,12 @@ class ChannelListView extends StatefulWidget { this.presence = false, this.memberLimit, this.messageLimit, - this.pagination = const PaginationParams( - limit: 25, - ), + @Deprecated( + "'pagination' is deprecated and shouldn't be used. " + "This property is no longer used, Please use 'limit' instead", + ) + this.pagination, + int? limit, this.onChannelTap, this.onChannelLongPress, this.channelWidget, @@ -92,7 +95,8 @@ class ChannelListView extends StatefulWidget { this.onDeletePressed, this.swipeActions, this.channelListController, - }) : super(key: key); + }) : limit = limit ?? pagination?.limit ?? 25, + super(key: key); /// If true a default swipe to action behaviour will be added to this widget final bool swipeToAction; @@ -129,7 +133,14 @@ class ChannelListView extends StatefulWidget { /// limit: the number of channels to return (max is 30) /// offset: the offset (max is 1000) /// message_limit: how many messages should be included to each channel - final PaginationParams pagination; + @Deprecated( + "'pagination' is deprecated and shouldn't be used. " + "This property is no longer used, Please use 'limit' instead", + ) + final PaginationParams? pagination; + + /// The amount of channels requested per API call. + final int limit; /// Function called when tapping on a channel /// By default it calls [Navigator.push] building a [MaterialPageRoute] @@ -218,7 +229,7 @@ class _ChannelListViewState extends State { presence: widget.presence, memberLimit: widget.memberLimit, messageLimit: widget.messageLimit, - pagination: widget.pagination, + limit: widget.limit, channelListController: _channelListController, listBuilder: widget.listBuilder ?? _buildListView, emptyBuilder: widget.emptyBuilder ?? _buildEmptyWidget, diff --git a/packages/stream_chat_flutter/lib/src/image_group.dart b/packages/stream_chat_flutter/lib/src/image_group.dart index 627406d0..88b309fe 100644 --- a/packages/stream_chat_flutter/lib/src/image_group.dart +++ b/packages/stream_chat_flutter/lib/src/image_group.dart @@ -15,6 +15,7 @@ class ImageGroup extends StatelessWidget { required this.size, this.onReturnAction, this.onShowMessage, + this.onAttachmentTap, }) : super(key: key); /// List of attachments to show @@ -23,6 +24,9 @@ class ImageGroup extends StatelessWidget { /// Callback when attachment is returned to from other screens final ValueChanged? onReturnAction; + /// Callback when attachment is tapped + final void Function(Message message, Attachment attachment)? onAttachmentTap; + /// Message which images are attached to final Message message; @@ -117,6 +121,10 @@ class ImageGroup extends StatelessWidget { BuildContext context, int index, ) async { + if (onAttachmentTap != null) { + return onAttachmentTap!(message, images[index]); + } + final channel = StreamChannel.of(context).channel; final res = await Navigator.push( diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index 4cd6d7ee..65c23804 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -608,6 +608,8 @@ class MessageInputState extends State { crossFadeState: _actionsShrunk ? CrossFadeState.showFirst : CrossFadeState.showSecond, + firstCurve: Curves.easeOut, + secondCurve: Curves.easeIn, firstChild: IconButton( onPressed: () { if (_actionsShrunk) { @@ -634,20 +636,17 @@ class MessageInputState extends State { !widget.showCommandsButton && widget.actions?.isNotEmpty != true ? const Offstage() - : FittedBox( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - if (!widget.disableAttachments) - _buildAttachmentButton(context), - if (widget.showCommandsButton && - widget.editMessage == null && - channel.state != null && - channel.config?.commands.isNotEmpty == true) - _buildCommandButton(context), - ...widget.actions ?? [], - ].insertBetween(const SizedBox(width: 8)), - ), + : Wrap( + children: [ + if (!widget.disableAttachments) + _buildAttachmentButton(context), + if (widget.showCommandsButton && + widget.editMessage == null && + channel.state != null && + channel.config?.commands.isNotEmpty == true) + _buildCommandButton(context), + ...widget.actions ?? [], + ].insertBetween(const SizedBox(width: 8)), ), duration: const Duration(milliseconds: 300), alignment: Alignment.center, @@ -1001,108 +1000,120 @@ class MessageInputState extends State { return AnimatedContainer( duration: const Duration(milliseconds: 300), + curve: Curves.easeOut, height: _openFilePickerSection ? _kMinMediaPickerSize : 0, - child: Material( - color: _streamChatTheme.colorTheme.inputBg, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( + child: SingleChildScrollView( + child: SizedBox( + height: _kMinMediaPickerSize, + child: Material( + color: _streamChatTheme.colorTheme.inputBg, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - IconButton( - icon: StreamSvgIcon.pictures( - color: _getIconColor(0), + Row( + children: [ + IconButton( + icon: StreamSvgIcon.pictures( + color: _getIconColor(0), + ), + onPressed: + _attachmentContainsFile && _attachments.isNotEmpty + ? null + : () { + setState(() { + _filePickerIndex = 0; + }); + }, + ), + IconButton( + iconSize: 32, + icon: StreamSvgIcon.files( + color: _getIconColor(1), + ), + onPressed: + !_attachmentContainsFile && _attachments.isNotEmpty + ? null + : () { + pickFile(DefaultAttachmentTypes.file); + }, + ), + IconButton( + icon: StreamSvgIcon.camera( + color: _getIconColor(2), + ), + onPressed: attachmentLimitCrossed || + (_attachmentContainsFile && + _attachments.isNotEmpty) + ? null + : () { + pickFile(DefaultAttachmentTypes.image, + camera: true); + }, + ), + IconButton( + padding: const EdgeInsets.all(0), + icon: StreamSvgIcon.record( + color: _getIconColor(3), + ), + onPressed: attachmentLimitCrossed || + (_attachmentContainsFile && + _attachments.isNotEmpty) + ? null + : () { + pickFile(DefaultAttachmentTypes.video, + camera: true); + }, + ), + ], + ), + DecoratedBox( + decoration: BoxDecoration( + color: _streamChatTheme.colorTheme.barsBg, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(16), + topRight: Radius.circular(16), + ), ), - onPressed: _attachmentContainsFile && _attachments.isNotEmpty - ? null - : () { - setState(() { - _filePickerIndex = 0; - }); - }, - ), - IconButton( - iconSize: 32, - icon: StreamSvgIcon.files( - color: _getIconColor(1), - ), - onPressed: !_attachmentContainsFile && _attachments.isNotEmpty - ? null - : () { - pickFile(DefaultAttachmentTypes.file); - }, - ), - IconButton( - icon: StreamSvgIcon.camera( - color: _getIconColor(2), - ), - onPressed: attachmentLimitCrossed || - (_attachmentContainsFile && _attachments.isNotEmpty) - ? null - : () { - pickFile(DefaultAttachmentTypes.image, camera: true); - }, - ), - IconButton( - padding: const EdgeInsets.all(0), - icon: StreamSvgIcon.record( - color: _getIconColor(3), - ), - onPressed: attachmentLimitCrossed || - (_attachmentContainsFile && _attachments.isNotEmpty) - ? null - : () { - pickFile(DefaultAttachmentTypes.video, camera: true); - }, - ), - ], - ), - DecoratedBox( - decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.barsBg, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(16), - topRight: Radius.circular(16), - ), - ), - child: Center( - child: Padding( - padding: const EdgeInsets.all(8), - child: Container( - width: 40, - height: 4, - decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.inputBg, - borderRadius: BorderRadius.circular(4), + child: Center( + child: Padding( + padding: const EdgeInsets.all(8), + child: Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: _streamChatTheme.colorTheme.inputBg, + borderRadius: BorderRadius.circular(4), + ), + ), ), ), ), - ), + if (_openFilePickerSection) + Expanded( + child: DecoratedBox( + decoration: BoxDecoration( + color: _streamChatTheme.colorTheme.barsBg, + borderRadius: BorderRadius.circular(8), + ), + child: _PickerWidget( + filePickerIndex: _filePickerIndex, + streamChatTheme: _streamChatTheme, + containsFile: _attachmentContainsFile, + selectedMedias: _attachments.keys.toList(), + onAddMoreFilesClick: pickFile, + onMediaSelected: (media) { + if (_attachments.containsKey(media.id)) { + setState(() => _attachments.remove(media.id)); + } else { + _addAssetAttachment(media); + } + }, + ), + ), + ), + ], ), - if (_openFilePickerSection) - Expanded( - child: DecoratedBox( - decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.barsBg, - borderRadius: BorderRadius.circular(8), - ), - child: _PickerWidget( - filePickerIndex: _filePickerIndex, - streamChatTheme: _streamChatTheme, - containsFile: _attachmentContainsFile, - selectedMedias: _attachments.keys.toList(), - onAddMoreFilesClick: pickFile, - onMediaSelected: (media) { - if (_attachments.containsKey(media.id)) { - setState(() => _attachments.remove(media.id)); - } else { - _addAssetAttachment(media); - } - }, - ), - ), - ), - ], + ), ), ), ); @@ -1246,7 +1257,7 @@ class MessageInputState extends State { Widget _buildReplyToMessage() { if (!_hasQuotedMessage) return const Offstage(); final containsUrl = widget.quotedMessage!.attachments - .any((element) => element.ogScrapeUrl != null) == + .any((element) => element.titleLink != null) == true; return QuotedMessageWidget( reverse: true, @@ -1964,7 +1975,7 @@ class _PickerWidgetState extends State<_PickerWidget> { future: requestPermission, builder: (context, snapshot) { if (!snapshot.hasData) { - return const Center(child: CircularProgressIndicator()); + return const Offstage(); } if (snapshot.data!) { 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 ab4ae04f..e325598d 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -991,7 +991,7 @@ class _MessageListViewState extends State { final isOnlyEmoji = message.text?.isOnlyEmoji ?? false; final hasUrlAttachment = - message.attachments.any((it) => it.ogScrapeUrl != null) == true; + message.attachments.any((it) => it.titleLink != null) == true; final borderSide = isOnlyEmoji || hasUrlAttachment || (isMyMessage && !hasFileAttachment) diff --git a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart index f80c688f..ab1a9178 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_list_view.dart @@ -53,12 +53,17 @@ typedef EmptyMessageSearchBuilder = Widget Function( /// Modify it to change the widget appearance. class MessageSearchListView extends StatefulWidget { /// Instantiate a new MessageSearchListView - const MessageSearchListView({ + MessageSearchListView({ Key? key, required this.filters, this.messageQuery, this.sortOptions, - this.paginationParams = const PaginationParams(limit: 30), + @Deprecated( + "'paginationParams' is deprecated and shouldn't be used. " + "This property is no longer used, Please use 'limit' instead", + ) + this.paginationParams, + int? limit, this.messageFilters, this.separatorBuilder, this.itemBuilder, @@ -71,7 +76,8 @@ class MessageSearchListView extends StatefulWidget { this.loadingBuilder, this.childBuilder, this.messageSearchListController, - }) : super(key: key); + }) : limit = limit ?? paginationParams?.limit ?? 30, + super(key: key); /// Message String to search on final String? messageQuery; @@ -93,7 +99,14 @@ class MessageSearchListView extends StatefulWidget { /// limit: the number of users to return (max is 30) /// offset: the offset (max is 1000) /// message_limit: how many messages should be included to each channel - final PaginationParams paginationParams; + @Deprecated( + "'paginationParams' is deprecated and shouldn't be used. " + "This property is no longer used, Please use 'limit' instead", + ) + final PaginationParams? paginationParams; + + /// The amount of messages requested per API call. + final int limit; /// The message query filters to use. /// You can query on any of the custom fields you've defined on the [Channel]. @@ -152,7 +165,7 @@ class _MessageSearchListViewState extends State { filters: widget.filters, sortOptions: widget.sortOptions, messageQuery: widget.messageQuery, - paginationParams: widget.paginationParams, + limit: widget.limit, messageFilters: widget.messageFilters, messageSearchListController: _messageSearchListController, emptyBuilder: widget.emptyBuilder ?? diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 7619f21b..01177603 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -96,7 +96,7 @@ class MessageWidget extends StatefulWidget { this.bottomRowBuilder, this.deletedBottomRowBuilder, this.onReturnAction, - Map? customAttachmentBuilders, + this.customAttachmentBuilders, this.readList, this.padding, this.textPadding = const EdgeInsets.symmetric( @@ -133,6 +133,7 @@ class MessageWidget extends StatefulWidget { messageTheme: messageTheme, onShowMessage: onShowMessage, onReturnAction: onReturnAction, + onAttachmentTap: onAttachmentTap, ), ), border, @@ -214,6 +215,11 @@ class MessageWidget extends StatefulWidget { ), onShowMessage: onShowMessage, onReturnAction: onReturnAction, + onAttachmentTap: onAttachmentTap != null + ? () { + onAttachmentTap(message, attachment); + } + : null, ); }).toList(), ), @@ -243,6 +249,11 @@ class MessageWidget extends StatefulWidget { mediaQueryData.size.width * 0.8, mediaQueryData.size.height * 0.3, ), + onAttachmentTap: onAttachmentTap != null + ? () { + onAttachmentTap(message, attachment); + } + : null, ), border, reverse, @@ -395,6 +406,9 @@ class MessageWidget extends StatefulWidget { /// Builder for respective attachment types final Map attachmentBuilders; + /// Builder for respective attachment types (user facing builder) + final Map? customAttachmentBuilders; + /// Center user avatar with bottom of the message final bool translateUserAvatar; @@ -519,7 +533,7 @@ class MessageWidget extends StatefulWidget { showPinButton: showPinButton ?? this.showPinButton, showPinHighlight: showPinHighlight ?? this.showPinHighlight, customAttachmentBuilders: - customAttachmentBuilders ?? attachmentBuilders, + customAttachmentBuilders ?? this.customAttachmentBuilders, translateUserAvatar: translateUserAvatar ?? this.translateUserAvatar, onQuotedMessageTap: onQuotedMessageTap ?? this.onQuotedMessageTap, onMessageTap: onMessageTap ?? this.onMessageTap, @@ -566,12 +580,11 @@ class _MessageWidgetState extends State bool get isOnlyEmoji => widget.message.text?.isOnlyEmoji == true; - bool get hasNonUrlAttachments => widget.message.attachments - .where((it) => it.ogScrapeUrl == null) - .isNotEmpty; + bool get hasNonUrlAttachments => + widget.message.attachments.where((it) => it.titleLink == null).isNotEmpty; bool get hasUrlAttachments => - widget.message.attachments.any((it) => it.ogScrapeUrl != null) == true; + widget.message.attachments.any((it) => it.titleLink != null) == true; bool get showBottomRow => showThreadReplyIndicator || @@ -975,9 +988,9 @@ class _MessageWidgetState extends State Widget _buildUrlAttachment() { final urlAttachment = widget.message.attachments - .firstWhere((element) => element.ogScrapeUrl != null); + .firstWhere((element) => element.titleLink != null); - final host = Uri.parse(urlAttachment.ogScrapeUrl!).host; + final host = Uri.parse(urlAttachment.titleLink!).host; final splitList = host.split('.'); final hostName = splitList.length == 3 ? splitList[1] : splitList[0]; final hostDisplayName = urlAttachment.authorName?.capitalize() ?? @@ -1143,7 +1156,7 @@ class _MessageWidgetState extends State final attachmentGroups = >{}; widget.message.attachments - .where((element) => element.ogScrapeUrl == null && element.type != null) + .where((element) => element.titleLink == null && element.type != null) .forEach((e) { if (attachmentGroups[e.type] == null) { attachmentGroups[e.type!] = []; diff --git a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart index ad3d936a..01068abd 100644 --- a/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/quoted_message_widget.dart @@ -97,8 +97,8 @@ class QuotedMessageWidget extends StatelessWidget { bool get _hasAttachments => message.attachments.isNotEmpty == true; - bool get _containsScrapeUrl => - message.attachments.any((element) => element.ogScrapeUrl != null) == true; + bool get _containsLinkAttachment => + message.attachments.any((element) => element.titleLink != null) == true; bool get _containsText => message.text?.isNotEmpty == true; @@ -198,9 +198,9 @@ class QuotedMessageWidget extends StatelessWidget { Widget _parseAttachments(BuildContext context) { Widget child; Attachment attachment; - if (_containsScrapeUrl) { + if (_containsLinkAttachment) { attachment = message.attachments.firstWhere( - (element) => element.ogScrapeUrl != null, + (element) => element.titleLink != null, ); child = _buildUrlAttachment(attachment); } else { @@ -280,7 +280,7 @@ class QuotedMessageWidget extends StatelessWidget { }; Color? _getBackgroundColor(BuildContext context) { - if (_containsScrapeUrl) { + if (_containsLinkAttachment) { return StreamChatTheme.of(context).colorTheme.linkBg; } return messageTheme.messageBackgroundColor; diff --git a/packages/stream_chat_flutter/lib/src/url_attachment.dart b/packages/stream_chat_flutter/lib/src/url_attachment.dart index 5d5ca489..b940f227 100644 --- a/packages/stream_chat_flutter/lib/src/url_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/url_attachment.dart @@ -30,10 +30,8 @@ class UrlAttachment extends StatelessWidget { final chatThemeData = StreamChatTheme.of(context); return GestureDetector( onTap: () { - launchURL( - context, - urlAttachment.ogScrapeUrl, - ); + final titleLink = urlAttachment.titleLink; + if (titleLink != null) launchURL(context, titleLink); }, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/packages/stream_chat_flutter/lib/src/user_list_view.dart b/packages/stream_chat_flutter/lib/src/user_list_view.dart index 4d650192..b946f800 100644 --- a/packages/stream_chat_flutter/lib/src/user_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/user_list_view.dart @@ -46,12 +46,17 @@ typedef UserItemBuilder = Widget Function(BuildContext, User, bool); /// Modify it to change the widget appearance. class UserListView extends StatefulWidget { /// Instantiate a new UserListView - const UserListView({ + UserListView({ Key? key, - this.filter, + this.filter = const Filter.empty(), this.sort, this.presence, - this.pagination = const PaginationParams(limit: 30), + @Deprecated( + "'pagination' is deprecated and shouldn't be used. " + "This property is no longer used, Please use 'limit' instead", + ) + this.pagination, + int? limit, this.onUserTap, this.onUserLongPress, this.userWidget, @@ -71,11 +76,13 @@ class UserListView extends StatefulWidget { crossAxisCount == 1 || groupAlphabetically == false, 'Cannot group alphabetically when crossAxisCount > 1', ), + limit = limit ?? pagination?.limit ?? 30, super(key: key); /// The query filters to use. /// You can query on any of the custom fields you've defined on the [Channel]. /// You can also filter other built-in channel fields. + // TODO: Make it non-nullable in a future breaking release final Filter? filter; /// The sorting used for the channels matching the filters. @@ -93,7 +100,14 @@ class UserListView extends StatefulWidget { /// limit: the number of users to return (max is 30) /// offset: the offset (max is 1000) /// message_limit: how many messages should be included to each channel - final PaginationParams pagination; + @Deprecated( + "'pagination' is deprecated and shouldn't be used. " + "This property is no longer used, Please use 'limit' instead", + ) + final PaginationParams? pagination; + + /// The amount of users requested per API call. + final int limit; /// Function called when tapping on a channel /// By default it calls [Navigator.push] building a [MaterialPageRoute] @@ -184,7 +198,7 @@ class _UserListViewState extends State ), listBuilder: widget.listBuilder ?? (context, list) => _buildListView(list), - pagination: widget.pagination, + limit: widget.limit, sort: widget.sort, filter: widget.filter, presence: widget.presence, diff --git a/packages/stream_chat_flutter/lib/src/utils.dart b/packages/stream_chat_flutter/lib/src/utils.dart index d97ea6a1..4dc9fee7 100644 --- a/packages/stream_chat_flutter/lib/src/utils.dart +++ b/packages/stream_chat_flutter/lib/src/utils.dart @@ -7,8 +7,8 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:stream_chat_flutter/src/extension.dart'; /// Launch URL -Future launchURL(BuildContext context, String? url) async { - if (url != null && await canLaunch(url)) { +Future launchURL(BuildContext context, String url) async { + if (await canLaunch(url)) { await launch(url); } else { ScaffoldMessenger.of(context).showSnackBar( diff --git a/packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart index 38b1c524..079c01e3 100644 --- a/packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/channel_list_view_theme_test.dart @@ -62,7 +62,7 @@ void main() { return Scaffold( body: StreamChannel( channel: MockChannel(), - child: const ChannelListView(), + child: ChannelListView(), ), ); }, diff --git a/packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart index a22eebd0..3aa20744 100644 --- a/packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/user_list_view_theme_test.dart @@ -56,7 +56,7 @@ void main() { home: Builder( builder: (BuildContext context) { _context = context; - return const Scaffold( + return Scaffold( body: UsersBloc( child: UserListView(), ), @@ -85,7 +85,7 @@ void main() { home: Builder( builder: (BuildContext context) { _context = context; - return const Scaffold( + return Scaffold( body: UsersBloc( child: UserListView(), ), diff --git a/packages/stream_chat_flutter_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index d0d89a6b..c4287a09 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,19 +1,44 @@ ## Upcoming -🛑️ Breaking Changes from `2.2.1` +⚠️ Deprecated -- `MessageSearchListViewCore` paginationParams property is now non-nullable with a default value. +- `MessageSearchListViewCore` `paginationParams` property is now deprecated in favor of `limit`. ```dart + // previous paginationParams = const PaginationParams(limit: 30) + + // new + limit = 30 ``` -- `UserListViewCore` pagination property is now non-nullable with a default value. +- `UserListViewCore` `pagination` property is now deprecated in favor of `limit`. ```dart + // previous pagination = const PaginationParams(limit: 30) + + // new + limit = 30 + ``` +- `ChannelListViewCore` `pagination` property is now deprecated in favor of `limit`. + ```dart + // previous + pagination = const PaginationParams(limit: 30) + + // new + limit = 30 + ``` + +🔄 Changed + +- `UserListViewCore` filter property now has a default value. + ```dart + filter = const Filter.empty() ``` 🐞 Fixed - Fixed `MessageSearchBloc` pagination. +- [[#673]](https://github.com/GetStream/stream-chat-flutter/issues/673): Fix `Core Widgets` not getting rebuild with new + data on configuration change. ## 2.2.1 diff --git a/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart b/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart index c5d3f69a..d3d6c312 100644 --- a/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/channel_list_core.dart @@ -56,7 +56,7 @@ import 'package:stream_chat_flutter_core/src/typedef.dart'; /// information about the channels. class ChannelListCore extends StatefulWidget { /// Instantiate a new ChannelListView - const ChannelListCore({ + ChannelListCore({ Key? key, required this.errorBuilder, required this.emptyBuilder, @@ -69,11 +69,15 @@ class ChannelListCore extends StatefulWidget { this.memberLimit, this.messageLimit, this.sort, - this.pagination = const PaginationParams( - limit: 25, - ), + @Deprecated( + "'pagination' is deprecated and shouldn't be used. " + "This property is no longer used, Please use 'limit' instead", + ) + this.pagination, this.channelListController, - }) : super(key: key); + int? limit, + }) : limit = limit ?? pagination?.limit ?? 25, + super(key: key); /// A [ChannelListController] allows reloading and pagination. /// Use [ChannelListController.loadData] and @@ -124,7 +128,14 @@ class ChannelListCore extends StatefulWidget { /// limit: the number of channels to return (max is 30) /// offset: the offset (max is 1000) /// message_limit: how many messages should be included to each channel - final PaginationParams pagination; + @Deprecated( + "'pagination' is deprecated and shouldn't be used. " + "This property is no longer used, Please use 'limit' instead", + ) + final PaginationParams? pagination; + + /// The amount of channels requested per API call. + final int limit; @override ChannelListCoreState createState() => ChannelListCoreState(); @@ -162,7 +173,7 @@ class ChannelListCoreState extends State { presence: widget.presence, memberLimit: widget.memberLimit, messageLimit: widget.messageLimit, - paginationParams: widget.pagination, + paginationParams: PaginationParams(limit: widget.limit), ); /// Fetches more channels with updated pagination and updates the widget @@ -174,7 +185,8 @@ class ChannelListCoreState extends State { presence: widget.presence, memberLimit: widget.memberLimit, messageLimit: widget.messageLimit, - paginationParams: widget.pagination.copyWith( + paginationParams: PaginationParams( + limit: widget.limit, offset: _channelsBloc.channels?.length ?? 0, ), ); @@ -214,15 +226,14 @@ class ChannelListCoreState extends State { void didUpdateWidget(ChannelListCore oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.filter?.toString() != oldWidget.filter?.toString() || + if (jsonEncode(widget.filter) != jsonEncode(oldWidget.filter) || jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) || widget.state != oldWidget.state || widget.watch != oldWidget.watch || widget.presence != oldWidget.presence || widget.messageLimit != oldWidget.messageLimit || widget.memberLimit != oldWidget.memberLimit || - widget.pagination.toJson().toString() != - oldWidget.pagination.toJson().toString()) { + widget.limit != oldWidget.limit) { loadData(); } diff --git a/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart b/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart index 28e092a7..f4d18a0b 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_search_list_core.dart @@ -38,7 +38,7 @@ class MessageSearchListCore extends StatefulWidget { /// * [errorBuilder] /// * [loadingBuilder] /// * [childBuilder] - const MessageSearchListCore({ + MessageSearchListCore({ Key? key, required this.emptyBuilder, required this.errorBuilder, @@ -47,9 +47,14 @@ class MessageSearchListCore extends StatefulWidget { required this.filters, this.messageQuery, this.sortOptions, - this.paginationParams = const PaginationParams(limit: 30), + @Deprecated( + "'pagination' is deprecated and shouldn't be used. " + "This property is no longer used, Please use 'limit' instead", + ) + this.paginationParams, this.messageFilters, this.messageSearchListController, + int? limit, }) : assert( messageQuery != null || messageFilters != null, 'Provide at least `query` or `messageFilters`', @@ -58,6 +63,13 @@ class MessageSearchListCore extends StatefulWidget { messageQuery == null || messageFilters == null, "Can't provide both `query` and `messageFilters` at the same time", ), + assert( + paginationParams?.offset == null || + paginationParams?.offset == 0 || + sortOptions == null, + 'Cannot specify `offset` with `sortOptions` parameter', + ), + limit = limit ?? paginationParams?.limit ?? 30, super(key: key); /// A [MessageSearchListController] allows reloading and pagination. @@ -84,7 +96,14 @@ class MessageSearchListCore extends StatefulWidget { /// Pagination parameters /// limit: the number of messages to return (max is 30) /// offset: the offset (max is 1000) - final PaginationParams paginationParams; + @Deprecated( + "'pagination' is deprecated and shouldn't be used. " + "This property is no longer used, Please use 'limit' instead", + ) + final PaginationParams? paginationParams; + + /// The amount of messages requested per API call. + final int limit; /// The message query filters to use. /// You can query on any of the custom fields you've defined on the [Channel]. @@ -157,19 +176,19 @@ class MessageSearchListCoreState extends State { filter: widget.filters, sort: widget.sortOptions, query: widget.messageQuery, - pagination: widget.paginationParams, messageFilter: widget.messageFilters, + pagination: PaginationParams(limit: widget.limit), ); /// Fetches more messages with updated pagination and updates the widget Future paginateData() { - PaginationParams pagination; + var pagination = PaginationParams(limit: widget.limit); if (widget.sortOptions != null) { - pagination = widget.paginationParams.copyWith( + pagination = pagination.copyWith( next: _messageSearchBloc?.nextId, ); } else { - pagination = widget.paginationParams.copyWith( + pagination = pagination.copyWith( offset: _messageSearchBloc?.messageResponses?.length, ); } @@ -185,13 +204,12 @@ class MessageSearchListCoreState extends State { @override void didUpdateWidget(MessageSearchListCore oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.filters.toString() != oldWidget.filters.toString() || + if (jsonEncode(widget.filters) != jsonEncode(oldWidget.filters) || jsonEncode(widget.sortOptions) != jsonEncode(oldWidget.sortOptions) || - widget.messageQuery?.toString() != oldWidget.messageQuery?.toString() || - widget.messageFilters?.toString() != - oldWidget.messageFilters?.toString() || - widget.paginationParams.toJson().toString() != - oldWidget.paginationParams.toJson().toString()) { + widget.messageQuery != oldWidget.messageQuery || + jsonEncode(widget.messageFilters) != + jsonEncode(oldWidget.messageFilters) || + widget.limit != oldWidget.limit) { loadData(); } diff --git a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart index e403be4e..839067bf 100644 --- a/packages/stream_chat_flutter_core/lib/src/user_list_core.dart +++ b/packages/stream_chat_flutter_core/lib/src/user_list_core.dart @@ -57,19 +57,25 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// [errorBuilder] must all be supplied and not null. class UserListCore extends StatefulWidget { /// Instantiate a new [UserListCore] - const UserListCore({ + UserListCore({ required this.errorBuilder, required this.emptyBuilder, required this.loadingBuilder, required this.listBuilder, Key? key, - this.filter, + this.filter = const Filter.empty(), this.sort, this.presence, - this.pagination = const PaginationParams(limit: 30), + @Deprecated( + "'pagination' is deprecated and shouldn't be used. " + "This property is no longer used, Please use 'limit' instead", + ) + this.pagination, this.groupAlphabetically = false, this.userListController, - }) : super(key: key); + int? limit, + }) : limit = limit ?? pagination?.limit ?? 30, + super(key: key); /// A [UserListController] allows reloading and pagination. /// Use [UserListController.loadData] and [UserListController.paginateData] @@ -91,6 +97,7 @@ class UserListCore extends StatefulWidget { /// The query filters to use. /// You can query on any of the custom fields you've defined on the [Channel]. /// You can also filter other built-in channel fields. + // TODO: Make it non-nullable in a future breaking release final Filter? filter; /// The sorting used for the channels matching the filters. @@ -106,7 +113,14 @@ class UserListCore extends StatefulWidget { /// limit: the number of users to return (max is 30) /// offset: the offset (max is 1000) /// message_limit: how many messages should be included to each channel - final PaginationParams pagination; + @Deprecated( + "'pagination' is deprecated and shouldn't be used. " + "This property is no longer used, Please use 'limit' instead", + ) + final PaginationParams? pagination; + + /// The amount of users requested per API call. + final int limit; /// Set it to true to group users by their first character /// @@ -193,7 +207,7 @@ class UserListCoreState extends State filter: widget.filter, sort: widget.sort, presence: widget.presence, - pagination: widget.pagination, + pagination: PaginationParams(limit: widget.limit), ); /// Fetches more users with updated pagination and updates the widget @@ -201,7 +215,8 @@ class UserListCoreState extends State filter: widget.filter, sort: widget.sort, presence: widget.presence, - pagination: widget.pagination.copyWith( + pagination: PaginationParams( + limit: widget.limit, offset: _usersBloc!.users?.length ?? 0, ), ); @@ -209,11 +224,10 @@ class UserListCoreState extends State @override void didUpdateWidget(UserListCore oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.filter?.toString() != oldWidget.filter?.toString() || + if (jsonEncode(widget.filter) != jsonEncode(oldWidget.filter) || jsonEncode(widget.sort) != jsonEncode(oldWidget.sort) || widget.presence != oldWidget.presence || - widget.pagination.toJson().toString() != - oldWidget.pagination.toJson().toString()) { + widget.limit != oldWidget.limit) { loadData(); } 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 47aa2bf9..dd87d2cf 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 @@ -508,4 +508,17 @@ void main() { )).called(1); }, ); + + test('`widget.limit` should match `widget.pagination.limit`', () { + const pagination = PaginationParams(limit: 30); + final channelListCore = ChannelListCore( + listBuilder: (_, __) => const Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), + pagination: pagination, + ); + + expect(channelListCore.limit, pagination.limit); + }); } diff --git a/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart b/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart index c689d90c..332289ea 100644 --- a/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart +++ b/packages/stream_chat_flutter_core/test/message_search_bloc_test.dart @@ -8,7 +8,7 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'matchers/get_message_response_matcher.dart'; import 'mocks.dart'; -const testFilter = Filter.custom(operator: '\$test', value: 'testValue'); +const testFilter = Filter.custom(key: 'test', value: 'testValue'); void main() { List _generateMessages({ 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 03396513..ca416595 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 @@ -6,8 +6,8 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'mocks.dart'; -const testFilter = Filter.custom(operator: '\$test', value: 'testValue'); -const testMessageFilter = Filter.custom(operator: '\$test', value: 'testValue'); +const testFilter = Filter.custom(key: 'test', value: 'testValue'); +const testMessageFilter = Filter.custom(key: 'test', value: 'testValue'); void main() { List _generateMessages({ @@ -551,4 +551,19 @@ void main() { )).called(1); }, ); + + test('`widget.limit` should match `widget.pagination.limit`', () { + const pagination = PaginationParams(limit: 30); + final messageSearchListCore = MessageSearchListCore( + childBuilder: (List messages) => const Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object? error) => const Offstage(), + filters: testFilter, + messageFilters: testMessageFilter, + paginationParams: pagination, + ); + + expect(messageSearchListCore.limit, pagination.limit); + }); } 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 77977bb8..ade58e0f 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 @@ -520,4 +520,17 @@ void main() { )).called(1); }, ); + + test('`widget.limit` should match `widget.pagination.limit`', () { + const pagination = PaginationParams(limit: 30); + final userListCore = UserListCore( + listBuilder: (_, __) => const Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), + pagination: pagination, + ); + + expect(userListCore.limit, pagination.limit); + }); }