diff --git a/.github/workflows/stream_flutter_workflow.yml b/.github/workflows/stream_flutter_workflow.yml index 0baaaff7..28063c18 100644 --- a/.github/workflows/stream_flutter_workflow.yml +++ b/.github/workflows/stream_flutter_workflow.yml @@ -2,7 +2,7 @@ name: stream_flutter_workflow env: ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' - flutter_version: "2.2.2" + flutter_version: "2.5.1" on: pull_request: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3bb5a923..b99a2b3c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -177,6 +177,14 @@ Develop is merged into master after the team performs various automated and QA t --- +# Versioning Policy + +All of the Stream Chat packages follow [semantic versioning (semver)](https://semver.org/). + +See our [versioning policy documentation](https://getstream.io/chat/docs/sdk/flutter/basics/versioning_policy/) for more information. + +--- + # Styleguides 💅 ![image](https://user-images.githubusercontent.com/20601437/124241186-d17a8680-db1b-11eb-9a21-3df305674ca9.png) diff --git a/README.md b/README.md index 62358d57..061a193d 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,11 @@ Every package folder includes a fully functional example with setup instructions We also provide a set of sample apps created using the Stream Flutter SDK at [this location](https://github.com/GetStream/flutter-samples). +## Versioning Policy + +All of the Stream Chat packages follow [semantic versioning (semver)](https://semver.org/). + +See our [versioning policy documentation](https://getstream.io/chat/docs/sdk/flutter/basics/versioning_policy/) for more information. ## We are hiring diff --git a/docusaurus/docs/Flutter/basics/versioning_policy.mdx b/docusaurus/docs/Flutter/basics/versioning_policy.mdx new file mode 100644 index 00000000..48f25d66 --- /dev/null +++ b/docusaurus/docs/Flutter/basics/versioning_policy.mdx @@ -0,0 +1,20 @@ +--- +id: versioning_policy +sidebar_position: 3 +title: Versioning Policy +--- + +All of the Stream Chat packages follow [semantic versioning (semver)](https://semver.org/). + +That means that with a version number x.y.z (major.minor.patch): +- When releasing bug fixes (backwards compatible), we make a patch release by changing the z number (ex: 3.6.2 to 3.6.3). A bug fix is defined as an internal change that fixes incorrect behavior. +- When releasing new features or non-critical fixes, we make a minor release by changing the y number (ex: 3.6.2 to 3.7.0). +- When releasing breaking changes (backward incompatible), we make a major release by changing the x number (ex: 3.6.2 to 4.0.0). + +See the [semantic versioning](https://dart.dev/tools/pub/versioning#semantic-versions) section from the Dart docs for more information. + +This versioning policy does not apply to prerelease packages (below major version of 1). See this +[StackOverflow thread](https://stackoverflow.com/questions/66201337/how-do-dart-package-versions-work-how-should-i-version-my-flutter-plugins) +for more information on Dart package versioning. + +Whenever possible, we will add deprecation warnings in preparation for future breaking changes. diff --git a/docusaurus/docs/Flutter/guides/understanding_filters.mdx b/docusaurus/docs/Flutter/guides/understanding_filters.mdx index e9d72ecf..292afd07 100644 --- a/docusaurus/docs/Flutter/guides/understanding_filters.mdx +++ b/docusaurus/docs/Flutter/guides/understanding_filters.mdx @@ -104,7 +104,16 @@ Filter.autoComplete('name', 'demo') The 'exists' filter matches values that exist, or don't exist, based on the specified boolean value. ```dart -Filter.exists('name', true) +Filter.exists('name') +``` + +#### Filter.notExists + +The 'notExists' filter checks if the specified key doesn't exist. This is a simplified call to `Filter.exists` +with the value set to false. + +```dart +Filter.notExists('name') ``` #### Filter.contains diff --git a/docusaurus/docs/Flutter/stream_chat_flutter/message_search_list_view.mdx b/docusaurus/docs/Flutter/stream_chat_flutter/message_search_list_view.mdx index 864b5e49..39bf2d9e 100644 --- a/docusaurus/docs/Flutter/stream_chat_flutter/message_search_list_view.mdx +++ b/docusaurus/docs/Flutter/stream_chat_flutter/message_search_list_view.mdx @@ -29,7 +29,7 @@ class MessageSearchPage extends StatelessWidget { child: MessageSearchListView( filters: Filter.in_('members', [StreamChat.of(context).user!.id],), messageQuery: 'your query here', - paginationParams: PaginationParams(limit: 20), + limit: 20, ), ), ); diff --git a/docusaurus/docs/Flutter/stream_chat_flutter_core/message_search_list_core.mdx b/docusaurus/docs/Flutter/stream_chat_flutter_core/message_search_list_core.mdx index 70cc026f..f3be5e85 100644 --- a/docusaurus/docs/Flutter/stream_chat_flutter_core/message_search_list_core.mdx +++ b/docusaurus/docs/Flutter/stream_chat_flutter_core/message_search_list_core.mdx @@ -28,9 +28,9 @@ class MessageSearchPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( body: MessageSearchListCore( - messageQuery: _messageFilter, - filters: _channelsFilter, - paginationParams: PaginationParams(limit: 20), + messageQuery: _messageFilter, + filters: _channelsFilter, + limit: 20, ), ); } diff --git a/packages/stream_chat/CHANGELOG.md b/packages/stream_chat/CHANGELOG.md index 61f00005..667365d0 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,4 +1,15 @@ -## Upcoming +## 3.1.1 + +✅ Added + +- Added `Filter.notExists`. + +🐞 Fixed + +- [[#710]](https://github.com/GetStream/stream-chat-flutter/issues/710) Fixed JWT requiring using `String` as id. +- Fixed expired CDN attachment links not updating correctly. + +## 3.0.0 🛑️ Breaking Changes from `2.2.1` @@ -13,6 +24,19 @@ ✅ Added - 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. +- [[#700]](https://github.com/GetStream/stream-chat-flutter/issues/700) Connecting user without providing `name` + uses `id` instead for setting `user.name`. ## 2.2.1 diff --git a/packages/stream_chat/lib/src/client/channel.dart b/packages/stream_chat/lib/src/client/channel.dart index c2c40018..2fd4bea9 100644 --- a/packages/stream_chat/lib/src/client/channel.dart +++ b/packages/stream_chat/lib/src/client/channel.dart @@ -351,7 +351,7 @@ class Channel { Future get initialized => _initializedCompleter.future; final _cancelableAttachmentUploadRequest = {}; - final _messageAttachmentsUploadCompleter = {}; + final _messageAttachmentsUploadCompleter = >{}; /// Cancels [attachmentId] upload request. Throws exception if the request /// hasn't even started yet, Already completed or Already cancelled. @@ -382,12 +382,10 @@ class Channel { String messageId, Iterable attachmentIds, ) { - final message = [ + var message = [ ...state!.messages, ...state!.threads.values.expand((messages) => messages), - ].firstWhereOrNull( - (it) => it.id == messageId, - ); + ].firstWhereOrNull((it) => it.id == messageId); if (message == null) { throw const StreamChatError('Error, Message not found'); @@ -409,11 +407,15 @@ class Channel { client.logger.info('Found ${attachments.length} attachments'); void updateAttachment(Attachment attachment) { - final index = - message.attachments.indexWhere((it) => it.id == attachment.id); + final index = message!.attachments.indexWhere( + (it) => it.id == attachment.id, + ); if (index != -1) { - message.attachments[index] = attachment; - state?.addMessage(message); + final newAttachments = [...message!.attachments]..[index] = attachment; + final updatedMessage = message!.copyWith(attachments: newAttachments); + state?.addMessage(updatedMessage); + // updating original message for next iteration + message = message!.merge(updatedMessage); } } @@ -476,7 +478,7 @@ class Channel { _cancelableAttachmentUploadRequest.remove(it.id); }); })).whenComplete(() { - if (message.attachments.every((it) => it.uploadState.isSuccess)) { + if (message!.attachments.every((it) => it.uploadState.isSuccess)) { _messageAttachmentsUploadCompleter.remove(messageId)?.complete(message); } }); @@ -1076,33 +1078,7 @@ class Channel { } else { // remove the passed message if response does // not contain message - final oldIndex = state!.messages.indexWhere((m) => m.id == messageId); - - // remove regular message if present - if (oldIndex != -1) { - final oldMessage = state!.messages[oldIndex]; - state!.updateChannelState(state!._channelState.copyWith( - messages: state?.messages?..remove(oldMessage), - channel: state?._channelState.channel, - )); - } else { - // remove thread message if present - // also reduces total reply count - final oldMessage = state!.threads.values - .expand((messages) => messages) - .firstWhereOrNull((m) => m.id == messageId); - if (oldMessage?.parentId != null) { - final parentMessage = state!.messages.firstWhereOrNull( - (element) => element.id == oldMessage!.parentId, - ); - if (parentMessage != null) { - state!.addMessage(parentMessage.copyWith( - replyCount: parentMessage.replyCount! - 1)); - } - state!.updateThreadInfo(oldMessage!.parentId!, - state!.threads[oldMessage.parentId!]!..remove(oldMessage)); - } - } + state!.removeMessage(message); await _client.chatPersistenceClient?.deleteMessageById(messageId); } return res; @@ -1521,7 +1497,7 @@ class ChannelClientState { } } - void _checkExpiredAttachmentMessages(ChannelState channelState) { + void _checkExpiredAttachmentMessages(ChannelState channelState) async { final expiredAttachmentMessagesId = channelState.messages .where((m) => !_updatedMessagesIds.contains(m.id) && @@ -1532,20 +1508,24 @@ class ChannelClientState { return false; } final uri = Uri.parse(url); - if (uri.host != 'stream-io-cdn.com' || + if (!uri.host.endsWith('stream-io-cdn.com') || uri.queryParameters['Expires'] == null) { return false; } - final expiration = - DateTime.parse(uri.queryParameters['Expires']!); + final secondsFromEpoch = + int.parse(uri.queryParameters['Expires']!); + final expiration = DateTime.fromMillisecondsSinceEpoch( + secondsFromEpoch * 1000); return expiration.isBefore(DateTime.now()); }) == true) .map((e) => e.id) .toList(); + if (expiredAttachmentMessagesId.isNotEmpty == true) { - _channel.getMessagesById(expiredAttachmentMessagesId); + await _channel._initializedCompleter.future; _updatedMessagesIds.addAll(expiredAttachmentMessagesId); + _channel.getMessagesById(expiredAttachmentMessagesId); } } @@ -1704,7 +1684,7 @@ class ChannelClientState { })); } - /// Add a message to this channel. + /// Add a [message] to this [channelState]. void addMessage(Message message) { if (message.parentId == null || message.showInChannel == true) { final newMessages = List.from(_channelState.messages); @@ -1735,6 +1715,35 @@ class ChannelClientState { } } + /// Remove a [message] from this [channelState]. + void removeMessage(Message message) { + final parentId = message.parentId; + // i.e. it's a thread message + // 1. Remove the thread message + // 2. Reduce total reply count of parent message + if (parentId != null) { + final allMessages = [...messages]; + final parentMessage = allMessages.firstWhereOrNull( + (it) => it.id == parentId, + ); + + // return if message not available in the memory + if (parentMessage == null) return; + final replyCount = parentMessage.replyCount; + // return if reply count is null or zero + if (replyCount == null || replyCount == 0) return; + + addMessage(parentMessage.copyWith(replyCount: replyCount - 1)); + updateThreadInfo(parentId, threads[parentId]!..remove(message)); + } else { + // Remove regular message + final allMessages = [...messages]; + if (allMessages.remove(message)) { + _channelState = _channelState.copyWith(messages: allMessages); + } + } + } + void _listenReadEvents() { if (_channelState.channel?.config.readEvents == false) { return; @@ -1862,15 +1871,11 @@ class ChannelClientState { if (newThreads.containsKey(parentId)) { newThreads[parentId] = [ - ...newThreads[parentId] - ?.where( - (newMessage) => !messages.any((m) => m.id == newMessage.id)) - .toList() ?? - [], ...messages, - ]; - - newThreads[parentId]!.sort(_sortByCreatedAt); + ...newThreads[parentId]!.where( + (newMessage) => !messages.any((m) => m.id == newMessage.id), + ), + ]..sort(_sortByCreatedAt); } else { newThreads[parentId] = messages; } diff --git a/packages/stream_chat/lib/src/client/client.dart b/packages/stream_chat/lib/src/client/client.dart index f9d10a92..ae6716fd 100644 --- a/packages/stream_chat/lib/src/client/client.dart +++ b/packages/stream_chat/lib/src/client/client.dart @@ -143,9 +143,6 @@ class StreamChatClient { late final RetryPolicy _retryPolicy; - /// sync state of the channels present inside state, defaults to false - bool _synced = false; - /// the last dateTime at the which all the channels were synced DateTime? _lastSyncedAt; @@ -406,10 +403,6 @@ class StreamChatClient { if (event.type == EventType.healthCheck) { return _handleHealthCheckEvent(event); } - if (!event.isLocal && _synced) { - _lastSyncedAt = event.createdAt; - _chatPersistenceClient?.updateLastSyncAt(event.createdAt); - } state.updateUser(event.user); return _eventController.add(event); } @@ -438,8 +431,6 @@ class StreamChatClient { type: EventType.connectionRecovered, online: true, )); - } else { - _synced = false; } } @@ -464,13 +455,11 @@ class StreamChatClient { Future sync({List? cids, DateTime? lastSyncAt}) async { cids ??= await _chatPersistenceClient?.getChannelCids(); if (cids == null || cids.isEmpty) { - _synced = true; return; } lastSyncAt ??= await _chatPersistenceClient?.getLastSyncAt(); if (lastSyncAt == null) { - _synced = true; return; } @@ -488,12 +477,10 @@ class StreamChatClient { handleEvent(event); } - _synced = true; final now = DateTime.now(); _lastSyncedAt = now; _chatPersistenceClient?.updateLastSyncAt(now); } catch (e, stk) { - _synced = false; logger.severe('Error during sync', e, stk); } } @@ -1325,6 +1312,7 @@ class StreamChatClient { // resetting state state.dispose(); state = ClientState(this); + _lastSyncedAt = null; // resetting credentials _tokenManager.reset(); @@ -1366,26 +1354,21 @@ class ClientState { .where((event) => event.me != null && event.type != EventType.healthCheck) .map((e) => e.me!) - .listen((user) { - currentUser = currentUser?.merge(user) ?? user; - final totalUnreadCount = user.totalUnreadCount; - _totalUnreadCountController.add(totalUnreadCount); - - final unreadChannels = user.unreadChannels; - if (unreadChannels != null) { - _unreadChannelsController.add(unreadChannels); - } - }), + .listen((user) => currentUser = currentUser?.merge(user) ?? user), _client .on() .map((event) => event.unreadChannels) .whereType() - .listen(_unreadChannelsController.add), + .listen((count) { + currentUser = currentUser?.copyWith(unreadChannels: count); + }), _client .on() .map((event) => event.totalUnreadCount) .whereType() - .listen(_totalUnreadCountController.add), + .listen((count) { + currentUser = currentUser?.copyWith(totalUnreadCount: count); + }), ]); _listenChannelDeleted(); @@ -1441,6 +1424,7 @@ class ClientState { /// Sets the user currently interacting with the client /// note: this fully overrides the [currentUser] set currentUser(OwnUser? user) { + _computeUnreadCounts(user); _currentUserController.add(user); } @@ -1506,6 +1490,18 @@ class ClientState { _channelsController.add(newChannels); } + void _computeUnreadCounts(OwnUser? user) { + final totalUnreadCount = user?.totalUnreadCount; + if (totalUnreadCount != null) { + _totalUnreadCountController.add(totalUnreadCount); + } + + final unreadChannels = user?.unreadChannels; + if (unreadChannels != null) { + _unreadChannelsController.add(unreadChannels); + } + } + final _channelsController = BehaviorSubject>.seeded({}); final _currentUserController = BehaviorSubject(); final _usersController = BehaviorSubject>.seeded({}); diff --git a/packages/stream_chat/lib/src/client/retry_queue.dart b/packages/stream_chat/lib/src/client/retry_queue.dart index 4c44433f..c441b5fb 100644 --- a/packages/stream_chat/lib/src/client/retry_queue.dart +++ b/packages/stream_chat/lib/src/client/retry_queue.dart @@ -6,8 +6,8 @@ import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/client/channel.dart'; import 'package:stream_chat/src/client/retry_policy.dart'; import 'package:stream_chat/src/core/error/error.dart'; -import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/src/core/models/message.dart'; +import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/stream_chat.dart'; /// The retry queue associated to a channel diff --git a/packages/stream_chat/lib/src/core/api/general_api.dart b/packages/stream_chat/lib/src/core/api/general_api.dart index c6243148..fa08a475 100644 --- a/packages/stream_chat/lib/src/core/api/general_api.dart +++ b/packages/stream_chat/lib/src/core/api/general_api.dart @@ -36,6 +36,10 @@ class GeneralApi { PaginationParams? pagination, Filter? messageFilters, }) async { + assert( + pagination?.offset == null || pagination?.offset == 0 || sort == null, + 'Cannot specify `offset` with `sort` parameter', + ); assert(() { if (query == null && messageFilters == null) { throw ArgumentError('Provide at least `query` or `messageFilters`'); diff --git a/packages/stream_chat/lib/src/core/api/requests.dart b/packages/stream_chat/lib/src/core/api/requests.dart index 29112d72..14995b10 100644 --- a/packages/stream_chat/lib/src/core/api/requests.dart +++ b/packages/stream_chat/lib/src/core/api/requests.dart @@ -15,7 +15,7 @@ class SortOption { /// ``` const SortOption( this.field, { - this.direction = DESC, + this.direction = SortOption.DESC, this.comparator, }); @@ -60,12 +60,16 @@ class PaginationParams extends Equatable { /// ``` const PaginationParams({ this.limit = 10, - this.offset = 0, + this.offset, + this.next, this.greaterThan, this.greaterThanOrEqual, this.lessThan, this.lessThanOrEqual, - }); + }) : assert( + offset == null || offset == 0 || next == null, + 'Cannot specify non-zero `offset` with `next` parameter', + ); /// Create a new instance from a json factory PaginationParams.fromJson(Map json) => @@ -75,7 +79,10 @@ class PaginationParams extends Equatable { final int limit; /// The offset of requesting items. - final int offset; + final int? offset; + + /// A key used to paginate. + final String? next; /// Filter on ids greater than the given value. @JsonKey(name: 'id_gt') @@ -100,6 +107,7 @@ class PaginationParams extends Equatable { PaginationParams copyWith({ int? limit, int? offset, + String? next, String? greaterThan, String? greaterThanOrEqual, String? lessThan, @@ -108,6 +116,7 @@ class PaginationParams extends Equatable { PaginationParams( limit: limit ?? this.limit, offset: offset ?? this.offset, + next: next ?? this.next, greaterThan: greaterThan ?? this.greaterThan, greaterThanOrEqual: greaterThanOrEqual ?? this.greaterThanOrEqual, lessThan: lessThan ?? this.lessThan, @@ -118,6 +127,7 @@ class PaginationParams extends Equatable { List get props => [ limit, offset, + next, 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 6cd935b8..ef995997 100644 --- a/packages/stream_chat/lib/src/core/api/requests.g.dart +++ b/packages/stream_chat/lib/src/core/api/requests.g.dart @@ -6,12 +6,11 @@ part of 'requests.dart'; // JsonSerializableGenerator // ************************************************************************** -SortOption _$SortOptionFromJson(Map json) { - return SortOption( - json['field'] as String, - direction: json['direction'] as int, - ); -} +SortOption _$SortOptionFromJson(Map json) => + SortOption( + json['field'] as String, + direction: json['direction'] as int? ?? SortOption.DESC, + ); Map _$SortOptionToJson(SortOption instance) => { @@ -19,21 +18,20 @@ Map _$SortOptionToJson(SortOption instance) => 'direction': instance.direction, }; -PaginationParams _$PaginationParamsFromJson(Map json) { - return PaginationParams( - limit: json['limit'] as int, - offset: json['offset'] as int, - greaterThan: json['id_gt'] as String?, - greaterThanOrEqual: json['id_gte'] as String?, - lessThan: json['id_lt'] as String?, - lessThanOrEqual: json['id_lte'] as String?, - ); -} +PaginationParams _$PaginationParamsFromJson(Map json) => + PaginationParams( + limit: json['limit'] as int? ?? 10, + offset: json['offset'] as int?, + next: json['next'] as String?, + greaterThan: json['id_gt'] as String?, + greaterThanOrEqual: json['id_gte'] as String?, + lessThan: json['id_lt'] as String?, + lessThanOrEqual: json['id_lte'] as String?, + ); Map _$PaginationParamsToJson(PaginationParams instance) { final val = { 'limit': instance.limit, - 'offset': instance.offset, }; void writeNotNull(String key, dynamic value) { @@ -42,6 +40,8 @@ Map _$PaginationParamsToJson(PaginationParams instance) { } } + writeNotNull('offset', instance.offset); + writeNotNull('next', instance.next); writeNotNull('id_gt', instance.greaterThan); writeNotNull('id_gte', instance.greaterThanOrEqual); writeNotNull('id_lt', instance.lessThan); diff --git a/packages/stream_chat/lib/src/core/api/responses.dart b/packages/stream_chat/lib/src/core/api/responses.dart index 033e91b2..5b52b65d 100644 --- a/packages/stream_chat/lib/src/core/api/responses.dart +++ b/packages/stream_chat/lib/src/core/api/responses.dart @@ -253,6 +253,12 @@ class SearchMessagesResponse extends _BaseResponse { @JsonKey(defaultValue: []) late List results; + /// Message id of where to start searching from for next [results] + late String? next; + + /// Message id of where to start searching from for previous [results] + late String? previous; + /// Create a new instance from a json static SearchMessagesResponse fromJson(Map json) => _$SearchMessagesResponseFromJson(json); diff --git a/packages/stream_chat/lib/src/core/api/responses.g.dart b/packages/stream_chat/lib/src/core/api/responses.g.dart index 15b64a05..13d7d77b 100644 --- a/packages/stream_chat/lib/src/core/api/responses.g.dart +++ b/packages/stream_chat/lib/src/core/api/responses.g.dart @@ -6,14 +6,13 @@ part of 'responses.dart'; // JsonSerializableGenerator // ************************************************************************** -ErrorResponse _$ErrorResponseFromJson(Map json) { - return ErrorResponse() - ..duration = json['duration'] as String? - ..code = json['code'] as int? - ..message = json['message'] as String? - ..statusCode = json['StatusCode'] as int? - ..moreInfo = json['more_info'] as String?; -} +ErrorResponse _$ErrorResponseFromJson(Map json) => + ErrorResponse() + ..duration = json['duration'] as String? + ..code = json['code'] as int? + ..message = json['message'] as String? + ..statusCode = json['StatusCode'] as int? + ..moreInfo = json['more_info'] as String?; Map _$ErrorResponseToJson(ErrorResponse instance) => { @@ -24,273 +23,253 @@ Map _$ErrorResponseToJson(ErrorResponse instance) => 'more_info': instance.moreInfo, }; -SyncResponse _$SyncResponseFromJson(Map json) { - return SyncResponse() - ..duration = json['duration'] as String? - ..events = (json['events'] as List?) - ?.map((e) => Event.fromJson(e as Map)) - .toList() ?? - []; -} +SyncResponse _$SyncResponseFromJson(Map json) => SyncResponse() + ..duration = json['duration'] as String? + ..events = (json['events'] as List?) + ?.map((e) => Event.fromJson(e as Map)) + .toList() ?? + []; QueryChannelsResponse _$QueryChannelsResponseFromJson( - Map json) { - return QueryChannelsResponse() - ..duration = json['duration'] as String? - ..channels = (json['channels'] as List?) - ?.map((e) => ChannelState.fromJson(e as Map)) - .toList() ?? - []; -} + Map json) => + QueryChannelsResponse() + ..duration = json['duration'] as String? + ..channels = (json['channels'] as List?) + ?.map((e) => ChannelState.fromJson(e as Map)) + .toList() ?? + []; TranslateMessageResponse _$TranslateMessageResponseFromJson( - Map json) { - return TranslateMessageResponse() - ..duration = json['duration'] as String? - ..message = Message.fromJson(json['message'] as Map); -} + Map json) => + TranslateMessageResponse() + ..duration = json['duration'] as String? + ..message = Message.fromJson(json['message'] as Map); -QueryMembersResponse _$QueryMembersResponseFromJson(Map json) { - return QueryMembersResponse() - ..duration = json['duration'] as String? - ..members = (json['members'] as List?) - ?.map((e) => Member.fromJson(e as Map)) - .toList() ?? - []; -} +QueryMembersResponse _$QueryMembersResponseFromJson( + Map json) => + QueryMembersResponse() + ..duration = json['duration'] as String? + ..members = (json['members'] as List?) + ?.map((e) => Member.fromJson(e as Map)) + .toList() ?? + []; -QueryUsersResponse _$QueryUsersResponseFromJson(Map json) { - return QueryUsersResponse() - ..duration = json['duration'] as String? - ..users = (json['users'] as List?) - ?.map((e) => User.fromJson(e as Map)) - .toList() ?? - []; -} +QueryUsersResponse _$QueryUsersResponseFromJson(Map json) => + QueryUsersResponse() + ..duration = json['duration'] as String? + ..users = (json['users'] as List?) + ?.map((e) => User.fromJson(e as Map)) + .toList() ?? + []; QueryReactionsResponse _$QueryReactionsResponseFromJson( - Map json) { - return QueryReactionsResponse() - ..duration = json['duration'] as String? - ..reactions = (json['reactions'] as List?) - ?.map((e) => Reaction.fromJson(e as Map)) - .toList() ?? - []; -} + Map json) => + QueryReactionsResponse() + ..duration = json['duration'] as String? + ..reactions = (json['reactions'] as List?) + ?.map((e) => Reaction.fromJson(e as Map)) + .toList() ?? + []; -QueryRepliesResponse _$QueryRepliesResponseFromJson(Map json) { - return QueryRepliesResponse() - ..duration = json['duration'] as String? - ..messages = (json['messages'] as List?) - ?.map((e) => Message.fromJson(e as Map)) - .toList() ?? - []; -} +QueryRepliesResponse _$QueryRepliesResponseFromJson( + Map json) => + QueryRepliesResponse() + ..duration = json['duration'] as String? + ..messages = (json['messages'] as List?) + ?.map((e) => Message.fromJson(e as Map)) + .toList() ?? + []; -ListDevicesResponse _$ListDevicesResponseFromJson(Map json) { - return ListDevicesResponse() - ..duration = json['duration'] as String? - ..devices = (json['devices'] as List?) - ?.map((e) => Device.fromJson(e as Map)) - .toList() ?? - []; -} +ListDevicesResponse _$ListDevicesResponseFromJson(Map json) => + ListDevicesResponse() + ..duration = json['duration'] as String? + ..devices = (json['devices'] as List?) + ?.map((e) => Device.fromJson(e as Map)) + .toList() ?? + []; -SendFileResponse _$SendFileResponseFromJson(Map json) { - return SendFileResponse() - ..duration = json['duration'] as String? - ..file = json['file'] as String; -} +SendFileResponse _$SendFileResponseFromJson(Map json) => + SendFileResponse() + ..duration = json['duration'] as String? + ..file = json['file'] as String; -SendImageResponse _$SendImageResponseFromJson(Map json) { - return SendImageResponse() - ..duration = json['duration'] as String? - ..file = json['file'] as String; -} +SendImageResponse _$SendImageResponseFromJson(Map json) => + SendImageResponse() + ..duration = json['duration'] as String? + ..file = json['file'] as String; -SendReactionResponse _$SendReactionResponseFromJson(Map json) { - return SendReactionResponse() - ..duration = json['duration'] as String? - ..message = Message.fromJson(json['message'] as Map) - ..reaction = Reaction.fromJson(json['reaction'] as Map); -} +SendReactionResponse _$SendReactionResponseFromJson( + Map json) => + SendReactionResponse() + ..duration = json['duration'] as String? + ..message = Message.fromJson(json['message'] as Map) + ..reaction = Reaction.fromJson(json['reaction'] as Map); ConnectGuestUserResponse _$ConnectGuestUserResponseFromJson( - Map json) { - return ConnectGuestUserResponse() - ..duration = json['duration'] as String? - ..accessToken = json['access_token'] as String - ..user = User.fromJson(json['user'] as Map); -} + Map json) => + ConnectGuestUserResponse() + ..duration = json['duration'] as String? + ..accessToken = json['access_token'] as String + ..user = User.fromJson(json['user'] as Map); -UpdateUsersResponse _$UpdateUsersResponseFromJson(Map json) { - return UpdateUsersResponse() - ..duration = json['duration'] as String? - ..users = (json['users'] as Map?)?.map( - (k, e) => MapEntry(k, User.fromJson(e as Map)), - ) ?? - {}; -} +UpdateUsersResponse _$UpdateUsersResponseFromJson(Map json) => + UpdateUsersResponse() + ..duration = json['duration'] as String? + ..users = (json['users'] as Map?)?.map( + (k, e) => MapEntry(k, User.fromJson(e as Map)), + ) ?? + {}; UpdateMessageResponse _$UpdateMessageResponseFromJson( - Map json) { - return UpdateMessageResponse() - ..duration = json['duration'] as String? - ..message = Message.fromJson(json['message'] as Map); -} + Map json) => + UpdateMessageResponse() + ..duration = json['duration'] as String? + ..message = Message.fromJson(json['message'] as Map); -SendMessageResponse _$SendMessageResponseFromJson(Map json) { - return SendMessageResponse() - ..duration = json['duration'] as String? - ..message = Message.fromJson(json['message'] as Map); -} +SendMessageResponse _$SendMessageResponseFromJson(Map json) => + SendMessageResponse() + ..duration = json['duration'] as String? + ..message = Message.fromJson(json['message'] as Map); -GetMessageResponse _$GetMessageResponseFromJson(Map json) { - return GetMessageResponse() - ..duration = json['duration'] as String? - ..message = Message.fromJson(json['message'] as Map) - ..channel = json['channel'] == null - ? null - : ChannelModel.fromJson(json['channel'] as Map); -} +GetMessageResponse _$GetMessageResponseFromJson(Map json) => + GetMessageResponse() + ..duration = json['duration'] as String? + ..message = Message.fromJson(json['message'] as Map) + ..channel = json['channel'] == null + ? null + : ChannelModel.fromJson(json['channel'] as Map); SearchMessagesResponse _$SearchMessagesResponseFromJson( - Map json) { - return SearchMessagesResponse() - ..duration = json['duration'] as String? - ..results = (json['results'] as List?) - ?.map((e) => GetMessageResponse.fromJson(e as Map)) - .toList() ?? - []; -} + Map json) => + SearchMessagesResponse() + ..duration = json['duration'] as String? + ..results = (json['results'] as List?) + ?.map( + (e) => GetMessageResponse.fromJson(e as Map)) + .toList() ?? + [] + ..next = json['next'] as String? + ..previous = json['previous'] as String?; GetMessagesByIdResponse _$GetMessagesByIdResponseFromJson( - Map json) { - return GetMessagesByIdResponse() - ..duration = json['duration'] as String? - ..messages = (json['messages'] as List?) - ?.map((e) => Message.fromJson(e as Map)) - .toList() ?? - []; -} + Map json) => + GetMessagesByIdResponse() + ..duration = json['duration'] as String? + ..messages = (json['messages'] as List?) + ?.map((e) => Message.fromJson(e as Map)) + .toList() ?? + []; UpdateChannelResponse _$UpdateChannelResponseFromJson( - Map json) { - return UpdateChannelResponse() - ..duration = json['duration'] as String? - ..channel = ChannelModel.fromJson(json['channel'] as Map) - ..members = (json['members'] as List?) - ?.map((e) => Member.fromJson(e as Map)) - .toList() - ..message = json['message'] == null - ? null - : Message.fromJson(json['message'] as Map); -} + Map json) => + UpdateChannelResponse() + ..duration = json['duration'] as String? + ..channel = ChannelModel.fromJson(json['channel'] as Map) + ..members = (json['members'] as List?) + ?.map((e) => Member.fromJson(e as Map)) + .toList() + ..message = json['message'] == null + ? null + : Message.fromJson(json['message'] as Map); PartialUpdateChannelResponse _$PartialUpdateChannelResponseFromJson( - Map json) { - return PartialUpdateChannelResponse() - ..duration = json['duration'] as String? - ..channel = ChannelModel.fromJson(json['channel'] as Map) - ..members = (json['members'] as List?) - ?.map((e) => Member.fromJson(e as Map)) - .toList(); -} + Map json) => + PartialUpdateChannelResponse() + ..duration = json['duration'] as String? + ..channel = ChannelModel.fromJson(json['channel'] as Map) + ..members = (json['members'] as List?) + ?.map((e) => Member.fromJson(e as Map)) + .toList(); InviteMembersResponse _$InviteMembersResponseFromJson( - Map json) { - return InviteMembersResponse() - ..duration = json['duration'] as String? - ..channel = ChannelModel.fromJson(json['channel'] as Map) - ..members = (json['members'] as List?) - ?.map((e) => Member.fromJson(e as Map)) - .toList() ?? - [] - ..message = json['message'] == null - ? null - : Message.fromJson(json['message'] as Map); -} + Map json) => + InviteMembersResponse() + ..duration = json['duration'] as String? + ..channel = ChannelModel.fromJson(json['channel'] as Map) + ..members = (json['members'] as List?) + ?.map((e) => Member.fromJson(e as Map)) + .toList() ?? + [] + ..message = json['message'] == null + ? null + : Message.fromJson(json['message'] as Map); RemoveMembersResponse _$RemoveMembersResponseFromJson( - Map json) { - return RemoveMembersResponse() - ..duration = json['duration'] as String? - ..channel = ChannelModel.fromJson(json['channel'] as Map) - ..members = (json['members'] as List?) - ?.map((e) => Member.fromJson(e as Map)) - .toList() ?? - [] - ..message = json['message'] == null - ? null - : Message.fromJson(json['message'] as Map); -} + Map json) => + RemoveMembersResponse() + ..duration = json['duration'] as String? + ..channel = ChannelModel.fromJson(json['channel'] as Map) + ..members = (json['members'] as List?) + ?.map((e) => Member.fromJson(e as Map)) + .toList() ?? + [] + ..message = json['message'] == null + ? null + : Message.fromJson(json['message'] as Map); -SendActionResponse _$SendActionResponseFromJson(Map json) { - return SendActionResponse() - ..duration = json['duration'] as String? - ..message = json['message'] == null - ? null - : Message.fromJson(json['message'] as Map); -} +SendActionResponse _$SendActionResponseFromJson(Map json) => + SendActionResponse() + ..duration = json['duration'] as String? + ..message = json['message'] == null + ? null + : Message.fromJson(json['message'] as Map); -AddMembersResponse _$AddMembersResponseFromJson(Map json) { - return AddMembersResponse() - ..duration = json['duration'] as String? - ..channel = ChannelModel.fromJson(json['channel'] as Map) - ..members = (json['members'] as List?) - ?.map((e) => Member.fromJson(e as Map)) - .toList() ?? - [] - ..message = json['message'] == null - ? null - : Message.fromJson(json['message'] as Map); -} +AddMembersResponse _$AddMembersResponseFromJson(Map json) => + AddMembersResponse() + ..duration = json['duration'] as String? + ..channel = ChannelModel.fromJson(json['channel'] as Map) + ..members = (json['members'] as List?) + ?.map((e) => Member.fromJson(e as Map)) + .toList() ?? + [] + ..message = json['message'] == null + ? null + : Message.fromJson(json['message'] as Map); -AcceptInviteResponse _$AcceptInviteResponseFromJson(Map json) { - return AcceptInviteResponse() - ..duration = json['duration'] as String? - ..channel = ChannelModel.fromJson(json['channel'] as Map) - ..members = (json['members'] as List?) - ?.map((e) => Member.fromJson(e as Map)) - .toList() ?? - [] - ..message = json['message'] == null - ? null - : Message.fromJson(json['message'] as Map); -} +AcceptInviteResponse _$AcceptInviteResponseFromJson( + Map json) => + AcceptInviteResponse() + ..duration = json['duration'] as String? + ..channel = ChannelModel.fromJson(json['channel'] as Map) + ..members = (json['members'] as List?) + ?.map((e) => Member.fromJson(e as Map)) + .toList() ?? + [] + ..message = json['message'] == null + ? null + : Message.fromJson(json['message'] as Map); -RejectInviteResponse _$RejectInviteResponseFromJson(Map json) { - return RejectInviteResponse() - ..duration = json['duration'] as String? - ..channel = ChannelModel.fromJson(json['channel'] as Map) - ..members = (json['members'] as List?) - ?.map((e) => Member.fromJson(e as Map)) - .toList() ?? - [] - ..message = json['message'] == null - ? null - : Message.fromJson(json['message'] as Map); -} +RejectInviteResponse _$RejectInviteResponseFromJson( + Map json) => + RejectInviteResponse() + ..duration = json['duration'] as String? + ..channel = ChannelModel.fromJson(json['channel'] as Map) + ..members = (json['members'] as List?) + ?.map((e) => Member.fromJson(e as Map)) + .toList() ?? + [] + ..message = json['message'] == null + ? null + : Message.fromJson(json['message'] as Map); -EmptyResponse _$EmptyResponseFromJson(Map json) { - return EmptyResponse()..duration = json['duration'] as String?; -} +EmptyResponse _$EmptyResponseFromJson(Map json) => + EmptyResponse()..duration = json['duration'] as String?; -ChannelStateResponse _$ChannelStateResponseFromJson(Map json) { - return ChannelStateResponse() - ..duration = json['duration'] as String? - ..channel = ChannelModel.fromJson(json['channel'] as Map) - ..messages = (json['messages'] as List?) - ?.map((e) => Message.fromJson(e as Map)) - .toList() ?? - [] - ..members = (json['members'] as List?) - ?.map((e) => Member.fromJson(e as Map)) - .toList() ?? - [] - ..watcherCount = json['watcher_count'] as int? ?? 0 - ..read = (json['read'] as List?) - ?.map((e) => Read.fromJson(e as Map)) - .toList() ?? - []; -} +ChannelStateResponse _$ChannelStateResponseFromJson( + Map json) => + ChannelStateResponse() + ..duration = json['duration'] as String? + ..channel = ChannelModel.fromJson(json['channel'] as Map) + ..messages = (json['messages'] as List?) + ?.map((e) => Message.fromJson(e as Map)) + .toList() ?? + [] + ..members = (json['members'] as List?) + ?.map((e) => Member.fromJson(e as Map)) + .toList() ?? + [] + ..watcherCount = json['watcher_count'] as int? ?? 0 + ..read = (json['read'] as List?) + ?.map((e) => Read.fromJson(e as Map)) + .toList() ?? + []; diff --git a/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart b/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart index 9947eefa..37f618f5 100644 --- a/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart +++ b/packages/stream_chat/lib/src/core/http/interceptor/auth_interceptor.dart @@ -38,7 +38,9 @@ class AuthInterceptor extends Interceptor { 'Authorization': token.rawValue, 'stream-auth-type': token.authType.raw, }; - options..queryParameters.addAll(params)..headers.addAll(headers); + options + ..queryParameters.addAll(params) + ..headers.addAll(headers); return handler.next(options); } diff --git a/packages/stream_chat/lib/src/core/http/token.dart b/packages/stream_chat/lib/src/core/http/token.dart index 7d746286..eba49b82 100644 --- a/packages/stream_chat/lib/src/core/http/token.dart +++ b/packages/stream_chat/lib/src/core/http/token.dart @@ -46,12 +46,16 @@ class Token extends Equatable { /// Creates a [Token] instance from the provided [rawValue] if it's valid. factory Token.fromRawValue(String rawValue) { final jwtBody = JsonWebToken.unverified(rawValue); - final userId = jwtBody.claims.getTyped('user_id'); + final userId = jwtBody.claims.getTyped('user_id'); assert( userId != null, 'Invalid `token`, It should contain `user_id`', ); - return Token._(rawValue: rawValue, userId: userId!, authType: AuthType.jwt); + return Token._( + rawValue: rawValue, + userId: userId!.toString(), + authType: AuthType.jwt, + ); } /// The token which can be used during the development. diff --git a/packages/stream_chat/lib/src/core/models/action.dart b/packages/stream_chat/lib/src/core/models/action.dart index 16a307e7..709fbfe0 100644 --- a/packages/stream_chat/lib/src/core/models/action.dart +++ b/packages/stream_chat/lib/src/core/models/action.dart @@ -21,7 +21,6 @@ class Action { final String name; /// The style of the action - @JsonKey(defaultValue: 'default') final String style; /// The test of the action diff --git a/packages/stream_chat/lib/src/core/models/action.g.dart b/packages/stream_chat/lib/src/core/models/action.g.dart index 9ae999ed..2c9148d9 100644 --- a/packages/stream_chat/lib/src/core/models/action.g.dart +++ b/packages/stream_chat/lib/src/core/models/action.g.dart @@ -6,15 +6,13 @@ part of 'action.dart'; // JsonSerializableGenerator // ************************************************************************** -Action _$ActionFromJson(Map json) { - return Action( - name: json['name'] as String, - style: json['style'] as String? ?? 'default', - text: json['text'] as String, - type: json['type'] as String, - value: json['value'] as String?, - ); -} +Action _$ActionFromJson(Map json) => Action( + name: json['name'] as String, + style: json['style'] as String? ?? 'default', + text: json['text'] as String, + type: json['type'] as String, + value: json['value'] as String?, + ); Map _$ActionToJson(Action instance) => { 'name': instance.name, diff --git a/packages/stream_chat/lib/src/core/models/attachment.dart b/packages/stream_chat/lib/src/core/models/attachment.dart index 8973aee2..ca7e0afc 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() @@ -110,10 +117,7 @@ class Attachment extends Equatable { late final UploadState uploadState; /// Map of custom channel extraData - @JsonKey( - includeIfNull: false, - defaultValue: {}, - ) + @JsonKey(includeIfNull: false) final Map extraData; /// The attachment ID. @@ -121,6 +125,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.g.dart b/packages/stream_chat/lib/src/core/models/attachment.g.dart index de129ab6..381eb99c 100644 --- a/packages/stream_chat/lib/src/core/models/attachment.g.dart +++ b/packages/stream_chat/lib/src/core/models/attachment.g.dart @@ -6,39 +6,37 @@ part of 'attachment.dart'; // JsonSerializableGenerator // ************************************************************************** -Attachment _$AttachmentFromJson(Map json) { - return Attachment( - id: json['id'] as String?, - type: json['type'] as String?, - titleLink: json['title_link'] as String?, - title: json['title'] as String?, - thumbUrl: json['thumb_url'] as String?, - text: json['text'] as String?, - pretext: json['pretext'] as String?, - ogScrapeUrl: json['og_scrape_url'] as String?, - imageUrl: json['image_url'] as String?, - footerIcon: json['footer_icon'] as String?, - footer: json['footer'] as String?, - fields: json['fields'], - fallback: json['fallback'] as String?, - color: json['color'] as String?, - authorName: json['author_name'] as String?, - authorLink: json['author_link'] as String?, - authorIcon: json['author_icon'] as String?, - assetUrl: json['asset_url'] as String?, - actions: (json['actions'] as List?) - ?.map((e) => Action.fromJson(e as Map)) - .toList() ?? - [], - extraData: json['extra_data'] as Map? ?? {}, - file: json['file'] == null - ? null - : AttachmentFile.fromJson(json['file'] as Map), - uploadState: json['upload_state'] == null - ? null - : UploadState.fromJson(json['upload_state'] as Map), - ); -} +Attachment _$AttachmentFromJson(Map json) => Attachment( + id: json['id'] as String?, + type: json['type'] as String?, + titleLink: json['title_link'] as String?, + title: json['title'] as String?, + thumbUrl: json['thumb_url'] as String?, + text: json['text'] as String?, + pretext: json['pretext'] as String?, + ogScrapeUrl: json['og_scrape_url'] as String?, + imageUrl: json['image_url'] as String?, + footerIcon: json['footer_icon'] as String?, + footer: json['footer'] as String?, + fields: json['fields'], + fallback: json['fallback'] as String?, + color: json['color'] as String?, + authorName: json['author_name'] as String?, + authorLink: json['author_link'] as String?, + authorIcon: json['author_icon'] as String?, + assetUrl: json['asset_url'] as String?, + actions: (json['actions'] as List?) + ?.map((e) => Action.fromJson(e as Map)) + .toList() ?? + [], + extraData: json['extra_data'] as Map? ?? const {}, + file: json['file'] == null + ? null + : AttachmentFile.fromJson(json['file'] as Map), + uploadState: json['upload_state'] == null + ? null + : UploadState.fromJson(json['upload_state'] as Map), + ); Map _$AttachmentToJson(Attachment instance) { final val = {}; 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/attachment_file.freezed.dart b/packages/stream_chat/lib/src/core/models/attachment_file.freezed.dart index 5d7075c3..4956274a 100644 --- a/packages/stream_chat/lib/src/core/models/attachment_file.freezed.dart +++ b/packages/stream_chat/lib/src/core/models/attachment_file.freezed.dart @@ -1,5 +1,6 @@ +// coverage:ignore-file // GENERATED CODE - DO NOT MODIFY BY HAND -// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target part of 'attachment_file.dart'; @@ -13,7 +14,7 @@ final _privateConstructorUsedError = UnsupportedError( 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); UploadState _$UploadStateFromJson(Map json) { - switch (json['runtimeType'] as String) { + switch (json['runtimeType'] as String?) { case 'preparing': return Preparing.fromJson(json); case 'inProgress': @@ -24,7 +25,8 @@ UploadState _$UploadStateFromJson(Map json) { return Failed.fromJson(json); default: - throw FallThroughError(); + throw CheckedFromJsonException(json, 'runtimeType', 'UploadState', + 'Invalid union type "${json['runtimeType']}"!'); } } @@ -72,6 +74,14 @@ mixin _$UploadState { }) => throw _privateConstructorUsedError; @optionalTypeArgs + TResult? whenOrNull({ + TResult Function()? preparing, + TResult Function(int uploaded, int total)? inProgress, + TResult Function()? success, + TResult Function(String error)? failed, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs TResult maybeWhen({ TResult Function()? preparing, TResult Function(int uploaded, int total)? inProgress, @@ -89,6 +99,14 @@ mixin _$UploadState { }) => throw _privateConstructorUsedError; @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(Preparing value)? preparing, + TResult Function(InProgress value)? inProgress, + TResult Function(Success value)? success, + TResult Function(Failed value)? failed, + }) => + throw _privateConstructorUsedError; + @optionalTypeArgs TResult maybeMap({ TResult Function(Preparing value)? preparing, TResult Function(InProgress value)? inProgress, @@ -138,7 +156,7 @@ class _$Preparing implements Preparing { const _$Preparing(); factory _$Preparing.fromJson(Map json) => - _$_$PreparingFromJson(json); + _$$PreparingFromJson(json); @override String toString() { @@ -164,6 +182,17 @@ class _$Preparing implements Preparing { return preparing(); } + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function()? preparing, + TResult Function(int uploaded, int total)? inProgress, + TResult Function()? success, + TResult Function(String error)? failed, + }) { + return preparing?.call(); + } + @override @optionalTypeArgs TResult maybeWhen({ @@ -190,6 +219,17 @@ class _$Preparing implements Preparing { return preparing(this); } + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(Preparing value)? preparing, + TResult Function(InProgress value)? inProgress, + TResult Function(Success value)? success, + TResult Function(Failed value)? failed, + }) { + return preparing?.call(this); + } + @override @optionalTypeArgs TResult maybeMap({ @@ -207,7 +247,7 @@ class _$Preparing implements Preparing { @override Map toJson() { - return _$_$PreparingToJson(this)..['runtimeType'] = 'preparing'; + return _$$PreparingToJson(this)..['runtimeType'] = 'preparing'; } } @@ -258,7 +298,7 @@ class _$InProgress implements InProgress { const _$InProgress({required this.uploaded, required this.total}); factory _$InProgress.fromJson(Map json) => - _$_$InProgressFromJson(json); + _$$InProgressFromJson(json); @override final int uploaded; @@ -303,6 +343,17 @@ class _$InProgress implements InProgress { return inProgress(uploaded, total); } + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function()? preparing, + TResult Function(int uploaded, int total)? inProgress, + TResult Function()? success, + TResult Function(String error)? failed, + }) { + return inProgress?.call(uploaded, total); + } + @override @optionalTypeArgs TResult maybeWhen({ @@ -329,6 +380,17 @@ class _$InProgress implements InProgress { return inProgress(this); } + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(Preparing value)? preparing, + TResult Function(InProgress value)? inProgress, + TResult Function(Success value)? success, + TResult Function(Failed value)? failed, + }) { + return inProgress?.call(this); + } + @override @optionalTypeArgs TResult maybeMap({ @@ -346,7 +408,7 @@ class _$InProgress implements InProgress { @override Map toJson() { - return _$_$InProgressToJson(this)..['runtimeType'] = 'inProgress'; + return _$$InProgressToJson(this)..['runtimeType'] = 'inProgress'; } } @@ -386,7 +448,7 @@ class _$Success implements Success { const _$Success(); factory _$Success.fromJson(Map json) => - _$_$SuccessFromJson(json); + _$$SuccessFromJson(json); @override String toString() { @@ -412,6 +474,17 @@ class _$Success implements Success { return success(); } + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function()? preparing, + TResult Function(int uploaded, int total)? inProgress, + TResult Function()? success, + TResult Function(String error)? failed, + }) { + return success?.call(); + } + @override @optionalTypeArgs TResult maybeWhen({ @@ -438,6 +511,17 @@ class _$Success implements Success { return success(this); } + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(Preparing value)? preparing, + TResult Function(InProgress value)? inProgress, + TResult Function(Success value)? success, + TResult Function(Failed value)? failed, + }) { + return success?.call(this); + } + @override @optionalTypeArgs TResult maybeMap({ @@ -455,7 +539,7 @@ class _$Success implements Success { @override Map toJson() { - return _$_$SuccessToJson(this)..['runtimeType'] = 'success'; + return _$$SuccessToJson(this)..['runtimeType'] = 'success'; } } @@ -500,7 +584,7 @@ class _$Failed implements Failed { const _$Failed({required this.error}); factory _$Failed.fromJson(Map json) => - _$_$FailedFromJson(json); + _$$FailedFromJson(json); @override final String error; @@ -538,6 +622,17 @@ class _$Failed implements Failed { return failed(error); } + @override + @optionalTypeArgs + TResult? whenOrNull({ + TResult Function()? preparing, + TResult Function(int uploaded, int total)? inProgress, + TResult Function()? success, + TResult Function(String error)? failed, + }) { + return failed?.call(error); + } + @override @optionalTypeArgs TResult maybeWhen({ @@ -564,6 +659,17 @@ class _$Failed implements Failed { return failed(this); } + @override + @optionalTypeArgs + TResult? mapOrNull({ + TResult Function(Preparing value)? preparing, + TResult Function(InProgress value)? inProgress, + TResult Function(Success value)? success, + TResult Function(Failed value)? failed, + }) { + return failed?.call(this); + } + @override @optionalTypeArgs TResult maybeMap({ @@ -581,7 +687,7 @@ class _$Failed implements Failed { @override Map toJson() { - return _$_$FailedToJson(this)..['runtimeType'] = 'failed'; + return _$$FailedToJson(this)..['runtimeType'] = 'failed'; } } diff --git a/packages/stream_chat/lib/src/core/models/attachment_file.g.dart b/packages/stream_chat/lib/src/core/models/attachment_file.g.dart index 5844ae16..f36c269d 100644 --- a/packages/stream_chat/lib/src/core/models/attachment_file.g.dart +++ b/packages/stream_chat/lib/src/core/models/attachment_file.g.dart @@ -6,14 +6,13 @@ part of 'attachment_file.dart'; // JsonSerializableGenerator // ************************************************************************** -AttachmentFile _$AttachmentFileFromJson(Map json) { - return AttachmentFile( - size: json['size'] as int?, - path: json['path'] as String?, - name: json['name'] as String?, - bytes: _fromString(json['bytes'] as String?), - ); -} +AttachmentFile _$AttachmentFileFromJson(Map json) => + AttachmentFile( + size: json['size'] as int?, + path: json['path'] as String?, + name: json['name'] as String?, + bytes: _fromString(json['bytes'] as String?), + ); Map _$AttachmentFileToJson(AttachmentFile instance) => { @@ -23,39 +22,31 @@ Map _$AttachmentFileToJson(AttachmentFile instance) => 'size': instance.size, }; -_$Preparing _$_$PreparingFromJson(Map json) { - return _$Preparing(); -} +_$Preparing _$$PreparingFromJson(Map json) => _$Preparing(); -Map _$_$PreparingToJson(_$Preparing instance) => +Map _$$PreparingToJson(_$Preparing instance) => {}; -_$InProgress _$_$InProgressFromJson(Map json) { - return _$InProgress( - uploaded: json['uploaded'] as int, - total: json['total'] as int, - ); -} +_$InProgress _$$InProgressFromJson(Map json) => _$InProgress( + uploaded: json['uploaded'] as int, + total: json['total'] as int, + ); -Map _$_$InProgressToJson(_$InProgress instance) => +Map _$$InProgressToJson(_$InProgress instance) => { 'uploaded': instance.uploaded, 'total': instance.total, }; -_$Success _$_$SuccessFromJson(Map json) { - return _$Success(); -} +_$Success _$$SuccessFromJson(Map json) => _$Success(); -Map _$_$SuccessToJson(_$Success instance) => +Map _$$SuccessToJson(_$Success instance) => {}; -_$Failed _$_$FailedFromJson(Map json) { - return _$Failed( - error: json['error'] as String, - ); -} +_$Failed _$$FailedFromJson(Map json) => _$Failed( + error: json['error'] as String, + ); -Map _$_$FailedToJson(_$Failed instance) => { +Map _$$FailedToJson(_$Failed instance) => { 'error': instance.error, }; diff --git a/packages/stream_chat/lib/src/core/models/channel_config.dart b/packages/stream_chat/lib/src/core/models/channel_config.dart index 2d5182b3..158738e6 100644 --- a/packages/stream_chat/lib/src/core/models/channel_config.dart +++ b/packages/stream_chat/lib/src/core/models/channel_config.dart @@ -31,15 +31,12 @@ class ChannelConfig { _$ChannelConfigFromJson(json); /// Moderation configuration - @JsonKey(defaultValue: 'flag') final String automod; /// List of available commands - @JsonKey(defaultValue: []) final List commands; /// True if the channel should send connect events - @JsonKey(defaultValue: false) final bool connectEvents; /// Date of channel creation @@ -49,43 +46,33 @@ class ChannelConfig { final DateTime updatedAt; /// Max channel message length - @JsonKey(defaultValue: 0) final int maxMessageLength; /// Duration of message retention - @JsonKey(defaultValue: '') final String messageRetention; /// True if users can be muted - @JsonKey(defaultValue: false) final bool mutes; /// True if reaction are active for this channel - @JsonKey(defaultValue: false) final bool reactions; /// True if readEvents are active for this channel - @JsonKey(defaultValue: false) final bool readEvents; /// True if reply message are active for this channel - @JsonKey(defaultValue: false) final bool replies; /// True if it's possible to perform a search in this channel - @JsonKey(defaultValue: false) final bool search; /// True if typing events should be sent for this channel - @JsonKey(defaultValue: false) final bool typingEvents; /// True if it's possible to upload files to this channel - @JsonKey(defaultValue: false) final bool uploads; /// True if urls appears as attachments - @JsonKey(defaultValue: false) final bool urlEnrichment; /// Serialize to json diff --git a/packages/stream_chat/lib/src/core/models/channel_config.g.dart b/packages/stream_chat/lib/src/core/models/channel_config.g.dart index 723281c0..48b2a739 100644 --- a/packages/stream_chat/lib/src/core/models/channel_config.g.dart +++ b/packages/stream_chat/lib/src/core/models/channel_config.g.dart @@ -6,32 +6,31 @@ part of 'channel_config.dart'; // JsonSerializableGenerator // ************************************************************************** -ChannelConfig _$ChannelConfigFromJson(Map json) { - return ChannelConfig( - automod: json['automod'] as String? ?? 'flag', - commands: (json['commands'] as List?) - ?.map((e) => Command.fromJson(e as Map)) - .toList() ?? - [], - connectEvents: json['connect_events'] as bool? ?? false, - createdAt: json['created_at'] == null - ? null - : DateTime.parse(json['created_at'] as String), - updatedAt: json['updated_at'] == null - ? null - : DateTime.parse(json['updated_at'] as String), - maxMessageLength: json['max_message_length'] as int? ?? 0, - messageRetention: json['message_retention'] as String? ?? '', - mutes: json['mutes'] as bool? ?? false, - reactions: json['reactions'] as bool? ?? false, - readEvents: json['read_events'] as bool? ?? false, - replies: json['replies'] as bool? ?? false, - search: json['search'] as bool? ?? false, - typingEvents: json['typing_events'] as bool? ?? false, - uploads: json['uploads'] as bool? ?? false, - urlEnrichment: json['url_enrichment'] as bool? ?? false, - ); -} +ChannelConfig _$ChannelConfigFromJson(Map json) => + ChannelConfig( + automod: json['automod'] as String? ?? 'flag', + commands: (json['commands'] as List?) + ?.map((e) => Command.fromJson(e as Map)) + .toList() ?? + const [], + connectEvents: json['connect_events'] as bool? ?? false, + createdAt: json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String), + updatedAt: json['updated_at'] == null + ? null + : DateTime.parse(json['updated_at'] as String), + maxMessageLength: json['max_message_length'] as int? ?? 0, + messageRetention: json['message_retention'] as String? ?? '', + mutes: json['mutes'] as bool? ?? false, + reactions: json['reactions'] as bool? ?? false, + readEvents: json['read_events'] as bool? ?? false, + replies: json['replies'] as bool? ?? false, + search: json['search'] as bool? ?? false, + typingEvents: json['typing_events'] as bool? ?? false, + uploads: json['uploads'] as bool? ?? false, + urlEnrichment: json['url_enrichment'] as bool? ?? false, + ); Map _$ChannelConfigToJson(ChannelConfig instance) => { diff --git a/packages/stream_chat/lib/src/core/models/channel_model.dart b/packages/stream_chat/lib/src/core/models/channel_model.dart index a4e6db79..7892cc10 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.dart @@ -1,7 +1,7 @@ import 'package:json_annotation/json_annotation.dart'; import 'package:stream_chat/src/core/models/channel_config.dart'; -import 'package:stream_chat/src/core/util/serializer.dart'; import 'package:stream_chat/src/core/models/user.dart'; +import 'package:stream_chat/src/core/util/serializer.dart'; part 'channel_model.g.dart'; @@ -59,7 +59,7 @@ class ChannelModel { final User? createdBy; /// True if this channel is frozen - @JsonKey(includeIfNull: false, defaultValue: false) + @JsonKey(includeIfNull: false) final bool frozen; /// The date of the last message @@ -79,18 +79,15 @@ class ChannelModel { final DateTime? deletedAt; /// The count of this channel members - @JsonKey(includeIfNull: false, toJson: Serializer.readOnly, defaultValue: 0) + @JsonKey(includeIfNull: false, toJson: Serializer.readOnly) final int memberCount; /// The number of seconds in a cooldown - @JsonKey(includeIfNull: false, defaultValue: 0) + @JsonKey(includeIfNull: false) final int cooldown; /// Map of custom channel extraData - @JsonKey( - includeIfNull: false, - defaultValue: {}, - ) + @JsonKey(includeIfNull: false) final Map extraData; /// The team the channel belongs to diff --git a/packages/stream_chat/lib/src/core/models/channel_model.g.dart b/packages/stream_chat/lib/src/core/models/channel_model.g.dart index d9adf94b..94d9a81a 100644 --- a/packages/stream_chat/lib/src/core/models/channel_model.g.dart +++ b/packages/stream_chat/lib/src/core/models/channel_model.g.dart @@ -6,36 +6,34 @@ part of 'channel_model.dart'; // JsonSerializableGenerator // ************************************************************************** -ChannelModel _$ChannelModelFromJson(Map json) { - return ChannelModel( - id: json['id'] as String?, - type: json['type'] as String?, - cid: json['cid'] as String?, - config: json['config'] == null - ? null - : ChannelConfig.fromJson(json['config'] as Map), - createdBy: json['created_by'] == null - ? null - : User.fromJson(json['created_by'] as Map), - frozen: json['frozen'] as bool? ?? false, - lastMessageAt: json['last_message_at'] == null - ? null - : DateTime.parse(json['last_message_at'] as String), - createdAt: json['created_at'] == null - ? null - : DateTime.parse(json['created_at'] as String), - updatedAt: json['updated_at'] == null - ? null - : DateTime.parse(json['updated_at'] as String), - deletedAt: json['deleted_at'] == null - ? null - : DateTime.parse(json['deleted_at'] as String), - memberCount: json['member_count'] as int? ?? 0, - extraData: json['extra_data'] as Map? ?? {}, - team: json['team'] as String?, - cooldown: json['cooldown'] as int? ?? 0, - ); -} +ChannelModel _$ChannelModelFromJson(Map json) => ChannelModel( + id: json['id'] as String?, + type: json['type'] as String?, + cid: json['cid'] as String?, + config: json['config'] == null + ? null + : ChannelConfig.fromJson(json['config'] as Map), + createdBy: json['created_by'] == null + ? null + : User.fromJson(json['created_by'] as Map), + frozen: json['frozen'] as bool? ?? false, + lastMessageAt: json['last_message_at'] == null + ? null + : DateTime.parse(json['last_message_at'] as String), + createdAt: json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String), + updatedAt: json['updated_at'] == null + ? null + : DateTime.parse(json['updated_at'] as String), + deletedAt: json['deleted_at'] == null + ? null + : DateTime.parse(json['deleted_at'] as String), + memberCount: json['member_count'] as int? ?? 0, + extraData: json['extra_data'] as Map? ?? const {}, + team: json['team'] as String?, + cooldown: json['cooldown'] as int? ?? 0, + ); Map _$ChannelModelToJson(ChannelModel instance) { final val = { diff --git a/packages/stream_chat/lib/src/core/models/channel_state.dart b/packages/stream_chat/lib/src/core/models/channel_state.dart index 998efa98..fb5fc64f 100644 --- a/packages/stream_chat/lib/src/core/models/channel_state.dart +++ b/packages/stream_chat/lib/src/core/models/channel_state.dart @@ -25,26 +25,21 @@ class ChannelState { final ChannelModel? channel; /// A paginated list of channel messages - @JsonKey(defaultValue: []) final List messages; /// A paginated list of channel members - @JsonKey(defaultValue: []) final List members; /// A paginated list of pinned messages - @JsonKey(defaultValue: []) final List pinnedMessages; /// The count of users watching the channel final int? watcherCount; /// A paginated list of users watching the channel - @JsonKey(defaultValue: []) final List watchers; /// The list of channel reads - @JsonKey(defaultValue: []) final List read; /// Create a new instance from a json diff --git a/packages/stream_chat/lib/src/core/models/channel_state.g.dart b/packages/stream_chat/lib/src/core/models/channel_state.g.dart index 39c6373f..0da51768 100644 --- a/packages/stream_chat/lib/src/core/models/channel_state.g.dart +++ b/packages/stream_chat/lib/src/core/models/channel_state.g.dart @@ -6,34 +6,32 @@ part of 'channel_state.dart'; // JsonSerializableGenerator // ************************************************************************** -ChannelState _$ChannelStateFromJson(Map json) { - return ChannelState( - channel: json['channel'] == null - ? null - : ChannelModel.fromJson(json['channel'] as Map), - messages: (json['messages'] as List?) - ?.map((e) => Message.fromJson(e as Map)) - .toList() ?? - [], - members: (json['members'] as List?) - ?.map((e) => Member.fromJson(e as Map)) - .toList() ?? - [], - pinnedMessages: (json['pinned_messages'] as List?) - ?.map((e) => Message.fromJson(e as Map)) - .toList() ?? - [], - watcherCount: json['watcher_count'] as int?, - watchers: (json['watchers'] as List?) - ?.map((e) => User.fromJson(e as Map)) - .toList() ?? - [], - read: (json['read'] as List?) - ?.map((e) => Read.fromJson(e as Map)) - .toList() ?? - [], - ); -} +ChannelState _$ChannelStateFromJson(Map json) => ChannelState( + channel: json['channel'] == null + ? null + : ChannelModel.fromJson(json['channel'] as Map), + messages: (json['messages'] as List?) + ?.map((e) => Message.fromJson(e as Map)) + .toList() ?? + const [], + members: (json['members'] as List?) + ?.map((e) => Member.fromJson(e as Map)) + .toList() ?? + const [], + pinnedMessages: (json['pinned_messages'] as List?) + ?.map((e) => Message.fromJson(e as Map)) + .toList() ?? + const [], + watcherCount: json['watcher_count'] as int?, + watchers: (json['watchers'] as List?) + ?.map((e) => User.fromJson(e as Map)) + .toList() ?? + const [], + read: (json['read'] as List?) + ?.map((e) => Read.fromJson(e as Map)) + .toList() ?? + const [], + ); Map _$ChannelStateToJson(ChannelState instance) => { diff --git a/packages/stream_chat/lib/src/core/models/command.g.dart b/packages/stream_chat/lib/src/core/models/command.g.dart index cf8be971..0fe9d54c 100644 --- a/packages/stream_chat/lib/src/core/models/command.g.dart +++ b/packages/stream_chat/lib/src/core/models/command.g.dart @@ -6,13 +6,11 @@ part of 'command.dart'; // JsonSerializableGenerator // ************************************************************************** -Command _$CommandFromJson(Map json) { - return Command( - name: json['name'] as String, - description: json['description'] as String, - args: json['args'] as String, - ); -} +Command _$CommandFromJson(Map json) => Command( + name: json['name'] as String, + description: json['description'] as String, + args: json['args'] as String, + ); Map _$CommandToJson(Command instance) => { 'name': instance.name, diff --git a/packages/stream_chat/lib/src/core/models/device.g.dart b/packages/stream_chat/lib/src/core/models/device.g.dart index 5fcd9435..4de1f064 100644 --- a/packages/stream_chat/lib/src/core/models/device.g.dart +++ b/packages/stream_chat/lib/src/core/models/device.g.dart @@ -6,12 +6,10 @@ part of 'device.dart'; // JsonSerializableGenerator // ************************************************************************** -Device _$DeviceFromJson(Map json) { - return Device( - id: json['id'] as String, - pushProvider: json['push_provider'] as String, - ); -} +Device _$DeviceFromJson(Map json) => Device( + id: json['id'] as String, + pushProvider: json['push_provider'] as String, + ); Map _$DeviceToJson(Device instance) => { 'id': instance.id, diff --git a/packages/stream_chat/lib/src/core/models/event.dart b/packages/stream_chat/lib/src/core/models/event.dart index 9250176d..aed35696 100644 --- a/packages/stream_chat/lib/src/core/models/event.dart +++ b/packages/stream_chat/lib/src/core/models/event.dart @@ -92,7 +92,6 @@ class Event { final bool isLocal; /// Map of custom channel extraData - @JsonKey(defaultValue: {}) final Map extraData; /// Known top level fields. @@ -184,7 +183,7 @@ class EventChannel extends ChannelModel { DateTime? deletedAt, required int memberCount, Map? extraData, - required int cooldown, + int cooldown = 0, String? team, }) : super( id: id, diff --git a/packages/stream_chat/lib/src/core/models/event.g.dart b/packages/stream_chat/lib/src/core/models/event.g.dart index 93d2e75b..29a4482b 100644 --- a/packages/stream_chat/lib/src/core/models/event.g.dart +++ b/packages/stream_chat/lib/src/core/models/event.g.dart @@ -6,42 +6,40 @@ part of 'event.dart'; // JsonSerializableGenerator // ************************************************************************** -Event _$EventFromJson(Map json) { - return Event( - type: json['type'] as String, - cid: json['cid'] as String?, - connectionId: json['connection_id'] as String?, - createdAt: json['created_at'] == null - ? null - : DateTime.parse(json['created_at'] as String), - me: json['me'] == null - ? null - : OwnUser.fromJson(json['me'] as Map), - user: json['user'] == null - ? null - : User.fromJson(json['user'] as Map), - message: json['message'] == null - ? null - : Message.fromJson(json['message'] as Map), - totalUnreadCount: json['total_unread_count'] as int?, - unreadChannels: json['unread_channels'] as int?, - reaction: json['reaction'] == null - ? null - : Reaction.fromJson(json['reaction'] as Map), - online: json['online'] as bool?, - channel: json['channel'] == null - ? null - : EventChannel.fromJson(json['channel'] as Map), - member: json['member'] == null - ? null - : Member.fromJson(json['member'] as Map), - channelId: json['channel_id'] as String?, - channelType: json['channel_type'] as String?, - parentId: json['parent_id'] as String?, - extraData: json['extra_data'] as Map? ?? {}, - isLocal: json['is_local'] as bool? ?? false, - ); -} +Event _$EventFromJson(Map json) => Event( + type: json['type'] as String? ?? 'local.event', + cid: json['cid'] as String?, + connectionId: json['connection_id'] as String?, + createdAt: json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String), + me: json['me'] == null + ? null + : OwnUser.fromJson(json['me'] as Map), + user: json['user'] == null + ? null + : User.fromJson(json['user'] as Map), + message: json['message'] == null + ? null + : Message.fromJson(json['message'] as Map), + totalUnreadCount: json['total_unread_count'] as int?, + unreadChannels: json['unread_channels'] as int?, + reaction: json['reaction'] == null + ? null + : Reaction.fromJson(json['reaction'] as Map), + online: json['online'] as bool?, + channel: json['channel'] == null + ? null + : EventChannel.fromJson(json['channel'] as Map), + member: json['member'] == null + ? null + : Member.fromJson(json['member'] as Map), + channelId: json['channel_id'] as String?, + channelType: json['channel_type'] as String?, + parentId: json['parent_id'] as String?, + extraData: json['extra_data'] as Map? ?? const {}, + isLocal: json['is_local'] as bool? ?? false, + ); Map _$EventToJson(Event instance) => { 'type': instance.type, @@ -64,30 +62,28 @@ Map _$EventToJson(Event instance) => { 'extra_data': instance.extraData, }; -EventChannel _$EventChannelFromJson(Map json) { - return EventChannel( - members: (json['members'] as List?) - ?.map((e) => Member.fromJson(e as Map)) - .toList(), - id: json['id'] as String?, - type: json['type'] as String?, - cid: json['cid'] as String, - config: ChannelConfig.fromJson(json['config'] as Map), - createdBy: json['created_by'] == null - ? null - : User.fromJson(json['created_by'] as Map), - frozen: json['frozen'] as bool? ?? false, - lastMessageAt: json['last_message_at'] == null - ? null - : DateTime.parse(json['last_message_at'] as String), - createdAt: DateTime.parse(json['created_at'] as String), - updatedAt: DateTime.parse(json['updated_at'] as String), - deletedAt: json['deleted_at'] == null - ? null - : DateTime.parse(json['deleted_at'] as String), - memberCount: json['member_count'] as int? ?? 0, - extraData: json['extra_data'] as Map? ?? {}, - cooldown: json['cooldown'] as int? ?? 0, - team: json['team'] as String?, - ); -} +EventChannel _$EventChannelFromJson(Map json) => EventChannel( + members: (json['members'] as List?) + ?.map((e) => Member.fromJson(e as Map)) + .toList(), + id: json['id'] as String?, + type: json['type'] as String?, + cid: json['cid'] as String, + config: ChannelConfig.fromJson(json['config'] as Map), + createdBy: json['created_by'] == null + ? null + : User.fromJson(json['created_by'] as Map), + frozen: json['frozen'] as bool? ?? false, + lastMessageAt: json['last_message_at'] == null + ? null + : DateTime.parse(json['last_message_at'] as String), + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), + deletedAt: json['deleted_at'] == null + ? null + : DateTime.parse(json['deleted_at'] as String), + memberCount: json['member_count'] as int, + extraData: json['extra_data'] as Map?, + cooldown: json['cooldown'] as int? ?? 0, + team: json['team'] as String?, + ); diff --git a/packages/stream_chat/lib/src/core/models/filter.dart b/packages/stream_chat/lib/src/core/models/filter.dart index d4536eab..108efcb2 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) => @@ -161,6 +167,9 @@ class Filter extends Equatable { factory Filter.exists(String key, {bool exists = true}) => Filter._(operator: FilterOperator.exists, key: key, value: exists); + /// Matches values that don't exist. + factory Filter.notExists(String key) => Filter.exists(key, exists: false); + /// Matches any list that contains the specified values factory Filter.contains(String key, Object value) => Filter._(operator: FilterOperator.contains, key: key, value: value); @@ -172,9 +181,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/lib/src/core/models/member.dart b/packages/stream_chat/lib/src/core/models/member.dart index 5c9bc95c..0dae7c04 100644 --- a/packages/stream_chat/lib/src/core/models/member.dart +++ b/packages/stream_chat/lib/src/core/models/member.dart @@ -42,7 +42,6 @@ class Member extends Equatable { final DateTime? inviteRejectedAt; /// True if the user has been invited to the channel - @JsonKey(defaultValue: false) final bool invited; /// The role of the user in the channel @@ -52,15 +51,12 @@ class Member extends Equatable { final String? userId; /// True if the user is a moderator of the channel - @JsonKey(defaultValue: false) final bool isModerator; /// True if the member is banned from the channel - @JsonKey(defaultValue: false) final bool banned; /// True if the member is shadow banned from the channel - @JsonKey(defaultValue: false) final bool shadowBanned; /// The date of creation diff --git a/packages/stream_chat/lib/src/core/models/member.g.dart b/packages/stream_chat/lib/src/core/models/member.g.dart index a75b458d..0e711b1a 100644 --- a/packages/stream_chat/lib/src/core/models/member.g.dart +++ b/packages/stream_chat/lib/src/core/models/member.g.dart @@ -6,31 +6,29 @@ part of 'member.dart'; // JsonSerializableGenerator // ************************************************************************** -Member _$MemberFromJson(Map json) { - return Member( - user: json['user'] == null - ? null - : User.fromJson(json['user'] as Map), - inviteAcceptedAt: json['invite_accepted_at'] == null - ? null - : DateTime.parse(json['invite_accepted_at'] as String), - inviteRejectedAt: json['invite_rejected_at'] == null - ? null - : DateTime.parse(json['invite_rejected_at'] as String), - invited: json['invited'] as bool? ?? false, - role: json['role'] as String?, - userId: json['user_id'] as String?, - isModerator: json['is_moderator'] as bool? ?? false, - createdAt: json['created_at'] == null - ? null - : DateTime.parse(json['created_at'] as String), - updatedAt: json['updated_at'] == null - ? null - : DateTime.parse(json['updated_at'] as String), - banned: json['banned'] as bool? ?? false, - shadowBanned: json['shadow_banned'] as bool? ?? false, - ); -} +Member _$MemberFromJson(Map json) => Member( + user: json['user'] == null + ? null + : User.fromJson(json['user'] as Map), + inviteAcceptedAt: json['invite_accepted_at'] == null + ? null + : DateTime.parse(json['invite_accepted_at'] as String), + inviteRejectedAt: json['invite_rejected_at'] == null + ? null + : DateTime.parse(json['invite_rejected_at'] as String), + invited: json['invited'] as bool? ?? false, + role: json['role'] as String?, + userId: json['user_id'] as String?, + isModerator: json['is_moderator'] as bool? ?? false, + createdAt: json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String), + updatedAt: json['updated_at'] == null + ? null + : DateTime.parse(json['updated_at'] as String), + banned: json['banned'] as bool? ?? false, + shadowBanned: json['shadow_banned'] as bool? ?? false, + ); Map _$MemberToJson(Member instance) => { 'user': instance.user?.toJson(), diff --git a/packages/stream_chat/lib/src/core/models/message.dart b/packages/stream_chat/lib/src/core/models/message.dart index 6ad8721c..490bd981 100644 --- a/packages/stream_chat/lib/src/core/models/message.dart +++ b/packages/stream_chat/lib/src/core/models/message.dart @@ -98,23 +98,16 @@ class Message extends Equatable { @JsonKey( includeIfNull: false, toJson: Serializer.readOnly, - defaultValue: 'regular', ) final String type; /// The list of attachments, either provided by the user or generated from a /// command or as a result of URL scraping. - @JsonKey( - includeIfNull: false, - defaultValue: [], - ) + @JsonKey(includeIfNull: false) final List attachments; /// The list of user mentioned in the message - @JsonKey( - toJson: User.toIds, - defaultValue: [], - ) + @JsonKey(toJson: User.toIds) final List mentionedUsers; /// A map describing the count of number of every reaction @@ -155,14 +148,12 @@ class Message extends Equatable { final bool? showInChannel; /// If true the message is silent - @JsonKey(defaultValue: false) final bool silent; /// If true the message is shadowed @JsonKey( includeIfNull: false, toJson: Serializer.readOnly, - defaultValue: false, ) final bool shadowed; @@ -183,7 +174,6 @@ class Message extends Equatable { final User? user; /// If true the message is pinned - @JsonKey(defaultValue: false) final bool pinned; /// Reserved field indicating when the message was pinned @@ -200,10 +190,7 @@ class Message extends Equatable { final User? pinnedBy; /// Message custom extraData - @JsonKey( - includeIfNull: false, - defaultValue: {}, - ) + @JsonKey(includeIfNull: false) final Map extraData; /// True if the message is a system info diff --git a/packages/stream_chat/lib/src/core/models/message.g.dart b/packages/stream_chat/lib/src/core/models/message.g.dart index 25254e2c..b3b58c58 100644 --- a/packages/stream_chat/lib/src/core/models/message.g.dart +++ b/packages/stream_chat/lib/src/core/models/message.g.dart @@ -6,72 +6,70 @@ part of 'message.dart'; // JsonSerializableGenerator // ************************************************************************** -Message _$MessageFromJson(Map json) { - return Message( - id: json['id'] as String?, - text: json['text'] as String?, - type: json['type'] as String? ?? 'regular', - attachments: (json['attachments'] as List?) - ?.map((e) => Attachment.fromJson(e as Map)) - .toList() ?? - [], - mentionedUsers: (json['mentioned_users'] as List?) - ?.map((e) => User.fromJson(e as Map)) - .toList() ?? - [], - silent: json['silent'] as bool? ?? false, - shadowed: json['shadowed'] as bool? ?? false, - reactionCounts: (json['reaction_counts'] as Map?)?.map( - (k, e) => MapEntry(k, e as int), - ), - reactionScores: (json['reaction_scores'] as Map?)?.map( - (k, e) => MapEntry(k, e as int), - ), - latestReactions: (json['latest_reactions'] as List?) - ?.map((e) => Reaction.fromJson(e as Map)) - .toList(), - ownReactions: (json['own_reactions'] as List?) - ?.map((e) => Reaction.fromJson(e as Map)) - .toList(), - parentId: json['parent_id'] as String?, - quotedMessage: json['quoted_message'] == null - ? null - : Message.fromJson(json['quoted_message'] as Map), - quotedMessageId: json['quoted_message_id'] as String?, - replyCount: json['reply_count'] as int?, - threadParticipants: (json['thread_participants'] as List?) - ?.map((e) => User.fromJson(e as Map)) - .toList(), - showInChannel: json['show_in_channel'] as bool?, - command: json['command'] as String?, - createdAt: json['created_at'] == null - ? null - : DateTime.parse(json['created_at'] as String), - updatedAt: json['updated_at'] == null - ? null - : DateTime.parse(json['updated_at'] as String), - user: json['user'] == null - ? null - : User.fromJson(json['user'] as Map), - pinned: json['pinned'] as bool? ?? false, - pinnedAt: json['pinned_at'] == null - ? null - : DateTime.parse(json['pinned_at'] as String), - pinExpires: json['pin_expires'] == null - ? null - : DateTime.parse(json['pin_expires'] as String), - pinnedBy: json['pinned_by'] == null - ? null - : User.fromJson(json['pinned_by'] as Map), - extraData: json['extra_data'] as Map? ?? {}, - deletedAt: json['deleted_at'] == null - ? null - : DateTime.parse(json['deleted_at'] as String), - i18n: (json['i18n'] as Map?)?.map( - (k, e) => MapEntry(k, e as String), - ), - ); -} +Message _$MessageFromJson(Map json) => Message( + id: json['id'] as String?, + text: json['text'] as String?, + type: json['type'] as String? ?? 'regular', + attachments: (json['attachments'] as List?) + ?.map((e) => Attachment.fromJson(e as Map)) + .toList() ?? + const [], + mentionedUsers: (json['mentioned_users'] as List?) + ?.map((e) => User.fromJson(e as Map)) + .toList() ?? + const [], + silent: json['silent'] as bool? ?? false, + shadowed: json['shadowed'] as bool? ?? false, + reactionCounts: (json['reaction_counts'] as Map?)?.map( + (k, e) => MapEntry(k, e as int), + ), + reactionScores: (json['reaction_scores'] as Map?)?.map( + (k, e) => MapEntry(k, e as int), + ), + latestReactions: (json['latest_reactions'] as List?) + ?.map((e) => Reaction.fromJson(e as Map)) + .toList(), + ownReactions: (json['own_reactions'] as List?) + ?.map((e) => Reaction.fromJson(e as Map)) + .toList(), + parentId: json['parent_id'] as String?, + quotedMessage: json['quoted_message'] == null + ? null + : Message.fromJson(json['quoted_message'] as Map), + quotedMessageId: json['quoted_message_id'] as String?, + replyCount: json['reply_count'] as int? ?? 0, + threadParticipants: (json['thread_participants'] as List?) + ?.map((e) => User.fromJson(e as Map)) + .toList(), + showInChannel: json['show_in_channel'] as bool?, + command: json['command'] as String?, + createdAt: json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String), + updatedAt: json['updated_at'] == null + ? null + : DateTime.parse(json['updated_at'] as String), + user: json['user'] == null + ? null + : User.fromJson(json['user'] as Map), + pinned: json['pinned'] as bool? ?? false, + pinnedAt: json['pinned_at'] == null + ? null + : DateTime.parse(json['pinned_at'] as String), + pinExpires: json['pin_expires'] == null + ? null + : DateTime.parse(json['pin_expires'] as String), + pinnedBy: json['pinned_by'] == null + ? null + : User.fromJson(json['pinned_by'] as Map), + extraData: json['extra_data'] as Map? ?? const {}, + deletedAt: json['deleted_at'] == null + ? null + : DateTime.parse(json['deleted_at'] as String), + i18n: (json['i18n'] as Map?)?.map( + (k, e) => MapEntry(k, e as String), + ), + ); Map _$MessageToJson(Message instance) { final val = { diff --git a/packages/stream_chat/lib/src/core/models/mute.g.dart b/packages/stream_chat/lib/src/core/models/mute.g.dart index b847a109..f57fd312 100644 --- a/packages/stream_chat/lib/src/core/models/mute.g.dart +++ b/packages/stream_chat/lib/src/core/models/mute.g.dart @@ -6,11 +6,9 @@ part of 'mute.dart'; // JsonSerializableGenerator // ************************************************************************** -Mute _$MuteFromJson(Map json) { - return Mute( - user: User.fromJson(json['user'] as Map), - channel: ChannelModel.fromJson(json['channel'] as Map), - createdAt: DateTime.parse(json['created_at'] as String), - updatedAt: DateTime.parse(json['updated_at'] as String), - ); -} +Mute _$MuteFromJson(Map json) => Mute( + user: User.fromJson(json['user'] as Map), + channel: ChannelModel.fromJson(json['channel'] as Map), + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), + ); diff --git a/packages/stream_chat/lib/src/core/models/own_user.dart b/packages/stream_chat/lib/src/core/models/own_user.dart index a65052f4..f65735ab 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.dart @@ -17,7 +17,7 @@ class OwnUser extends User { this.devices = const [], this.mutes = const [], this.totalUnreadCount = 0, - this.unreadChannels, + this.unreadChannels = 0, this.channelMutes = const [], required String id, String? role, @@ -54,8 +54,6 @@ class OwnUser extends User { factory OwnUser.fromUser(User user) => OwnUser( id: user.id, role: user.role, - name: user.name, - image: user.image, createdAt: user.createdAt, updatedAt: user.updatedAt, lastActive: user.lastActive, @@ -116,8 +114,6 @@ class OwnUser extends User { return copyWith( id: other.id, role: other.role, - name: other.name, - image: other.image, banned: other.banned, channelMutes: other.channelMutes, createdAt: other.createdAt, @@ -135,24 +131,24 @@ class OwnUser extends User { } /// List of user devices. - @JsonKey(includeIfNull: false, defaultValue: []) + @JsonKey(includeIfNull: false) final List devices; /// List of users muted by the user. - @JsonKey(includeIfNull: false, defaultValue: []) + @JsonKey(includeIfNull: false) final List mutes; /// List of users muted by the user. - @JsonKey(includeIfNull: false, defaultValue: []) + @JsonKey(includeIfNull: false) final List channelMutes; /// Total unread messages by the user. - @JsonKey(includeIfNull: false, defaultValue: 0) + @JsonKey(includeIfNull: false) final int totalUnreadCount; /// Total unread channels by the user. @JsonKey(includeIfNull: false) - final int? unreadChannels; + final int unreadChannels; /// Known top level fields. /// diff --git a/packages/stream_chat/lib/src/core/models/own_user.g.dart b/packages/stream_chat/lib/src/core/models/own_user.g.dart index ca4acdea..41b6e2f5 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.g.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.g.dart @@ -6,39 +6,37 @@ part of 'own_user.dart'; // JsonSerializableGenerator // ************************************************************************** -OwnUser _$OwnUserFromJson(Map json) { - return OwnUser( - devices: (json['devices'] as List?) - ?.map((e) => Device.fromJson(e as Map)) - .toList() ?? - [], - mutes: (json['mutes'] as List?) - ?.map((e) => Mute.fromJson(e as Map)) - .toList() ?? - [], - totalUnreadCount: json['total_unread_count'] as int? ?? 0, - unreadChannels: json['unread_channels'] as int?, - channelMutes: (json['channel_mutes'] as List?) - ?.map((e) => Mute.fromJson(e as Map)) - .toList() ?? - [], - id: json['id'] as String, - role: json['role'] as String?, - createdAt: json['created_at'] == null - ? null - : DateTime.parse(json['created_at'] as String), - updatedAt: json['updated_at'] == null - ? null - : DateTime.parse(json['updated_at'] as String), - lastActive: json['last_active'] == null - ? null - : DateTime.parse(json['last_active'] as String), - online: json['online'] as bool? ?? false, - extraData: json['extra_data'] as Map? ?? {}, - banned: json['banned'] as bool? ?? false, - teams: - (json['teams'] as List?)?.map((e) => e as String).toList() ?? - [], - language: json['language'] as String?, - ); -} +OwnUser _$OwnUserFromJson(Map json) => OwnUser( + devices: (json['devices'] as List?) + ?.map((e) => Device.fromJson(e as Map)) + .toList() ?? + const [], + mutes: (json['mutes'] as List?) + ?.map((e) => Mute.fromJson(e as Map)) + .toList() ?? + const [], + totalUnreadCount: json['total_unread_count'] as int? ?? 0, + unreadChannels: json['unread_channels'] as int? ?? 0, + channelMutes: (json['channel_mutes'] as List?) + ?.map((e) => Mute.fromJson(e as Map)) + .toList() ?? + const [], + id: json['id'] as String, + role: json['role'] as String?, + createdAt: json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String), + updatedAt: json['updated_at'] == null + ? null + : DateTime.parse(json['updated_at'] as String), + lastActive: json['last_active'] == null + ? null + : DateTime.parse(json['last_active'] as String), + online: json['online'] as bool? ?? false, + extraData: json['extra_data'] as Map? ?? const {}, + banned: json['banned'] as bool? ?? false, + teams: + (json['teams'] as List?)?.map((e) => e as String).toList() ?? + const [], + language: json['language'] as String?, + ); diff --git a/packages/stream_chat/lib/src/core/models/reaction.dart b/packages/stream_chat/lib/src/core/models/reaction.dart index 57d0b48d..f16fb09f 100644 --- a/packages/stream_chat/lib/src/core/models/reaction.dart +++ b/packages/stream_chat/lib/src/core/models/reaction.dart @@ -1,6 +1,6 @@ import 'package:json_annotation/json_annotation.dart'; -import 'package:stream_chat/src/core/util/serializer.dart'; import 'package:stream_chat/src/core/models/user.dart'; +import 'package:stream_chat/src/core/util/serializer.dart'; part 'reaction.g.dart'; @@ -41,7 +41,6 @@ class Reaction { final User? user; /// The score of the reaction (ie. number of reactions sent) - @JsonKey(defaultValue: 0) final int score; /// The userId that sent the reaction @@ -49,10 +48,7 @@ class Reaction { final String? userId; /// Reaction custom extraData - @JsonKey( - includeIfNull: false, - defaultValue: {}, - ) + @JsonKey(includeIfNull: false) final Map extraData; /// Map of custom user extraData diff --git a/packages/stream_chat/lib/src/core/models/reaction.g.dart b/packages/stream_chat/lib/src/core/models/reaction.g.dart index 3e92150d..3554106d 100644 --- a/packages/stream_chat/lib/src/core/models/reaction.g.dart +++ b/packages/stream_chat/lib/src/core/models/reaction.g.dart @@ -6,21 +6,19 @@ part of 'reaction.dart'; // JsonSerializableGenerator // ************************************************************************** -Reaction _$ReactionFromJson(Map json) { - return Reaction( - messageId: json['message_id'] as String?, - createdAt: json['created_at'] == null - ? null - : DateTime.parse(json['created_at'] as String), - type: json['type'] as String, - user: json['user'] == null - ? null - : User.fromJson(json['user'] as Map), - userId: json['user_id'] as String?, - score: json['score'] as int? ?? 0, - extraData: json['extra_data'] as Map? ?? {}, - ); -} +Reaction _$ReactionFromJson(Map json) => Reaction( + messageId: json['message_id'] as String?, + createdAt: json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String), + type: json['type'] as String, + user: json['user'] == null + ? null + : User.fromJson(json['user'] as Map), + userId: json['user_id'] as String?, + score: json['score'] as int? ?? 0, + extraData: json['extra_data'] as Map? ?? const {}, + ); Map _$ReactionToJson(Reaction instance) { final val = { diff --git a/packages/stream_chat/lib/src/core/models/read.dart b/packages/stream_chat/lib/src/core/models/read.dart index 55912762..812fc6f7 100644 --- a/packages/stream_chat/lib/src/core/models/read.dart +++ b/packages/stream_chat/lib/src/core/models/read.dart @@ -23,7 +23,6 @@ class Read { final User user; /// Number of unread messages - @JsonKey(defaultValue: 0) final int unreadMessages; /// Serialize to json diff --git a/packages/stream_chat/lib/src/core/models/read.g.dart b/packages/stream_chat/lib/src/core/models/read.g.dart index 93c832d8..f44a21d5 100644 --- a/packages/stream_chat/lib/src/core/models/read.g.dart +++ b/packages/stream_chat/lib/src/core/models/read.g.dart @@ -6,13 +6,11 @@ part of 'read.dart'; // JsonSerializableGenerator // ************************************************************************** -Read _$ReadFromJson(Map json) { - return Read( - lastRead: DateTime.parse(json['last_read'] as String), - user: User.fromJson(json['user'] as Map), - unreadMessages: json['unread_messages'] as int? ?? 0, - ); -} +Read _$ReadFromJson(Map json) => Read( + lastRead: DateTime.parse(json['last_read'] as String), + user: User.fromJson(json['user'] as Map), + unreadMessages: json['unread_messages'] as int? ?? 0, + ); Map _$ReadToJson(Read instance) => { 'last_read': instance.lastRead.toIso8601String(), diff --git a/packages/stream_chat/lib/src/core/models/user.dart b/packages/stream_chat/lib/src/core/models/user.dart index 5a5ef8e5..44df4399 100644 --- a/packages/stream_chat/lib/src/core/models/user.dart +++ b/packages/stream_chat/lib/src/core/models/user.dart @@ -100,7 +100,6 @@ class User extends Equatable { @JsonKey( includeIfNull: false, toJson: Serializer.readOnly, - defaultValue: [], ) final List teams; @@ -118,19 +117,20 @@ class User extends Equatable { /// True if user is online. @JsonKey( - includeIfNull: false, toJson: Serializer.readOnly, defaultValue: false) + includeIfNull: false, + toJson: Serializer.readOnly, + ) final bool online; /// True if user is banned from the chat. @JsonKey( - includeIfNull: false, toJson: Serializer.readOnly, defaultValue: false) + includeIfNull: false, + toJson: Serializer.readOnly, + ) final bool banned; /// Map of custom user extraData. - @JsonKey( - includeIfNull: false, - defaultValue: {}, - ) + @JsonKey(includeIfNull: false) final Map extraData; /// The language this user prefers. diff --git a/packages/stream_chat/lib/src/core/models/user.g.dart b/packages/stream_chat/lib/src/core/models/user.g.dart index ab2f04d5..82c799a0 100644 --- a/packages/stream_chat/lib/src/core/models/user.g.dart +++ b/packages/stream_chat/lib/src/core/models/user.g.dart @@ -6,28 +6,26 @@ part of 'user.dart'; // JsonSerializableGenerator // ************************************************************************** -User _$UserFromJson(Map json) { - return User( - id: json['id'] as String, - role: json['role'] as String?, - createdAt: json['created_at'] == null - ? null - : DateTime.parse(json['created_at'] as String), - updatedAt: json['updated_at'] == null - ? null - : DateTime.parse(json['updated_at'] as String), - lastActive: json['last_active'] == null - ? null - : DateTime.parse(json['last_active'] as String), - extraData: json['extra_data'] as Map? ?? {}, - online: json['online'] as bool? ?? false, - banned: json['banned'] as bool? ?? false, - teams: - (json['teams'] as List?)?.map((e) => e as String).toList() ?? - [], - language: json['language'] as String?, - ); -} +User _$UserFromJson(Map json) => User( + id: json['id'] as String, + role: json['role'] as String?, + createdAt: json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String), + updatedAt: json['updated_at'] == null + ? null + : DateTime.parse(json['updated_at'] as String), + lastActive: json['last_active'] == null + ? null + : DateTime.parse(json['last_active'] as String), + extraData: json['extra_data'] as Map? ?? const {}, + online: json['online'] as bool? ?? false, + banned: json['banned'] as bool? ?? false, + teams: + (json['teams'] as List?)?.map((e) => e as String).toList() ?? + const [], + language: json['language'] as String?, + ); Map _$UserToJson(User instance) { final val = { diff --git a/packages/stream_chat/lib/src/ws/websocket.dart b/packages/stream_chat/lib/src/ws/websocket.dart index b26d811e..05382fb7 100644 --- a/packages/stream_chat/lib/src/ws/websocket.dart +++ b/packages/stream_chat/lib/src/ws/websocket.dart @@ -6,15 +6,15 @@ import 'package:logging/logging.dart'; import 'package:meta/meta.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/core/error/error.dart'; -import 'package:stream_chat/src/ws/connection_status.dart'; import 'package:stream_chat/src/core/http/token.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; import 'package:stream_chat/src/core/models/event.dart'; import 'package:stream_chat/src/core/models/user.dart'; import 'package:stream_chat/src/event_type.dart'; +import 'package:stream_chat/src/ws/connection_status.dart'; import 'package:stream_chat/src/ws/timer_helper.dart'; -import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/status.dart' as status; +import 'package:web_socket_channel/web_socket_channel.dart'; /// Typedef which exposes an [Event] as the only parameter. typedef EventHandler = void Function(Event); diff --git a/packages/stream_chat/lib/version.dart b/packages/stream_chat/lib/version.dart index b208e15c..680b2d9f 100644 --- a/packages/stream_chat/lib/version.dart +++ b/packages/stream_chat/lib/version.dart @@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart'; /// Current package version /// Used in [StreamChatClient] to build the `x-stream-client` header // ignore: constant_identifier_names -const PACKAGE_VERSION = '2.2.1'; +const PACKAGE_VERSION = '3.1.1'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index a07606ba..0638bc51 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat homepage: https://getstream.io/ description: The official Dart client for Stream Chat, a service for building chat applications. -version: 2.2.1 +version: 3.1.1 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -28,6 +28,6 @@ dependencies: dev_dependencies: build_runner: ^2.0.1 freezed: ^0.14.1+3 - json_serializable: ^4.1.0 + json_serializable: ^5.0.2 mocktail: ^0.1.1 - test: ^1.17.7 \ No newline at end of file + test: ^1.18.2 diff --git a/packages/stream_chat/test/src/client/channel_test.dart b/packages/stream_chat/test/src/client/channel_test.dart index 08ebd091..6ec26d27 100644 --- a/packages/stream_chat/test/src/client/channel_test.dart +++ b/packages/stream_chat/test/src/client/channel_test.dart @@ -334,20 +334,87 @@ void main() { expectLater( // skipping first seed message list -> [] messages channel.state?.messagesStream.skip(1), - emitsInOrder([ + emitsInOrder( [ - isSameMessageAs( - message.copyWith(status: MessageSendingStatus.sending), - matchSendingStatus: true, - ), + // preparing attachments to upload + [ + isSameMessageAs( + message.copyWith( + status: MessageSendingStatus.sending, + attachments: [ + ...attachments.map((it) => it.copyWith( + uploadState: const UploadState.preparing())) + ], + ), + matchSendingStatus: true, + matchAttachments: true, + matchAttachmentsUploadState: true, + ), + ], + // 0th attachment is successfully uploaded + [ + isSameMessageAs( + message.copyWith( + status: MessageSendingStatus.sending, + attachments: [...attachments]..[0] = + attachments[0].copyWith( + uploadState: const UploadState.success(), + ), + ), + matchSendingStatus: true, + matchAttachments: true, + matchAttachmentsUploadState: true, + ), + ], + // 0th and 1st attachment is successfully uploaded + [ + isSameMessageAs( + message.copyWith( + status: MessageSendingStatus.sending, + attachments: [...attachments] + ..[0] = attachments[0].copyWith( + uploadState: const UploadState.success(), + ) + ..[1] = attachments[1].copyWith( + uploadState: const UploadState.success(), + ), + ), + matchSendingStatus: true, + matchAttachments: true, + matchAttachmentsUploadState: true, + ), + ], + // all the attachments are successfully uploaded + [ + isSameMessageAs( + message.copyWith( + status: MessageSendingStatus.sending, + attachments: [ + ...attachments.map((it) => + it.copyWith(uploadState: const UploadState.success())) + ], + ), + matchSendingStatus: true, + matchAttachments: true, + matchAttachmentsUploadState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + status: MessageSendingStatus.sent, + attachments: [ + ...attachments.map((it) => + it.copyWith(uploadState: const UploadState.success())) + ], + ), + matchSendingStatus: true, + matchAttachments: true, + matchAttachmentsUploadState: true, + ), + ], ], - [ - isSameMessageAs( - message.copyWith(status: MessageSendingStatus.sent), - matchSendingStatus: true, - ), - ], - ]), + ), ); final res = await channel.sendMessage(message); @@ -472,20 +539,87 @@ void main() { expectLater( // skipping first seed message list -> [] messages channel.state?.messagesStream.skip(1), - emitsInOrder([ + emitsInOrder( [ - isSameMessageAs( - message.copyWith(status: MessageSendingStatus.updating), - matchSendingStatus: true, - ), + // preparing attachments to upload + [ + isSameMessageAs( + message.copyWith( + status: MessageSendingStatus.updating, + attachments: [ + ...attachments.map((it) => it.copyWith( + uploadState: const UploadState.preparing())) + ], + ), + matchSendingStatus: true, + matchAttachments: true, + matchAttachmentsUploadState: true, + ), + ], + // 0th attachment is successfully uploaded + [ + isSameMessageAs( + message.copyWith( + status: MessageSendingStatus.updating, + attachments: [...attachments]..[0] = + attachments[0].copyWith( + uploadState: const UploadState.success(), + ), + ), + matchSendingStatus: true, + matchAttachments: true, + matchAttachmentsUploadState: true, + ), + ], + // 0th and 1st attachment is successfully uploaded + [ + isSameMessageAs( + message.copyWith( + status: MessageSendingStatus.updating, + attachments: [...attachments] + ..[0] = attachments[0].copyWith( + uploadState: const UploadState.success(), + ) + ..[1] = attachments[1].copyWith( + uploadState: const UploadState.success(), + ), + ), + matchSendingStatus: true, + matchAttachments: true, + matchAttachmentsUploadState: true, + ), + ], + // all the attachments are successfully uploaded + [ + isSameMessageAs( + message.copyWith( + status: MessageSendingStatus.updating, + attachments: [ + ...attachments.map((it) => + it.copyWith(uploadState: const UploadState.success())) + ], + ), + matchSendingStatus: true, + matchAttachments: true, + matchAttachmentsUploadState: true, + ), + ], + [ + isSameMessageAs( + message.copyWith( + status: MessageSendingStatus.sent, + attachments: [ + ...attachments.map((it) => + it.copyWith(uploadState: const UploadState.success())) + ], + ), + matchSendingStatus: true, + matchAttachments: true, + matchAttachmentsUploadState: true, + ), + ], ], - [ - isSameMessageAs( - message.copyWith(status: MessageSendingStatus.sent), - matchSendingStatus: true, - ), - ], - ]), + ), ); final res = await channel.updateMessage(message); diff --git a/packages/stream_chat/test/src/client/client_test.dart b/packages/stream_chat/test/src/client/client_test.dart index a1a3fc96..b59faa38 100644 --- a/packages/stream_chat/test/src/client/client_test.dart +++ b/packages/stream_chat/test/src/client/client_test.dart @@ -2313,5 +2313,27 @@ void main() { )).called(1); verifyNoMoreInteractions(api.message); }); + + test( + '''setting the `currentUser` should also compute and update the unreadCounts''', + () { + final state = client.state; + final initialUser = OwnUser.fromUser(user); + + expect(state.currentUser, initialUser); + expect(state.totalUnreadCount, 0); + expect(state.unreadChannels, 0); + + final updateUser = initialUser.copyWith( + totalUnreadCount: 33, + unreadChannels: 33, + ); + state.currentUser = updateUser; + + expect(state.currentUser, updateUser); + expect(state.totalUnreadCount, 33); + expect(state.unreadChannels, 33); + }, + ); }); } diff --git a/packages/stream_chat/test/src/core/api/general_api_test.dart b/packages/stream_chat/test/src/core/api/general_api_test.dart index afb3f6aa..570d7c77 100644 --- a/packages/stream_chat/test/src/core/api/general_api_test.dart +++ b/packages/stream_chat/test/src/core/api/general_api_test.dart @@ -86,6 +86,24 @@ void main() { }, ); + test( + 'should throw if `pagination.offset` and `sort` both are provided', + () async { + final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); + const sort = [SortOption('test-field')]; + const pagination = PaginationParams(offset: 10); + try { + await generalApi.searchMessages( + filter, + sort: sort, + pagination: pagination, + ); + } catch (e) { + expect(e, isA()); + } + }, + ); + test('should run successfully with `query`', () async { final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); const query = 'test-query'; diff --git a/packages/stream_chat/test/src/core/api/requests_test.dart b/packages/stream_chat/test/src/core/api/requests_test.dart index 76253403..876a3f61 100644 --- a/packages/stream_chat/test/src/core/api/requests_test.dart +++ b/packages/stream_chat/test/src/core/api/requests_test.dart @@ -9,11 +9,23 @@ void main() { expect(j, {'field': 'name', 'direction': -1}); }); - test('PaginationParams', () { - const option = PaginationParams(); - final j = option.toJson(); - expect(j, containsPair('limit', 10)); - expect(j, containsPair('offset', 0)); + group('PaginationParams', () { + test('default', () { + const option = PaginationParams(); + final j = option.toJson(); + expect(j, containsPair('limit', 10)); + }); + + test( + 'should throw if non-zero `offset` and `next` both are provided', + () { + try { + PaginationParams(offset: 10, next: 'next-message-id'); + } catch (e) { + expect(e, isA()); + } + }, + ); }); }); } diff --git a/packages/stream_chat/test/src/core/api/responses_test.dart b/packages/stream_chat/test/src/core/api/responses_test.dart index 77e7abc3..a61d825b 100644 --- a/packages/stream_chat/test/src/core/api/responses_test.dart +++ b/packages/stream_chat/test/src/core/api/responses_test.dart @@ -1,6 +1,5 @@ import 'dart:convert'; -import 'package:test/test.dart'; import 'package:stream_chat/src/core/api/responses.dart'; import 'package:stream_chat/src/core/models/device.dart'; import 'package:stream_chat/src/core/models/member.dart'; @@ -8,6 +7,7 @@ import 'package:stream_chat/src/core/models/message.dart'; import 'package:stream_chat/src/core/models/reaction.dart'; import 'package:stream_chat/src/core/models/read.dart'; import 'package:stream_chat/stream_chat.dart'; +import 'package:test/test.dart'; void main() { group('src/api/responses', () { 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..f836d845 100644 --- a/packages/stream_chat/test/src/core/models/filter_test.dart +++ b/packages/stream_chat/test/src/core/models/filter_test.dart @@ -1,7 +1,7 @@ import 'dart:convert'; -import 'package:test/test.dart'; import 'package:stream_chat/src/core/models/filter.dart'; +import 'package:test/test.dart'; void main() { group('operators', () { @@ -114,7 +114,7 @@ void main() { test('notExists', () { const key = 'testKey'; - final filter = Filter.exists(key, exists: false); + final filter = Filter.notExists(key); expect(filter.key, key); expect(filter.value, isFalse); expect(filter.operator, FilterOperator.exists.rawValue); @@ -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/test/src/core/models/own_user_test.dart b/packages/stream_chat/test/src/core/models/own_user_test.dart index e1d12ada..ea84185f 100644 --- a/packages/stream_chat/test/src/core/models/own_user_test.dart +++ b/packages/stream_chat/test/src/core/models/own_user_test.dart @@ -175,5 +175,26 @@ void main() { expect(newUser.teams, ['team1', 'team2']); expect(newUser.language, 'fr'); }); + + test( + 'fromUser should not override name with id if not available in extraData', + () { + final user = User(id: 'test-id'); + expect(user.id, 'test-id'); + expect(user.name, 'test-id'); + + final encodedUser = user.toJson(); + expect(encodedUser['id'], 'test-id'); + expect(encodedUser['name'], null); + + final ownUser = OwnUser.fromUser(user); + expect(user.id, 'test-id'); + expect(user.name, 'test-id'); + + final encodedOwnUser = ownUser.toJson(); + expect(encodedOwnUser['id'], 'test-id'); + expect(encodedOwnUser['name'], null); + }, + ); }); } diff --git a/packages/stream_chat/test/src/core/models/serialization_test.dart b/packages/stream_chat/test/src/core/models/serialization_test.dart index 605b5b58..63b1428f 100644 --- a/packages/stream_chat/test/src/core/models/serialization_test.dart +++ b/packages/stream_chat/test/src/core/models/serialization_test.dart @@ -1,5 +1,5 @@ -import 'package:test/test.dart'; import 'package:stream_chat/src/core/util/serializer.dart'; +import 'package:test/test.dart'; void main() { group('src/models/serialization', () { diff --git a/packages/stream_chat/test/src/core/util/extension_test.dart b/packages/stream_chat/test/src/core/util/extension_test.dart index 98f9f784..ae3c08d8 100644 --- a/packages/stream_chat/test/src/core/util/extension_test.dart +++ b/packages/stream_chat/test/src/core/util/extension_test.dart @@ -1,5 +1,5 @@ -import 'package:test/test.dart'; import 'package:stream_chat/src/core/util/extension.dart'; +import 'package:test/test.dart'; void main() { test('`.withNullifyer` converts the type into non-nullable', () { diff --git a/packages/stream_chat/test/src/fakes.dart b/packages/stream_chat/test/src/fakes.dart index 8d3f6d37..3dcd8ebd 100644 --- a/packages/stream_chat/test/src/fakes.dart +++ b/packages/stream_chat/test/src/fakes.dart @@ -6,11 +6,11 @@ import 'package:rxdart/rxdart.dart'; import 'package:stream_chat/src/core/api/channel_api.dart'; import 'package:stream_chat/src/core/api/device_api.dart'; import 'package:stream_chat/src/core/api/general_api.dart'; +import 'package:stream_chat/src/core/api/guest_api.dart'; import 'package:stream_chat/src/core/api/message_api.dart'; import 'package:stream_chat/src/core/api/moderation_api.dart'; import 'package:stream_chat/src/core/api/stream_chat_api.dart'; import 'package:stream_chat/src/core/api/user_api.dart'; -import 'package:stream_chat/src/core/api/guest_api.dart'; import 'package:stream_chat/src/core/http/token.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; import 'package:stream_chat/src/ws/websocket.dart'; diff --git a/packages/stream_chat/test/src/matchers.dart b/packages/stream_chat/test/src/matchers.dart index 27cfa43d..e54b113d 100644 --- a/packages/stream_chat/test/src/matchers.dart +++ b/packages/stream_chat/test/src/matchers.dart @@ -1,6 +1,7 @@ import 'package:collection/collection.dart'; import 'package:dio/dio.dart' show MultipartFile; import 'package:stream_chat/src/client/channel.dart'; +import 'package:stream_chat/src/core/models/attachment.dart'; import 'package:stream_chat/src/core/models/channel_state.dart'; import 'package:stream_chat/src/core/models/event.dart'; import 'package:stream_chat/src/core/models/message.dart'; @@ -46,12 +47,16 @@ Matcher isSameMessageAs( bool matchText = false, bool matchReactions = false, bool matchSendingStatus = false, + bool matchAttachments = false, + bool matchAttachmentsUploadState = false, }) => _IsSameMessageAs( targetMessage: targetMessage, matchText: matchText, matchReactions: matchReactions, matchSendingStatus: matchSendingStatus, + matchAttachments: matchAttachments, + matchAttachmentsUploadState: matchAttachmentsUploadState, ); class _IsSameMessageAs extends Matcher { @@ -60,12 +65,16 @@ class _IsSameMessageAs extends Matcher { this.matchText = false, this.matchReactions = false, this.matchSendingStatus = false, + this.matchAttachments = false, + this.matchAttachmentsUploadState = false, }); final Message targetMessage; final bool matchText; final bool matchReactions; final bool matchSendingStatus; + final bool matchAttachments; + final bool matchAttachmentsUploadState; @override Description describe(Description description) => @@ -96,6 +105,56 @@ class _IsSameMessageAs extends Matcher { ?.map((it) => '${it.type}-${it.messageId}') .toList()); } + if (matchAttachments) { + bool matchAttachments() { + final attachments = message.attachments; + final targetAttachments = targetMessage.attachments; + if (identical(attachments, targetAttachments)) return true; + final length = attachments.length; + if (length != targetAttachments.length) return false; + for (var i = 0; i < length; i++) { + if (!isSameAttachmentAs( + attachments[i], + matchUploadState: matchAttachmentsUploadState, + ).matches(targetAttachments[i], matchState)) return false; + } + return true; + } + + matches &= matchAttachments(); + } + return matches; + } +} + +Matcher isSameAttachmentAs( + Attachment targetAttachment, { + bool matchUploadState = false, +}) => + _IsSameAttachmentAs( + targetAttachment: targetAttachment, + matchUploadState: matchUploadState, + ); + +class _IsSameAttachmentAs extends Matcher { + const _IsSameAttachmentAs({ + required this.targetAttachment, + this.matchUploadState = false, + }); + + final Attachment targetAttachment; + final bool matchUploadState; + + @override + Description describe(Description description) => + description.add('is same attachment as $targetAttachment'); + + @override + bool matches(covariant Attachment attachment, Map matchState) { + var matches = attachment.id == targetAttachment.id; + if (matchUploadState) { + matches &= attachment.uploadState == targetAttachment.uploadState; + } return matches; } } diff --git a/packages/stream_chat/test/src/ws/websocket_test.dart b/packages/stream_chat/test/src/ws/websocket_test.dart index 94033bf0..5519b2b3 100644 --- a/packages/stream_chat/test/src/ws/websocket_test.dart +++ b/packages/stream_chat/test/src/ws/websocket_test.dart @@ -1,11 +1,11 @@ import 'dart:async'; import 'dart:convert'; +import 'package:mocktail/mocktail.dart'; import 'package:stream_chat/src/core/http/token_manager.dart'; +import 'package:stream_chat/src/ws/websocket.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:test/test.dart'; -import 'package:mocktail/mocktail.dart'; -import 'package:stream_chat/src/ws/websocket.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import '../fakes.dart'; diff --git a/packages/stream_chat_flutter/CHANGELOG.md b/packages/stream_chat_flutter/CHANGELOG.md index 02e2b214..ffc30572 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,3 +1,98 @@ +## Upcoming + +- Updated Dart SDK constraints to `>=2.14.0 <3.0.0` + +## 3.1.1 + +- Updated `stream_chat_flutter_core` dependency to [`3.1.1`](https://pub.dev/packages/stream_chat_flutter_core/changelog). +- Updated `file_picker`, `image_gallery_saver`, and `video_thumbnail` to the latest versions. + +🐞 Fixed + +- [[#687]](https://github.com/GetStream/stream-chat-flutter/issues/687): Fix Users losing their place in the conversation after replying in threads. +- Fixed floating date stream subscription causing "Bad state: stream has already been listened.” error. +- Fixed `String` capitalize extension not working on empty strings. + +✅ Added + +- Added `MessageInput.customOverlays` property to add custom overlays to the message input. +- Added `MessageInput.mentionAllAppUsers` property to mention all app users in the message input. +- The `MessageInput` now supports local search for channels with less than 100 members. +- Added `MessageListView.paginationLoadingIndicatorBuilder` to override the default loading indicator shown while paginating the message list. +- Added new `linkBackgroundColor` in `MessageTheme` for setting background colors of link attachments. + +⚠️ Deprecated + +- `MessageInput.mentionsTileBuilder` is now deprecated in favor of `MessageInput.userMentionsTileBuilder`. +- `MentionTile` is now deprecated in favor of `UserMentionsTile`. + +## 3.0.0 + +- Updated `stream_chat_flutter_core` dependency to [`3.0.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog). + +🛑️ Breaking Changes from `2.2.1` + +- `UserListView` `filter` property now is non-nullable. + +🐞 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. +- `MessageInput` overlays now follow the `MessageInput` focus. +- [[#674]](https://github.com/GetStream/stream-chat-flutter/issues/674): Check scrollController is attached before calling jump in MessageListView. +- Fixed `MessageListView` header and footer when `reverse: false`. + +🔄 Changed + +- Animation curves changed from default `Curves.linear` to `Curves.easeOut` and `Curves.easeIn` for attachment controls. +- Removed default padding in `DateDivider` in `MessageListView` + +✅ Added + +- Added `MessageInput.customPortalOptions` property to add custom overlays to the `MessageInput`. + +## 2.2.1 + +⚠️ Deprecated + +- `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 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 - Updated `stream_chat_flutter_core` dependency to 2.2.1 @@ -7,14 +102,13 @@ ✅ Added - [#516](https://github.com/GetStream/stream-chat-flutter/issues/516): - Added `StreamChatThemeData.placeholderUserImage` for building a widget when the `UserAvatar` image - is loading + Added `StreamChatThemeData.placeholderUserImage` for building a widget when the `UserAvatar` image is loading - Added a `backgroundColor` property to the following widgets: - - `ChannelHeader` - - `ChannelListHeader` - - `GalleryHeader` - - `GalleryFooter` - - `ThreadHeader` + - `ChannelHeader` + - `ChannelListHeader` + - `GalleryHeader` + - `GalleryFooter` + - `ThreadHeader` - Added `MessageInput.attachmentLimit` in order to limit the no. of attachments that can be sent with a single message. - Added `MessageInput.onAttachmentLimitExceed` callback which will be called when the `attachmentLimit` is exceeded. This will override the default error alert behaviour. @@ -34,9 +128,8 @@ You can call `.copyWith` to customize just a subset of properties. 🔄 Changed -Theming has been upgraded! Most theme classes now have `InheritedTheme` classes associated with -them, and have been upgraded with some goodies like `lerp` functions. Here's the full naming -breakdown: +Theming has been upgraded! Most theme classes now have `InheritedTheme` classes associated with them, and have been +upgraded with some goodies like `lerp` functions. Here's the full naming breakdown: * `AvatarTheme` is now `AvatarThemeData` * `ChannelHeaderTheme` is now `ChannelHeaderThemeData` @@ -53,18 +146,18 @@ breakdown: 🐞 Fixed -- Fixed `MessageInput` textField case where `input` is not enabled if the file picked from the - camera is null. +- Fixed `MessageInput` textField case where `input` is not enabled if the file picked from the camera is null. - Fixed date dividers position/alignment in non reversed `MessageListView`. - Fixed `MessageListView` not opening to the right initialMessage if `StreamChannel.initialMessageId` is set. -- Fixed null check errors when accessing `message.text` in `MessageWidget` and `MessageListView`; this occurred when sending a message with no text. +- Fixed null check errors when accessing `message.text` in `MessageWidget` and `MessageListView`; this occurred when + sending a message with no text. ## 2.1.2 🐞 Fixed -- [#590](https://github.com/GetStream/stream-chat-flutter/issues/590): livestream use case, no - members when sending message +- [#590](https://github.com/GetStream/stream-chat-flutter/issues/590): livestream use case, no members when sending + message ## 2.1.1 @@ -82,8 +175,7 @@ breakdown: 🔄 Changed - `StreamChat.of(context).user` is now deprecated in favor of `StreamChat.of(context).currentUser`. -- `StreamChat.of(context).userStream` is now deprecated in favor - of `StreamChat.of(context).currentUserStream`. +- `StreamChat.of(context).userStream` is now deprecated in favor of `StreamChat.of(context).currentUserStream`. 🐞 Fixed @@ -136,8 +228,7 @@ You can call `.copyWith` to customize just a subset of properties - Added video compress options (frame and quality) to `MessageInput` - TypingIndicator now has a property called `parentId` to show typing indicator specific to threads -- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView - header/footer +- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView header/footer - `MessageWidget` accepts a `userAvatarBuilder` - Added pinMessage ui support - Added `MessageListView.threadSeparatorBuilder` property @@ -146,12 +237,10 @@ You can call `.copyWith` to customize just a subset of properties 🐞 Fixed -- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text - box when editing message -- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator - use case -- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without - a reload +- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing + message +- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case +- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without a reload - `MessageListView` not rendering if the user is not a member of the channel - Fix `MessageInput` overflow when there are no actions - Minor fixes and improvements @@ -204,18 +293,15 @@ You can call `.copyWith` to customize just a subset of properties. ✅ Added - TypingIndicator now has a property called `parentId` to show typing indicator specific to threads -- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView - header/footer +- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView header/footer - `MessageWidget` accepts a `userAvatarBuilder` 🐞 Fixed -- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text - box when editing message -- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator - use case -- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without - a reload +- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing + message +- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case +- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without a reload - `MessageListView` not rendering if the user is not a member of the channel ## 2.0.0-nullsafety.7 @@ -285,8 +371,7 @@ You can call `.copyWith` to customize just a subset of properties. - Show error messages as system and keep them in the message input - Remove notification badge logic - Use shimmer while loading images -- Polished `StreamChatTheme` adding more options and a new `MessageInputTheme` dedicated - to `MessageInput` +- Polished `StreamChatTheme` adding more options and a new `MessageInputTheme` dedicated to `MessageInput` - Add possibility to specify custom message actions using `MessageWidget.customActions` - Added `MessageListView.onAttachmentTap` callback - Fixed message newline issue @@ -343,8 +428,7 @@ You can call `.copyWith` to customize just a subset of properties. - Improved api documentation - Updated `stream_chat` dependency to `^1.0.0-beta` - Extracted sample app into dedicated [repo](https://github.com/GetStream/flutter-samples) -- Reimplemented existing widgets - using [stream_chat_flutter_core](https://pub.dev/packages/stream_chat_flutter_core) +- Reimplemented existing widgets using [stream_chat_flutter_core](https://pub.dev/packages/stream_chat_flutter_core) ## 0.2.21 @@ -361,8 +445,8 @@ You can call `.copyWith` to customize just a subset of properties. ## 0.2.20+2 -- Added `shouldAddChannel` to ChannelsBloc in order to check if a channel has to be added to the - list when a new message arrives +- Added `shouldAddChannel` to ChannelsBloc in order to check if a channel has to be added to the list when a new message + arrives ## 0.2.20+1 @@ -396,8 +480,7 @@ You can call `.copyWith` to customize just a subset of properties. ## 0.2.16 -- Do not wrap channel preview builder. Users will have to implement they're custom onTap/onLongPress - implementation +- Do not wrap channel preview builder. Users will have to implement they're custom onTap/onLongPress implementation - Make public autofocus field of the TextField of message_input ## 0.2.15 @@ -582,11 +665,10 @@ You can call `.copyWith` to customize just a subset of properties. ## 0.2.1-alpha+1 -- Removed the additional `Navigator` in `StreamChat` widget. It was added to make the app have - the `StreamChat` widget as ancestor in every route. Now the recommended way to add `StreamChat` to - your app is using the `builder` property of your `MaterialApp` widget. Otherwise you can use it in - the usual way, but you need to add a `StreamChat` widget to every route of your app. - Read [this issue](https://github.com/GetStream/stream-chat-flutter/issues/47) for more +- Removed the additional `Navigator` in `StreamChat` widget. It was added to make the app have the `StreamChat` widget + as ancestor in every route. Now the recommended way to add `StreamChat` to your app is using the `builder` property of + your `MaterialApp` widget. Otherwise you can use it in the usual way, but you need to add a `StreamChat` widget to + every route of your app. Read [this issue](https://github.com/GetStream/stream-chat-flutter/issues/47) for more information. ```dart @@ -688,8 +770,8 @@ Widget build(BuildContext context) { - Add gesture (vertical drag down) to close the keyboard -- Add keyboard type parameters (set it to TextInputType.text to show the submit button that will - even close the keyboard) +- Add keyboard type parameters (set it to TextInputType.text to show the submit button that will even close the + keyboard) The property showVideoFullScreen was added mainly because of this issue brianegan/chewie#261 diff --git a/packages/stream_chat_flutter/example/ios/Flutter/AppFrameworkInfo.plist b/packages/stream_chat_flutter/example/ios/Flutter/AppFrameworkInfo.plist index 6b4c0f78..f2872cf4 100644 --- a/packages/stream_chat_flutter/example/ios/Flutter/AppFrameworkInfo.plist +++ b/packages/stream_chat_flutter/example/ios/Flutter/AppFrameworkInfo.plist @@ -21,6 +21,6 @@ CFBundleVersion 1.0 MinimumOSVersion - 8.0 + 9.0 diff --git a/packages/stream_chat_flutter/example/lib/split_view.dart b/packages/stream_chat_flutter/example/lib/split_view.dart index 52c0e885..1fd3ccb6 100644 --- a/packages/stream_chat_flutter/example/lib/split_view.dart +++ b/packages/stream_chat_flutter/example/lib/split_view.dart @@ -106,9 +106,7 @@ class ChannelListPage extends StatelessWidget { [StreamChat.of(context).currentUser!.id], ), sort: const [SortOption('last_message_at')], - pagination: const PaginationParams( - limit: 20, - ), + limit: 20, ), ), ); diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart index 5fa9cfdb..6abc260e 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_2.dart @@ -83,9 +83,7 @@ class ChannelListPage extends StatelessWidget { [StreamChat.of(context).currentUser!.id], ), sort: const [SortOption('last_message_at')], - pagination: const PaginationParams( - limit: 20, - ), + limit: 20, channelWidget: const ChannelPage(), ), ), diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart index 824c0efa..538dbd68 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_3.dart @@ -85,9 +85,7 @@ class ChannelListPage extends StatelessWidget { ), channelPreviewBuilder: _channelPreviewBuilder, // sort: [SortOption('last_message_at')], - pagination: const PaginationParams( - limit: 20, - ), + limit: 20, channelWidget: const ChannelPage(), ), ), diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart index df94cf06..5a05e4f9 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_4.dart @@ -69,9 +69,7 @@ class ChannelListPage extends StatelessWidget { [StreamChat.of(context).currentUser!.id], ), sort: const [SortOption('last_message_at')], - pagination: const PaginationParams( - limit: 20, - ), + limit: 20, channelWidget: const ChannelPage(), ), ), diff --git a/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart index b9089d6a..86ac2e1f 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_5.dart @@ -75,9 +75,7 @@ class ChannelListPage extends StatelessWidget { [StreamChat.of(context).currentUser!.id], ), sort: const [SortOption('last_message_at')], - pagination: const PaginationParams( - limit: 20, - ), + limit: 20, channelWidget: const ChannelPage(), ), ), 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 b64281cb..af59d300 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart @@ -34,7 +34,7 @@ Future main() async { await client.connectUser( User(id: 'super-band-9'), - '''eyJ0eXAiO«iJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''', + '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''', ); runApp( @@ -102,9 +102,7 @@ class ChannelListPage extends StatelessWidget { [StreamChat.of(context).currentUser!.id], ), sort: const [SortOption('last_message_at')], - pagination: const PaginationParams( - limit: 20, - ), + limit: 20, channelWidget: const ChannelPage(), ), ), 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/attachment_upload_state_builder.dart b/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart index af46515e..1a0ba989 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/attachment_upload_state_builder.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/upload_progress_indicator.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; /// Widget to build in progress typedef InProgressBuilder = Widget Function(BuildContext, int, int); 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..4d2e8825 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/file_attachment.dart @@ -1,15 +1,14 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; +import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/upload_progress_indicator.dart'; import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; - -import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart'; /// Widget for displaying file attachments class FileAttachment extends AttachmentWidget { @@ -258,7 +257,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..8272a962 100644 --- a/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/giphy_attachment.dart @@ -2,10 +2,10 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:shimmer/shimmer.dart'; import 'package:stream_chat_flutter/src/attachment/attachment_widget.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/visible_footnote.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; /// Widget for showing a GIF attachment class GiphyAttachment extends AttachmentWidget { @@ -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/url_attachment.dart b/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart similarity index 91% rename from packages/stream_chat_flutter/lib/src/url_attachment.dart rename to packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart index 5d5ca489..d56bcd6a 100644 --- a/packages/stream_chat_flutter/lib/src/url_attachment.dart +++ b/packages/stream_chat_flutter/lib/src/attachment/url_attachment.dart @@ -10,6 +10,7 @@ class UrlAttachment extends StatelessWidget { Key? key, required this.urlAttachment, required this.hostDisplayName, + required this.messageTheme, this.textPadding = const EdgeInsets.symmetric( horizontal: 16, vertical: 8, @@ -25,15 +26,16 @@ class UrlAttachment extends StatelessWidget { /// Padding for text final EdgeInsets textPadding; + /// [MessageThemeData] for showing image title + final MessageThemeData messageTheme; + @override Widget build(BuildContext context) { 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, @@ -60,7 +62,7 @@ class UrlAttachment extends StatelessWidget { borderRadius: const BorderRadius.only( topRight: Radius.circular(16), ), - color: chatThemeData.colorTheme.linkBg, + color: messageTheme.linkBackgroundColor, ), child: Padding( padding: const EdgeInsets.only( diff --git a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart index 5723234d..f4be36e1 100644 --- a/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart +++ b/packages/stream_chat_flutter/lib/src/channel_bottom_sheet.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/channel_info.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Bottom Sheet with options class ChannelBottomSheet extends StatefulWidget { diff --git a/packages/stream_chat_flutter/lib/src/channel_header.dart b/packages/stream_chat_flutter/lib/src/channel_header.dart index 1c12b132..62f994dc 100644 --- a/packages/stream_chat_flutter/lib/src/channel_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_header.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:stream_chat_flutter/src/back_button.dart'; import 'package:stream_chat_flutter/src/channel_info.dart'; import 'package:stream_chat_flutter/src/channel_name.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/info_tile.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header_paint.png) @@ -137,12 +138,17 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget { break; } + final theme = Theme.of(context); + return InfoTile( showMessage: showConnectionStateTile && showStatus, message: statusString, child: AppBar( - textTheme: Theme.of(context).textTheme, - brightness: Theme.of(context).brightness, + toolbarTextStyle: theme.textTheme.bodyText2, + titleTextStyle: theme.textTheme.headline6, + systemOverlayStyle: theme.brightness == Brightness.dark + ? SystemUiOverlayStyle.light + : SystemUiOverlayStyle.dark, elevation: 1, leading: leadingWidget, backgroundColor: backgroundColor ?? channelHeaderTheme.color, diff --git a/packages/stream_chat_flutter/lib/src/channel_info.dart b/packages/stream_chat_flutter/lib/src/channel_info.dart index de9d74b9..6bd61dad 100644 --- a/packages/stream_chat_flutter/lib/src/channel_info.dart +++ b/packages/stream_chat_flutter/lib/src/channel_info.dart @@ -1,8 +1,8 @@ import 'package:collection/collection.dart' show IterableExtension; import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Widget which shows channel info class ChannelInfo extends StatelessWidget { diff --git a/packages/stream_chat_flutter/lib/src/channel_list_header.dart b/packages/stream_chat_flutter/lib/src/channel_list_header.dart index 4ce8b0d2..2dfc1ffb 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_header.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_header.dart @@ -1,6 +1,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/stream_neumorphic_button.dart'; @@ -121,12 +122,16 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget { final chatThemeData = StreamChatTheme.of(context); final channelListHeaderThemeData = ChannelListHeaderTheme.of(context); + final theme = Theme.of(context); return InfoTile( showMessage: showConnectionStateTile && showStatus, message: statusString, child: AppBar( - textTheme: Theme.of(context).textTheme, - brightness: Theme.of(context).brightness, + toolbarTextStyle: theme.textTheme.bodyText2, + titleTextStyle: theme.textTheme.headline6, + systemOverlayStyle: theme.brightness == Brightness.dark + ? SystemUiOverlayStyle.light + : SystemUiOverlayStyle.dark, elevation: 1, backgroundColor: backgroundColor ?? channelListHeaderThemeData.color, 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..96b27823 100644 --- a/packages/stream_chat_flutter/lib/src/channel_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/channel_list_view.dart @@ -4,11 +4,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:shimmer/shimmer.dart'; import 'package:stream_chat_flutter/src/channel_bottom_sheet.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; /// Callback called when tapping on a channel typedef ChannelTapCallback = void Function(Channel, Widget?); @@ -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/channel_name.dart b/packages/stream_chat_flutter/lib/src/channel_name.dart index 918aa076..258893c8 100644 --- a/packages/stream_chat_flutter/lib/src/channel_name.dart +++ b/packages/stream_chat_flutter/lib/src/channel_name.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; /// It shows the current [Channel] name using a [Text] widget. /// diff --git a/packages/stream_chat_flutter/lib/src/commands_overlay.dart b/packages/stream_chat_flutter/lib/src/commands_overlay.dart new file mode 100644 index 00000000..ffaec928 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/commands_overlay.dart @@ -0,0 +1,210 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// Overlay for displaying commands that can be used +class CommandsOverlay extends StatelessWidget { + /// Constructor for creating a [CommandsOverlay] + const CommandsOverlay({ + required this.text, + required this.onCommandResult, + required this.size, + required this.channel, + Key? key, + }) : super(key: key); + + /// The size of the overlay + final Size size; + + /// Query for searching commands + final String text; + + /// The channel to search for users + final Channel channel; + + /// Callback called when a command is selected + final ValueChanged onCommandResult; + + @override + Widget build(BuildContext context) { + final _streamChatTheme = StreamChatTheme.of(context); + final commands = channel.config?.commands + .where((c) => c.name.contains(text.replaceFirst('/', ''))) + .toList() ?? + []; + + if (commands.isEmpty) { + return const SizedBox(); + } + + return Padding( + padding: const EdgeInsets.all(4), + child: Card( + elevation: 2, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + color: _streamChatTheme.colorTheme.barsBg, + clipBehavior: Clip.hardEdge, + child: Container( + constraints: BoxConstraints.loose(size), + decoration: BoxDecoration( + color: _streamChatTheme.colorTheme.barsBg, + borderRadius: BorderRadius.circular(8)), + child: ListView( + padding: const EdgeInsets.all(0), + shrinkWrap: true, + children: [ + if (commands.isNotEmpty) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + ), + child: StreamSvgIcon.lightning( + color: _streamChatTheme.colorTheme.accentPrimary, + ), + ), + Text( + context.translations.instantCommandsLabel, + style: TextStyle( + color: _streamChatTheme.colorTheme.textHighEmphasis + .withOpacity(.5), + ), + ) + ], + ), + ), + const SizedBox( + height: 10, + ), + ...commands + .map( + (c) => InkWell( + onTap: () { + onCommandResult(c); + }, + child: SizedBox( + height: 40, + child: Row( + children: [ + const SizedBox( + width: 16, + ), + _buildCommandIcon(_streamChatTheme, c.name), + const SizedBox( + width: 8, + ), + Text.rich( + TextSpan( + text: c.name.capitalize(), + style: const TextStyle( + fontWeight: FontWeight.bold), + children: [ + TextSpan( + text: ' /${c.name} ${c.args}', + style: _streamChatTheme.textTheme.body + .copyWith( + // ignore: lines_longer_than_80_chars + color: _streamChatTheme + // ignore: lines_longer_than_80_chars + .colorTheme + .textLowEmphasis, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ) + .toList(), + ], + ), + ), + ), + ); + } + + Widget _buildCommandIcon( + StreamChatThemeData _streamChatTheme, String iconType) { + switch (iconType) { + case 'giphy': + return CircleAvatar( + radius: 12, + child: StreamSvgIcon.giphyIcon( + size: 24, + ), + ); + case 'ban': + return CircleAvatar( + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, + radius: 12, + child: StreamSvgIcon.iconUserDelete( + size: 16, + color: Colors.white, + ), + ); + case 'flag': + return CircleAvatar( + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, + radius: 12, + child: StreamSvgIcon.flag( + size: 14, + color: Colors.white, + ), + ); + case 'imgur': + return CircleAvatar( + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, + radius: 12, + child: ClipOval( + child: StreamSvgIcon.imgur( + size: 24, + ), + ), + ); + case 'mute': + return CircleAvatar( + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, + radius: 12, + child: StreamSvgIcon.mute( + size: 16, + color: Colors.white, + ), + ); + case 'unban': + return CircleAvatar( + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, + radius: 12, + child: StreamSvgIcon.userAdd( + size: 16, + color: Colors.white, + ), + ); + case 'unmute': + return CircleAvatar( + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, + radius: 12, + child: StreamSvgIcon.volumeUp( + size: 16, + color: Colors.white, + ), + ); + default: + return CircleAvatar( + backgroundColor: _streamChatTheme.colorTheme.accentPrimary, + radius: 12, + child: StreamSvgIcon.lightning( + size: 16, + color: Colors.white, + ), + ); + } + } +} diff --git a/packages/stream_chat_flutter/lib/src/date_divider.dart b/packages/stream_chat_flutter/lib/src/date_divider.dart index 3d85299c..7c977f60 100644 --- a/packages/stream_chat_flutter/lib/src/date_divider.dart +++ b/packages/stream_chat_flutter/lib/src/date_divider.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; -import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; /// It shows a date divider depending on the date difference class DateDivider extends StatelessWidget { diff --git a/packages/stream_chat_flutter/lib/src/emoji/emoji.dart b/packages/stream_chat_flutter/lib/src/emoji/emoji.dart index 38af85af..36717000 100644 --- a/packages/stream_chat_flutter/lib/src/emoji/emoji.dart +++ b/packages/stream_chat_flutter/lib/src/emoji/emoji.dart @@ -26,7 +26,8 @@ // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -import 'package:collection/collection.dart' show IterableExtension; +import 'package:collection/collection.dart' + show IterableExtension, ListEquality; /// All Groups enum EmojiGroup { @@ -114240,7 +114241,6 @@ final emojiRegex = RegExp( class Emoji { static const variationSelector16 = 65039; static const ZWJ = 8205; - final String? name; final String? char; final String? shortName; @@ -114252,14 +114252,39 @@ class Emoji { /// Emoji class. /// [name] of emoji. [char] and character of emoji. [shortName] and a digest name of emoji, [emojiGroup] is emoji's group and [emojiSubgroup] is emoji's subgroup. [keywords] list of keywords for emoji. [modifiable] `true` if emoji has skin. - Emoji( - {this.name, - this.char, - this.shortName, - this.emojiGroup, - this.emojiSubgroup, - this.keywords = const [], - this.modifiable = false}); + Emoji({ + this.name, + this.char, + this.shortName, + this.emojiGroup, + this.emojiSubgroup, + this.keywords = const [], + this.modifiable = false, + }); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is Emoji && + runtimeType == other.runtimeType && + name == other.name && + char == other.char && + shortName == other.shortName && + emojiGroup == other.emojiGroup && + emojiSubgroup == other.emojiSubgroup && + const ListEquality().equals(keywords, other.keywords) && + modifiable == other.modifiable; + + @override + int get hashCode => Object.hash( + name.hashCode, + char.hashCode, + shortName.hashCode, + emojiGroup.hashCode, + emojiSubgroup.hashCode, + keywords.hashCode, + modifiable.hashCode, + ); /// Runes of Emoji Character List get charRunes { @@ -114339,9 +114364,11 @@ class Emoji { return _emojis.firstWhereOrNull((Emoji emoji) => emoji.name == name); } - /// Returns Emoji by [name] as short name. - static Emoji? byShortName(String name) { - return _emojis.firstWhereOrNull((Emoji emoji) => emoji.char == name); + /// Returns Emoji by [shortName] as short name. + static Emoji? byShortName(String shortName) { + return _emojis.firstWhereOrNull( + (Emoji emoji) => emoji.shortName == shortName, + ); } /// Returns list of Emojis in a same [group] diff --git a/packages/stream_chat_flutter/lib/src/emoji_overlay.dart b/packages/stream_chat_flutter/lib/src/emoji_overlay.dart new file mode 100644 index 00000000..c37dd687 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/emoji_overlay.dart @@ -0,0 +1,118 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/emoji/emoji.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; +import 'package:substring_highlight/substring_highlight.dart'; + +/// Overlay for displaying emoji that can be used +class EmojiOverlay extends StatelessWidget { + /// Constructor for creating a [EmojiOverlay] + const EmojiOverlay({ + required this.query, + required this.onEmojiResult, + required this.size, + Key? key, + }) : super(key: key); + + /// The size of the overlay + final Size size; + + /// Query for searching emoji + final String query; + + /// Callback called when an emoji is selected + final ValueChanged onEmojiResult; + + @override + Widget build(BuildContext context) { + final _streamChatTheme = StreamChatTheme.of(context); + final _emojiNames = + Emoji.all().where((it) => it.name != null).map((e) => e.name!); + + final emojis = _emojiNames + .where((e) => e.contains(query)) + .map(Emoji.byName) + .where((e) => e != null); + + if (emojis.isEmpty) { + return const SizedBox(); + } + + return Card( + margin: const EdgeInsets.all(8), + elevation: 2, + color: _streamChatTheme.colorTheme.barsBg, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + clipBehavior: Clip.hardEdge, + child: Container( + constraints: BoxConstraints.loose(size), + decoration: BoxDecoration( + boxShadow: const [ + BoxShadow( + spreadRadius: -8, + blurRadius: 5, + offset: Offset(0, -4), + ), + ], + color: _streamChatTheme.colorTheme.barsBg, + ), + child: ListView.builder( + padding: const EdgeInsets.all(0), + shrinkWrap: true, + itemCount: emojis.length + 1, + itemBuilder: (context, i) { + if (i == 0) { + return Padding( + padding: const EdgeInsets.only(left: 8, top: 8), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: StreamSvgIcon.smile( + color: _streamChatTheme.colorTheme.accentPrimary, + ), + ), + Flexible( + child: Text( + context.translations.emojiMatchingQueryText( + query, + ), + style: TextStyle( + color: _streamChatTheme.colorTheme.textHighEmphasis + .withOpacity(.5), + ), + ), + ) + ], + ), + ); + } + + final emoji = emojis.elementAt(i - 1)!; + final themeData = Theme.of(context); + return ListTile( + title: SubstringHighlight( + text: + // ignore: lines_longer_than_80_chars + "${emoji.char} ${emoji.name!.replaceAll('_', ' ')}", + term: query, + textStyleHighlight: themeData.textTheme.headline6!.copyWith( + fontSize: 14.5, + fontWeight: FontWeight.bold, + ), + textStyle: themeData.textTheme.headline6!.copyWith( + fontSize: 14.5, + ), + ), + onTap: () { + onEmojiResult(emoji); + }, + ); + }, + ), + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/extension.dart b/packages/stream_chat_flutter/lib/src/extension.dart index 19bf4523..0072a811 100644 --- a/packages/stream_chat_flutter/lib/src/extension.dart +++ b/packages/stream_chat_flutter/lib/src/extension.dart @@ -1,4 +1,5 @@ import 'package:characters/characters.dart'; +import 'package:diacritic/diacritic.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:stream_chat_flutter/src/emoji/emoji.dart'; @@ -11,7 +12,7 @@ final _emojiChars = Emoji.chars(); extension StringExtension on String { /// Returns the capitalized string String capitalize() => - '${this[0].toUpperCase()}${substring(1).toLowerCase()}'; + isNotEmpty ? '${this[0].toUpperCase()}${substring(1).toLowerCase()}' : ''; /// Returns whether the string contains only emoji's or not. /// @@ -24,6 +25,12 @@ extension StringExtension on String { final characters = trim().characters; return characters.every(_emojiChars.contains); } + + /// Removes accents and diacritics from the given String. + String get diacriticsInsensitive => removeDiacritics(this); + + /// Levenshtein distance between this and [t]. + int levenshteinDistance(String t) => levenshtein(this, t); } /// List extension @@ -170,3 +177,49 @@ extension IconButtonX on IconButton { icon: icon ?? this.icon, ); } + +/// Extensions on List +extension UserListX on List { + /// It does an search on a list of [User] and returns users with + /// `id` or `name` containing the [query]. + /// + /// Results are returned sorted by their edit distance from the + /// searched string, distance is calculated using the [levenshtein] algorithm. + List search(String query) { + String normalize(String input) => input.toLowerCase().diacriticsInsensitive; + + final normalizedQuery = normalize(query); + + final matchingUsers = {}; // User:lDistance + + for (final user in this) { + final normalizedId = normalize(user.id); + final normalizedUserName = normalize(user.name); + final lDistance = normalizedUserName.levenshteinDistance(normalizedQuery); + final containsId = normalizedId.contains(normalizedQuery); + final containsName = normalizedUserName.contains(normalizedQuery); + if (lDistance < 3 || containsId || containsName) { + matchingUsers[user] = lDistance; + } + } + + final entries = matchingUsers.entries.toList(growable: false) + ..sort((prev, curr) { + bool containsQuery(User user) => + normalize(user.id).contains(normalizedQuery) || + normalize(user.name).contains(normalizedQuery); + + final containsInPrev = containsQuery(prev.key); + final containsInCurr = containsQuery(curr.key); + + if (containsInPrev && !containsInCurr) { + return -1; + } else if (!containsInPrev && containsInCurr) { + return 1; + } + return prev.value.compareTo(curr.value); + }); + + return entries.map((e) => e.key).toList(growable: false); + } +} 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 bef2a531..a959229c 100644 --- a/packages/stream_chat_flutter/lib/src/full_screen_media.dart +++ b/packages/stream_chat_flutter/lib/src/full_screen_media.dart @@ -5,11 +5,11 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:chewie/chewie.dart'; import 'package:flutter/material.dart'; import 'package:photo_view/photo_view.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/gallery_footer.dart'; import 'package:stream_chat_flutter/src/gallery_header.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:video_player/video_player.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; /// Return action for coming back from pages enum ReturnActionType { @@ -112,6 +112,8 @@ class _FullScreenMediaState extends State attachment.assetUrl ?? attachment.thumbUrl; return PhotoView( + loadingBuilder: (context, image) => + const Offstage(), imageProvider: (imageUrl == null && attachment.localUri != null && attachment.file?.bytes != null) diff --git a/packages/stream_chat_flutter/lib/src/gallery_footer.dart b/packages/stream_chat_flutter/lib/src/gallery_footer.dart index 2b2858f8..eaa03fb7 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_footer.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_footer.dart @@ -7,12 +7,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; /// Footer widget for media display class GalleryFooter extends StatefulWidget implements PreferredSizeWidget { diff --git a/packages/stream_chat_flutter/lib/src/gallery_header.dart b/packages/stream_chat_flutter/lib/src/gallery_header.dart index 74eeb4ee..fc5f67b4 100644 --- a/packages/stream_chat_flutter/lib/src/gallery_header.dart +++ b/packages/stream_chat_flutter/lib/src/gallery_header.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:stream_chat_flutter/src/attachment_actions_modal.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; @@ -57,9 +58,13 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget { @override Widget build(BuildContext context) { final galleryHeaderThemeData = GalleryHeaderTheme.of(context); + final theme = Theme.of(context); return AppBar( - textTheme: Theme.of(context).textTheme, - brightness: Theme.of(context).brightness, + toolbarTextStyle: theme.textTheme.bodyText2, + titleTextStyle: theme.textTheme.headline6, + systemOverlayStyle: theme.brightness == Brightness.dark + ? SystemUiOverlayStyle.light + : SystemUiOverlayStyle.dark, elevation: 1, leading: showBackButton ? IconButton( 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/mention_tile.dart b/packages/stream_chat_flutter/lib/src/mention_tile.dart index d9469215..c302ccb8 100644 --- a/packages/stream_chat_flutter/lib/src/mention_tile.dart +++ b/packages/stream_chat_flutter/lib/src/mention_tile.dart @@ -4,6 +4,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// This widget is used for showing user tiles for mentions /// Use [title], [subtitle], [leading], [trailing] for /// substituting widgets in respective positions +@Deprecated('Use `UserMentionTile` instead. Will be removed in future release') class MentionTile extends StatelessWidget { /// Constructor for creating a [MentionTile] widget const MentionTile( diff --git a/packages/stream_chat_flutter/lib/src/message_input.dart b/packages/stream_chat_flutter/lib/src/message_input.dart index f47aa3ed..fed1cc4e 100644 --- a/packages/stream_chat_flutter/lib/src/message_input.dart +++ b/packages/stream_chat_flutter/lib/src/message_input.dart @@ -2,27 +2,30 @@ import 'dart:async'; import 'dart:math'; import 'package:cached_network_image/cached_network_image.dart'; +import 'package:collection/collection.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:image_picker/image_picker.dart'; import 'package:photo_manager/photo_manager.dart'; import 'package:shimmer/shimmer.dart'; +import 'package:stream_chat_flutter/src/commands_overlay.dart'; import 'package:stream_chat_flutter/src/emoji/emoji.dart'; +import 'package:stream_chat_flutter/src/emoji_overlay.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/media_list_view.dart'; import 'package:stream_chat_flutter/src/message_list_view.dart'; +import 'package:stream_chat_flutter/src/overlays.dart'; import 'package:stream_chat_flutter/src/quoted_message_widget.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; +import 'package:stream_chat_flutter/src/user_mentions_overlay.dart'; import 'package:stream_chat_flutter/src/video_service.dart'; import 'package:stream_chat_flutter/src/video_thumbnail_image.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import 'package:substring_highlight/substring_highlight.dart'; import 'package:video_compress/video_compress.dart'; export 'package:video_compress/video_compress.dart' show VideoQuality; @@ -53,13 +56,20 @@ typedef AttachmentThumbnailBuilder = Widget Function( Attachment, ); -/// Builder function for building a mention tile -/// Use [MentionTile] for the default implementation +/// Builder function for building a mention tile. typedef MentionTileBuilder = Widget Function( BuildContext context, Member member, ); +/// Builder function for building a user mention tile. +/// +/// Use [UserMentionTile] for the default implementation. +typedef UserMentionTileBuilder = Widget Function( + BuildContext context, + User user, +); + /// Widget builder for action button. /// /// [defaultActionButton] is the default [IconButton] configuration, @@ -169,7 +179,7 @@ class MessageInput extends StatefulWidget { this.disableAttachments = false, this.initialMessage, this.textEditingController, - this.actions, + this.actions = const [], this.actionsLocation = ActionsLocation.left, this.attachmentThumbnailBuilders, this.focusNode, @@ -181,7 +191,9 @@ class MessageInput extends StatefulWidget { this.idleSendButton, this.activeSendButton, this.showCommandsButton = true, - this.mentionsTileBuilder, + @Deprecated('''Use `userMentionsTileBuilder` instead. Will be removed in future release''') + this.mentionsTileBuilder, + this.userMentionsTileBuilder, this.maxAttachmentSize = _kDefaultMaxAttachmentSize, this.compressedVideoQuality = VideoQuality.DefaultQuality, this.compressedVideoFrameRate = 30, @@ -190,12 +202,17 @@ class MessageInput extends StatefulWidget { this.onAttachmentLimitExceed, this.attachmentButtonBuilder, this.commandButtonBuilder, + this.customOverlays = const [], + this.mentionAllAppUsers = false, }) : assert( initialMessage == null || editMessage == null, "Can't provide both `initialMessage` and `editMessage`", ), super(key: key); + /// List of options for showing overlays + final List customOverlays; + /// Message to edit final Message? editMessage; @@ -242,7 +259,7 @@ class MessageInput extends StatefulWidget { final TextEditingController? textEditingController; /// List of action widgets - final List? actions; + final List actions; /// The location of the custom actions final ActionsLocation actionsLocation; @@ -271,9 +288,12 @@ class MessageInput extends StatefulWidget { /// Send button widget in an active state final Widget? activeSendButton; - /// Customize the tile for the mentions overlay + /// Customize the tile for the mentions overlay. final MentionTileBuilder? mentionsTileBuilder; + /// Customize the tile for the mentions overlay. + final UserMentionTileBuilder? userMentionsTileBuilder; + /// A callback for error reporting final ErrorListener? onError; @@ -297,6 +317,11 @@ class MessageInput extends StatefulWidget { /// calling `.copyWith`. final ActionButtonBuilder? commandButtonBuilder; + /// When enabled mentions search users across the entire app. + /// + /// Defaults to false. + final bool mentionAllAppUsers; + @override MessageInputState createState() => MessageInputState(); @@ -318,11 +343,11 @@ class MessageInputState extends State { final List _mentionedUsers = []; final _imagePicker = ImagePicker(); - late final FocusNode _focusNode; + late final _focusNode = widget.focusNode ?? FocusNode(); bool _inputEnabled = true; bool _commandEnabled = false; - OverlayEntry? _commandsOverlay, _mentionsOverlay, _emojiOverlay; - late Iterable _emojiNames; + bool _showCommandsOverlay = false; + bool _showMentionsOverlay = false; Command? _chosenCommand; bool _actionsShrunk = false; @@ -330,10 +355,9 @@ class MessageInputState extends State { bool _openFilePickerSection = false; int _filePickerIndex = 0; - final _keyboardVisibilityController = KeyboardVisibilityController(); - /// The editing controller passed to the input TextField - late final TextEditingController textEditingController; + late final TextEditingController textEditingController = + widget.textEditingController ?? TextEditingController(); late StreamChatThemeData _streamChatTheme; late MessageInputThemeData _messageInputTheme; @@ -345,29 +369,10 @@ class MessageInputState extends State { @override void initState() { super.initState(); - _focusNode = widget.focusNode ?? FocusNode(); - _emojiNames = - Emoji.all().where((it) => it.name != null).map((e) => e.name!); - - if (!kIsWeb) { - _keyboardListener = - _keyboardVisibilityController.onChange.listen((visible) { - if (_focusNode.hasFocus) { - _onChanged(context, textEditingController.text); - } - }); - } - - textEditingController = - widget.textEditingController ?? TextEditingController(); if (widget.editMessage != null || widget.initialMessage != null) { _parseExistingMessage(widget.editMessage ?? widget.initialMessage!); } - - textEditingController.addListener(() { - _onChanged(context, textEditingController.text); - }); - + textEditingController.addListener(_onChangedDebounced); _focusNode.addListener(() { if (_focusNode.hasFocus) { _openFilePickerSection = false; @@ -379,6 +384,9 @@ class MessageInputState extends State { Timer? _slowModeTimer; void _startSlowMode() { + if (!mounted) { + return; + } final channel = StreamChannel.of(context).channel; final cooldownStartedAt = channel.cooldownStartedAt; if (cooldownStartedAt != null) { @@ -472,7 +480,35 @@ class MessageInputState extends State { child: child, ); } - return child; + + return MultiOverlay( + childAnchor: Alignment.topCenter, + overlayAnchor: Alignment.bottomCenter, + overlayOptions: [ + OverlayOptions( + visible: _showCommandsOverlay, + widget: _buildCommandsOverlayEntry(), + ), + OverlayOptions( + visible: _focusNode.hasFocus && + textEditingController.text.isNotEmpty && + textEditingController.selection.baseOffset > 0 && + textEditingController.text + .substring( + 0, + textEditingController.selection.baseOffset, + ) + .contains(':'), + widget: _buildEmojiOverlay(), + ), + OverlayOptions( + visible: _showMentionsOverlay, + widget: _buildMentionsOverlayEntry(), + ), + ...widget.customOverlays, + ], + child: child, + ); } Flex _buildTextField(BuildContext context) => Flex( @@ -578,6 +614,8 @@ class MessageInputState extends State { crossFadeState: _actionsShrunk ? CrossFadeState.showFirst : CrossFadeState.showSecond, + firstCurve: Curves.easeOut, + secondCurve: Curves.easeIn, firstChild: IconButton( onPressed: () { if (_actionsShrunk) { @@ -602,22 +640,19 @@ class MessageInputState extends State { ), secondChild: widget.disableAttachments && !widget.showCommandsButton && - widget.actions?.isNotEmpty != true + 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, @@ -784,51 +819,32 @@ class MessageInputState extends State { ).merge(passedDecoration); } - Timer? _debounce; + late final _onChangedDebounced = debounce( + () { + var value = textEditingController.text; + if (!mounted) return; + value = value.trim(); - String? _previousValue; + final channel = StreamChannel.of(context).channel; + if (value.isNotEmpty) { + channel.keyStroke(widget.parentMessage?.id).catchError((e) {}); + } - void _onChanged(BuildContext context, String s) { - if (s == _previousValue) { - return; - } - _previousValue = s; + var actionsLength = widget.actions.length; + if (widget.showCommandsButton) actionsLength += 1; + if (!widget.disableAttachments) actionsLength += 1; - if (_debounce?.isActive == true) _debounce!.cancel(); - _debounce = Timer( - const Duration(milliseconds: 350), - () { - if (!mounted) { - return; - } - StreamChannel.of(context) - .channel - .keyStroke(widget.parentMessage?.id) - .catchError((e) {}); + setState(() { + _actionsShrunk = value.isNotEmpty && actionsLength > 1; + }); - setState(() { - _actionsShrunk = s.trim().isNotEmpty && - ((widget.actions?.length ?? 0) + - (widget.showCommandsButton ? 1 : 0) + - (widget.disableAttachments ? 0 : 1) > - 1); - }); - - _commandsOverlay?.remove(); - _commandsOverlay = null; - _mentionsOverlay?.remove(); - _mentionsOverlay = null; - _emojiOverlay?.remove(); - _emojiOverlay = null; - - _checkCommands(s.trim(), context); - - _checkMentions(s, context); - - _checkEmoji(s, context); - }, - ); - } + _checkCommands(value, context); + _checkMentions(value, context); + _checkEmoji(value, context); + }, + const Duration(milliseconds: 350), + leading: true, + ); String _getHint(BuildContext context) { if (_commandEnabled && _chosenCommand!.name == 'giphy') { @@ -848,10 +864,7 @@ class MessageInputState extends State { if (s.isNotEmpty && textEditingController.selection.baseOffset > 0 && textEditingController.text - .substring( - 0, - textEditingController.selection.baseOffset, - ) + .substring(0, textEditingController.selection.baseOffset) .contains(':')) { final textToSelection = textEditingController.text .substring(0, textEditingController.value.selection.start); @@ -861,12 +874,6 @@ class MessageInputState extends State { if (textToSelection.endsWith(':') && emoji != null) { _chooseEmoji(splits.sublist(0, splits.length - 1), emoji); - } else { - _emojiOverlay = _buildEmojiOverlay(); - - if (_emojiOverlay != null) { - Overlay.of(context)!.insert(_emojiOverlay!); - } } } } @@ -879,166 +886,50 @@ class MessageInputState extends State { .split(' ') .last .contains('@')) { - _mentionsOverlay = _buildMentionsOverlayEntry(); - if (_mentionsOverlay != null) { - Overlay.of(context)!.insert(_mentionsOverlay!); + if (!_showMentionsOverlay) { + setState(() { + _showMentionsOverlay = true; + }); } + } else if (_showMentionsOverlay) { + setState(() { + _showMentionsOverlay = false; + }); } } void _checkCommands(String s, BuildContext context) { if (s.startsWith('/')) { - final matchedCommandsList = StreamChannel.of(context) - .channel - .config - ?.commands - .where((element) => element.name == s.substring(1)) - .toList() ?? - []; - - if (matchedCommandsList.length == 1) { - _chosenCommand = matchedCommandsList[0]; - textEditingController.clear(); + final allCommands = StreamChannel.of(context).channel.config?.commands; + final command = + allCommands?.firstWhereOrNull((it) => it.name == s.substring(1)); + if (command != null) { + return _setCommand(command); + } else if (!_showCommandsOverlay) { setState(() { - _commandEnabled = true; + _showCommandsOverlay = true; }); - _commandsOverlay?.remove(); - _commandsOverlay = null; - } else { - _commandsOverlay = _buildCommandsOverlayEntry(); - if (_commandsOverlay != null) { - Overlay.of(context)!.insert(_commandsOverlay!); - } } + } else if (_showCommandsOverlay) { + setState(() { + _showCommandsOverlay = false; + }); } } - OverlayEntry? _buildCommandsOverlayEntry() { + Widget _buildCommandsOverlayEntry() { final text = textEditingController.text.trimLeft(); - final commands = StreamChannel.of(context) - .channel - .config - ?.commands - .where((c) => c.name.contains(text.replaceFirst('/', ''))) - .toList() ?? - []; - if (commands.isEmpty) { - return null; + final renderObject = context.findRenderObject() as RenderBox?; + if (renderObject == null) { + return const Offstage(); } - - // ignore: cast_nullable_to_non_nullable - final renderBox = context.findRenderObject() as RenderBox; - final size = renderBox.size; - - final child = Padding( - padding: const EdgeInsets.all(8), - child: Card( - elevation: 2, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - color: _streamChatTheme.colorTheme.barsBg, - clipBehavior: Clip.hardEdge, - child: Container( - constraints: BoxConstraints.loose(const Size.fromHeight(400)), - decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.barsBg, - borderRadius: BorderRadius.circular(8)), - child: ListView( - padding: const EdgeInsets.all(0), - shrinkWrap: true, - children: [ - if (commands.isNotEmpty) - Padding( - padding: const EdgeInsets.only(top: 8), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - ), - child: StreamSvgIcon.lightning( - color: _streamChatTheme.colorTheme.accentPrimary, - ), - ), - Text( - context.translations.instantCommandsLabel, - style: TextStyle( - color: _streamChatTheme.colorTheme.textHighEmphasis - .withOpacity(.5), - ), - ) - ], - ), - ), - const SizedBox( - height: 10, - ), - ...commands - .map( - (c) => InkWell( - onTap: () { - _setCommand(c); - }, - child: SizedBox( - height: 40, - child: Row( - children: [ - const SizedBox( - width: 16, - ), - _buildCommandIcon(c.name), - const SizedBox( - width: 8, - ), - Text.rich( - TextSpan( - text: c.name.capitalize(), - style: const TextStyle( - fontWeight: FontWeight.bold), - children: [ - TextSpan( - text: ' /${c.name} ${c.args}', - style: _streamChatTheme.textTheme.body - .copyWith( - // ignore: lines_longer_than_80_chars - color: _streamChatTheme - // ignore: lines_longer_than_80_chars - .colorTheme - .textLowEmphasis, - ), - ), - ], - ), - ), - ], - ), - ), - ), - ) - .toList(), - ], - ), - ), - ), + return CommandsOverlay( + channel: StreamChannel.of(context).channel, + size: Size(renderObject.size.width - 16, 400), + text: text, + onCommandResult: _setCommand, ); - return OverlayEntry( - builder: (context) => Positioned( - bottom: size.height + MediaQuery.of(context).viewInsets.bottom, - left: 0, - right: 0, - child: TweenAnimationBuilder( - tween: Tween(begin: 0, end: 1), - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOutExpo, - builder: (context, val, child) => Transform.scale( - scale: val, - child: child, - ), - child: child, - ), - )); } Widget _buildFilePickerSection() { @@ -1088,109 +979,123 @@ class MessageInputState extends State { } return AnimatedContainer( - duration: const Duration(milliseconds: 300), + duration: _openFilePickerSection + ? const Duration(milliseconds: 300) + : const Duration(), + 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); - } - }, - ), - ), - ), - ], + ), ), ), ); @@ -1250,300 +1155,78 @@ class MessageInputState extends State { }); } - Widget _buildCommandIcon(String iconType) { - switch (iconType) { - case 'giphy': - return CircleAvatar( - radius: 12, - child: StreamSvgIcon.giphyIcon( - size: 24, - ), - ); - case 'ban': - return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentPrimary, - radius: 12, - child: StreamSvgIcon.iconUserDelete( - size: 16, - color: Colors.white, - ), - ); - case 'flag': - return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentPrimary, - radius: 12, - child: StreamSvgIcon.flag( - size: 14, - color: Colors.white, - ), - ); - case 'imgur': - return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentPrimary, - radius: 12, - child: ClipOval( - child: StreamSvgIcon.imgur( - size: 24, - ), - ), - ); - case 'mute': - return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentPrimary, - radius: 12, - child: StreamSvgIcon.mute( - size: 16, - color: Colors.white, - ), - ); - case 'unban': - return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentPrimary, - radius: 12, - child: StreamSvgIcon.userAdd( - size: 16, - color: Colors.white, - ), - ); - case 'unmute': - return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentPrimary, - radius: 12, - child: StreamSvgIcon.volumeUp( - size: 16, - color: Colors.white, - ), - ); - default: - return CircleAvatar( - backgroundColor: _streamChatTheme.colorTheme.accentPrimary, - radius: 12, - child: StreamSvgIcon.lightning( - size: 16, - color: Colors.white, - ), - ); + Widget _buildMentionsOverlayEntry() { + if (textEditingController.value.selection.start < 0) { + return const Offstage(); } - } - OverlayEntry? _buildMentionsOverlayEntry() { final splits = textEditingController.text .substring(0, textEditingController.value.selection.start) .split('@'); final query = splits.last.toLowerCase(); - Future>? queryMembers; - - final channelState = StreamChannel.of(context); - if (query.isNotEmpty) { - queryMembers = channelState.channel - .queryMembers(filter: Filter.autoComplete('name', query)) - .then((res) => res.members); - } - - final members = channelState.channel.state?.members - .where((m) => m.user?.name.toLowerCase().contains(query) == true) - .toList() ?? - []; - - if (members.isEmpty) { - return null; - } - // ignore: cast_nullable_to_non_nullable - final renderBox = context.findRenderObject() as RenderBox; - final size = renderBox.size; - final child = Card( - margin: const EdgeInsets.all(8), - elevation: 2, - color: _streamChatTheme.colorTheme.barsBg, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - clipBehavior: Clip.hardEdge, - child: Container( - constraints: BoxConstraints.loose(const Size.fromHeight(240)), - decoration: BoxDecoration( - color: _streamChatTheme.colorTheme.barsBg, - ), - child: FutureBuilder>( - future: queryMembers ?? Future.value(members), - initialData: members, - builder: (context, snapshot) => ListView( - padding: const EdgeInsets.all(0), - shrinkWrap: true, - children: [ - const SizedBox( - height: 8, - ), - ...snapshot.data! - .where((it) => it.user != null) - .map( - (m) => Material( - color: _streamChatTheme.colorTheme.barsBg, - child: InkWell( - onTap: () { - if (m.user != null) { - _mentionedUsers.add(m.user!); - } + final renderObject = context.findRenderObject() as RenderBox; - splits[splits.length - 1] = m.user!.name; - final rejoin = splits.join('@'); + var tileBuilder = widget.userMentionsTileBuilder; + if (tileBuilder == null && widget.mentionsTileBuilder != null) { + tileBuilder = (context, user) { + final member = Member( + user: user, + userId: user.id, + createdAt: user.createdAt, + updatedAt: user.updatedAt, + ); + return widget.mentionsTileBuilder!(context, member); + }; + } - textEditingController.value = TextEditingValue( - text: rejoin + - textEditingController.text.substring( - textEditingController.selection.start), - selection: TextSelection.collapsed( - offset: rejoin.length, - ), - ); - _debounce!.cancel(); - _mentionsOverlay?.remove(); - _mentionsOverlay = null; - }, - child: widget.mentionsTileBuilder != null - ? widget.mentionsTileBuilder!(context, m) - : MentionTile(m), - ), - ), - ) - .toList(), - const SizedBox( - height: 8, - ), - ], + return UserMentionsOverlay( + query: query, + mentionAllAppUsers: widget.mentionAllAppUsers, + client: StreamChat.of(context).client, + channel: StreamChannel.of(context).channel, + size: Size(renderObject.size.width - 16, 400), + mentionsTileBuilder: tileBuilder, + onMentionUserTap: (user) { + _mentionedUsers.add(user); + splits[splits.length - 1] = user.name; + final rejoin = splits.join('@'); + + textEditingController.value = TextEditingValue( + text: rejoin + + textEditingController.text + .substring(textEditingController.selection.start), + selection: TextSelection.collapsed( + offset: rejoin.length, ), - ), - ), - ); - return OverlayEntry( - builder: (context) => Positioned( - bottom: size.height + MediaQuery.of(context).viewInsets.bottom, - left: 0, - right: 0, - child: TweenAnimationBuilder( - tween: Tween(begin: 0, end: 1), - duration: const Duration(milliseconds: 300), - curve: Curves.easeInOutExpo, - builder: (context, val, child) => Transform.scale( - scale: val, - child: child, - ), - child: child, - ), - ), + ); + _onChangedDebounced.cancel(); + setState(() => _showMentionsOverlay = false); + }, ); } - OverlayEntry? _buildEmojiOverlay() { + Widget _buildEmojiOverlay() { + if (textEditingController.value.selection.baseOffset < 0) { + return const Offstage(); + } + final splits = textEditingController.text - .substring(0, textEditingController.value.selection.start) + .substring(0, textEditingController.value.selection.baseOffset) .split(':'); + final query = splits.last.toLowerCase(); - - if (query.isEmpty) { - return null; - } - - final emojis = _emojiNames - .where((e) => e.contains(query)) - .map(Emoji.byName) - .where((e) => e != null); - - if (emojis.isEmpty) { - return null; - } - // ignore: cast_nullable_to_non_nullable - final renderBox = context.findRenderObject() as RenderBox; - final size = renderBox.size; + final renderObject = context.findRenderObject() as RenderBox; - return OverlayEntry( - builder: (context) => Positioned( - bottom: size.height + MediaQuery.of(context).viewInsets.bottom, - left: 0, - right: 0, - child: Card( - margin: const EdgeInsets.all(8), - elevation: 2, - color: _streamChatTheme.colorTheme.barsBg, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - ), - clipBehavior: Clip.hardEdge, - child: Container( - constraints: BoxConstraints.loose(const Size.fromHeight(200)), - decoration: BoxDecoration( - boxShadow: const [ - BoxShadow( - spreadRadius: -8, - blurRadius: 5, - offset: Offset(0, -4), - ), - ], - color: _streamChatTheme.colorTheme.barsBg, - ), - child: ListView.builder( - padding: const EdgeInsets.all(0), - shrinkWrap: true, - itemCount: emojis.length + 1, - itemBuilder: (context, i) { - if (i == 0) { - return Padding( - padding: const EdgeInsets.only(left: 8, top: 8), - child: Row( - children: [ - Padding( - padding: - const EdgeInsets.symmetric(horizontal: 8), - child: StreamSvgIcon.smile( - color: _streamChatTheme - .colorTheme.accentPrimary, - ), - ), - Flexible( - child: Text( - context.translations.emojiMatchingQueryText( - query, - ), - style: TextStyle( - color: _streamChatTheme - .colorTheme.textHighEmphasis - .withOpacity(.5), - ), - ), - ) - ], - ), - ); - } - - final emoji = emojis.elementAt(i - 1)!; - final themeData = Theme.of(context); - return ListTile( - title: SubstringHighlight( - text: - // ignore: lines_longer_than_80_chars - "${emoji.char} ${emoji.name!.replaceAll('_', ' ')}", - term: query, - textStyleHighlight: - themeData.textTheme.headline6!.copyWith( - fontSize: 14.5, - fontWeight: FontWeight.bold, - ), - textStyle: themeData.textTheme.headline6!.copyWith( - fontSize: 14.5, - ), - ), - onTap: () { - _chooseEmoji(splits, emoji); - }, - ); - }), - ), - ), - )); + return EmojiOverlay( + size: Size(renderObject.size.width - 16, 200), + query: query, + onEmojiResult: (emoji) { + _chooseEmoji(splits, emoji); + }, + ); } void _chooseEmoji(List splits, Emoji emoji) { @@ -1557,9 +1240,6 @@ class MessageInputState extends State { offset: rejoin.length, ), ); - - _emojiOverlay?.remove(); - _emojiOverlay = null; } void _setCommand(Command c) { @@ -1567,15 +1247,14 @@ class MessageInputState extends State { setState(() { _chosenCommand = c; _commandEnabled = true; + _showCommandsOverlay = false; }); - _commandsOverlay?.remove(); - _commandsOverlay = null; } 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, @@ -1762,7 +1441,7 @@ class MessageInputState extends State { icon: StreamSvgIcon.lightning( color: s.isNotEmpty ? _streamChatTheme.colorTheme.disabled - : (_commandsOverlay != null + : (_showCommandsOverlay ? _messageInputTheme.actionButtonColor : _messageInputTheme.actionButtonIdleColor), ), @@ -1778,19 +1457,9 @@ class MessageInputState extends State { await Future.delayed(const Duration(milliseconds: 300)); } - if (_commandsOverlay == null) { - setState(() { - _commandsOverlay = _buildCommandsOverlayEntry(); - if (_commandsOverlay != null) { - Overlay.of(context)!.insert(_commandsOverlay!); - } - }); - } else { - setState(() { - _commandsOverlay?.remove(); - _commandsOverlay = null; - }); - } + setState(() { + _showCommandsOverlay = !_showCommandsOverlay; + }); }, ); @@ -1812,12 +1481,8 @@ class MessageInputState extends State { ), splashRadius: 24, onPressed: () async { - _emojiOverlay?.remove(); - _emojiOverlay = null; - _commandsOverlay?.remove(); - _commandsOverlay = null; - _mentionsOverlay?.remove(); - _mentionsOverlay = null; + _showCommandsOverlay = false; + _showMentionsOverlay = false; if (_openFilePickerSection) { setState(() => _openFilePickerSection = false); @@ -2103,11 +1768,6 @@ class MessageInputState extends State { _commandEnabled = false; }); - _commandsOverlay?.remove(); - _commandsOverlay = null; - _mentionsOverlay?.remove(); - _mentionsOverlay = null; - Message message; if (widget.editMessage != null) { message = widget.editMessage!.copyWith( @@ -2174,8 +1834,6 @@ class MessageInputState extends State { } } - StreamSubscription? _keyboardListener; - void _showErrorAlert(String description) { showModalBottomSheet( backgroundColor: _streamChatTheme.colorTheme.barsBg, @@ -2249,12 +1907,9 @@ class MessageInputState extends State { @override void dispose() { - _commandsOverlay?.remove(); - _emojiOverlay?.remove(); - _mentionsOverlay?.remove(); - _keyboardListener?.cancel(); textEditingController.dispose(); _stopSlowMode(); + _onChangedDebounced.cancel(); super.dispose(); } @@ -2311,78 +1966,75 @@ class _PickerWidgetState extends State<_PickerWidget> { if (widget.filePickerIndex != 0) { return const Offstage(); } - return RepaintBoundary( - child: FutureBuilder( - future: requestPermission, - builder: (context, snapshot) { - if (!snapshot.hasData) { - return const Center(child: CircularProgressIndicator()); - } + return FutureBuilder( + future: requestPermission, + builder: (context, snapshot) { + if (!snapshot.hasData) { + return const Offstage(); + } - if (snapshot.data!) { - if (widget.containsFile) { - return GestureDetector( - onTap: () { - widget.onAddMoreFilesClick(DefaultAttachmentTypes.file); - }, - child: Container( - constraints: const BoxConstraints.expand(), - color: widget.streamChatTheme.colorTheme.inputBg, - alignment: Alignment.center, + if (snapshot.data!) { + if (widget.containsFile) { + return GestureDetector( + onTap: () { + widget.onAddMoreFilesClick(DefaultAttachmentTypes.file); + }, + child: Container( + constraints: const BoxConstraints.expand(), + color: widget.streamChatTheme.colorTheme.inputBg, + alignment: Alignment.center, + child: Text( + context.translations.addMoreFilesLabel, + style: TextStyle( + color: widget.streamChatTheme.colorTheme.accentPrimary, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + } + return MediaListView( + selectedIds: widget.selectedMedias, + onSelect: widget.onMediaSelected, + ); + } + + return InkWell( + onTap: () async { + PhotoManager.openSetting(); + }, + child: Container( + color: widget.streamChatTheme.colorTheme.inputBg, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + SvgPicture.asset( + 'svgs/icon_picture_empty_state.svg', + package: 'stream_chat_flutter', + height: 140, + color: widget.streamChatTheme.colorTheme.disabled, + ), + Text( + context.translations.enablePhotoAndVideoAccessMessage, + style: widget.streamChatTheme.textTheme.body.copyWith( + color: widget.streamChatTheme.colorTheme.textLowEmphasis), + textAlign: TextAlign.center, + ), + const SizedBox(height: 6), + Center( child: Text( - context.translations.addMoreFilesLabel, - style: TextStyle( + context.translations.allowGalleryAccessMessage, + style: widget.streamChatTheme.textTheme.bodyBold.copyWith( color: widget.streamChatTheme.colorTheme.accentPrimary, - fontWeight: FontWeight.bold, ), ), ), - ); - } - return MediaListView( - selectedIds: widget.selectedMedias, - onSelect: widget.onMediaSelected, - ); - } - - return InkWell( - onTap: () async { - PhotoManager.openSetting(); - }, - child: Container( - color: widget.streamChatTheme.colorTheme.inputBg, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - SvgPicture.asset( - 'svgs/icon_picture_empty_state.svg', - package: 'stream_chat_flutter', - height: 140, - color: widget.streamChatTheme.colorTheme.disabled, - ), - Text( - context.translations.enablePhotoAndVideoAccessMessage, - style: widget.streamChatTheme.textTheme.body.copyWith( - color: - widget.streamChatTheme.colorTheme.textLowEmphasis), - textAlign: TextAlign.center, - ), - const SizedBox(height: 6), - Center( - child: Text( - context.translations.allowGalleryAccessMessage, - style: widget.streamChatTheme.textTheme.bodyBold.copyWith( - color: widget.streamChatTheme.colorTheme.accentPrimary, - ), - ), - ), - ], - ), + ], ), - ); - }, - ), + ), + ); + }, ); } } 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 4d1c0929..56e3d48d 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -169,6 +169,7 @@ class MessageListView extends StatefulWidget { this.messageListController, this.reverse = true, this.paginationLimit = 20, + this.paginationLoadingIndicatorBuilder, }) : super(key: key); /// Function used to build a custom message widget @@ -284,6 +285,9 @@ class MessageListView extends StatefulWidget { /// Use [ChannelListController.paginateData] pagination. final MessageListController? messageListController; + /// Builder used to build the loading indicator shown while paginating. + final WidgetBuilder? paginationLoadingIndicatorBuilder; + @override _MessageListViewState createState() => _MessageListViewState(); } @@ -293,7 +297,6 @@ class _MessageListViewState extends State { void Function(Message)? _onThreadTap; bool _showScrollToBottom = false; late final ItemPositionsListener _itemPositionListener; - late final Stream> _itemPositionStream; int? _messageListLength; StreamChannelState? streamChannel; late StreamChatThemeData _streamTheme; @@ -499,14 +502,18 @@ class _MessageListViewState extends State { return _buildThreadSeparator(); } if (i == itemCount - 3) { - if (widget.headerBuilder == null) { + if (widget.reverse + ? widget.headerBuilder == null + : widget.footerBuilder == null) { if (_isThreadConversation) return const Offstage(); return const SizedBox(height: 52); } return const SizedBox(height: 8); } if (i == 0) { - if (widget.footerBuilder == null) { + if (widget.reverse + ? widget.footerBuilder == null + : widget.headerBuilder == null) { return const SizedBox(height: 30); } return const SizedBox(height: 8); @@ -530,13 +537,13 @@ class _MessageListViewState extends State { ? widget.dateDividerBuilder!( nextMessage.createdAt.toLocal(), ) - : DateDivider( - dateTime: nextMessage.createdAt.toLocal(), + : Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: DateDivider( + dateTime: nextMessage.createdAt.toLocal(), + ), ); - return Padding( - padding: const EdgeInsets.symmetric(vertical: 12), - child: divider, - ); + return divider; } final timeDiff = Jiffy(nextMessage.createdAt.toLocal()).diff( @@ -565,27 +572,42 @@ class _MessageListViewState extends State { } if (i == itemCount - 2) { - return widget.headerBuilder?.call(context) ?? - const Offstage(); + if (widget.reverse) { + return widget.headerBuilder?.call(context) ?? + const Offstage(); + } else { + return widget.footerBuilder?.call(context) ?? + const Offstage(); + } } + final indicatorBuilder = + widget.paginationLoadingIndicatorBuilder; + if (i == itemCount - 3) { - return _buildLoadingIndicator( + return _loadingIndicator( streamChannel!, QueryDirection.top, + indicatorBuilder: indicatorBuilder, ); } if (i == 1) { - return _buildLoadingIndicator( + return _loadingIndicator( streamChannel!, QueryDirection.bottom, + indicatorBuilder: indicatorBuilder, ); } if (i == 0) { - return widget.footerBuilder?.call(context) ?? - const Offstage(); + if (widget.reverse) { + return widget.footerBuilder?.call(context) ?? + const Offstage(); + } else { + return widget.headerBuilder?.call(context) ?? + const Offstage(); + } } const bottomMessageIndex = 2; // 1 -> loader // 0 -> footer @@ -657,7 +679,8 @@ class _MessageListViewState extends State { right: 0, child: BetterStreamBuilder>( initialData: _itemPositionListener.itemPositions.value, - stream: _itemPositionStream, + stream: _valueListenableToStreamAdapter( + _itemPositionListener.itemPositions), comparator: (a, b) { if (a == null || b == null) { return false; @@ -808,15 +831,17 @@ class _MessageListViewState extends State { }, ); - Widget _buildLoadingIndicator( + Widget _loadingIndicator( StreamChannelState streamChannel, - QueryDirection direction, - ) => + QueryDirection direction, { + WidgetBuilder? indicatorBuilder, + }) => _LoadingIndicator( direction: direction, streamTheme: _streamTheme, streamChannel: streamChannel, isThreadConversation: _isThreadConversation, + indicatorBuilder: indicatorBuilder, ); Widget _buildBottomMessage( @@ -996,7 +1021,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) @@ -1185,8 +1210,6 @@ class _MessageListViewState extends State { _scrollController = widget.scrollController ?? ItemScrollController(); _itemPositionListener = widget.itemPositionListener ?? ItemPositionsListener.create(); - _itemPositionStream = - _valueListenableToStreamAdapter(_itemPositionListener.itemPositions); _getOnThreadTap(); super.initState(); @@ -1204,10 +1227,12 @@ class _MessageListViewState extends State { initialAlignment = _initialAlignment; WidgetsBinding.instance!.addPostFrameCallback((timeStamp) { - _scrollController?.jumpTo( - index: initialIndex, - alignment: initialAlignment, - ); + if (_scrollController?.isAttached == true) { + _scrollController?.jumpTo( + index: initialIndex, + alignment: initialAlignment, + ); + } }); _messageNewListener = @@ -1216,8 +1241,9 @@ class _MessageListViewState extends State { _bottomPaginationActive = false; _topPaginationActive = false; } - if (event.message!.user!.id == - streamChannel!.channel.client.state.currentUser!.id) { + if (event.message?.parentId == widget.parentMessage?.id && + event.message!.user!.id == + streamChannel!.channel.client.state.currentUser!.id) { WidgetsBinding.instance!.addPostFrameCallback((_) { _scrollController?.jumpTo( index: 0, @@ -1280,12 +1306,14 @@ class _LoadingIndicator extends StatelessWidget { required this.isThreadConversation, required this.direction, required this.streamChannel, + this.indicatorBuilder, }) : super(key: key); final StreamChatThemeData streamTheme; final bool isThreadConversation; final QueryDirection direction; final StreamChannelState streamChannel; + final WidgetBuilder? indicatorBuilder; @override Widget build(BuildContext context) { @@ -1304,12 +1332,13 @@ class _LoadingIndicator extends StatelessWidget { ), builder: (context, data) { if (!data) return const Offstage(); - return const Center( - child: Padding( - padding: EdgeInsets.all(8), - child: CircularProgressIndicator(), - ), - ); + return indicatorBuilder?.call(context) ?? + const Center( + child: Padding( + padding: EdgeInsets.all(8), + child: CircularProgressIndicator(), + ), + ); }, ); } diff --git a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart index d2c69e4b..e9f2e55e 100644 --- a/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart +++ b/packages/stream_chat_flutter/lib/src/message_reactions_modal.dart @@ -1,6 +1,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/reaction_bubble.dart'; import 'package:stream_chat_flutter/src/reaction_picker.dart'; import 'package:stream_chat_flutter/src/stream_chat.dart'; @@ -8,7 +9,6 @@ import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/src/user_avatar.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; /// Modal widget for displaying message reactions class MessageReactionsModal extends StatelessWidget { diff --git a/packages/stream_chat_flutter/lib/src/message_search_item.dart b/packages/stream_chat_flutter/lib/src/message_search_item.dart index ab41532c..89d43531 100644 --- a/packages/stream_chat_flutter/lib/src/message_search_item.dart +++ b/packages/stream_chat_flutter/lib/src/message_search_item.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; /// It shows the current [Message] preview. /// 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 0fd45ea1..daf34ff4 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 @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/info_tile.dart'; import 'package:stream_chat_flutter/src/message_search_item.dart'; import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; /// Callback called when tapping on a user typedef MessageSearchItemTapCallback = void Function(GetMessageResponse); @@ -30,13 +30,13 @@ typedef EmptyMessageSearchBuilder = Widget Function( /// Widget build(BuildContext context) { /// return Scaffold( /// body: MessageSearchListView( -/// messageQuery: _channelQuery, -/// filters: { -/// 'members': { -/// r'$in': [user.id] -/// } -/// }, -/// paginationParams: PaginationParams(limit: 20), +/// messageQuery: _channelQuery, +/// filters: { +/// 'members': { +/// r'$in': [user.id] +/// } +/// }, +/// limit: 20, /// ), /// ); /// } @@ -58,7 +58,7 @@ class MessageSearchListView extends StatefulWidget { required this.filters, this.messageQuery, this.sortOptions, - this.paginationParams, + this.limit = 30, this.messageFilters, this.separatorBuilder, this.itemBuilder, @@ -89,11 +89,8 @@ class MessageSearchListView extends StatefulWidget { /// Direction can be ascending or descending. final List? sortOptions; - /// Pagination parameters - /// 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; + /// 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 +149,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 41ebcab6..835e2ed9 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -7,6 +7,7 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:flutter_portal/flutter_portal.dart'; import 'package:jiffy/jiffy.dart'; +import 'package:stream_chat_flutter/src/attachment/url_attachment.dart'; import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/image_group.dart'; import 'package:stream_chat_flutter/src/message_action.dart'; @@ -15,7 +16,6 @@ import 'package:stream_chat_flutter/src/message_reactions_modal.dart'; import 'package:stream_chat_flutter/src/quoted_message_widget.dart'; import 'package:stream_chat_flutter/src/reaction_bubble.dart'; import 'package:stream_chat_flutter/src/theme/themes.dart'; -import 'package:stream_chat_flutter/src/url_attachment.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; /// Widget builder for building attachments @@ -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, @@ -567,11 +581,11 @@ class _MessageWidgetState extends State bool get isOnlyEmoji => widget.message.text?.isOnlyEmoji == true; bool get hasNonUrlAttachments => widget.message.attachments - .where((it) => it.ogScrapeUrl == null) + .where((it) => it.titleLink == null || it.type == 'giphy') .isNotEmpty; - bool get hasUrlAttachments => - widget.message.attachments.any((it) => it.ogScrapeUrl != null) == true; + bool get hasUrlAttachments => widget.message.attachments + .any((it) => it.titleLink != null && it.type != 'giphy'); bool get showBottomRow => showThreadReplyIndicator || @@ -984,9 +998,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() ?? @@ -997,6 +1011,7 @@ class _MessageWidgetState extends State urlAttachment: urlAttachment, hostDisplayName: hostDisplayName, textPadding: widget.textPadding, + messageTheme: widget.messageTheme, ); } @@ -1155,7 +1170,9 @@ 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) || + element.type == 'giphy') .forEach((e) { if (attachmentGroups[e.type] == null) { attachmentGroups[e.type!] = []; @@ -1335,7 +1352,7 @@ class _MessageWidgetState extends State } if (hasUrlAttachments) { - return _streamChatTheme.colorTheme.linkBg; + return widget.messageTheme.linkBackgroundColor; } if (isOnlyEmoji) { diff --git a/packages/stream_chat_flutter/lib/src/overlays.dart b/packages/stream_chat_flutter/lib/src/overlays.dart new file mode 100644 index 00000000..8d36be50 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/overlays.dart @@ -0,0 +1,63 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_portal/flutter_portal.dart'; + +/// Class that contains the parameters for building an overlay entry +class OverlayOptions { + /// Constructs a new overlay options object + /// [visible] - the visibility of the overlay + /// [widget] - the widget to be displayed + OverlayOptions({ + required this.visible, + required this.widget, + }); + + /// the visibility of the overlay + final bool visible; + + /// the widget to be displayed + final Widget widget; +} + +/// Widget that renders a single overlay widget from a list of [overlayOptions] +/// It shows the first one that is visible +class MultiOverlay extends StatelessWidget { + /// Constructs a new MultiOverlay widget + /// [overlayOptions] - the list of overlay options + /// [overlayAnchor] - the anchor relative to the overlay + /// [childAnchor] - the anchor relative to the child + /// [child] - the child widget + const MultiOverlay({ + Key? key, + required this.overlayOptions, + required this.child, + required this.overlayAnchor, + required this.childAnchor, + }) : super(key: key); + + /// The list of overlay options + final List overlayOptions; + + /// The child widget + final Widget child; + + /// The anchor relative to the overlay + final Alignment? overlayAnchor; + + /// The anchor relative to the child + final Alignment? childAnchor; + + @override + Widget build(BuildContext context) { + final visibleOverlay = + overlayOptions.firstWhereOrNull((element) => element.visible); + + return PortalEntry( + childAnchor: childAnchor, + portalAnchor: overlayAnchor, + visible: visibleOverlay != null, + portal: visibleOverlay?.widget, + child: child, + ); + } +} 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..f1dc9711 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,8 +280,8 @@ class QuotedMessageWidget extends StatelessWidget { }; Color? _getBackgroundColor(BuildContext context) { - if (_containsScrapeUrl) { - return StreamChatTheme.of(context).colorTheme.linkBg; + if (_containsLinkAttachment) { + return messageTheme.linkBackgroundColor; } return messageTheme.messageBackgroundColor; } diff --git a/packages/stream_chat_flutter/lib/src/stream_chat.dart b/packages/stream_chat_flutter/lib/src/stream_chat.dart index f185d288..af617d34 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat.dart @@ -100,7 +100,9 @@ class StreamChatState extends State { return Theme( data: materialTheme.copyWith( primaryIconTheme: streamTheme.primaryIconTheme, - accentColor: streamTheme.colorTheme.accentPrimary, + colorScheme: materialTheme.colorScheme.copyWith( + secondary: streamTheme.colorTheme.accentPrimary, + ), ), child: StreamChatCore( client: client, diff --git a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart index f7a05d7e..3fb13238 100644 --- a/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart +++ b/packages/stream_chat_flutter/lib/src/stream_chat_theme.dart @@ -131,7 +131,7 @@ class StreamChatThemeData { final defaultTheme = StreamChatThemeData(brightness: theme.brightness); final customizedTheme = StreamChatThemeData.fromColorAndTextTheme( defaultTheme.colorTheme.copyWith( - accentPrimary: theme.accentColor, + accentPrimary: theme.colorScheme.secondary, ), defaultTheme.textTheme, ); @@ -223,6 +223,7 @@ class StreamChatThemeData { messageLinksStyle: TextStyle( color: accentColor, ), + linkBackgroundColor: colorTheme.linkBg, ), otherMessageTheme: MessageThemeData( reactionsBackgroundColor: colorTheme.disabled, @@ -246,6 +247,7 @@ class StreamChatThemeData { width: 32, ), ), + linkBackgroundColor: colorTheme.linkBg, ), messageInputTheme: MessageInputThemeData( borderRadius: BorderRadius.circular(20), diff --git a/packages/stream_chat_flutter/lib/src/theme/message_theme.dart b/packages/stream_chat_flutter/lib/src/theme/message_theme.dart index 023a2917..7bed2ceb 100644 --- a/packages/stream_chat_flutter/lib/src/theme/message_theme.dart +++ b/packages/stream_chat_flutter/lib/src/theme/message_theme.dart @@ -17,6 +17,7 @@ class MessageThemeData with Diagnosticable { this.reactionsMaskColor, this.avatarTheme, this.createdAtStyle, + this.linkBackgroundColor, }); /// Text style for message text @@ -52,6 +53,9 @@ class MessageThemeData with Diagnosticable { /// Theme of the avatar final AvatarThemeData? avatarTheme; + /// Background color for messages with url attachments. + final Color? linkBackgroundColor; + /// Copy with a theme MessageThemeData copyWith({ TextStyle? messageTextStyle, @@ -65,6 +69,7 @@ class MessageThemeData with Diagnosticable { Color? reactionsBackgroundColor, Color? reactionsBorderColor, Color? reactionsMaskColor, + Color? linkBackgroundColor, }) => MessageThemeData( messageTextStyle: messageTextStyle ?? this.messageTextStyle, @@ -80,6 +85,7 @@ class MessageThemeData with Diagnosticable { reactionsBackgroundColor ?? this.reactionsBackgroundColor, reactionsBorderColor: reactionsBorderColor ?? this.reactionsBorderColor, reactionsMaskColor: reactionsMaskColor ?? this.reactionsMaskColor, + linkBackgroundColor: linkBackgroundColor ?? this.linkBackgroundColor, ); /// Linearly interpolate from one [MessageThemeData] to another. @@ -105,6 +111,8 @@ class MessageThemeData with Diagnosticable { reactionsMaskColor: Color.lerp(a.reactionsMaskColor, b.reactionsMaskColor, t), repliesStyle: TextStyle.lerp(a.repliesStyle, b.repliesStyle, t), + linkBackgroundColor: + Color.lerp(a.linkBackgroundColor, b.linkBackgroundColor, t), ); /// Merge with a theme @@ -127,6 +135,7 @@ class MessageThemeData with Diagnosticable { reactionsBackgroundColor: other.reactionsBackgroundColor, reactionsBorderColor: other.reactionsBorderColor, reactionsMaskColor: other.reactionsMaskColor, + linkBackgroundColor: other.linkBackgroundColor, ); } @@ -145,7 +154,8 @@ class MessageThemeData with Diagnosticable { reactionsBackgroundColor == other.reactionsBackgroundColor && reactionsBorderColor == other.reactionsBorderColor && reactionsMaskColor == other.reactionsMaskColor && - avatarTheme == other.avatarTheme; + avatarTheme == other.avatarTheme && + linkBackgroundColor == other.linkBackgroundColor; @override int get hashCode => @@ -159,7 +169,8 @@ class MessageThemeData with Diagnosticable { reactionsBackgroundColor.hashCode ^ reactionsBorderColor.hashCode ^ reactionsMaskColor.hashCode ^ - avatarTheme.hashCode; + avatarTheme.hashCode ^ + linkBackgroundColor.hashCode; @override void debugFillProperties(DiagnosticPropertiesBuilder properties) { @@ -175,6 +186,7 @@ class MessageThemeData with Diagnosticable { ..add(DiagnosticsProperty('avatarTheme', avatarTheme)) ..add(ColorProperty('reactionsBackgroundColor', reactionsBackgroundColor)) ..add(ColorProperty('reactionsBorderColor', reactionsBorderColor)) - ..add(ColorProperty('reactionsMaskColor', reactionsMaskColor)); + ..add(ColorProperty('reactionsMaskColor', reactionsMaskColor)) + ..add(ColorProperty('linkBackgroundColor', linkBackgroundColor)); } } diff --git a/packages/stream_chat_flutter/lib/src/thread_header.dart b/packages/stream_chat_flutter/lib/src/thread_header.dart index 55a270fc..dfe13b41 100644 --- a/packages/stream_chat_flutter/lib/src/thread_header.dart +++ b/packages/stream_chat_flutter/lib/src/thread_header.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/thread_header.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/thread_header_paint.png) @@ -127,10 +128,14 @@ class ThreadHeader extends StatelessWidget implements PreferredSizeWidget { ], ); + final theme = Theme.of(context); return AppBar( automaticallyImplyLeading: false, - textTheme: Theme.of(context).textTheme, - brightness: Theme.of(context).brightness, + toolbarTextStyle: theme.textTheme.bodyText2, + titleTextStyle: theme.textTheme.headline6, + systemOverlayStyle: theme.brightness == Brightness.dark + ? SystemUiOverlayStyle.light + : SystemUiOverlayStyle.dark, elevation: 1, leading: leading ?? (showBackButton diff --git a/packages/stream_chat_flutter/lib/src/typing_indicator.dart b/packages/stream_chat_flutter/lib/src/typing_indicator.dart index 48752f07..5d425e58 100644 --- a/packages/stream_chat_flutter/lib/src/typing_indicator.dart +++ b/packages/stream_chat_flutter/lib/src/typing_indicator.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:lottie/lottie.dart'; -import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// Widget to show the current list of typing users class TypingIndicator extends StatelessWidget { diff --git a/packages/stream_chat_flutter/lib/src/user_avatar.dart b/packages/stream_chat_flutter/lib/src/user_avatar.dart index f9a1fd02..a923011c 100644 --- a/packages/stream_chat_flutter/lib/src/user_avatar.dart +++ b/packages/stream_chat_flutter/lib/src/user_avatar.dart @@ -66,6 +66,12 @@ class UserAvatar extends StatelessWidget { final placeholder = this.placeholder ?? streamChatTheme.placeholderUserImage; + final backupGradientAvatar = ClipRRect( + borderRadius: borderRadius ?? + streamChatTheme.ownMessageTheme.avatarTheme?.borderRadius, + child: streamChatTheme.defaultUserImage(context, user), + ); + Widget avatar = FittedBox( fit: BoxFit.cover, child: Container( @@ -76,8 +82,7 @@ class UserAvatar extends StatelessWidget { fit: BoxFit.cover, filterQuality: FilterQuality.high, imageUrl: user.image!, - errorWidget: (context, __, ___) => - streamChatTheme.defaultUserImage(context, user), + errorWidget: (context, __, ___) => backupGradientAvatar, placeholder: placeholder != null ? (context, __) => placeholder(context, user) : null, @@ -91,11 +96,7 @@ class UserAvatar extends StatelessWidget { ), ), ) - : ClipRRect( - borderRadius: borderRadius ?? - streamChatTheme.ownMessageTheme.avatarTheme?.borderRadius, - child: streamChatTheme.defaultUserImage(context, user), - ), + : backupGradientAvatar, ), ); diff --git a/packages/stream_chat_flutter/lib/src/user_item.dart b/packages/stream_chat_flutter/lib/src/user_item.dart index a55693aa..a2622385 100644 --- a/packages/stream_chat_flutter/lib/src/user_item.dart +++ b/packages/stream_chat_flutter/lib/src/user_item.dart @@ -1,10 +1,10 @@ import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/user_list_view.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; /// /// It shows the current [User] preview. 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 eb45f918..f7c9fa72 100644 --- a/packages/stream_chat_flutter/lib/src/user_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/user_list_view.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; /// Callback called when tapping on a user typedef UserTapCallback = void Function(User, Widget?); @@ -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, + @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,12 +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. - final Filter? filter; + final Filter filter; /// The sorting used for the channels matching the filters. /// Sorting is based on field and direction, multiple sorting options can @@ -93,8 +99,15 @@ 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 + @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] /// with the widget [userWidget] as child. @@ -184,7 +197,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/user_mention_tile.dart b/packages/stream_chat_flutter/lib/src/user_mention_tile.dart new file mode 100644 index 00000000..ff9e145b --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/user_mention_tile.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +/// This widget is used for showing user tiles for mentions +/// Use [title], [subtitle], [leading], [trailing] for +/// substituting widgets in respective positions +class UserMentionTile extends StatelessWidget { + /// Constructor for creating a [UserMentionTile] widget + const UserMentionTile( + this.user, { + Key? key, + this.title, + this.subtitle, + this.leading, + this.trailing, + }) : super(key: key); + + /// User to display in the tile + final User user; + + /// Widget to display as title + final Widget? title; + + /// Widget to display below [title] + final Widget? subtitle; + + /// Widget at the start of the tile + final Widget? leading; + + /// Widget at the end of tile + final Widget? trailing; + + @override + Widget build(BuildContext context) { + final chatThemeData = StreamChatTheme.of(context); + return SizedBox( + height: 56, + child: Row( + children: [ + const SizedBox( + width: 16, + ), + leading ?? + UserAvatar( + user: user, + constraints: BoxConstraints.tight(const Size(40, 40)), + ), + const SizedBox(width: 8), + Expanded( + child: Align( + alignment: Alignment.centerLeft, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + title ?? + Text( + user.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: chatThemeData.textTheme.bodyBold, + ), + const SizedBox(height: 2), + subtitle ?? + Text( + '@${user.id}', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: chatThemeData.textTheme.footnoteBold.copyWith( + color: chatThemeData.colorTheme.textLowEmphasis, + ), + ), + ], + ), + ), + ), + trailing ?? + Padding( + padding: const EdgeInsets.only( + right: 18, + left: 8, + ), + child: StreamSvgIcon.mentions( + color: chatThemeData.colorTheme.accentPrimary, + ), + ), + ], + ), + ); + } +} diff --git a/packages/stream_chat_flutter/lib/src/user_mentions_overlay.dart b/packages/stream_chat_flutter/lib/src/user_mentions_overlay.dart new file mode 100644 index 00000000..e3830231 --- /dev/null +++ b/packages/stream_chat_flutter/lib/src/user_mentions_overlay.dart @@ -0,0 +1,191 @@ +import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; +import 'package:stream_chat_flutter/src/user_mention_tile.dart'; +import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; + +/// Builder function for building a mention tile. +/// +/// Use [UserMentionTile] for the default implementation. +typedef MentionTileBuilder = Widget Function( + BuildContext context, + User user, +); + +/// Overlay for displaying users that can be mentioned. +class UserMentionsOverlay extends StatefulWidget { + /// Constructor for creating a [UserMentionsOverlay]. + UserMentionsOverlay({ + Key? key, + required this.query, + required this.channel, + required this.size, + this.client, + this.limit = 10, + this.mentionAllAppUsers = false, + this.mentionsTileBuilder, + this.onMentionUserTap, + }) : assert( + channel.state != null, + 'Channel ${channel.cid} is not yet initialized', + ), + assert( + !mentionAllAppUsers || (mentionAllAppUsers && client != null), + 'StreamChatClient is required in order to use mentionAllAppUsers', + ), + super(key: key); + + /// Query for searching users. + final String query; + + /// Limit applied on user search results. + final int limit; + + /// The size of the overlay. + final Size size; + + /// The channel to search for users. + final Channel channel; + + /// The client to search for users in case [mentionAllAppUsers] is True. + final StreamChatClient? client; + + /// When enabled mentions search users across the entire app. + /// + /// Defaults to false. + final bool mentionAllAppUsers; + + /// Customize the tile for the mentions overlay. + final MentionTileBuilder? mentionsTileBuilder; + + /// Callback called when a user is selected. + final void Function(User user)? onMentionUserTap; + + @override + _UserMentionsOverlayState createState() => _UserMentionsOverlayState(); +} + +class _UserMentionsOverlayState extends State { + late Future> userMentionsFuture; + + @override + void initState() { + super.initState(); + userMentionsFuture = queryMentions(widget.query); + } + + @override + void didUpdateWidget(covariant UserMentionsOverlay oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.channel != oldWidget.channel || + widget.query != oldWidget.query || + widget.mentionAllAppUsers != oldWidget.mentionAllAppUsers || + widget.limit != oldWidget.limit) { + userMentionsFuture = queryMentions(widget.query); + } + } + + @override + Widget build(BuildContext context) { + final theme = StreamChatTheme.of(context); + return Card( + margin: const EdgeInsets.all(8), + elevation: 2, + color: theme.colorTheme.barsBg, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + clipBehavior: Clip.hardEdge, + child: Container( + constraints: BoxConstraints.loose(widget.size), + decoration: BoxDecoration(color: theme.colorTheme.barsBg), + child: FutureBuilder>( + future: userMentionsFuture, + builder: (context, snapshot) { + if (snapshot.hasError) return const Offstage(); + if (!snapshot.hasData) return const Offstage(); + final users = snapshot.data!; + return ListView.builder( + padding: const EdgeInsets.all(0), + shrinkWrap: true, + itemCount: users.length, + itemBuilder: (context, index) { + final user = users[index]; + return Material( + color: theme.colorTheme.barsBg, + child: InkWell( + onTap: () => widget.onMentionUserTap?.call(user), + child: widget.mentionsTileBuilder?.call(context, user) ?? + UserMentionTile(user), + ), + ); + }, + ); + }, + ), + ), + ); + } + + List get membersAndWatchers { + final state = widget.channel.state!; + return { + ...state.watchers, + ...state.members.map((it) => it.user), + }.whereType().toList(growable: false); + } + + Future> queryMentions(String query) async { + if (widget.mentionAllAppUsers) { + return _queryUsers(query); + } + + var channelState = widget.channel.state; + + channelState = channelState!; + final members = channelState.members; + + // By default, we return maximum 100 members via queryChannels api call. + // Thus it is safe to assume, that if number of members in channel.state + // is < 100, then all the members are already available on client side + // and we don't need to make any api call to queryMembers endpoint. + if (members.length < 100) { + final matchingUsers = membersAndWatchers.search(query); + return matchingUsers.toList(growable: false); + } + + final result = await _queryMembers(query); + return result + .map((it) => it.user) + .whereType() + .toList(growable: false); + } + + Future> _queryMembers(String query) async { + final response = await widget.channel.queryMembers( + pagination: PaginationParams(limit: widget.limit), + filter: query.isEmpty + ? const Filter.empty() + : Filter.autoComplete('name', query), + ); + return response.members; + } + + Future> _queryUsers(String query) async { + assert( + widget.client != null, + 'StreamChatClient is required in order to query all app users', + ); + final response = await widget.client!.queryUsers( + pagination: PaginationParams(limit: widget.limit), + filter: query.isEmpty + ? const Filter.empty() + : Filter.or([ + Filter.autoComplete('id', query), + Filter.autoComplete('name', query), + ]), + sort: [const SortOption('id', direction: SortOption.ASC)], + ); + return response.users; + } +} diff --git a/packages/stream_chat_flutter/lib/src/utils.dart b/packages/stream_chat_flutter/lib/src/utils.dart index d97ea6a1..9b85e8c2 100644 --- a/packages/stream_chat_flutter/lib/src/utils.dart +++ b/packages/stream_chat_flutter/lib/src/utils.dart @@ -1,14 +1,15 @@ import 'dart:async'; +import 'dart:math' as math; import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; 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( @@ -388,3 +389,39 @@ class Tuple2 { @override int get hashCode => item1.hashCode ^ item2.hashCode; } + +/// Levenshtein algorithm implementation based on: +/// http://en.wikipedia.org/wiki/Levenshtein_distance#Iterative_with_two_matrix_rows +int levenshtein(String s, String t, {bool caseSensitive = true}) { + if (!caseSensitive) { + // ignore: parameter_assignments + s = s.toLowerCase(); + // ignore: parameter_assignments + t = t.toLowerCase(); + } + if (s == t) return 0; + if (s.isEmpty) return t.length; + if (t.isEmpty) return s.length; + + final v0 = List.filled(t.length + 1, 0); + final v1 = List.filled(t.length + 1, 0); + + for (var i = 0; i < t.length + 1; i < i++) { + v0[i] = i; + } + + for (var i = 0; i < s.length; i++) { + v1[0] = i + 1; + + for (var j = 0; j < t.length; j++) { + final cost = (s[i] == t[j]) ? 0 : 1; + v1[j + 1] = math.min(v1[j] + 1, math.min(v0[j + 1] + 1, v0[j] + cost)); + } + + for (var j = 0; j < t.length + 1; j++) { + v0[j] = v1[j]; + } + } + + return v1[t.length]; +} diff --git a/packages/stream_chat_flutter/lib/src/visible_footnote.dart b/packages/stream_chat_flutter/lib/src/visible_footnote.dart index 8d16ce41..18bb976d 100644 --- a/packages/stream_chat_flutter/lib/src/visible_footnote.dart +++ b/packages/stream_chat_flutter/lib/src/visible_footnote.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; -import 'package:stream_chat_flutter/src/extension.dart'; /// Widget for displaying a footnote class VisibleFootnote extends StatelessWidget { diff --git a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart index 42188fa0..0447fc68 100644 --- a/packages/stream_chat_flutter/lib/stream_chat_flutter.dart +++ b/packages/stream_chat_flutter/lib/stream_chat_flutter.dart @@ -44,5 +44,6 @@ export 'src/unread_indicator.dart'; export 'src/user_avatar.dart'; export 'src/user_item.dart'; export 'src/user_list_view.dart'; +export 'src/user_mention_tile.dart'; export 'src/utils.dart'; export 'src/visible_footnote.dart'; diff --git a/packages/stream_chat_flutter/pubspec.yaml b/packages/stream_chat_flutter/pubspec.yaml index 94ab5fbb..031a8f2e 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -1,12 +1,12 @@ name: stream_chat_flutter homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK. Build your own chat experience using Dart and Flutter. -version: 2.2.1 +version: 3.1.1 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues environment: - sdk: '>=2.12.0 <3.0.0' + sdk: '>=2.14.0 <3.0.0' flutter: ">=1.17.0" dependencies: @@ -14,18 +14,18 @@ dependencies: characters: ^1.1.0 chewie: ^1.2.0 collection: ^1.15.0 + diacritic: ^0.1.3 dio: ^4.0.0 ezanimation: ^0.5.0 - file_picker: ^3.0.1 + file_picker: ^4.1.3 flutter: sdk: flutter - flutter_keyboard_visibility: ^5.0.1 flutter_markdown: ^0.6.1 flutter_portal: ^0.4.0 flutter_slidable: ^0.6.0 flutter_svg: ^0.22.0 http_parser: ^4.0.0 - image_gallery_saver: ^1.6.9 + image_gallery_saver: ^1.7.0 image_picker: ^0.8.2 jiffy: ^4.1.0 lottie: ^1.0.1 @@ -41,13 +41,13 @@ dependencies: path: packages/scrollable_positioned_list share_plus: ^2.0.3 shimmer: ^2.0.0 - stream_chat_flutter_core: ^2.2.1 + stream_chat_flutter_core: ^3.1.1 substring_highlight: ^1.0.26 synchronized: ^3.0.0 url_launcher: ^6.0.3 video_compress: ^3.0.0 video_player: ^2.1.0 - video_thumbnail: ^0.3.3 + video_thumbnail: ^0.4.3 visibility_detector: ^0.2.0 flutter: @@ -61,7 +61,7 @@ flutter: dev_dependencies: flutter_test: sdk: flutter - golden_toolkit: ^0.9.0 + golden_toolkit: ^0.10.0 mocktail: ^0.1.2 path: ^1.8.0 pedantic: ^1.11.0 diff --git a/packages/stream_chat_flutter/test/src/emoji_test.dart b/packages/stream_chat_flutter/test/src/emoji_test.dart new file mode 100644 index 00000000..ce63faa9 --- /dev/null +++ b/packages/stream_chat_flutter/test/src/emoji_test.dart @@ -0,0 +1,79 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/src/emoji/emoji.dart'; + +void main() { + group('src/emoji', () { + test('${Emoji.byShortName} should bring the correct emoji', () { + final resultEmoji = Emoji.byShortName('smiley'); + expect(resultEmoji, _mockEmoji); + }); + }); +} + +final _mockEmoji = Emoji( + name: 'grinning face with big eyes', + char: '\u{1F603}', + shortName: 'smiley', + emojiGroup: EmojiGroup.smileysEmotion, + emojiSubgroup: EmojiSubgroup.faceSmiling, + keywords: [ + 'face', + 'mouth', + 'open', + 'smile', + 'uc6', + 'smiley', + 'happy', + 'silly', + 'laugh', + 'good', + 'smile', + 'teeth', + 'fun', + 'smileys', + 'mood', + 'emotion', + 'emotions', + 'emotional', + 'hooray', + 'cheek', + 'cheeky', + 'excited', + 'feliz', + 'heureux', + 'cheerful', + 'delighted', + 'ecstatic', + 'elated', + 'glad', + 'joy', + 'merry', + 'funny', + 'laughing', + 'lol', + 'rofl', + 'lmao', + 'lmfao', + 'hilarious', + 'ha', + 'haha', + 'chuckle', + 'comedy', + 'giggle', + 'hehe', + 'joyful', + 'laugh out loud', + 'rire', + 'tee hee', + 'jaja', + 'good job', + 'nice', + 'well done', + 'bravo', + 'congratulations', + 'congrats', + 'smiles', + 'dentist', + ':-D', + '=D' + ]); diff --git a/packages/stream_chat_flutter/test/src/extension_test.dart b/packages/stream_chat_flutter/test/src/extension_test.dart new file mode 100644 index 00000000..696a19fc --- /dev/null +++ b/packages/stream_chat_flutter/test/src/extension_test.dart @@ -0,0 +1,70 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat_flutter/src/extension.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +void main() { + group('List.search', () { + test('should work fine', () { + final tommaso = User(id: 'tommaso', name: 'Tommaso'); + final thierry = User(id: 'thierry', name: 'Thierry'); + final users = [tommaso, thierry]; + + expect(users.search('Tom'), [tommaso]); + expect(users.search('Thier'), [thierry]); + }); + + test('should search using UpperCased', () { + final tommaso = User(id: 'tommaso', name: 'Tommaso'); + final thierry = User(id: 'thierry', name: 'Thierry'); + final users = [tommaso, thierry]; + + expect(users.search('tom'), [tommaso]); + expect(users.search('thier'), [thierry]); + }); + + test('should search by .id or .name', () { + final user1 = User(id: 'searchingThis'); + final user2 = User(id: 'x', name: 'searchingThis'); + + expect([user1].search('sear'), [user1]); + expect([user2].search('sear'), [user2]); + }); + + test('should search transliterated', () { + final tommaso = User(id: 'tommaso', name: 'Tommaso'); + final thierry = User(id: 'thierry', name: 'Thierry'); + final users = [tommaso, thierry]; + + expect(users.search('tóm'), [tommaso]); + expect(users.search('thíer'), [thierry]); + }); + + test('search and sorted by distance', () { + final tommaso = User(id: 'tommaso', name: 'Tommaso'); + final tomas = User(id: 'tomas', name: 'Tomas'); + final users = [tommaso, tomas]; + + expect(users.search('tom'), [tomas, tommaso]); + }); + + test('should work fine with cyrillic diacritics', () { + final petyo = User(id: '42', name: 'Петьо'); + final anastasia = User(id: '13', name: 'Анастасiя'); + final dmitriy = User(id: '99', name: 'Дмитрий'); + final users = [petyo, anastasia, dmitriy]; + + expect(users.search('petyo'), []); + expect(users.search('Пе'), [petyo]); + expect(users.search('Ана'), [anastasia]); + expect(users.search('Дмитри'), [dmitriy]); + expect(users.search('Дмитрии'), [dmitriy]); + }); + + test('should work fine with french diacritics', () { + final user = User(id: 'fra', name: 'françois'); + + expect([user].search('françois'), [user]); + expect([user].search('franc'), [user]); + }); + }); +} diff --git a/packages/stream_chat_flutter/test/src/message_action_modal_test.dart b/packages/stream_chat_flutter/test/src/message_action_modal_test.dart index 38518b19..9210ac49 100644 --- a/packages/stream_chat_flutter/test/src/message_action_modal_test.dart +++ b/packages/stream_chat_flutter/test/src/message_action_modal_test.dart @@ -263,9 +263,11 @@ void main() { final client = MockClient(); final clientState = MockClientState(); final channel = MockChannel(); + final channelState = MockChannelState(); when(() => client.state).thenReturn(clientState); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); + when(() => channel.state).thenReturn(channelState); final themeData = ThemeData(); final streamTheme = StreamChatThemeData.fromTheme(themeData); diff --git a/packages/stream_chat_flutter/test/src/message_list_view_test.dart b/packages/stream_chat_flutter/test/src/message_list_view_test.dart new file mode 100644 index 00000000..8217f6de --- /dev/null +++ b/packages/stream_chat_flutter/test/src/message_list_view_test.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; + +import 'mocks.dart'; + +void main() { + late StreamChatClient client; + late Channel channel; + + setUp(() { + client = MockClient(); + channel = MockChannel(); + when(() => channel.on(any(), any(), any(), any())) + .thenAnswer((_) => const Stream.empty()); + final channelClientState = MockChannelState(); + when(() => channel.state).thenReturn(channelClientState); + when(() => channelClientState.threadsStream) + .thenAnswer((_) => const Stream.empty()); + when(() => channelClientState.messagesStream) + .thenAnswer((_) => const Stream.empty()); + when(() => channelClientState.messages).thenReturn([]); + when(() => channelClientState.isUpToDate).thenReturn(true); + }); + + // https://github.com/GetStream/stream-chat-flutter/issues/674 + testWidgets('renders empty message list view', (tester) async { + const emptyWidgetKey = Key('empty_widget'); + + await tester.pumpWidget( + MaterialApp( + home: StreamChat( + client: client, + child: StreamChannel( + channel: channel, + child: MessageListView( + emptyBuilder: (_) => Container(key: emptyWidgetKey), + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect(find.byType(MessageListView), findsOneWidget); + expect(find.byKey(emptyWidgetKey), findsOneWidget); + }); +} 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/gallery_footer_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart index fb8d7035..6a73e34e 100644 --- a/packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/gallery_footer_theme_test.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart' hide TextTheme; import 'package:flutter_test/flutter_test.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; class MockStreamChatClient extends Mock implements StreamChatClient {} diff --git a/packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart index aefbb319..534008ba 100644 --- a/packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/gallery_header_theme_test.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat_flutter/stream_chat_flutter.dart'; class MockStreamChatClient extends Mock implements StreamChatClient {} diff --git a/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart index 4da1c7dc..69c63abe 100644 --- a/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_search_list_view_theme_test.dart @@ -69,6 +69,7 @@ void main() { body: MessageSearchBloc( child: MessageSearchListView( filters: Filter.in_('members', const ['test_id']), + messageQuery: 'test query', ), ), ); @@ -100,6 +101,7 @@ void main() { body: MessageSearchBloc( child: MessageSearchListView( filters: Filter.in_('members', const ['test_id']), + messageQuery: 'test query', ), ), ); diff --git a/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart b/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart index 5908d864..502be84a 100644 --- a/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart +++ b/packages/stream_chat_flutter/test/src/theme/message_theme_test.dart @@ -59,6 +59,7 @@ final _messageThemeControl = MessageThemeData( messageLinksStyle: TextStyle( color: ColorTheme.light().accentPrimary, ), + linkBackgroundColor: ColorTheme.light().linkBg, ); final _messageThemeControlDark = MessageThemeData( @@ -87,4 +88,5 @@ final _messageThemeControlDark = MessageThemeData( messageLinksStyle: TextStyle( color: ColorTheme.dark().accentPrimary, ), + linkBackgroundColor: ColorTheme.dark().linkBg, ); 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 1378ced0..96bae0c7 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,3 +1,53 @@ +## 3.1.1 + +- Updated `stream_chat` dependency to [`3.1.1`](https://pub.dev/packages/stream_chat/changelog). + +## 3.0.0 + +- Updated `stream_chat` dependency to [`3.0.0`](https://pub.dev/packages/stream_chat/changelog). + +🛑️ Breaking Changes from `2.2.1` + +- `MessageSearchListViewCore` `paginationParams` property is now deprecated in favor of `limit`. + ```dart + // previous + paginationParams = const PaginationParams(limit: 30) + + // new + limit = 30 + ``` +- `UserListCore` `pagination` property is now deprecated in favor of `limit`. + ```dart + // previous + pagination = const PaginationParams(limit: 30) + + // new + limit = 30 + ``` +- `ChannelListCore` `pagination` property is now deprecated in favor of `limit`. + ```dart + // previous + pagination = const PaginationParams(limit: 30) + + // new + limit = 30 + ``` + +- `UserListCore` `filter` property now is non-nullable. + +🔄 Changed + +- `UserListCore` 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 - Updated `stream_chat` dependency to 2.2.1 @@ -5,13 +55,17 @@ ## 2.2.0 🛑️ Breaking Changes from `2.1.1` + - Renamed `BetterStreamBuilder.loadingBuilder` to `.noDataBuilder` 🔄 Changed + - `BetterStreamBuilder.initialData` is now nullable/not-required. 🐞 Fixed -- [#612](https://github.com/GetStream/stream-chat-flutter/issues/612) `ChannelListView` pagination doesn't work after refresh + +- [#612](https://github.com/GetStream/stream-chat-flutter/issues/612) `ChannelListView` pagination doesn't work after + refresh ## 2.1.1 @@ -20,12 +74,15 @@ ## 2.1.0 🛑️ Breaking Changes from `2.0.0` + - Changed default message filter of `MessageListCore` ✅ Added + - Added `MessageListCore.paginationLimit` 🔄 Changed + - `StreamChatCore.of(context).user` is now deprecated in favor of `StreamChatCore.of(context).currentUser`. - `StreamChatCore.of(context).userStream` is now deprecated in favor of `StreamChatCore.of(context).currentUserStream`. @@ -34,7 +91,8 @@ 🛑️ Breaking Changes from `1.5.3` - migrate this package to null safety -- `channelsBloc.queryChannels()`, `ChannelListCore` options param/property is removed in favor of individual params/properties +- `channelsBloc.queryChannels()`, `ChannelListCore` options param/property is removed in favor of individual + params/properties - `options.state` -> bool state - `options.watch` -> bool watch - `options.presence` -> bool presence @@ -51,6 +109,7 @@ - Performance improvements ## 2.0.0-nullsafety.9 + - Update llc dependency ## 2.0.0-nullsafety.8 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..8ac28e76 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 @@ -69,10 +69,8 @@ class ChannelListCore extends StatefulWidget { this.memberLimit, this.messageLimit, this.sort, - this.pagination = const PaginationParams( - limit: 25, - ), this.channelListController, + this.limit = 25, }) : super(key: key); /// A [ChannelListController] allows reloading and pagination. @@ -120,11 +118,8 @@ class ChannelListCore extends StatefulWidget { /// Number of messages to fetch in each channel final int? messageLimit; - /// Pagination parameters - /// 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; + /// The amount of channels requested per API call. + final int limit; @override ChannelListCoreState createState() => ChannelListCoreState(); @@ -162,7 +157,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 +169,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 +210,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/channels_bloc.dart b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart index 6687eef5..1aed7198 100644 --- a/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/channels_bloc.dart @@ -105,7 +105,8 @@ class ChannelsBlocState extends State }) async { final client = _streamChatCoreState!.client; - final clear = paginationParams.offset == 0; + final offset = paginationParams.offset; + final clear = offset == null || offset == 0; if (clear && _paginationEnded) { _paginationEnded = false; } diff --git a/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart b/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart index 44ac9b3f..6477dfcd 100644 --- a/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/message_search_bloc.dart @@ -43,6 +43,12 @@ class MessageSearchBlocState extends State with AutomaticKeepAliveClientMixin { late StreamChatCoreState _streamChatCoreState; + /// The key used to paginate next items. + String? nextId; + + /// The key used to paginate previous items. + String? previousId; + /// The current messages list List? get messageResponses => _messageResponses.valueOrNull; @@ -59,6 +65,8 @@ class MessageSearchBlocState extends State Stream get queryMessagesLoading => _queryMessagesLoadingController.stream; + bool _paginationEnded = false; + /// Calls [StreamChatClient.search] updating /// [messagesStream] and [queryMessagesLoading] stream Future search({ @@ -66,21 +74,34 @@ class MessageSearchBlocState extends State Filter? messageFilter, List? sort, String? query, - PaginationParams? pagination, + PaginationParams pagination = const PaginationParams(limit: 30), }) async { final client = _streamChatCoreState.client; - if (_queryMessagesLoadingController.value == true) return; + var clear = false; + if (sort != null) { + clear |= pagination.next == null; + } else { + final offset = pagination.offset; + clear |= offset == null || offset == 0; + } + + if (clear && _paginationEnded) { + _paginationEnded = false; + } + + if ((!clear && _paginationEnded) || + _queryMessagesLoadingController.value == true) { + return; + } if (_messageResponses.hasValue) { _queryMessagesLoadingController.add(true); } try { - final clear = pagination == null || pagination.offset == 0; - final oldMessages = List.from(messageResponses ?? []); - final messages = await client.search( + final response = await client.search( filter, sort: sort, query: query, @@ -88,15 +109,29 @@ class MessageSearchBlocState extends State messageFilters: messageFilter, ); + final next = response.next; + final previous = response.previous; + + nextId = next != null && next.isNotEmpty + ? next + : /*reset nextId if we get nothing*/ null; + previousId = previous != null && previous.isNotEmpty + ? previous + : /*reset previousId if we get nothing*/ null; + + final newMessages = response.results; if (clear) { - _messageResponses.add(messages.results); + _messageResponses.add(newMessages); } else { - final temp = oldMessages + messages.results; + final temp = oldMessages + newMessages; _messageResponses.add(temp); } if (_messageResponses.hasValue && _queryMessagesLoadingController.value) { _queryMessagesLoadingController.add(false); } + if (newMessages.isEmpty || newMessages.length < pagination.limit) { + _paginationEnded = true; + } } catch (e, stk) { // reset loading controller _queryMessagesLoadingController.add(false); 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 234b5c9a..565743f2 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 @@ -18,9 +18,9 @@ import 'package:stream_chat_flutter_core/src/typedef.dart'; /// Widget build(BuildContext context) { /// return Scaffold( /// body: MessageSearchListCore( -/// messageQuery: _messageFilter, -/// filters: _channelsFilter, -/// paginationParams: PaginationParams(limit: 20), +/// messageQuery: _messageFilter, +/// filters: _channelsFilter, +/// limit: 20, /// ), /// ); /// } @@ -38,7 +38,7 @@ class MessageSearchListCore extends StatefulWidget { /// * [errorBuilder] /// * [loadingBuilder] /// * [childBuilder] - const MessageSearchListCore({ + MessageSearchListCore({ Key? key, required this.emptyBuilder, required this.errorBuilder, @@ -47,10 +47,30 @@ class MessageSearchListCore extends StatefulWidget { required this.filters, this.messageQuery, this.sortOptions, - this.paginationParams, + @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, - }) : super(key: key); + int? limit, + }) : assert( + messageQuery != null || messageFilters != null, + 'Provide at least `query` or `messageFilters`', + ), + assert( + 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. /// Use [MessageSearchListController.loadData] and @@ -74,11 +94,17 @@ class MessageSearchListCore extends StatefulWidget { final List? sortOptions; /// Pagination parameters - /// limit: the number of users to return (max is 30) + /// limit: the number of messages to return (max is 30) /// offset: the offset (max is 1000) - /// message_limit: how many messages should be included to each channel + @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]. /// You can also filter other built-in channel fields. @@ -150,31 +176,40 @@ 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() => _messageSearchBloc!.search( - filter: widget.filters, - sort: widget.sortOptions, - pagination: widget.paginationParams!.copyWith( - offset: _messageSearchBloc!.messageResponses?.length ?? 0, - ), - query: widget.messageQuery, - messageFilter: widget.messageFilters, + Future paginateData() { + var pagination = PaginationParams(limit: widget.limit); + if (widget.sortOptions != null) { + pagination = pagination.copyWith( + next: _messageSearchBloc?.nextId, ); + } else { + pagination = pagination.copyWith( + offset: _messageSearchBloc?.messageResponses?.length, + ); + } + return _messageSearchBloc!.search( + filter: widget.filters, + sort: widget.sortOptions, + pagination: pagination, + query: widget.messageQuery, + messageFilter: widget.messageFilters, + ); + } @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/stream_channel.dart b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart index fb7b79f2..443529ac 100644 --- a/packages/stream_chat_flutter_core/lib/src/stream_channel.dart +++ b/packages/stream_chat_flutter_core/lib/src/stream_channel.dart @@ -152,13 +152,9 @@ class StreamChannelState extends State { int limit = 20, }) { if (direction == QueryDirection.top) { - return _queryTopMessages( - limit: limit, - ); + return _queryTopMessages(limit: limit); } - return _queryBottomMessages( - limit: limit, - ); + return _queryBottomMessages(limit: limit); } /// Calls [channel.getReplies] updating [queryMessage] stream @@ -311,6 +307,20 @@ class StreamChannelState extends State { return message; } + /// Query channel members. + Future> queryMembers({ + Filter? filter, + List? sort, + PaginationParams? pagination, + }) async { + final response = await channel.queryMembers( + filter: filter, + sort: sort, + pagination: pagination, + ); + return response.members; + } + /// Reloads the channel with latest message Future reloadChannel() => _queryAtMessage(before: 30); 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 57d61fe6..05e9730b 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 @@ -63,12 +63,12 @@ class UserListCore extends StatefulWidget { required this.loadingBuilder, required this.listBuilder, Key? key, - this.filter, + this.filter = const Filter.empty(), this.sort, this.presence, - this.pagination, this.groupAlphabetically = false, this.userListController, + this.limit = 30, }) : super(key: key); /// A [UserListController] allows reloading and pagination. @@ -91,7 +91,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. - final Filter? filter; + final Filter filter; /// The sorting used for the channels matching the filters. /// Sorting is based on field and direction, multiple sorting options can be @@ -102,11 +102,8 @@ class UserListCore extends StatefulWidget { /// If true you’ll receive user presence updates via the websocket events final bool? presence; - /// Pagination parameters - /// 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; + /// The amount of users requested per API call. + final int limit; /// Set it to true to group users by their first character /// @@ -193,7 +190,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 +198,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 +207,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/lib/src/users_bloc.dart b/packages/stream_chat_flutter_core/lib/src/users_bloc.dart index e7916478..d112961d 100644 --- a/packages/stream_chat_flutter_core/lib/src/users_bloc.dart +++ b/packages/stream_chat_flutter_core/lib/src/users_bloc.dart @@ -57,6 +57,8 @@ class UsersBlocState extends State late StreamChatCoreState _streamChatCore; + bool _paginationEnded = false; + /// The Query Users method allows you to search for users and see if they are /// online/offline. /// [API Reference](https://getstream.io/chat/docs/flutter-dart/query_users/?language=dart) @@ -64,19 +66,27 @@ class UsersBlocState extends State Filter? filter, List? sort, bool? presence, - PaginationParams? pagination, + PaginationParams pagination = const PaginationParams(limit: 30), }) async { final client = _streamChatCore.client; - if (_queryUsersLoadingController.value == true) return; + final offset = pagination.offset; + final clear = offset == null || offset == 0; + + if (clear && _paginationEnded) { + _paginationEnded = false; + } + + if ((!clear && _paginationEnded) || + _queryUsersLoadingController.value == true) { + return; + } if (_usersController.hasValue) { _queryUsersLoadingController.add(true); } try { - final clear = pagination == null || pagination.offset == 0; - final oldUsers = List.from(users ?? []); final usersResponse = await client.queryUsers( @@ -86,6 +96,7 @@ class UsersBlocState extends State pagination: pagination, ); + final newUsers = usersResponse.users; if (clear) { _usersController.add(usersResponse.users); } else { @@ -95,6 +106,9 @@ class UsersBlocState extends State if (_usersController.hasValue && _queryUsersLoadingController.value) { _queryUsersLoadingController.add(false); } + if (newUsers.isEmpty || newUsers.length < pagination.limit) { + _paginationEnded = true; + } } catch (e, stk) { // reset loading controller _queryUsersLoadingController.add(false); diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index 85b57124..128c58fb 100644 --- a/packages/stream_chat_flutter_core/pubspec.yaml +++ b/packages/stream_chat_flutter_core/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_flutter_core homepage: https://github.com/GetStream/stream-chat-flutter description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and Flutter. -version: 2.2.1 +version: 3.1.1 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -16,7 +16,7 @@ dependencies: sdk: flutter meta: ^1.3.0 rxdart: ^0.27.0 - stream_chat: ^2.2.1 + stream_chat: ^3.1.1 dev_dependencies: fake_async: ^1.2.0 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..8407be67 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 @@ -129,7 +129,7 @@ void main() { emptyBuilder: (BuildContext context) => const Offstage(), errorBuilder: (BuildContext context, Object error) => Container(key: errorWidgetKey), - pagination: pagination, + limit: pagination.limit, ); final mockClient = MockClient(); @@ -186,7 +186,7 @@ void main() { loadingBuilder: (BuildContext context) => const Offstage(), emptyBuilder: (BuildContext context) => Container(key: emptyWidgetKey), errorBuilder: (BuildContext context, Object error) => const Offstage(), - pagination: pagination, + limit: pagination.limit, ); final mockClient = MockClient(); @@ -243,7 +243,7 @@ void main() { loadingBuilder: (BuildContext context) => const Offstage(), emptyBuilder: (BuildContext context) => const Offstage(), errorBuilder: (BuildContext context, Object error) => const Offstage(), - pagination: pagination, + limit: pagination.limit, ); final mockClient = MockClient(); @@ -306,7 +306,7 @@ void main() { loadingBuilder: (BuildContext context) => const Offstage(), emptyBuilder: (BuildContext context) => const Offstage(), errorBuilder: (BuildContext context, Object error) => const Offstage(), - pagination: pagination, + limit: pagination.limit, ); final mockClient = MockClient(); @@ -420,7 +420,7 @@ void main() { emptyBuilder: (BuildContext context) => const Offstage(), errorBuilder: (BuildContext context, Object error) => const Offstage(), - pagination: pagination.copyWith(limit: limit), + limit: limit, ); final mockClient = MockClient(); @@ -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(), + limit: pagination.limit, + ); + + expect(channelListCore.limit, pagination.limit); + }); } diff --git a/packages/stream_chat_flutter_core/test/channels_bloc_test.dart b/packages/stream_chat_flutter_core/test/channels_bloc_test.dart index 2aee9eba..a237a23c 100644 --- a/packages/stream_chat_flutter_core/test/channels_bloc_test.dart +++ b/packages/stream_chat_flutter_core/test/channels_bloc_test.dart @@ -558,7 +558,6 @@ void main() { config: ChannelConfig(), createdAt: DateTime.now(), memberCount: 1, - cooldown: 0, ), ); 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 979ca5b2..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({ @@ -31,8 +31,7 @@ void main() { ); testWidgets( - 'messageSearchBlocState.search() should throw if used where ' - 'StreamChat is not present in the widget tree', + '''messageSearchBlocState.search() should throw if used where StreamChat is not present in the widget tree''', (tester) async { const messageSearchBloc = MessageSearchBloc( child: Offstage(), @@ -74,7 +73,10 @@ void main() { messageFilters: any(named: 'messageFilters'), paginationParams: any(named: 'paginationParams'), )).thenAnswer( - (_) async => SearchMessagesResponse()..results = messageResponseList, + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, ); messageSearchBlocState.search(filter: testFilter); @@ -95,8 +97,7 @@ void main() { ); testWidgets( - 'messageSearchBlocState.messagesStream should emit error ' - 'if client.search() throws', + '''messageSearchBlocState.messagesStream should emit error if client.search() throws''', (tester) async { const messageSearchBlocKey = Key('messageSearchBloc'); const childKey = Key('child'); @@ -144,9 +145,7 @@ void main() { ); testWidgets( - 'calling messageSearchBlocState.search() again with an offset ' - 'should emit new data through messagesStream and also emit loading state ' - 'through queryMessagesLoading', + '''calling messageSearchBlocState.search() again with an offset should emit new data through messagesStream and also emit loading state through queryMessagesLoading''', (tester) async { const messageSearchBlocKey = Key('messageSearchBloc'); const childKey = Key('child'); @@ -168,19 +167,23 @@ void main() { find.byKey(messageSearchBlocKey), ); - final messageResponseList = _generateMessages(); + const pagination = PaginationParams(limit: 25); + final messageResponseList = _generateMessages(count: 25); when(() => mockClient.search( testFilter, query: any(named: 'query'), sort: any(named: 'sort'), messageFilters: any(named: 'messageFilters'), - paginationParams: any(named: 'paginationParams'), + paginationParams: pagination, )).thenAnswer( - (_) async => SearchMessagesResponse()..results = messageResponseList, + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, ); - messageSearchBlocState.search(filter: testFilter); + messageSearchBlocState.search(pagination: pagination, filter: testFilter); await expectLater( messageSearchBlocState.messagesStream, @@ -192,22 +195,24 @@ void main() { query: any(named: 'query'), sort: any(named: 'sort'), messageFilters: any(named: 'messageFilters'), - paginationParams: any(named: 'paginationParams'), + paginationParams: pagination, )).called(1); final offset = messageResponseList.length; final paginatedMessageResponseList = _generateMessages(offset: offset); - final pagination = PaginationParams(offset: offset); + final newPagination = pagination.copyWith(offset: offset); when(() => mockClient.search( testFilter, query: any(named: 'query'), sort: any(named: 'sort'), messageFilters: any(named: 'messageFilters'), - paginationParams: pagination, + paginationParams: newPagination, )).thenAnswer( - (_) async => - SearchMessagesResponse()..results = paginatedMessageResponseList, + (_) async => SearchMessagesResponse() + ..results = paginatedMessageResponseList + ..next = null + ..previous = null, ); messageSearchBlocState.search(pagination: pagination, filter: testFilter); @@ -236,9 +241,7 @@ void main() { ); testWidgets( - 'calling messageSearchBlocState.search() again with an offset ' - 'should emit error through queryUsersLoading if ' - 'client.search() throws', + '''calling messageSearchBlocState.search() again with an offset should emit error through queryUsersLoading if client.search() throws''', (tester) async { const messageSearchBlocKey = Key('messageSearchBloc'); const childKey = Key('child'); @@ -260,19 +263,23 @@ void main() { find.byKey(messageSearchBlocKey), ); - final messageResponseList = _generateMessages(); + const pagination = PaginationParams(limit: 25); + final messageResponseList = _generateMessages(count: 25); when(() => mockClient.search( testFilter, query: any(named: 'query'), sort: any(named: 'sort'), messageFilters: any(named: 'messageFilters'), - paginationParams: any(named: 'paginationParams'), + paginationParams: pagination, )).thenAnswer( - (_) async => SearchMessagesResponse()..results = messageResponseList, + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, ); - messageSearchBlocState.search(filter: testFilter); + messageSearchBlocState.search(pagination: pagination, filter: testFilter); await expectLater( messageSearchBlocState.messagesStream, @@ -284,11 +291,11 @@ void main() { query: any(named: 'query'), sort: any(named: 'sort'), messageFilters: any(named: 'messageFilters'), - paginationParams: any(named: 'paginationParams'), + paginationParams: pagination, )).called(1); final offset = messageResponseList.length; - final pagination = PaginationParams(offset: offset); + final newPagination = pagination.copyWith(offset: offset); const error = 'Error! Error! Error!'; when(() => mockClient.search( @@ -296,10 +303,13 @@ void main() { query: any(named: 'query'), sort: any(named: 'sort'), messageFilters: any(named: 'messageFilters'), - paginationParams: pagination, + paginationParams: newPagination, )).thenThrow(error); - messageSearchBlocState.search(pagination: pagination, filter: testFilter); + messageSearchBlocState.search( + pagination: newPagination, + filter: testFilter, + ); await expectLater( messageSearchBlocState.queryMessagesLoading, @@ -311,8 +321,80 @@ void main() { query: any(named: 'query'), sort: any(named: 'sort'), messageFilters: any(named: 'messageFilters'), - paginationParams: pagination, + paginationParams: newPagination, )).called(1); }, ); + + testWidgets( + '''calling messageSearchBlocState.search() again with an offset should do nothing and return if pagination is completed''', + (tester) async { + const messageSearchBlocKey = Key('messageSearchBloc'); + const childKey = Key('child'); + const messageSearchBloc = MessageSearchBloc( + key: messageSearchBlocKey, + child: Offstage(key: childKey), + ); + + final mockClient = MockClient(); + + await tester.pumpWidget( + StreamChatCore( + client: mockClient, + child: messageSearchBloc, + ), + ); + + final messageSearchBlocState = tester.state( + find.byKey(messageSearchBlocKey), + ); + + const pagination = PaginationParams(limit: 25); + + final messageResponseList = _generateMessages(count: 20); + + when(() => mockClient.search( + testFilter, + query: any(named: 'query'), + sort: any(named: 'sort'), + messageFilters: any(named: 'messageFilters'), + paginationParams: pagination, + )).thenAnswer( + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, + ); + + messageSearchBlocState.search(pagination: pagination, filter: testFilter); + + await expectLater( + messageSearchBlocState.messagesStream, + emits(isSameMessageResponseListAs(messageResponseList)), + ); + + verify(() => mockClient.search( + testFilter, + query: any(named: 'query'), + sort: any(named: 'sort'), + messageFilters: any(named: 'messageFilters'), + paginationParams: pagination, + )).called(1); + + final offset = messageResponseList.length; + final newPagination = pagination.copyWith(offset: offset); + + messageSearchBlocState.search( + filter: testFilter, + pagination: newPagination, + ); + + // should emit nothing. + await expectLater( + // skipping the initial data (behaviorSubject). + messageSearchBlocState.messagesStream.skip(1), + emitsInOrder([]), + ); + }, + ); } 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 6b700c76..475c6c23 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,7 +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 testFilter = Filter.custom(key: 'test', value: 'testValue'); +const testMessageFilter = Filter.custom(key: 'test', value: 'testValue'); void main() { List _generateMessages({ @@ -28,6 +29,40 @@ void main() { }, ); + testWidgets( + 'should throw if both `messageQuery` and `messageFilters` are provided', + (tester) async { + expect( + () => MessageSearchListCore( + childBuilder: (_) => const Offstage(), + loadingBuilder: (_) => const Offstage(), + emptyBuilder: (_) => const Offstage(), + errorBuilder: (_, __) => const Offstage(), + filters: testFilter, + messageFilters: testMessageFilter, + messageQuery: 'test', + ), + throwsAssertionError, + ); + }, + ); + + testWidgets( + 'should throw if both `messageQuery` and `messageFilters` are not provided', + (tester) async { + expect( + () => MessageSearchListCore( + childBuilder: (_) => const Offstage(), + loadingBuilder: (_) => const Offstage(), + emptyBuilder: (_) => const Offstage(), + errorBuilder: (_, __) => const Offstage(), + filters: testFilter, + ), + throwsAssertionError, + ); + }, + ); + testWidgets( 'should throw if MessageSearchListCore is used where MessageSearchBloc ' 'is not present in the widget tree', @@ -40,6 +75,7 @@ void main() { emptyBuilder: (BuildContext context) => const Offstage(), errorBuilder: (BuildContext context, Object? error) => const Offstage(), filters: testFilter, + messageFilters: testMessageFilter, ); await tester.pumpWidget(messageSearchListCore); @@ -61,6 +97,7 @@ void main() { emptyBuilder: (BuildContext context) => const Offstage(), errorBuilder: (BuildContext context, Object? error) => const Offstage(), filters: testFilter, + messageFilters: testMessageFilter, ); final mockClient = MockClient(); @@ -92,6 +129,7 @@ void main() { errorBuilder: (BuildContext context, Object error) => const Offstage(), messageSearchListController: controller, filters: testFilter, + messageFilters: testMessageFilter, ); expect(controller.loadData, isNull); @@ -129,6 +167,7 @@ void main() { key: errorWidgetKey, ), filters: testFilter, + messageFilters: testMessageFilter, ); final mockClient = MockClient(); @@ -138,7 +177,7 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: any(named: 'paginationParams'), )).thenThrow(error); @@ -159,7 +198,7 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: any(named: 'paginationParams'), )).called(1); }, @@ -179,6 +218,7 @@ void main() { const Offstage(key: emptyWidgetKey), errorBuilder: (BuildContext context, Object error) => const Offstage(), filters: testFilter, + messageFilters: testMessageFilter, ); final mockClient = MockClient(); @@ -188,10 +228,13 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: any(named: 'paginationParams'), )).thenAnswer( - (_) async => SearchMessagesResponse()..results = messageResponseList, + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, ); await tester.pumpWidget( @@ -211,7 +254,7 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: any(named: 'paginationParams'), )).called(1); }, @@ -231,6 +274,7 @@ void main() { emptyBuilder: (BuildContext context) => const Offstage(), errorBuilder: (BuildContext context, Object error) => const Offstage(), filters: testFilter, + messageFilters: testMessageFilter, ); final mockClient = MockClient(); @@ -240,10 +284,13 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: any(named: 'paginationParams'), )).thenAnswer( - (_) async => SearchMessagesResponse()..results = messageResponseList, + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, ); await tester.pumpWidget( @@ -263,7 +310,7 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: any(named: 'paginationParams'), )).called(1); }, @@ -275,7 +322,7 @@ void main() { (tester) async { const messageSearchListCoreKey = Key('messageSearchListCore'); const childWidgetKey = Key('childWidget'); - const pagination = PaginationParams(); + const pagination = PaginationParams(limit: 25); final messageSearchListCore = MessageSearchListCore( key: messageSearchListCoreKey, childBuilder: (List messages) => Container( @@ -287,21 +334,25 @@ void main() { loadingBuilder: (BuildContext context) => const Offstage(), emptyBuilder: (BuildContext context) => const Offstage(), errorBuilder: (BuildContext context, Object error) => const Offstage(), - paginationParams: pagination, + limit: pagination.limit, filters: testFilter, + messageFilters: testMessageFilter, ); final mockClient = MockClient(); - final messageResponseList = _generateMessages(); + final messageResponseList = _generateMessages(count: 25); when(() => mockClient.search( testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: pagination, )).thenAnswer( - (_) async => SearchMessagesResponse()..results = messageResponseList, + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, ); await tester.pumpWidget( @@ -332,7 +383,7 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: pagination, )).called(1); @@ -348,11 +399,13 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: updatedPagination, )).thenAnswer( - (_) async => - SearchMessagesResponse()..results = paginatedMessageResponseList, + (_) async => SearchMessagesResponse() + ..results = paginatedMessageResponseList + ..next = null + ..previous = null, ); await messageSearchListCoreState.paginateData(); @@ -372,7 +425,7 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: updatedPagination, )).called(1); }, @@ -404,8 +457,9 @@ void main() { emptyBuilder: (BuildContext context) => const Offstage(), errorBuilder: (BuildContext context, Object error) => const Offstage(), - paginationParams: pagination.copyWith(limit: limit), + limit: limit, filters: testFilter, + messageFilters: testMessageFilter, ); final mockClient = MockClient(); @@ -415,10 +469,13 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: pagination, )).thenAnswer( - (_) async => SearchMessagesResponse()..results = messageResponseList, + (_) async => SearchMessagesResponse() + ..results = messageResponseList + ..next = null + ..previous = null, ); await tester.pumpWidget( @@ -453,7 +510,7 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: pagination, )).called(1); @@ -466,11 +523,13 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: updatedPagination, )).thenAnswer( - (_) async => - SearchMessagesResponse()..results = updatedMessageResponseList, + (_) async => SearchMessagesResponse() + ..results = updatedMessageResponseList + ..next = null + ..previous = null, ); await tester.pumpAndSettle(); @@ -487,9 +546,24 @@ void main() { testFilter, query: any(named: 'query'), sort: any(named: 'sort'), - messageFilters: any(named: 'messageFilters'), + messageFilters: testMessageFilter, paginationParams: updatedPagination, )).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, + limit: pagination.limit, + ); + + 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 eac8dc71..c916ce17 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 @@ -321,7 +321,7 @@ void main() { (tester) async { const userListCoreKey = Key('userListCore'); const listWidgetKey = Key('listWidget'); - const pagination = PaginationParams(); + const pagination = PaginationParams(limit: 15); final userListCore = UserListCore( key: userListCoreKey, listBuilder: (_, items) => Container( @@ -341,13 +341,13 @@ void main() { loadingBuilder: (BuildContext context) => const Offstage(), emptyBuilder: (BuildContext context) => const Offstage(), errorBuilder: (BuildContext context, Object error) => const Offstage(), - pagination: pagination, + limit: pagination.limit, groupAlphabetically: true, ); final mockClient = MockClient(); - final users = _generateUsers(); + final users = _generateUsers(count: 15); when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), @@ -447,7 +447,7 @@ void main() { emptyBuilder: (BuildContext context) => const Offstage(), errorBuilder: (BuildContext context, Object error) => const Offstage(), - pagination: pagination.copyWith(limit: limit), + limit: limit, groupAlphabetically: true, ); @@ -520,4 +520,17 @@ void main() { )).called(1); }, ); + + test('`widget.limit` should match `widget.pagination.limit`', () { + const limit = 20; + final userListCore = UserListCore( + listBuilder: (_, __) => const Offstage(), + loadingBuilder: (BuildContext context) => const Offstage(), + emptyBuilder: (BuildContext context) => const Offstage(), + errorBuilder: (BuildContext context, Object error) => const Offstage(), + limit: limit, + ); + + expect(userListCore.limit, limit); + }); } diff --git a/packages/stream_chat_flutter_core/test/users_bloc_test.dart b/packages/stream_chat_flutter_core/test/users_bloc_test.dart index af088a76..549f9291 100644 --- a/packages/stream_chat_flutter_core/test/users_bloc_test.dart +++ b/packages/stream_chat_flutter_core/test/users_bloc_test.dart @@ -164,16 +164,17 @@ void main() { find.byKey(usersBlocKey), ); - final users = _generateUsers(); + const pagination = PaginationParams(limit: 25); + final users = _generateUsers(count: 25); when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), presence: any(named: 'presence'), - pagination: any(named: 'pagination'), + pagination: pagination, )).thenAnswer((_) async => QueryUsersResponse()..users = users); - usersBlocState.queryUsers(); + usersBlocState.queryUsers(pagination: pagination); await expectLater( usersBlocState.usersStream, @@ -184,23 +185,23 @@ void main() { filter: any(named: 'filter'), sort: any(named: 'sort'), presence: any(named: 'presence'), - pagination: any(named: 'pagination'), + pagination: pagination, )).called(1); final offset = users.length; final paginatedUsers = _generateUsers(offset: offset); - final pagination = PaginationParams(offset: offset); + final newPagination = pagination.copyWith(offset: offset); when(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: pagination, - )) - .thenAnswer( - (_) async => QueryUsersResponse()..users = paginatedUsers); + filter: any(named: 'filter'), + sort: any(named: 'sort'), + presence: any(named: 'presence'), + pagination: newPagination, + )).thenAnswer( + (_) async => QueryUsersResponse()..users = paginatedUsers, + ); - usersBlocState.queryUsers(pagination: pagination); + usersBlocState.queryUsers(pagination: newPagination); await Future.wait([ expectLater( @@ -217,7 +218,7 @@ void main() { filter: any(named: 'filter'), sort: any(named: 'sort'), presence: any(named: 'presence'), - pagination: pagination, + pagination: newPagination, )).called(1); }, ); @@ -247,13 +248,89 @@ void main() { find.byKey(usersBlocKey), ); - final users = _generateUsers(); + const pagination = PaginationParams(limit: 25); + final users = _generateUsers(count: 25); when(() => mockClient.queryUsers( filter: any(named: 'filter'), sort: any(named: 'sort'), presence: any(named: 'presence'), - pagination: any(named: 'pagination'), + pagination: pagination, + )).thenAnswer((_) async => QueryUsersResponse()..users = users); + + usersBlocState.queryUsers(pagination: pagination); + + await expectLater( + usersBlocState.usersStream, + emits(isSameUserListAs(users)), + ); + + verify(() => mockClient.queryUsers( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + presence: any(named: 'presence'), + pagination: pagination, + )).called(1); + + final offset = users.length; + final newPagination = pagination.copyWith(offset: offset); + + const error = 'Error! Error! Error!'; + + when(() => mockClient.queryUsers( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + presence: any(named: 'presence'), + pagination: newPagination, + )).thenThrow(error); + + usersBlocState.queryUsers(pagination: newPagination); + + await expectLater( + usersBlocState.queryUsersLoading, + emitsError(error), + ); + + verify(() => mockClient.queryUsers( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + presence: any(named: 'presence'), + pagination: newPagination, + )).called(1); + }, + ); + + testWidgets( + '''calling usersBlocState.queryUsers() again with an offset should do nothing and return if pagination is completed''', + (tester) async { + const usersBlocKey = Key('usersBloc'); + const childKey = Key('child'); + const usersBloc = UsersBloc( + key: usersBlocKey, + child: Offstage(key: childKey), + ); + + final mockClient = MockClient(); + + await tester.pumpWidget( + StreamChatCore( + client: mockClient, + child: usersBloc, + ), + ); + + final usersBlocState = tester.state( + find.byKey(usersBlocKey), + ); + + const pagination = PaginationParams(limit: 30); + final users = _generateUsers(count: 25); + + when(() => mockClient.queryUsers( + filter: any(named: 'filter'), + sort: any(named: 'sort'), + presence: any(named: 'presence'), + pagination: pagination, )).thenAnswer((_) async => QueryUsersResponse()..users = users); usersBlocState.queryUsers(); @@ -271,30 +348,16 @@ void main() { )).called(1); final offset = users.length; - final pagination = PaginationParams(offset: offset); + final newPagination = pagination.copyWith(offset: offset); - const error = 'Error! Error! Error!'; - - when(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: pagination, - )).thenThrow(error); - - usersBlocState.queryUsers(pagination: pagination); + usersBlocState.queryUsers(pagination: newPagination); + // should emit nothing. await expectLater( - usersBlocState.queryUsersLoading, - emitsError(error), + // skipping the initial data (behaviorSubject). + usersBlocState.usersStream, + emitsInOrder([]), ); - - verify(() => mockClient.queryUsers( - filter: any(named: 'filter'), - sort: any(named: 'sort'), - presence: any(named: 'presence'), - pagination: pagination, - )).called(1); }, ); } diff --git a/packages/stream_chat_localizations/CHANGELOG.md b/packages/stream_chat_localizations/CHANGELOG.md index d8328475..c1c469bf 100644 --- a/packages/stream_chat_localizations/CHANGELOG.md +++ b/packages/stream_chat_localizations/CHANGELOG.md @@ -1,4 +1,6 @@ -## Upcoming +## 2.0.0 + +- Updated `stream_chat_flutter` dependency to [`3.0.0`](https://pub.dev/packages/stream_chat_flutter/changelog). 🐞 Fixed diff --git a/packages/stream_chat_localizations/pubspec.yaml b/packages/stream_chat_localizations/pubspec.yaml index 0ed72acd..552ee5ec 100644 --- a/packages/stream_chat_localizations/pubspec.yaml +++ b/packages/stream_chat_localizations/pubspec.yaml @@ -1,6 +1,6 @@ name: stream_chat_localizations description: The Official localizations for Stream Chat Flutter, a service for building chat applications -version: 1.1.0 +version: 2.0.0 homepage: https://github.com/GetStream/stream-chat-flutter repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -14,7 +14,7 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - stream_chat_flutter: ^2.2.0 + stream_chat_flutter: ^3.0.0 dev_dependencies: flutter_test: diff --git a/packages/stream_chat_localizations/test/override_test.dart b/packages/stream_chat_localizations/test/override_test.dart index 3e134b6f..513dd336 100644 --- a/packages/stream_chat_localizations/test/override_test.dart +++ b/packages/stream_chat_localizations/test/override_test.dart @@ -2,9 +2,9 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_localizations/src/stream_chat_localizations.dart'; -import 'package:flutter_test/flutter_test.dart'; class FooStreamChatLocalizations extends StreamChatLocalizationsEn { FooStreamChatLocalizations( diff --git a/packages/stream_chat_persistence/CHANGELOG.md b/packages/stream_chat_persistence/CHANGELOG.md index 139056bd..f8a2434f 100644 --- a/packages/stream_chat_persistence/CHANGELOG.md +++ b/packages/stream_chat_persistence/CHANGELOG.md @@ -1,9 +1,9 @@ -## Upcoming +## 3.0.0 +- Updated `stream_chat` dependency to [`3.0.0`](https://pub.dev/packages/stream_chat/changelog). - [[#604]](https://github.com/GetStream/stream-chat-flutter/issues/604) Fix cascade deletion by enabling `pragma foreign_keys`. - Added a new table `PinnedMessageReactions` and dao `PinnedMessageReactionDao` specifically for pinned messages. -- Updated `stream_chat`: `^2.2.0` -> `TODO` ## 2.2.0 diff --git a/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart b/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart index c87b94d4..d1ed0925 100644 --- a/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/channel_query_dao.dart @@ -112,8 +112,9 @@ class ChannelQueryDao extends DatabaseAccessor cachedChannels.sort(chainedComparator); - if (paginationParams?.offset != null && cachedChannels.isNotEmpty) { - cachedChannels.removeRange(0, paginationParams!.offset); + final offset = paginationParams?.offset; + if (offset != null && offset > 0 && cachedChannels.isNotEmpty) { + cachedChannels.removeRange(0, offset); } if (paginationParams?.limit != null) { diff --git a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart index b7d9a5db..ef140239 100644 --- a/packages/stream_chat_persistence/lib/src/dao/message_dao.dart +++ b/packages/stream_chat_persistence/lib/src/dao/message_dao.dart @@ -117,8 +117,9 @@ class MessageDao extends DatabaseAccessor msgList.removeRange(0, greaterThanIndex); } } - if (options?.limit != null) { - return msgList.take(options!.limit).toList(); + final limit = options?.limit; + if (limit != null && limit > 0) { + return msgList.take(limit).toList(); } } return msgList; diff --git a/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart b/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart index 27aa23b5..d70426e1 100644 --- a/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart +++ b/packages/stream_chat_persistence/lib/src/db/shared/native_db.dart @@ -7,11 +7,10 @@ import 'package:moor/isolate.dart'; import 'package:moor/moor.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/src/stream_chat_persistence_client.dart'; import 'package:stream_chat_persistence/stream_chat_persistence.dart'; -import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; - /// A Helper class to construct new instances of [MoorChatDatabase] specifically /// for native platform applications class SharedDB { diff --git a/packages/stream_chat_persistence/lib/src/db/shared/web_db.dart b/packages/stream_chat_persistence/lib/src/db/shared/web_db.dart index bb664443..5b1d8311 100644 --- a/packages/stream_chat_persistence/lib/src/db/shared/web_db.dart +++ b/packages/stream_chat_persistence/lib/src/db/shared/web_db.dart @@ -1,8 +1,7 @@ // coverage:ignore-file import 'package:moor/moor_web.dart'; -import 'package:stream_chat_persistence/src/stream_chat_persistence_client.dart'; - import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:stream_chat_persistence/src/stream_chat_persistence_client.dart'; /// A Helper class to construct new instances of [MoorChatDatabase] specifically /// for Web applications diff --git a/packages/stream_chat_persistence/pubspec.yaml b/packages/stream_chat_persistence/pubspec.yaml index a8e5f991..f6409fa2 100644 --- a/packages/stream_chat_persistence/pubspec.yaml +++ b/packages/stream_chat_persistence/pubspec.yaml @@ -1,7 +1,7 @@ name: stream_chat_persistence homepage: https://github.com/GetStream/stream-chat-flutter description: Official Stream Chat Persistence library. Build your own chat experience using Dart and Flutter. -version: 2.2.0 +version: 3.0.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -18,11 +18,12 @@ dependencies: path: ^1.8.0 path_provider: ^2.0.1 sqlite3_flutter_libs: ^0.5.0 - stream_chat: ^2.2.0 + stream_chat: ^3.0.0 dev_dependencies: build_runner: ^2.0.1 + flutter_test: + sdk: flutter mocktail: ^0.1.1 moor_generator: ^4.2.1 pedantic: ^1.11.0 - test: ^1.17.7 diff --git a/packages/stream_chat_persistence/test/src/converter/list_coverter_test.dart b/packages/stream_chat_persistence/test/src/converter/list_coverter_test.dart index 73ce2452..9cee72b0 100644 --- a/packages/stream_chat_persistence/test/src/converter/list_coverter_test.dart +++ b/packages/stream_chat_persistence/test/src/converter/list_coverter_test.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:test/test.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_persistence/src/converter/list_converter.dart'; void main() { diff --git a/packages/stream_chat_persistence/test/src/converter/map_converter_test.dart b/packages/stream_chat_persistence/test/src/converter/map_converter_test.dart index 4727cb0d..c7754892 100644 --- a/packages/stream_chat_persistence/test/src/converter/map_converter_test.dart +++ b/packages/stream_chat_persistence/test/src/converter/map_converter_test.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:test/test.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat_persistence/src/converter/map_converter.dart'; void main() { diff --git a/packages/stream_chat_persistence/test/src/converter/message_sending_status_converter_test.dart b/packages/stream_chat_persistence/test/src/converter/message_sending_status_converter_test.dart index 1035a759..840f0469 100644 --- a/packages/stream_chat_persistence/test/src/converter/message_sending_status_converter_test.dart +++ b/packages/stream_chat_persistence/test/src/converter/message_sending_status_converter_test.dart @@ -1,4 +1,4 @@ -import 'package:test/test.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/converter/message_sending_status_converter.dart'; diff --git a/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart index c0a00100..ff75c13c 100644 --- a/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/channel_dao_test.dart @@ -1,7 +1,7 @@ +import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/dao/channel_dao.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; -import 'package:test/test.dart'; import '../../stream_chat_persistence_client_test.dart'; diff --git a/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart index 81e629b6..2ea1e73a 100644 --- a/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/channel_query_dao_test.dart @@ -1,7 +1,7 @@ +import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/dao/channel_query_dao.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; -import 'package:test/test.dart'; import '../../stream_chat_persistence_client_test.dart'; import '../utils/date_matcher.dart'; diff --git a/packages/stream_chat_persistence/test/src/dao/connection_event_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/connection_event_dao_test.dart index 40c385ba..6b576169 100644 --- a/packages/stream_chat_persistence/test/src/dao/connection_event_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/connection_event_dao_test.dart @@ -1,7 +1,7 @@ +import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/dao/connection_event_dao.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; -import 'package:test/test.dart'; import '../../stream_chat_persistence_client_test.dart'; import '../utils/date_matcher.dart'; diff --git a/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart index 4ca3880b..33531ca9 100644 --- a/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/member_dao_test.dart @@ -1,9 +1,9 @@ import 'dart:math' as math; +import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/dao/dao.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; -import 'package:test/test.dart'; import '../../stream_chat_persistence_client_test.dart'; import '../utils/date_matcher.dart'; diff --git a/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart index d069f9f9..31546ecf 100644 --- a/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/message_dao_test.dart @@ -1,9 +1,9 @@ import 'dart:math' as math; +import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/dao/dao.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; -import 'package:test/test.dart'; import '../../stream_chat_persistence_client_test.dart'; diff --git a/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart index f8fae456..45f2ed0a 100644 --- a/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/pinned_message_dao_test.dart @@ -1,9 +1,9 @@ import 'dart:math' as math; +import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/dao/dao.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; -import 'package:test/test.dart'; import '../../stream_chat_persistence_client_test.dart'; diff --git a/packages/stream_chat_persistence/test/src/dao/pinned_message_reaction_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/pinned_message_reaction_dao_test.dart index aa92c5cc..5c451ec1 100644 --- a/packages/stream_chat_persistence/test/src/dao/pinned_message_reaction_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/pinned_message_reaction_dao_test.dart @@ -1,9 +1,9 @@ import 'dart:math' as math; +import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/dao/pinned_message_reaction_dao.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; -import 'package:test/test.dart'; import '../../stream_chat_persistence_client_test.dart'; diff --git a/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart index 21570d4e..8109e75e 100644 --- a/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/reaction_dao_test.dart @@ -1,9 +1,9 @@ import 'dart:math' as math; +import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/dao/reaction_dao.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; -import 'package:test/test.dart'; import '../../stream_chat_persistence_client_test.dart'; diff --git a/packages/stream_chat_persistence/test/src/dao/read_dao_test.dart b/packages/stream_chat_persistence/test/src/dao/read_dao_test.dart index ca556632..1cebd1eb 100644 --- a/packages/stream_chat_persistence/test/src/dao/read_dao_test.dart +++ b/packages/stream_chat_persistence/test/src/dao/read_dao_test.dart @@ -1,7 +1,7 @@ +import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/dao/dao.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; -import 'package:test/test.dart'; import '../../stream_chat_persistence_client_test.dart'; import '../utils/date_matcher.dart'; 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 5b5d04d4..ab88881a 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 @@ -1,9 +1,9 @@ import 'dart:math' as math; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/dao/dao.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; -import 'package:test/test.dart'; -import 'package:stream_chat/stream_chat.dart'; import '../../stream_chat_persistence_client_test.dart'; diff --git a/packages/stream_chat_persistence/test/src/db/moor_chat_database_test.dart b/packages/stream_chat_persistence/test/src/db/moor_chat_database_test.dart index 2ec4c76b..eb1b6933 100644 --- a/packages/stream_chat_persistence/test/src/db/moor_chat_database_test.dart +++ b/packages/stream_chat_persistence/test/src/db/moor_chat_database_test.dart @@ -1,4 +1,4 @@ -import 'package:test/test.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:moor/ffi.dart'; import 'package:moor/isolate.dart'; import 'package:moor/moor.dart' hide isNotNull; diff --git a/packages/stream_chat_persistence/test/src/mapper/channel_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/channel_mapper_test.dart index 93789cc7..2c87097b 100644 --- a/packages/stream_chat_persistence/test/src/mapper/channel_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/channel_mapper_test.dart @@ -1,9 +1,10 @@ import 'dart:math' as math; -import 'package:test/test.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/src/mapper/channel_mapper.dart'; + import '../utils/date_matcher.dart'; void main() { diff --git a/packages/stream_chat_persistence/test/src/mapper/event_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/event_mapper_test.dart index 3182162b..a7c72d0d 100644 --- a/packages/stream_chat_persistence/test/src/mapper/event_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/event_mapper_test.dart @@ -1,7 +1,7 @@ +import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/src/mapper/event_mapper.dart'; -import 'package:test/test.dart'; import '../utils/date_matcher.dart'; diff --git a/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart index feca753b..19f299b3 100644 --- a/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/member_mapper_test.dart @@ -1,6 +1,6 @@ import 'dart:math' as math; -import 'package:test/test.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/src/mapper/member_mapper.dart'; diff --git a/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart index 0f9e8cf6..d26d905b 100644 --- a/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/message_mapper_test.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'dart:math' as math; -import 'package:test/test.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/src/mapper/message_mapper.dart'; diff --git a/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart index 5150df62..9f258b76 100644 --- a/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/pinned_message_mapper_test.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'dart:math' as math; -import 'package:test/test.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/src/mapper/pinned_message_mapper.dart'; diff --git a/packages/stream_chat_persistence/test/src/mapper/pinned_message_reaction_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/pinned_message_reaction_mapper_test.dart index bd8d0ba8..f031d941 100644 --- a/packages/stream_chat_persistence/test/src/mapper/pinned_message_reaction_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/pinned_message_reaction_mapper_test.dart @@ -1,4 +1,4 @@ -import 'package:test/test.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/src/mapper/pinned_message_reaction_mapper.dart'; diff --git a/packages/stream_chat_persistence/test/src/mapper/reaction_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/reaction_mapper_test.dart index cb5d231e..831d6068 100644 --- a/packages/stream_chat_persistence/test/src/mapper/reaction_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/reaction_mapper_test.dart @@ -1,4 +1,4 @@ -import 'package:test/test.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/src/mapper/reaction_mapper.dart'; diff --git a/packages/stream_chat_persistence/test/src/mapper/read_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/read_mapper_test.dart index 73c87da3..f152e476 100644 --- a/packages/stream_chat_persistence/test/src/mapper/read_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/read_mapper_test.dart @@ -1,4 +1,4 @@ -import 'package:test/test.dart'; +import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/src/mapper/read_mapper.dart'; diff --git a/packages/stream_chat_persistence/test/src/mapper/user_mapper_test.dart b/packages/stream_chat_persistence/test/src/mapper/user_mapper_test.dart index eca2a32a..7b6ef2e8 100644 --- a/packages/stream_chat_persistence/test/src/mapper/user_mapper_test.dart +++ b/packages/stream_chat_persistence/test/src/mapper/user_mapper_test.dart @@ -1,5 +1,6 @@ import 'dart:math' as math; -import 'package:test/test.dart'; + +import 'package:flutter_test/flutter_test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/src/mapper/user_mapper.dart'; diff --git a/packages/stream_chat_persistence/test/src/utils/date_matcher.dart b/packages/stream_chat_persistence/test/src/utils/date_matcher.dart index dec68db0..5f55f46a 100644 --- a/packages/stream_chat_persistence/test/src/utils/date_matcher.dart +++ b/packages/stream_chat_persistence/test/src/utils/date_matcher.dart @@ -1,4 +1,4 @@ -import 'package:test/test.dart'; +import 'package:flutter_test/flutter_test.dart'; Matcher isSameDateAs(DateTime targetDate) => _IsSameDateAs(targetDate: targetDate); diff --git a/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart b/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart index 25e926b1..5c2b6bff 100644 --- a/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart +++ b/packages/stream_chat_persistence/test/stream_chat_persistence_client_test.dart @@ -1,9 +1,10 @@ +import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:moor/ffi.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/src/stream_chat_persistence_client.dart'; -import 'package:test/test.dart'; + import 'mock_chat_database.dart'; import 'src/utils/date_matcher.dart';