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 9c925f88..07c926c2 100644 --- a/packages/stream_chat/CHANGELOG.md +++ b/packages/stream_chat/CHANGELOG.md @@ -1,5 +1,15 @@ ## Upcoming +✅ Added + +- Added `Filter.notExists`. + +🐞 Fixed + +- [[#710]](https://github.com/GetStream/stream-chat-flutter/issues/710) Fixed JWT requiring using `String` as id. + +## 3.0.0 + 🛑️ Breaking Changes from `2.2.1` - Added 6 new methods in `ChatPersistenceClient`. @@ -16,13 +26,16 @@ - 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']` + , `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..0c6b0ed1 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; @@ -1704,7 +1680,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 +1711,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 +1867,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 9487e6e7..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(); 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/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/channel_model.dart b/packages/stream_chat/lib/src/core/models/channel_model.dart index a4e6db79..31ac4923 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'; diff --git a/packages/stream_chat/lib/src/core/models/filter.dart b/packages/stream_chat/lib/src/core/models/filter.dart index ca494bdb..108efcb2 100644 --- a/packages/stream_chat/lib/src/core/models/filter.dart +++ b/packages/stream_chat/lib/src/core/models/filter.dart @@ -167,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); 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 07c6cf30..eb64eed3 100644 --- a/packages/stream_chat/lib/src/core/models/own_user.dart +++ b/packages/stream_chat/lib/src/core/models/own_user.dart @@ -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, diff --git a/packages/stream_chat/lib/src/core/models/reaction.dart b/packages/stream_chat/lib/src/core/models/reaction.dart index 57d0b48d..2b0ffbf6 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'; 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..4eda59c0 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.0.0'; diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index a07606ba..dd394c1e 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.0.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues 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/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/filter_test.dart b/packages/stream_chat/test/src/core/models/filter_test.dart index e396c2b3..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); 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 7f9af105..35ce4094 100644 --- a/packages/stream_chat_flutter/CHANGELOG.md +++ b/packages/stream_chat_flutter/CHANGELOG.md @@ -1,15 +1,41 @@ ## Upcoming +✅ 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. + +⚠️ 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 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..2fbdcbee 100644 --- a/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart +++ b/packages/stream_chat_flutter/example/lib/tutorial_part_6.dart @@ -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_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 6045539f..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 { 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 e165fc6d..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 { 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 0f06e269..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?); 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..9551a40d 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'; @@ -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/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 cee653d2..82087fab 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; @@ -472,7 +477,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( @@ -604,7 +637,7 @@ class MessageInputState extends State { ), secondChild: widget.disableAttachments && !widget.showCommandsButton && - widget.actions?.isNotEmpty != true + widget.actions.isNotEmpty != true ? const Offstage() : Wrap( children: [ @@ -615,7 +648,7 @@ class MessageInputState extends State { channel.state != null && channel.config?.commands.isNotEmpty == true) _buildCommandButton(context), - ...widget.actions ?? [], + ...widget.actions, ].insertBetween(const SizedBox(width: 8)), ), duration: const Duration(milliseconds: 300), @@ -783,51 +816,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') { @@ -847,10 +861,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); @@ -860,12 +871,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!); - } } } } @@ -878,166 +883,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() { @@ -1087,7 +976,9 @@ 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: SingleChildScrollView( @@ -1261,300 +1152,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) { @@ -1568,9 +1237,6 @@ class MessageInputState extends State { offset: rejoin.length, ), ); - - _emojiOverlay?.remove(); - _emojiOverlay = null; } void _setCommand(Command c) { @@ -1578,9 +1244,8 @@ class MessageInputState extends State { setState(() { _chosenCommand = c; _commandEnabled = true; + _showCommandsOverlay = false; }); - _commandsOverlay?.remove(); - _commandsOverlay = null; } Widget _buildReplyToMessage() { @@ -1773,7 +1438,7 @@ class MessageInputState extends State { icon: StreamSvgIcon.lightning( color: s.isNotEmpty ? _streamChatTheme.colorTheme.disabled - : (_commandsOverlay != null + : (_showCommandsOverlay ? _messageInputTheme.actionButtonColor : _messageInputTheme.actionButtonIdleColor), ), @@ -1789,19 +1454,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; + }); }, ); @@ -1823,12 +1478,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); @@ -2114,11 +1765,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( @@ -2185,8 +1831,6 @@ class MessageInputState extends State { } } - StreamSubscription? _keyboardListener; - void _showErrorAlert(String description) { showModalBottomSheet( backgroundColor: _streamChatTheme.colorTheme.barsBg, @@ -2260,12 +1904,9 @@ class MessageInputState extends State { @override void dispose() { - _commandsOverlay?.remove(); - _emojiOverlay?.remove(); - _mentionsOverlay?.remove(); - _keyboardListener?.cancel(); textEditingController.dispose(); _stopSlowMode(); + _onChangedDebounced.cancel(); super.dispose(); } 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 61053106..4f76a3bf 100644 --- a/packages/stream_chat_flutter/lib/src/message_list_view.dart +++ b/packages/stream_chat_flutter/lib/src/message_list_view.dart @@ -494,14 +494,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); @@ -525,13 +529,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( @@ -560,8 +564,13 @@ 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(); + } } if (i == itemCount - 3) { @@ -579,8 +588,13 @@ class _MessageListViewState extends State { } 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 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 ab1a9178..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, /// ), /// ); /// } @@ -53,17 +53,12 @@ typedef EmptyMessageSearchBuilder = Widget Function( /// Modify it to change the widget appearance. class MessageSearchListView extends StatefulWidget { /// Instantiate a new MessageSearchListView - MessageSearchListView({ + const MessageSearchListView({ Key? key, required this.filters, this.messageQuery, this.sortOptions, - @Deprecated( - "'paginationParams' is deprecated and shouldn't be used. " - "This property is no longer used, Please use 'limit' instead", - ) - this.paginationParams, - int? limit, + this.limit = 30, this.messageFilters, this.separatorBuilder, this.itemBuilder, @@ -76,8 +71,7 @@ class MessageSearchListView extends StatefulWidget { this.loadingBuilder, this.childBuilder, this.messageSearchListController, - }) : limit = limit ?? paginationParams?.limit ?? 30, - super(key: key); + }) : super(key: key); /// Message String to search on final String? messageQuery; @@ -95,16 +89,6 @@ 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 - @Deprecated( - "'paginationParams' is deprecated and shouldn't be used. " - "This property is no longer used, Please use 'limit' instead", - ) - final PaginationParams? paginationParams; - /// The amount of messages requested per API call. final int limit; diff --git a/packages/stream_chat_flutter/lib/src/message_widget.dart b/packages/stream_chat_flutter/lib/src/message_widget.dart index 01177603..6731e15a 100644 --- a/packages/stream_chat_flutter/lib/src/message_widget.dart +++ b/packages/stream_chat_flutter/lib/src/message_widget.dart @@ -580,11 +580,12 @@ class _MessageWidgetState extends State bool get isOnlyEmoji => widget.message.text?.isOnlyEmoji == true; - bool get hasNonUrlAttachments => - widget.message.attachments.where((it) => it.titleLink == null).isNotEmpty; + bool get hasNonUrlAttachments => widget.message.attachments + .where((it) => it.titleLink == null || it.type == 'giphy') + .isNotEmpty; - bool get hasUrlAttachments => - widget.message.attachments.any((it) => it.titleLink != null) == true; + bool get hasUrlAttachments => widget.message.attachments + .any((it) => it.titleLink != null && it.type != 'giphy'); bool get showBottomRow => showThreadReplyIndicator || @@ -1156,7 +1157,9 @@ class _MessageWidgetState extends State final attachmentGroups = >{}; widget.message.attachments - .where((element) => element.titleLink == 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!] = []; 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/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..0e50f132 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, ); 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 b946f800..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?); @@ -82,8 +82,7 @@ class UserListView extends StatefulWidget { /// The query filters to use. /// You can query on any of the custom fields you've defined on the [Channel]. /// You can also filter other built-in channel fields. - // TODO: Make it non-nullable in a future breaking release - final Filter? filter; + final Filter filter; /// The sorting used for the channels matching the filters. /// Sorting is based on field and direction, multiple sorting options can 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 4dc9fee7..9b85e8c2 100644 --- a/packages/stream_chat_flutter/lib/src/utils.dart +++ b/packages/stream_chat_flutter/lib/src/utils.dart @@ -1,10 +1,11 @@ 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 { @@ -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 907e9a7f..69cee1e9 100644 --- a/packages/stream_chat_flutter/pubspec.yaml +++ b/packages/stream_chat_flutter/pubspec.yaml @@ -1,7 +1,7 @@ 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.0.0 repository: https://github.com/GetStream/stream-chat-flutter issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues @@ -14,12 +14,12 @@ 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 flutter: sdk: flutter - flutter_keyboard_visibility: ^5.0.1 flutter_markdown: ^0.6.1 flutter_portal: ^0.4.0 flutter_slidable: ^0.6.0 @@ -37,7 +37,7 @@ dependencies: scrollable_positioned_list: ^0.2.0-nullsafety.0 share_plus: ^2.0.3 shimmer: ^2.0.0 - stream_chat_flutter_core: ^2.2.1 + stream_chat_flutter_core: ^3.0.0 substring_highlight: ^1.0.26 synchronized: ^3.0.0 url_launcher: ^6.0.3 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/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_core/CHANGELOG.md b/packages/stream_chat_flutter_core/CHANGELOG.md index c4287a09..cc1394a1 100644 --- a/packages/stream_chat_flutter_core/CHANGELOG.md +++ b/packages/stream_chat_flutter_core/CHANGELOG.md @@ -1,6 +1,8 @@ -## Upcoming +## 3.0.0 -⚠️ Deprecated +- 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 @@ -10,7 +12,7 @@ // new limit = 30 ``` -- `UserListViewCore` `pagination` property is now deprecated in favor of `limit`. +- `UserListCore` `pagination` property is now deprecated in favor of `limit`. ```dart // previous pagination = const PaginationParams(limit: 30) @@ -18,7 +20,7 @@ // new limit = 30 ``` -- `ChannelListViewCore` `pagination` property is now deprecated in favor of `limit`. +- `ChannelListCore` `pagination` property is now deprecated in favor of `limit`. ```dart // previous pagination = const PaginationParams(limit: 30) @@ -27,9 +29,11 @@ limit = 30 ``` +- `UserListCore` `filter` property now is non-nullable. + 🔄 Changed -- `UserListViewCore` filter property now has a default value. +- `UserListCore` filter property now has a default value. ```dart filter = const Filter.empty() ``` 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 d3d6c312..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 @@ -56,7 +56,7 @@ import 'package:stream_chat_flutter_core/src/typedef.dart'; /// information about the channels. class ChannelListCore extends StatefulWidget { /// Instantiate a new ChannelListView - ChannelListCore({ + const ChannelListCore({ Key? key, required this.errorBuilder, required this.emptyBuilder, @@ -69,15 +69,9 @@ class ChannelListCore extends StatefulWidget { this.memberLimit, this.messageLimit, this.sort, - @Deprecated( - "'pagination' is deprecated and shouldn't be used. " - "This property is no longer used, Please use 'limit' instead", - ) - this.pagination, this.channelListController, - int? limit, - }) : limit = limit ?? pagination?.limit ?? 25, - super(key: key); + this.limit = 25, + }) : super(key: key); /// A [ChannelListController] allows reloading and pagination. /// Use [ChannelListController.loadData] and @@ -124,16 +118,6 @@ 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 - @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; 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 f4d18a0b..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, /// ), /// ); /// } 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 839067bf..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 @@ -57,7 +57,7 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; /// [errorBuilder] must all be supplied and not null. class UserListCore extends StatefulWidget { /// Instantiate a new [UserListCore] - UserListCore({ + const UserListCore({ required this.errorBuilder, required this.emptyBuilder, required this.loadingBuilder, @@ -66,16 +66,10 @@ class UserListCore extends StatefulWidget { this.filter = const Filter.empty(), this.sort, this.presence, - @Deprecated( - "'pagination' is deprecated and shouldn't be used. " - "This property is no longer used, Please use 'limit' instead", - ) - this.pagination, this.groupAlphabetically = false, this.userListController, - int? limit, - }) : limit = limit ?? pagination?.limit ?? 30, - super(key: key); + this.limit = 30, + }) : super(key: key); /// A [UserListController] allows reloading and pagination. /// Use [UserListController.loadData] and [UserListController.paginateData] @@ -97,8 +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. - // TODO: Make it non-nullable in a future breaking release - 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 @@ -109,16 +102,6 @@ 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 - @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; diff --git a/packages/stream_chat_flutter_core/pubspec.yaml b/packages/stream_chat_flutter_core/pubspec.yaml index 85b57124..e5520921 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.0.0 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.0.0 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 dd87d2cf..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(); @@ -516,7 +516,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, ); expect(channelListCore.limit, pagination.limit); 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 ca416595..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 @@ -334,7 +334,7 @@ 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, ); @@ -457,7 +457,7 @@ 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, ); @@ -561,7 +561,7 @@ void main() { errorBuilder: (BuildContext context, Object? error) => const Offstage(), filters: testFilter, messageFilters: testMessageFilter, - paginationParams: pagination, + 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 ade58e0f..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 @@ -341,7 +341,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, groupAlphabetically: true, ); @@ -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, ); @@ -522,15 +522,15 @@ void main() { ); test('`widget.limit` should match `widget.pagination.limit`', () { - const pagination = PaginationParams(limit: 30); + 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(), - pagination: pagination, + limit: limit, ); - expect(userListCore.limit, pagination.limit); + expect(userListCore.limit, limit); }); } 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/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..4f3ab8e9 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,7 +18,7 @@ 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 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..a5fe7a49 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,7 +1,7 @@ import 'dart:convert'; -import 'package:test/test.dart'; import 'package:stream_chat_persistence/src/converter/list_converter.dart'; +import 'package:test/test.dart'; void main() { group('mapToDart', () { 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..6e14140b 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,7 +1,7 @@ import 'dart:convert'; -import 'package:test/test.dart'; import 'package:stream_chat_persistence/src/converter/map_converter.dart'; +import 'package:test/test.dart'; void main() { group('mapToDart', () { 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..c53d8fb4 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,6 +1,6 @@ -import 'package:test/test.dart'; import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat_persistence/src/converter/message_sending_status_converter.dart'; +import 'package:test/test.dart'; void main() { group('mapToDart', () { 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..1fd4eafd 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: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..101b44ed 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,8 +1,8 @@ -import 'package:test/test.dart'; import 'package:moor/ffi.dart'; import 'package:moor/isolate.dart'; import 'package:moor/moor.dart' hide isNotNull; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; +import 'package:test/test.dart'; DatabaseConnection _backgroundConnection() => DatabaseConnection.fromExecutor(VmDatabase.memory()); 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..b45f46b4 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: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 'package:test/test.dart'; + import '../utils/date_matcher.dart'; void main() { 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..b3f834ef 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,9 +1,9 @@ import 'dart:math' as math; -import 'package:test/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'; +import 'package:test/test.dart'; import '../utils/date_matcher.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..52a58a1a 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,10 +1,10 @@ import 'dart:convert'; import 'dart:math' as math; -import 'package:test/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'; +import 'package:test/test.dart'; import '../utils/date_matcher.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..40c4ff6c 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,10 +1,10 @@ import 'dart:convert'; import 'dart:math' as math; -import 'package:test/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'; +import 'package:test/test.dart'; import '../utils/date_matcher.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..b814b7dc 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,7 +1,7 @@ -import 'package:test/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'; +import 'package:test/test.dart'; import '../utils/date_matcher.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..e211f685 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,7 +1,7 @@ -import 'package:test/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'; +import 'package:test/test.dart'; import '../utils/date_matcher.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..eb296a75 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,7 +1,7 @@ -import 'package:test/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'; +import 'package:test/test.dart'; import '../utils/date_matcher.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..55dbbff4 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,8 +1,9 @@ import 'dart:math' as math; -import 'package:test/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'; +import 'package:test/test.dart'; import '../utils/date_matcher.dart';