Merge branch 'develop' into docs/versioning-policy

This commit is contained in:
Salvatore Giordano
2021-10-01 09:42:18 +02:00
104 changed files with 1704 additions and 870 deletions
@@ -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. The 'exists' filter matches values that exist, or don't exist, based on the specified boolean value.
```dart ```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 #### Filter.contains
@@ -29,7 +29,7 @@ class MessageSearchPage extends StatelessWidget {
child: MessageSearchListView( child: MessageSearchListView(
filters: Filter.in_('members', [StreamChat.of(context).user!.id],), filters: Filter.in_('members', [StreamChat.of(context).user!.id],),
messageQuery: 'your query here', messageQuery: 'your query here',
paginationParams: PaginationParams(limit: 20), limit: 20,
), ),
), ),
); );
@@ -30,7 +30,7 @@ class MessageSearchPage extends StatelessWidget {
body: MessageSearchListCore( body: MessageSearchListCore(
messageQuery: _messageFilter, messageQuery: _messageFilter,
filters: _channelsFilter, filters: _channelsFilter,
paginationParams: PaginationParams(limit: 20), limit: 20,
), ),
); );
} }
+14 -1
View File
@@ -1,5 +1,15 @@
## Upcoming ## 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` 🛑️ Breaking Changes from `2.2.1`
- Added 6 new methods in `ChatPersistenceClient`. - Added 6 new methods in `ChatPersistenceClient`.
@@ -16,13 +26,16 @@
- Added support for `next`, `previous` value pagination in `client.search` - Added support for `next`, `previous` value pagination in `client.search`
, [read more.](https://getstream.io/chat/docs/other-rest/search/#pagination) , [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` - `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. , `attachment.extraData['mime_type]` is same respectively.
🐞 Fixed 🐞 Fixed
- [[#659]](https://github.com/GetStream/stream-chat-flutter/issues/659) Fixed unread count not updating correctly. - [[#659]](https://github.com/GetStream/stream-chat-flutter/issues/659) Fixed unread count not updating correctly.
- Fix `Filter.empty()` json encoding. - 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 ## 2.2.1
@@ -351,7 +351,7 @@ class Channel {
Future<bool> get initialized => _initializedCompleter.future; Future<bool> get initialized => _initializedCompleter.future;
final _cancelableAttachmentUploadRequest = <String, CancelToken>{}; final _cancelableAttachmentUploadRequest = <String, CancelToken>{};
final _messageAttachmentsUploadCompleter = <String, Completer>{}; final _messageAttachmentsUploadCompleter = <String, Completer<Message>>{};
/// Cancels [attachmentId] upload request. Throws exception if the request /// Cancels [attachmentId] upload request. Throws exception if the request
/// hasn't even started yet, Already completed or Already cancelled. /// hasn't even started yet, Already completed or Already cancelled.
@@ -382,12 +382,10 @@ class Channel {
String messageId, String messageId,
Iterable<String> attachmentIds, Iterable<String> attachmentIds,
) { ) {
final message = [ var message = [
...state!.messages, ...state!.messages,
...state!.threads.values.expand((messages) => messages), ...state!.threads.values.expand((messages) => messages),
].firstWhereOrNull( ].firstWhereOrNull((it) => it.id == messageId);
(it) => it.id == messageId,
);
if (message == null) { if (message == null) {
throw const StreamChatError('Error, Message not found'); throw const StreamChatError('Error, Message not found');
@@ -409,11 +407,15 @@ class Channel {
client.logger.info('Found ${attachments.length} attachments'); client.logger.info('Found ${attachments.length} attachments');
void updateAttachment(Attachment attachment) { void updateAttachment(Attachment attachment) {
final index = final index = message!.attachments.indexWhere(
message.attachments.indexWhere((it) => it.id == attachment.id); (it) => it.id == attachment.id,
);
if (index != -1) { if (index != -1) {
message.attachments[index] = attachment; final newAttachments = [...message!.attachments]..[index] = attachment;
state?.addMessage(message); 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); _cancelableAttachmentUploadRequest.remove(it.id);
}); });
})).whenComplete(() { })).whenComplete(() {
if (message.attachments.every((it) => it.uploadState.isSuccess)) { if (message!.attachments.every((it) => it.uploadState.isSuccess)) {
_messageAttachmentsUploadCompleter.remove(messageId)?.complete(message); _messageAttachmentsUploadCompleter.remove(messageId)?.complete(message);
} }
}); });
@@ -1076,33 +1078,7 @@ class Channel {
} else { } else {
// remove the passed message if response does // remove the passed message if response does
// not contain message // not contain message
final oldIndex = state!.messages.indexWhere((m) => m.id == messageId); state!.removeMessage(message);
// 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));
}
}
await _client.chatPersistenceClient?.deleteMessageById(messageId); await _client.chatPersistenceClient?.deleteMessageById(messageId);
} }
return res; return res;
@@ -1704,7 +1680,7 @@ class ChannelClientState {
})); }));
} }
/// Add a message to this channel. /// Add a [message] to this [channelState].
void addMessage(Message message) { void addMessage(Message message) {
if (message.parentId == null || message.showInChannel == true) { if (message.parentId == null || message.showInChannel == true) {
final newMessages = List<Message>.from(_channelState.messages); final newMessages = List<Message>.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() { void _listenReadEvents() {
if (_channelState.channel?.config.readEvents == false) { if (_channelState.channel?.config.readEvents == false) {
return; return;
@@ -1862,15 +1867,11 @@ class ChannelClientState {
if (newThreads.containsKey(parentId)) { if (newThreads.containsKey(parentId)) {
newThreads[parentId] = [ newThreads[parentId] = [
...newThreads[parentId]
?.where(
(newMessage) => !messages.any((m) => m.id == newMessage.id))
.toList() ??
[],
...messages, ...messages,
]; ...newThreads[parentId]!.where(
(newMessage) => !messages.any((m) => m.id == newMessage.id),
newThreads[parentId]!.sort(_sortByCreatedAt); ),
]..sort(_sortByCreatedAt);
} else { } else {
newThreads[parentId] = messages; newThreads[parentId] = messages;
} }
@@ -143,9 +143,6 @@ class StreamChatClient {
late final RetryPolicy _retryPolicy; 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 /// the last dateTime at the which all the channels were synced
DateTime? _lastSyncedAt; DateTime? _lastSyncedAt;
@@ -406,10 +403,6 @@ class StreamChatClient {
if (event.type == EventType.healthCheck) { if (event.type == EventType.healthCheck) {
return _handleHealthCheckEvent(event); return _handleHealthCheckEvent(event);
} }
if (!event.isLocal && _synced) {
_lastSyncedAt = event.createdAt;
_chatPersistenceClient?.updateLastSyncAt(event.createdAt);
}
state.updateUser(event.user); state.updateUser(event.user);
return _eventController.add(event); return _eventController.add(event);
} }
@@ -438,8 +431,6 @@ class StreamChatClient {
type: EventType.connectionRecovered, type: EventType.connectionRecovered,
online: true, online: true,
)); ));
} else {
_synced = false;
} }
} }
@@ -464,13 +455,11 @@ class StreamChatClient {
Future<void> sync({List<String>? cids, DateTime? lastSyncAt}) async { Future<void> sync({List<String>? cids, DateTime? lastSyncAt}) async {
cids ??= await _chatPersistenceClient?.getChannelCids(); cids ??= await _chatPersistenceClient?.getChannelCids();
if (cids == null || cids.isEmpty) { if (cids == null || cids.isEmpty) {
_synced = true;
return; return;
} }
lastSyncAt ??= await _chatPersistenceClient?.getLastSyncAt(); lastSyncAt ??= await _chatPersistenceClient?.getLastSyncAt();
if (lastSyncAt == null) { if (lastSyncAt == null) {
_synced = true;
return; return;
} }
@@ -488,12 +477,10 @@ class StreamChatClient {
handleEvent(event); handleEvent(event);
} }
_synced = true;
final now = DateTime.now(); final now = DateTime.now();
_lastSyncedAt = now; _lastSyncedAt = now;
_chatPersistenceClient?.updateLastSyncAt(now); _chatPersistenceClient?.updateLastSyncAt(now);
} catch (e, stk) { } catch (e, stk) {
_synced = false;
logger.severe('Error during sync', e, stk); logger.severe('Error during sync', e, stk);
} }
} }
@@ -1325,6 +1312,7 @@ class StreamChatClient {
// resetting state // resetting state
state.dispose(); state.dispose();
state = ClientState(this); state = ClientState(this);
_lastSyncedAt = null;
// resetting credentials // resetting credentials
_tokenManager.reset(); _tokenManager.reset();
@@ -6,8 +6,8 @@ import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/src/client/channel.dart'; import 'package:stream_chat/src/client/channel.dart';
import 'package:stream_chat/src/client/retry_policy.dart'; import 'package:stream_chat/src/client/retry_policy.dart';
import 'package:stream_chat/src/core/error/error.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/core/models/message.dart';
import 'package:stream_chat/src/event_type.dart';
import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/stream_chat.dart';
/// The retry queue associated to a channel /// The retry queue associated to a channel
@@ -46,12 +46,16 @@ class Token extends Equatable {
/// Creates a [Token] instance from the provided [rawValue] if it's valid. /// Creates a [Token] instance from the provided [rawValue] if it's valid.
factory Token.fromRawValue(String rawValue) { factory Token.fromRawValue(String rawValue) {
final jwtBody = JsonWebToken.unverified(rawValue); final jwtBody = JsonWebToken.unverified(rawValue);
final userId = jwtBody.claims.getTyped<String>('user_id'); final userId = jwtBody.claims.getTyped('user_id');
assert( assert(
userId != null, userId != null,
'Invalid `token`, It should contain `user_id`', '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. /// The token which can be used during the development.
@@ -1,7 +1,7 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'package:stream_chat/src/core/models/channel_config.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/models/user.dart';
import 'package:stream_chat/src/core/util/serializer.dart';
part 'channel_model.g.dart'; part 'channel_model.g.dart';
@@ -167,6 +167,9 @@ class Filter extends Equatable {
factory Filter.exists(String key, {bool exists = true}) => factory Filter.exists(String key, {bool exists = true}) =>
Filter._(operator: FilterOperator.exists, key: key, value: exists); 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 /// Matches any list that contains the specified values
factory Filter.contains(String key, Object value) => factory Filter.contains(String key, Object value) =>
Filter._(operator: FilterOperator.contains, key: key, value: value); Filter._(operator: FilterOperator.contains, key: key, value: value);
@@ -54,8 +54,6 @@ class OwnUser extends User {
factory OwnUser.fromUser(User user) => OwnUser( factory OwnUser.fromUser(User user) => OwnUser(
id: user.id, id: user.id,
role: user.role, role: user.role,
name: user.name,
image: user.image,
createdAt: user.createdAt, createdAt: user.createdAt,
updatedAt: user.updatedAt, updatedAt: user.updatedAt,
lastActive: user.lastActive, lastActive: user.lastActive,
@@ -116,8 +114,6 @@ class OwnUser extends User {
return copyWith( return copyWith(
id: other.id, id: other.id,
role: other.role, role: other.role,
name: other.name,
image: other.image,
banned: other.banned, banned: other.banned,
channelMutes: other.channelMutes, channelMutes: other.channelMutes,
createdAt: other.createdAt, createdAt: other.createdAt,
@@ -1,6 +1,6 @@
import 'package:json_annotation/json_annotation.dart'; 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/models/user.dart';
import 'package:stream_chat/src/core/util/serializer.dart';
part 'reaction.g.dart'; part 'reaction.g.dart';
@@ -6,15 +6,15 @@ import 'package:logging/logging.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/src/core/error/error.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.dart';
import 'package:stream_chat/src/core/http/token_manager.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/event.dart';
import 'package:stream_chat/src/core/models/user.dart'; import 'package:stream_chat/src/core/models/user.dart';
import 'package:stream_chat/src/event_type.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: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/status.dart' as status;
import 'package:web_socket_channel/web_socket_channel.dart';
/// Typedef which exposes an [Event] as the only parameter. /// Typedef which exposes an [Event] as the only parameter.
typedef EventHandler = void Function(Event); typedef EventHandler = void Function(Event);
+1 -1
View File
@@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart';
/// Current package version /// Current package version
/// Used in [StreamChatClient] to build the `x-stream-client` header /// Used in [StreamChatClient] to build the `x-stream-client` header
// ignore: constant_identifier_names // ignore: constant_identifier_names
const PACKAGE_VERSION = '2.2.1'; const PACKAGE_VERSION = '3.0.0';
+1 -1
View File
@@ -1,7 +1,7 @@
name: stream_chat name: stream_chat
homepage: https://getstream.io/ homepage: https://getstream.io/
description: The official Dart client for Stream Chat, a service for building chat applications. 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 repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -334,20 +334,87 @@ void main() {
expectLater( expectLater(
// skipping first seed message list -> [] messages // skipping first seed message list -> [] messages
channel.state?.messagesStream.skip(1), channel.state?.messagesStream.skip(1),
emitsInOrder([ emitsInOrder(
[
// preparing attachments to upload
[ [
isSameMessageAs( isSameMessageAs(
message.copyWith(status: MessageSendingStatus.sending), message.copyWith(
status: MessageSendingStatus.sending,
attachments: [
...attachments.map((it) => it.copyWith(
uploadState: const UploadState.preparing()))
],
),
matchSendingStatus: true, 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( isSameMessageAs(
message.copyWith(status: MessageSendingStatus.sent), message.copyWith(
status: MessageSendingStatus.sent,
attachments: [
...attachments.map((it) =>
it.copyWith(uploadState: const UploadState.success()))
],
),
matchSendingStatus: true, matchSendingStatus: true,
matchAttachments: true,
matchAttachmentsUploadState: true,
), ),
], ],
]), ],
),
); );
final res = await channel.sendMessage(message); final res = await channel.sendMessage(message);
@@ -472,20 +539,87 @@ void main() {
expectLater( expectLater(
// skipping first seed message list -> [] messages // skipping first seed message list -> [] messages
channel.state?.messagesStream.skip(1), channel.state?.messagesStream.skip(1),
emitsInOrder([ emitsInOrder(
[
// preparing attachments to upload
[ [
isSameMessageAs( isSameMessageAs(
message.copyWith(status: MessageSendingStatus.updating), message.copyWith(
status: MessageSendingStatus.updating,
attachments: [
...attachments.map((it) => it.copyWith(
uploadState: const UploadState.preparing()))
],
),
matchSendingStatus: true, 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( isSameMessageAs(
message.copyWith(status: MessageSendingStatus.sent), message.copyWith(
status: MessageSendingStatus.sent,
attachments: [
...attachments.map((it) =>
it.copyWith(uploadState: const UploadState.success()))
],
),
matchSendingStatus: true, matchSendingStatus: true,
matchAttachments: true,
matchAttachmentsUploadState: true,
), ),
], ],
]), ],
),
); );
final res = await channel.updateMessage(message); final res = await channel.updateMessage(message);
@@ -1,6 +1,5 @@
import 'dart:convert'; import 'dart:convert';
import 'package:test/test.dart';
import 'package:stream_chat/src/core/api/responses.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/device.dart';
import 'package:stream_chat/src/core/models/member.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/reaction.dart';
import 'package:stream_chat/src/core/models/read.dart'; import 'package:stream_chat/src/core/models/read.dart';
import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/stream_chat.dart';
import 'package:test/test.dart';
void main() { void main() {
group('src/api/responses', () { group('src/api/responses', () {
@@ -1,7 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:test/test.dart';
import 'package:stream_chat/src/core/models/filter.dart'; import 'package:stream_chat/src/core/models/filter.dart';
import 'package:test/test.dart';
void main() { void main() {
group('operators', () { group('operators', () {
@@ -114,7 +114,7 @@ void main() {
test('notExists', () { test('notExists', () {
const key = 'testKey'; const key = 'testKey';
final filter = Filter.exists(key, exists: false); final filter = Filter.notExists(key);
expect(filter.key, key); expect(filter.key, key);
expect(filter.value, isFalse); expect(filter.value, isFalse);
expect(filter.operator, FilterOperator.exists.rawValue); expect(filter.operator, FilterOperator.exists.rawValue);
@@ -175,5 +175,26 @@ void main() {
expect(newUser.teams, ['team1', 'team2']); expect(newUser.teams, ['team1', 'team2']);
expect(newUser.language, 'fr'); 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);
},
);
}); });
} }
@@ -1,5 +1,5 @@
import 'package:test/test.dart';
import 'package:stream_chat/src/core/util/serializer.dart'; import 'package:stream_chat/src/core/util/serializer.dart';
import 'package:test/test.dart';
void main() { void main() {
group('src/models/serialization', () { group('src/models/serialization', () {
@@ -1,5 +1,5 @@
import 'package:test/test.dart';
import 'package:stream_chat/src/core/util/extension.dart'; import 'package:stream_chat/src/core/util/extension.dart';
import 'package:test/test.dart';
void main() { void main() {
test('`.withNullifyer` converts the type into non-nullable', () { test('`.withNullifyer` converts the type into non-nullable', () {
+1 -1
View File
@@ -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/channel_api.dart';
import 'package:stream_chat/src/core/api/device_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/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/message_api.dart';
import 'package:stream_chat/src/core/api/moderation_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/stream_chat_api.dart';
import 'package:stream_chat/src/core/api/user_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.dart';
import 'package:stream_chat/src/core/http/token_manager.dart'; import 'package:stream_chat/src/core/http/token_manager.dart';
import 'package:stream_chat/src/ws/websocket.dart'; import 'package:stream_chat/src/ws/websocket.dart';
@@ -1,6 +1,7 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:dio/dio.dart' show MultipartFile; import 'package:dio/dio.dart' show MultipartFile;
import 'package:stream_chat/src/client/channel.dart'; 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/channel_state.dart';
import 'package:stream_chat/src/core/models/event.dart'; import 'package:stream_chat/src/core/models/event.dart';
import 'package:stream_chat/src/core/models/message.dart'; import 'package:stream_chat/src/core/models/message.dart';
@@ -46,12 +47,16 @@ Matcher isSameMessageAs(
bool matchText = false, bool matchText = false,
bool matchReactions = false, bool matchReactions = false,
bool matchSendingStatus = false, bool matchSendingStatus = false,
bool matchAttachments = false,
bool matchAttachmentsUploadState = false,
}) => }) =>
_IsSameMessageAs( _IsSameMessageAs(
targetMessage: targetMessage, targetMessage: targetMessage,
matchText: matchText, matchText: matchText,
matchReactions: matchReactions, matchReactions: matchReactions,
matchSendingStatus: matchSendingStatus, matchSendingStatus: matchSendingStatus,
matchAttachments: matchAttachments,
matchAttachmentsUploadState: matchAttachmentsUploadState,
); );
class _IsSameMessageAs extends Matcher { class _IsSameMessageAs extends Matcher {
@@ -60,12 +65,16 @@ class _IsSameMessageAs extends Matcher {
this.matchText = false, this.matchText = false,
this.matchReactions = false, this.matchReactions = false,
this.matchSendingStatus = false, this.matchSendingStatus = false,
this.matchAttachments = false,
this.matchAttachmentsUploadState = false,
}); });
final Message targetMessage; final Message targetMessage;
final bool matchText; final bool matchText;
final bool matchReactions; final bool matchReactions;
final bool matchSendingStatus; final bool matchSendingStatus;
final bool matchAttachments;
final bool matchAttachmentsUploadState;
@override @override
Description describe(Description description) => Description describe(Description description) =>
@@ -96,6 +105,56 @@ class _IsSameMessageAs extends Matcher {
?.map((it) => '${it.type}-${it.messageId}') ?.map((it) => '${it.type}-${it.messageId}')
.toList()); .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; return matches;
} }
} }
@@ -1,11 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:mocktail/mocktail.dart';
import 'package:stream_chat/src/core/http/token_manager.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:stream_chat/stream_chat.dart';
import 'package:test/test.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 'package:web_socket_channel/web_socket_channel.dart';
import '../fakes.dart'; import '../fakes.dart';
+26
View File
@@ -1,15 +1,41 @@
## Upcoming ## 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 🐞 Fixed
- [[#668]](https://github.com/GetStream/stream-chat-flutter/issues/668): Fix `MessageInput` rendering errors in case - [[#668]](https://github.com/GetStream/stream-chat-flutter/issues/668): Fix `MessageInput` rendering errors in case
there are no actions available to show. there are no actions available to show.
- [[#349]](https://github.com/GetStream/stream-chat-flutter/issues/349): Fix `MessageInput` attachment render overflow error. - [[#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. - [[#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 🔄 Changed
- Animation curves changed from default `Curves.linear` to `Curves.easeOut` and `Curves.easeIn` for attachment controls. - 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 ## 2.2.1
@@ -21,6 +21,6 @@
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0</string> <string>1.0</string>
<key>MinimumOSVersion</key> <key>MinimumOSVersion</key>
<string>8.0</string> <string>9.0</string>
</dict> </dict>
</plist> </plist>
@@ -106,11 +106,9 @@ class ChannelListPage extends StatelessWidget {
[StreamChat.of(context).currentUser!.id], [StreamChat.of(context).currentUser!.id],
), ),
sort: const [SortOption('last_message_at')], sort: const [SortOption('last_message_at')],
pagination: const PaginationParams(
limit: 20, limit: 20,
), ),
), ),
),
); );
} }
@@ -83,9 +83,7 @@ class ChannelListPage extends StatelessWidget {
[StreamChat.of(context).currentUser!.id], [StreamChat.of(context).currentUser!.id],
), ),
sort: const [SortOption('last_message_at')], sort: const [SortOption('last_message_at')],
pagination: const PaginationParams(
limit: 20, limit: 20,
),
channelWidget: const ChannelPage(), channelWidget: const ChannelPage(),
), ),
), ),
@@ -85,9 +85,7 @@ class ChannelListPage extends StatelessWidget {
), ),
channelPreviewBuilder: _channelPreviewBuilder, channelPreviewBuilder: _channelPreviewBuilder,
// sort: [SortOption('last_message_at')], // sort: [SortOption('last_message_at')],
pagination: const PaginationParams(
limit: 20, limit: 20,
),
channelWidget: const ChannelPage(), channelWidget: const ChannelPage(),
), ),
), ),
@@ -69,9 +69,7 @@ class ChannelListPage extends StatelessWidget {
[StreamChat.of(context).currentUser!.id], [StreamChat.of(context).currentUser!.id],
), ),
sort: const [SortOption('last_message_at')], sort: const [SortOption('last_message_at')],
pagination: const PaginationParams(
limit: 20, limit: 20,
),
channelWidget: const ChannelPage(), channelWidget: const ChannelPage(),
), ),
), ),
@@ -75,9 +75,7 @@ class ChannelListPage extends StatelessWidget {
[StreamChat.of(context).currentUser!.id], [StreamChat.of(context).currentUser!.id],
), ),
sort: const [SortOption('last_message_at')], sort: const [SortOption('last_message_at')],
pagination: const PaginationParams(
limit: 20, limit: 20,
),
channelWidget: const ChannelPage(), channelWidget: const ChannelPage(),
), ),
), ),
@@ -102,9 +102,7 @@ class ChannelListPage extends StatelessWidget {
[StreamChat.of(context).currentUser!.id], [StreamChat.of(context).currentUser!.id],
), ),
sort: const [SortOption('last_message_at')], sort: const [SortOption('last_message_at')],
pagination: const PaginationParams(
limit: 20, limit: 20,
),
channelWidget: const ChannelPage(), channelWidget: const ChannelPage(),
), ),
), ),
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; 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/src/upload_progress_indicator.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Widget to build in progress /// Widget to build in progress
typedef InProgressBuilder = Widget Function(BuildContext, int, int); typedef InProgressBuilder = Widget Function(BuildContext, int, int);
@@ -1,15 +1,14 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.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_chat_theme.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.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/upload_progress_indicator.dart';
import 'package:stream_chat_flutter/src/utils.dart'; import 'package:stream_chat_flutter/src/utils.dart';
import 'package:stream_chat_flutter/src/video_thumbnail_image.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_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 /// Widget for displaying file attachments
class FileAttachment extends AttachmentWidget { class FileAttachment extends AttachmentWidget {
@@ -2,10 +2,10 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/src/attachment/attachment_widget.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/src/visible_footnote.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Widget for showing a GIF attachment /// Widget for showing a GIF attachment
class GiphyAttachment extends AttachmentWidget { class GiphyAttachment extends AttachmentWidget {
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/channel_info.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/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Bottom Sheet with options /// Bottom Sheet with options
class ChannelBottomSheet extends StatefulWidget { class ChannelBottomSheet extends StatefulWidget {
@@ -1,12 +1,13 @@
import 'package:flutter/material.dart'; 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/back_button.dart';
import 'package:stream_chat_flutter/src/channel_info.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/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/info_tile.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.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/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.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.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header_paint.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; break;
} }
final theme = Theme.of(context);
return InfoTile( return InfoTile(
showMessage: showConnectionStateTile && showStatus, showMessage: showConnectionStateTile && showStatus,
message: statusString, message: statusString,
child: AppBar( child: AppBar(
textTheme: Theme.of(context).textTheme, toolbarTextStyle: theme.textTheme.bodyText2,
brightness: Theme.of(context).brightness, titleTextStyle: theme.textTheme.headline6,
systemOverlayStyle: theme.brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
elevation: 1, elevation: 1,
leading: leadingWidget, leading: leadingWidget,
backgroundColor: backgroundColor ?? channelHeaderTheme.color, backgroundColor: backgroundColor ?? channelHeaderTheme.color,
@@ -1,8 +1,8 @@
import 'package:collection/collection.dart' show IterableExtension; import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.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/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Widget which shows channel info /// Widget which shows channel info
class ChannelInfo extends StatelessWidget { class ChannelInfo extends StatelessWidget {
@@ -1,6 +1,7 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/stream_neumorphic_button.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 chatThemeData = StreamChatTheme.of(context);
final channelListHeaderThemeData = ChannelListHeaderTheme.of(context); final channelListHeaderThemeData = ChannelListHeaderTheme.of(context);
final theme = Theme.of(context);
return InfoTile( return InfoTile(
showMessage: showConnectionStateTile && showStatus, showMessage: showConnectionStateTile && showStatus,
message: statusString, message: statusString,
child: AppBar( child: AppBar(
textTheme: Theme.of(context).textTheme, toolbarTextStyle: theme.textTheme.bodyText2,
brightness: Theme.of(context).brightness, titleTextStyle: theme.textTheme.headline6,
systemOverlayStyle: theme.brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
elevation: 1, elevation: 1,
backgroundColor: backgroundColor:
backgroundColor ?? channelListHeaderThemeData.color, backgroundColor ?? channelListHeaderThemeData.color,
@@ -4,11 +4,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/src/channel_bottom_sheet.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/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/src/theme/themes.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Callback called when tapping on a channel /// Callback called when tapping on a channel
typedef ChannelTapCallback = void Function(Channel, Widget?); typedef ChannelTapCallback = void Function(Channel, Widget?);
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.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/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.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. /// It shows the current [Channel] name using a [Text] widget.
/// ///
@@ -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<Command> 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,
),
);
}
}
}
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.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/extension.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
/// It shows a date divider depending on the date difference /// It shows a date divider depending on the date difference
class DateDivider extends StatelessWidget { class DateDivider extends StatelessWidget {
@@ -26,7 +26,8 @@
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. // 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 /// All Groups
enum EmojiGroup { enum EmojiGroup {
@@ -114240,7 +114241,6 @@ final emojiRegex = RegExp(
class Emoji { class Emoji {
static const variationSelector16 = 65039; static const variationSelector16 = 65039;
static const ZWJ = 8205; static const ZWJ = 8205;
final String? name; final String? name;
final String? char; final String? char;
final String? shortName; final String? shortName;
@@ -114252,14 +114252,39 @@ class Emoji {
/// Emoji class. /// 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. /// [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( Emoji({
{this.name, this.name,
this.char, this.char,
this.shortName, this.shortName,
this.emojiGroup, this.emojiGroup,
this.emojiSubgroup, this.emojiSubgroup,
this.keywords = const [], this.keywords = const [],
this.modifiable = false}); 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 /// Runes of Emoji Character
List<int> get charRunes { List<int> get charRunes {
@@ -114339,9 +114364,11 @@ class Emoji {
return _emojis.firstWhereOrNull((Emoji emoji) => emoji.name == name); return _emojis.firstWhereOrNull((Emoji emoji) => emoji.name == name);
} }
/// Returns Emoji by [name] as short name. /// Returns Emoji by [shortName] as short name.
static Emoji? byShortName(String name) { static Emoji? byShortName(String shortName) {
return _emojis.firstWhereOrNull((Emoji emoji) => emoji.char == name); return _emojis.firstWhereOrNull(
(Emoji emoji) => emoji.shortName == shortName,
);
} }
/// Returns list of Emojis in a same [group] /// Returns list of Emojis in a same [group]
@@ -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<Emoji> 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);
},
);
},
),
),
);
}
}
@@ -1,4 +1,5 @@
import 'package:characters/characters.dart'; import 'package:characters/characters.dart';
import 'package:diacritic/diacritic.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/emoji/emoji.dart'; import 'package:stream_chat_flutter/src/emoji/emoji.dart';
@@ -24,6 +25,12 @@ extension StringExtension on String {
final characters = trim().characters; final characters = trim().characters;
return characters.every(_emojiChars.contains); 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 /// List extension
@@ -170,3 +177,49 @@ extension IconButtonX on IconButton {
icon: icon ?? this.icon, icon: icon ?? this.icon,
); );
} }
/// Extensions on List<User>
extension UserListX on List<User> {
/// 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<User> search(String query) {
String normalize(String input) => input.toLowerCase().diacriticsInsensitive;
final normalizedQuery = normalize(query);
final matchingUsers = <User, int>{}; // 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);
}
}
@@ -5,11 +5,11 @@ import 'package:cached_network_image/cached_network_image.dart';
import 'package:chewie/chewie.dart'; import 'package:chewie/chewie.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.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_footer.dart';
import 'package:stream_chat_flutter/src/gallery_header.dart'; import 'package:stream_chat_flutter/src/gallery_header.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:video_player/video_player.dart'; import 'package:video_player/video_player.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Return action for coming back from pages /// Return action for coming back from pages
enum ReturnActionType { enum ReturnActionType {
@@ -112,6 +112,8 @@ class _FullScreenMediaState extends State<FullScreenMedia>
attachment.assetUrl ?? attachment.assetUrl ??
attachment.thumbUrl; attachment.thumbUrl;
return PhotoView( return PhotoView(
loadingBuilder: (context, image) =>
const Offstage(),
imageProvider: (imageUrl == null && imageProvider: (imageUrl == null &&
attachment.localUri != null && attachment.localUri != null &&
attachment.file?.bytes != null) attachment.file?.bytes != null)
@@ -7,12 +7,12 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.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/stream_chat_theme.dart';
import 'package:stream_chat_flutter/src/theme/themes.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/src/video_thumbnail_image.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Footer widget for media display /// Footer widget for media display
class GalleryFooter extends StatefulWidget implements PreferredSizeWidget { class GalleryFooter extends StatefulWidget implements PreferredSizeWidget {
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; 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/attachment_actions_modal.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.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/stream_svg_icon.dart';
@@ -57,9 +58,13 @@ class GalleryHeader extends StatelessWidget implements PreferredSizeWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final galleryHeaderThemeData = GalleryHeaderTheme.of(context); final galleryHeaderThemeData = GalleryHeaderTheme.of(context);
final theme = Theme.of(context);
return AppBar( return AppBar(
textTheme: Theme.of(context).textTheme, toolbarTextStyle: theme.textTheme.bodyText2,
brightness: Theme.of(context).brightness, titleTextStyle: theme.textTheme.headline6,
systemOverlayStyle: theme.brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
elevation: 1, elevation: 1,
leading: showBackButton leading: showBackButton
? IconButton( ? IconButton(
@@ -4,6 +4,7 @@ import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// This widget is used for showing user tiles for mentions /// This widget is used for showing user tiles for mentions
/// Use [title], [subtitle], [leading], [trailing] for /// Use [title], [subtitle], [leading], [trailing] for
/// substituting widgets in respective positions /// substituting widgets in respective positions
@Deprecated('Use `UserMentionTile` instead. Will be removed in future release')
class MentionTile extends StatelessWidget { class MentionTile extends StatelessWidget {
/// Constructor for creating a [MentionTile] widget /// Constructor for creating a [MentionTile] widget
const MentionTile( const MentionTile(
@@ -2,27 +2,30 @@ import 'dart:async';
import 'dart:math'; import 'dart:math';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:collection/collection.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:photo_manager/photo_manager.dart'; import 'package:photo_manager/photo_manager.dart';
import 'package:shimmer/shimmer.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/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/extension.dart';
import 'package:stream_chat_flutter/src/media_list_view.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/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/quoted_message_widget.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.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/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_service.dart';
import 'package:stream_chat_flutter/src/video_thumbnail_image.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/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.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'; import 'package:video_compress/video_compress.dart';
export 'package:video_compress/video_compress.dart' show VideoQuality; export 'package:video_compress/video_compress.dart' show VideoQuality;
@@ -53,13 +56,20 @@ typedef AttachmentThumbnailBuilder = Widget Function(
Attachment, Attachment,
); );
/// Builder function for building a mention tile /// Builder function for building a mention tile.
/// Use [MentionTile] for the default implementation
typedef MentionTileBuilder = Widget Function( typedef MentionTileBuilder = Widget Function(
BuildContext context, BuildContext context,
Member member, 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. /// Widget builder for action button.
/// ///
/// [defaultActionButton] is the default [IconButton] configuration, /// [defaultActionButton] is the default [IconButton] configuration,
@@ -169,7 +179,7 @@ class MessageInput extends StatefulWidget {
this.disableAttachments = false, this.disableAttachments = false,
this.initialMessage, this.initialMessage,
this.textEditingController, this.textEditingController,
this.actions, this.actions = const [],
this.actionsLocation = ActionsLocation.left, this.actionsLocation = ActionsLocation.left,
this.attachmentThumbnailBuilders, this.attachmentThumbnailBuilders,
this.focusNode, this.focusNode,
@@ -181,7 +191,9 @@ class MessageInput extends StatefulWidget {
this.idleSendButton, this.idleSendButton,
this.activeSendButton, this.activeSendButton,
this.showCommandsButton = true, this.showCommandsButton = true,
@Deprecated('''Use `userMentionsTileBuilder` instead. Will be removed in future release''')
this.mentionsTileBuilder, this.mentionsTileBuilder,
this.userMentionsTileBuilder,
this.maxAttachmentSize = _kDefaultMaxAttachmentSize, this.maxAttachmentSize = _kDefaultMaxAttachmentSize,
this.compressedVideoQuality = VideoQuality.DefaultQuality, this.compressedVideoQuality = VideoQuality.DefaultQuality,
this.compressedVideoFrameRate = 30, this.compressedVideoFrameRate = 30,
@@ -190,12 +202,17 @@ class MessageInput extends StatefulWidget {
this.onAttachmentLimitExceed, this.onAttachmentLimitExceed,
this.attachmentButtonBuilder, this.attachmentButtonBuilder,
this.commandButtonBuilder, this.commandButtonBuilder,
this.customOverlays = const [],
this.mentionAllAppUsers = false,
}) : assert( }) : assert(
initialMessage == null || editMessage == null, initialMessage == null || editMessage == null,
"Can't provide both `initialMessage` and `editMessage`", "Can't provide both `initialMessage` and `editMessage`",
), ),
super(key: key); super(key: key);
/// List of options for showing overlays
final List<OverlayOptions> customOverlays;
/// Message to edit /// Message to edit
final Message? editMessage; final Message? editMessage;
@@ -242,7 +259,7 @@ class MessageInput extends StatefulWidget {
final TextEditingController? textEditingController; final TextEditingController? textEditingController;
/// List of action widgets /// List of action widgets
final List<Widget>? actions; final List<Widget> actions;
/// The location of the custom actions /// The location of the custom actions
final ActionsLocation actionsLocation; final ActionsLocation actionsLocation;
@@ -271,9 +288,12 @@ class MessageInput extends StatefulWidget {
/// Send button widget in an active state /// Send button widget in an active state
final Widget? activeSendButton; final Widget? activeSendButton;
/// Customize the tile for the mentions overlay /// Customize the tile for the mentions overlay.
final MentionTileBuilder? mentionsTileBuilder; final MentionTileBuilder? mentionsTileBuilder;
/// Customize the tile for the mentions overlay.
final UserMentionTileBuilder? userMentionsTileBuilder;
/// A callback for error reporting /// A callback for error reporting
final ErrorListener? onError; final ErrorListener? onError;
@@ -297,6 +317,11 @@ class MessageInput extends StatefulWidget {
/// calling `.copyWith`. /// calling `.copyWith`.
final ActionButtonBuilder? commandButtonBuilder; final ActionButtonBuilder? commandButtonBuilder;
/// When enabled mentions search users across the entire app.
///
/// Defaults to false.
final bool mentionAllAppUsers;
@override @override
MessageInputState createState() => MessageInputState(); MessageInputState createState() => MessageInputState();
@@ -318,11 +343,11 @@ class MessageInputState extends State<MessageInput> {
final List<User> _mentionedUsers = []; final List<User> _mentionedUsers = [];
final _imagePicker = ImagePicker(); final _imagePicker = ImagePicker();
late final FocusNode _focusNode; late final _focusNode = widget.focusNode ?? FocusNode();
bool _inputEnabled = true; bool _inputEnabled = true;
bool _commandEnabled = false; bool _commandEnabled = false;
OverlayEntry? _commandsOverlay, _mentionsOverlay, _emojiOverlay; bool _showCommandsOverlay = false;
late Iterable<String> _emojiNames; bool _showMentionsOverlay = false;
Command? _chosenCommand; Command? _chosenCommand;
bool _actionsShrunk = false; bool _actionsShrunk = false;
@@ -330,10 +355,9 @@ class MessageInputState extends State<MessageInput> {
bool _openFilePickerSection = false; bool _openFilePickerSection = false;
int _filePickerIndex = 0; int _filePickerIndex = 0;
final _keyboardVisibilityController = KeyboardVisibilityController();
/// The editing controller passed to the input TextField /// The editing controller passed to the input TextField
late final TextEditingController textEditingController; late final TextEditingController textEditingController =
widget.textEditingController ?? TextEditingController();
late StreamChatThemeData _streamChatTheme; late StreamChatThemeData _streamChatTheme;
late MessageInputThemeData _messageInputTheme; late MessageInputThemeData _messageInputTheme;
@@ -345,29 +369,10 @@ class MessageInputState extends State<MessageInput> {
@override @override
void initState() { void initState() {
super.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) { if (widget.editMessage != null || widget.initialMessage != null) {
_parseExistingMessage(widget.editMessage ?? widget.initialMessage!); _parseExistingMessage(widget.editMessage ?? widget.initialMessage!);
} }
textEditingController.addListener(_onChangedDebounced);
textEditingController.addListener(() {
_onChanged(context, textEditingController.text);
});
_focusNode.addListener(() { _focusNode.addListener(() {
if (_focusNode.hasFocus) { if (_focusNode.hasFocus) {
_openFilePickerSection = false; _openFilePickerSection = false;
@@ -472,7 +477,35 @@ class MessageInputState extends State<MessageInput> {
child: child, 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( Flex _buildTextField(BuildContext context) => Flex(
@@ -604,7 +637,7 @@ class MessageInputState extends State<MessageInput> {
), ),
secondChild: widget.disableAttachments && secondChild: widget.disableAttachments &&
!widget.showCommandsButton && !widget.showCommandsButton &&
widget.actions?.isNotEmpty != true widget.actions.isNotEmpty != true
? const Offstage() ? const Offstage()
: Wrap( : Wrap(
children: <Widget>[ children: <Widget>[
@@ -615,7 +648,7 @@ class MessageInputState extends State<MessageInput> {
channel.state != null && channel.state != null &&
channel.config?.commands.isNotEmpty == true) channel.config?.commands.isNotEmpty == true)
_buildCommandButton(context), _buildCommandButton(context),
...widget.actions ?? [], ...widget.actions,
].insertBetween(const SizedBox(width: 8)), ].insertBetween(const SizedBox(width: 8)),
), ),
duration: const Duration(milliseconds: 300), duration: const Duration(milliseconds: 300),
@@ -783,51 +816,32 @@ class MessageInputState extends State<MessageInput> {
).merge(passedDecoration); ).merge(passedDecoration);
} }
Timer? _debounce; late final _onChangedDebounced = debounce(
String? _previousValue;
void _onChanged(BuildContext context, String s) {
if (s == _previousValue) {
return;
}
_previousValue = s;
if (_debounce?.isActive == true) _debounce!.cancel();
_debounce = Timer(
const Duration(milliseconds: 350),
() { () {
if (!mounted) { var value = textEditingController.text;
return; if (!mounted) return;
value = value.trim();
final channel = StreamChannel.of(context).channel;
if (value.isNotEmpty) {
channel.keyStroke(widget.parentMessage?.id).catchError((e) {});
} }
StreamChannel.of(context)
.channel var actionsLength = widget.actions.length;
.keyStroke(widget.parentMessage?.id) if (widget.showCommandsButton) actionsLength += 1;
.catchError((e) {}); if (!widget.disableAttachments) actionsLength += 1;
setState(() { setState(() {
_actionsShrunk = s.trim().isNotEmpty && _actionsShrunk = value.isNotEmpty && actionsLength > 1;
((widget.actions?.length ?? 0) +
(widget.showCommandsButton ? 1 : 0) +
(widget.disableAttachments ? 0 : 1) >
1);
}); });
_commandsOverlay?.remove(); _checkCommands(value, context);
_commandsOverlay = null; _checkMentions(value, context);
_mentionsOverlay?.remove(); _checkEmoji(value, context);
_mentionsOverlay = null;
_emojiOverlay?.remove();
_emojiOverlay = null;
_checkCommands(s.trim(), context);
_checkMentions(s, context);
_checkEmoji(s, context);
}, },
const Duration(milliseconds: 350),
leading: true,
); );
}
String _getHint(BuildContext context) { String _getHint(BuildContext context) {
if (_commandEnabled && _chosenCommand!.name == 'giphy') { if (_commandEnabled && _chosenCommand!.name == 'giphy') {
@@ -847,10 +861,7 @@ class MessageInputState extends State<MessageInput> {
if (s.isNotEmpty && if (s.isNotEmpty &&
textEditingController.selection.baseOffset > 0 && textEditingController.selection.baseOffset > 0 &&
textEditingController.text textEditingController.text
.substring( .substring(0, textEditingController.selection.baseOffset)
0,
textEditingController.selection.baseOffset,
)
.contains(':')) { .contains(':')) {
final textToSelection = textEditingController.text final textToSelection = textEditingController.text
.substring(0, textEditingController.value.selection.start); .substring(0, textEditingController.value.selection.start);
@@ -860,12 +871,6 @@ class MessageInputState extends State<MessageInput> {
if (textToSelection.endsWith(':') && emoji != null) { if (textToSelection.endsWith(':') && emoji != null) {
_chooseEmoji(splits.sublist(0, splits.length - 1), emoji); _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<MessageInput> {
.split(' ') .split(' ')
.last .last
.contains('@')) { .contains('@')) {
_mentionsOverlay = _buildMentionsOverlayEntry(); if (!_showMentionsOverlay) {
if (_mentionsOverlay != null) { setState(() {
Overlay.of(context)!.insert(_mentionsOverlay!); _showMentionsOverlay = true;
});
} }
} else if (_showMentionsOverlay) {
setState(() {
_showMentionsOverlay = false;
});
} }
} }
void _checkCommands(String s, BuildContext context) { void _checkCommands(String s, BuildContext context) {
if (s.startsWith('/')) { if (s.startsWith('/')) {
final matchedCommandsList = StreamChannel.of(context) final allCommands = StreamChannel.of(context).channel.config?.commands;
.channel final command =
.config allCommands?.firstWhereOrNull((it) => it.name == s.substring(1));
?.commands if (command != null) {
.where((element) => element.name == s.substring(1)) return _setCommand(command);
.toList() ?? } else if (!_showCommandsOverlay) {
[];
if (matchedCommandsList.length == 1) {
_chosenCommand = matchedCommandsList[0];
textEditingController.clear();
setState(() { 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 text = textEditingController.text.trimLeft();
final commands = StreamChannel.of(context)
.channel
.config
?.commands
.where((c) => c.name.contains(text.replaceFirst('/', '')))
.toList() ??
[];
if (commands.isEmpty) { final renderObject = context.findRenderObject() as RenderBox?;
return null; if (renderObject == null) {
return const Offstage();
} }
return CommandsOverlay(
// ignore: cast_nullable_to_non_nullable channel: StreamChannel.of(context).channel,
final renderBox = context.findRenderObject() as RenderBox; size: Size(renderObject.size.width - 16, 400),
final size = renderBox.size; text: text,
onCommandResult: _setCommand,
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 OverlayEntry(
builder: (context) => Positioned(
bottom: size.height + MediaQuery.of(context).viewInsets.bottom,
left: 0,
right: 0,
child: TweenAnimationBuilder<double>(
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() { Widget _buildFilePickerSection() {
@@ -1087,7 +976,9 @@ class MessageInputState extends State<MessageInput> {
} }
return AnimatedContainer( return AnimatedContainer(
duration: const Duration(milliseconds: 300), duration: _openFilePickerSection
? const Duration(milliseconds: 300)
: const Duration(),
curve: Curves.easeOut, curve: Curves.easeOut,
height: _openFilePickerSection ? _kMinMediaPickerSize : 0, height: _openFilePickerSection ? _kMinMediaPickerSize : 0,
child: SingleChildScrollView( child: SingleChildScrollView(
@@ -1261,300 +1152,78 @@ class MessageInputState extends State<MessageInput> {
}); });
} }
Widget _buildCommandIcon(String iconType) { Widget _buildMentionsOverlayEntry() {
switch (iconType) { if (textEditingController.value.selection.start < 0) {
case 'giphy': return const Offstage();
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,
),
);
}
} }
OverlayEntry? _buildMentionsOverlayEntry() {
final splits = textEditingController.text final splits = textEditingController.text
.substring(0, textEditingController.value.selection.start) .substring(0, textEditingController.value.selection.start)
.split('@'); .split('@');
final query = splits.last.toLowerCase(); final query = splits.last.toLowerCase();
Future<List<Member>>? 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 // ignore: cast_nullable_to_non_nullable
final renderBox = context.findRenderObject() as RenderBox; final renderObject = context.findRenderObject() as RenderBox;
final size = renderBox.size;
final child = Card( var tileBuilder = widget.userMentionsTileBuilder;
margin: const EdgeInsets.all(8), if (tileBuilder == null && widget.mentionsTileBuilder != null) {
elevation: 2, tileBuilder = (context, user) {
color: _streamChatTheme.colorTheme.barsBg, final member = Member(
shape: RoundedRectangleBorder( user: user,
borderRadius: BorderRadius.circular(8), userId: user.id,
), createdAt: user.createdAt,
clipBehavior: Clip.hardEdge, updatedAt: user.updatedAt,
child: Container( );
constraints: BoxConstraints.loose(const Size.fromHeight(240)), return widget.mentionsTileBuilder!(context, member);
decoration: BoxDecoration( };
color: _streamChatTheme.colorTheme.barsBg,
),
child: FutureBuilder<List<Member>>(
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!);
} }
splits[splits.length - 1] = m.user!.name; 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('@'); final rejoin = splits.join('@');
textEditingController.value = TextEditingValue( textEditingController.value = TextEditingValue(
text: rejoin + text: rejoin +
textEditingController.text.substring( textEditingController.text
textEditingController.selection.start), .substring(textEditingController.selection.start),
selection: TextSelection.collapsed( selection: TextSelection.collapsed(
offset: rejoin.length, offset: rejoin.length,
), ),
); );
_debounce!.cancel(); _onChangedDebounced.cancel();
_mentionsOverlay?.remove(); setState(() => _showMentionsOverlay = false);
_mentionsOverlay = null;
}, },
child: widget.mentionsTileBuilder != null
? widget.mentionsTileBuilder!(context, m)
: MentionTile(m),
),
),
)
.toList(),
const SizedBox(
height: 8,
),
],
),
),
),
);
return OverlayEntry(
builder: (context) => Positioned(
bottom: size.height + MediaQuery.of(context).viewInsets.bottom,
left: 0,
right: 0,
child: TweenAnimationBuilder<double>(
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,
),
),
); );
} }
OverlayEntry? _buildEmojiOverlay() { Widget _buildEmojiOverlay() {
if (textEditingController.value.selection.baseOffset < 0) {
return const Offstage();
}
final splits = textEditingController.text final splits = textEditingController.text
.substring(0, textEditingController.value.selection.start) .substring(0, textEditingController.value.selection.baseOffset)
.split(':'); .split(':');
final query = splits.last.toLowerCase(); 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 // ignore: cast_nullable_to_non_nullable
final renderBox = context.findRenderObject() as RenderBox; final renderObject = context.findRenderObject() as RenderBox;
final size = renderBox.size;
return OverlayEntry( return EmojiOverlay(
builder: (context) => Positioned( size: Size(renderObject.size.width - 16, 200),
bottom: size.height + MediaQuery.of(context).viewInsets.bottom, query: query,
left: 0, onEmojiResult: (emoji) {
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); _chooseEmoji(splits, emoji);
}, },
); );
}),
),
),
));
} }
void _chooseEmoji(List<String> splits, Emoji emoji) { void _chooseEmoji(List<String> splits, Emoji emoji) {
@@ -1568,9 +1237,6 @@ class MessageInputState extends State<MessageInput> {
offset: rejoin.length, offset: rejoin.length,
), ),
); );
_emojiOverlay?.remove();
_emojiOverlay = null;
} }
void _setCommand(Command c) { void _setCommand(Command c) {
@@ -1578,9 +1244,8 @@ class MessageInputState extends State<MessageInput> {
setState(() { setState(() {
_chosenCommand = c; _chosenCommand = c;
_commandEnabled = true; _commandEnabled = true;
_showCommandsOverlay = false;
}); });
_commandsOverlay?.remove();
_commandsOverlay = null;
} }
Widget _buildReplyToMessage() { Widget _buildReplyToMessage() {
@@ -1773,7 +1438,7 @@ class MessageInputState extends State<MessageInput> {
icon: StreamSvgIcon.lightning( icon: StreamSvgIcon.lightning(
color: s.isNotEmpty color: s.isNotEmpty
? _streamChatTheme.colorTheme.disabled ? _streamChatTheme.colorTheme.disabled
: (_commandsOverlay != null : (_showCommandsOverlay
? _messageInputTheme.actionButtonColor ? _messageInputTheme.actionButtonColor
: _messageInputTheme.actionButtonIdleColor), : _messageInputTheme.actionButtonIdleColor),
), ),
@@ -1789,19 +1454,9 @@ class MessageInputState extends State<MessageInput> {
await Future.delayed(const Duration(milliseconds: 300)); await Future.delayed(const Duration(milliseconds: 300));
} }
if (_commandsOverlay == null) {
setState(() { setState(() {
_commandsOverlay = _buildCommandsOverlayEntry(); _showCommandsOverlay = !_showCommandsOverlay;
if (_commandsOverlay != null) {
Overlay.of(context)!.insert(_commandsOverlay!);
}
}); });
} else {
setState(() {
_commandsOverlay?.remove();
_commandsOverlay = null;
});
}
}, },
); );
@@ -1823,12 +1478,8 @@ class MessageInputState extends State<MessageInput> {
), ),
splashRadius: 24, splashRadius: 24,
onPressed: () async { onPressed: () async {
_emojiOverlay?.remove(); _showCommandsOverlay = false;
_emojiOverlay = null; _showMentionsOverlay = false;
_commandsOverlay?.remove();
_commandsOverlay = null;
_mentionsOverlay?.remove();
_mentionsOverlay = null;
if (_openFilePickerSection) { if (_openFilePickerSection) {
setState(() => _openFilePickerSection = false); setState(() => _openFilePickerSection = false);
@@ -2114,11 +1765,6 @@ class MessageInputState extends State<MessageInput> {
_commandEnabled = false; _commandEnabled = false;
}); });
_commandsOverlay?.remove();
_commandsOverlay = null;
_mentionsOverlay?.remove();
_mentionsOverlay = null;
Message message; Message message;
if (widget.editMessage != null) { if (widget.editMessage != null) {
message = widget.editMessage!.copyWith( message = widget.editMessage!.copyWith(
@@ -2185,8 +1831,6 @@ class MessageInputState extends State<MessageInput> {
} }
} }
StreamSubscription? _keyboardListener;
void _showErrorAlert(String description) { void _showErrorAlert(String description) {
showModalBottomSheet( showModalBottomSheet(
backgroundColor: _streamChatTheme.colorTheme.barsBg, backgroundColor: _streamChatTheme.colorTheme.barsBg,
@@ -2260,12 +1904,9 @@ class MessageInputState extends State<MessageInput> {
@override @override
void dispose() { void dispose() {
_commandsOverlay?.remove();
_emojiOverlay?.remove();
_mentionsOverlay?.remove();
_keyboardListener?.cancel();
textEditingController.dispose(); textEditingController.dispose();
_stopSlowMode(); _stopSlowMode();
_onChangedDebounced.cancel();
super.dispose(); super.dispose();
} }
@@ -494,14 +494,18 @@ class _MessageListViewState extends State<MessageListView> {
return _buildThreadSeparator(); return _buildThreadSeparator();
} }
if (i == itemCount - 3) { if (i == itemCount - 3) {
if (widget.headerBuilder == null) { if (widget.reverse
? widget.headerBuilder == null
: widget.footerBuilder == null) {
if (_isThreadConversation) return const Offstage(); if (_isThreadConversation) return const Offstage();
return const SizedBox(height: 52); return const SizedBox(height: 52);
} }
return const SizedBox(height: 8); return const SizedBox(height: 8);
} }
if (i == 0) { 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: 30);
} }
return const SizedBox(height: 8); return const SizedBox(height: 8);
@@ -525,13 +529,13 @@ class _MessageListViewState extends State<MessageListView> {
? widget.dateDividerBuilder!( ? widget.dateDividerBuilder!(
nextMessage.createdAt.toLocal(), nextMessage.createdAt.toLocal(),
) )
: DateDivider( : Padding(
dateTime: nextMessage.createdAt.toLocal(),
);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
child: divider, child: DateDivider(
dateTime: nextMessage.createdAt.toLocal(),
),
); );
return divider;
} }
final timeDiff = final timeDiff =
Jiffy(nextMessage.createdAt.toLocal()).diff( Jiffy(nextMessage.createdAt.toLocal()).diff(
@@ -560,8 +564,13 @@ class _MessageListViewState extends State<MessageListView> {
} }
if (i == itemCount - 2) { if (i == itemCount - 2) {
if (widget.reverse) {
return widget.headerBuilder?.call(context) ?? return widget.headerBuilder?.call(context) ??
const Offstage(); const Offstage();
} else {
return widget.footerBuilder?.call(context) ??
const Offstage();
}
} }
if (i == itemCount - 3) { if (i == itemCount - 3) {
@@ -579,8 +588,13 @@ class _MessageListViewState extends State<MessageListView> {
} }
if (i == 0) { if (i == 0) {
if (widget.reverse) {
return widget.footerBuilder?.call(context) ?? return widget.footerBuilder?.call(context) ??
const Offstage(); const Offstage();
} else {
return widget.headerBuilder?.call(context) ??
const Offstage();
}
} }
const bottomMessageIndex = 2; // 1 -> loader // 0 -> footer const bottomMessageIndex = 2; // 1 -> loader // 0 -> footer
@@ -1,6 +1,7 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart'; 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_bubble.dart';
import 'package:stream_chat_flutter/src/reaction_picker.dart'; import 'package:stream_chat_flutter/src/reaction_picker.dart';
import 'package:stream_chat_flutter/src/stream_chat.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/src/user_avatar.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Modal widget for displaying message reactions /// Modal widget for displaying message reactions
class MessageReactionsModal extends StatelessWidget { class MessageReactionsModal extends StatelessWidget {
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.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/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.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. /// It shows the current [Message] preview.
/// ///
@@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; 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/info_tile.dart';
import 'package:stream_chat_flutter/src/message_search_item.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/src/theme/themes.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Callback called when tapping on a user /// Callback called when tapping on a user
typedef MessageSearchItemTapCallback = void Function(GetMessageResponse); typedef MessageSearchItemTapCallback = void Function(GetMessageResponse);
@@ -36,7 +36,7 @@ typedef EmptyMessageSearchBuilder = Widget Function(
/// r'$in': [user.id] /// r'$in': [user.id]
/// } /// }
/// }, /// },
/// paginationParams: PaginationParams(limit: 20), /// limit: 20,
/// ), /// ),
/// ); /// );
/// } /// }
@@ -53,17 +53,12 @@ typedef EmptyMessageSearchBuilder = Widget Function(
/// Modify it to change the widget appearance. /// Modify it to change the widget appearance.
class MessageSearchListView extends StatefulWidget { class MessageSearchListView extends StatefulWidget {
/// Instantiate a new MessageSearchListView /// Instantiate a new MessageSearchListView
MessageSearchListView({ const MessageSearchListView({
Key? key, Key? key,
required this.filters, required this.filters,
this.messageQuery, this.messageQuery,
this.sortOptions, this.sortOptions,
@Deprecated( this.limit = 30,
"'paginationParams' is deprecated and shouldn't be used. "
"This property is no longer used, Please use 'limit' instead",
)
this.paginationParams,
int? limit,
this.messageFilters, this.messageFilters,
this.separatorBuilder, this.separatorBuilder,
this.itemBuilder, this.itemBuilder,
@@ -76,8 +71,7 @@ class MessageSearchListView extends StatefulWidget {
this.loadingBuilder, this.loadingBuilder,
this.childBuilder, this.childBuilder,
this.messageSearchListController, this.messageSearchListController,
}) : limit = limit ?? paginationParams?.limit ?? 30, }) : super(key: key);
super(key: key);
/// Message String to search on /// Message String to search on
final String? messageQuery; final String? messageQuery;
@@ -95,16 +89,6 @@ class MessageSearchListView extends StatefulWidget {
/// Direction can be ascending or descending. /// Direction can be ascending or descending.
final List<SortOption>? sortOptions; final List<SortOption>? 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. /// The amount of messages requested per API call.
final int limit; final int limit;
@@ -580,11 +580,12 @@ class _MessageWidgetState extends State<MessageWidget>
bool get isOnlyEmoji => widget.message.text?.isOnlyEmoji == true; bool get isOnlyEmoji => widget.message.text?.isOnlyEmoji == true;
bool get hasNonUrlAttachments => bool get hasNonUrlAttachments => widget.message.attachments
widget.message.attachments.where((it) => it.titleLink == null).isNotEmpty; .where((it) => it.titleLink == null || it.type == 'giphy')
.isNotEmpty;
bool get hasUrlAttachments => bool get hasUrlAttachments => widget.message.attachments
widget.message.attachments.any((it) => it.titleLink != null) == true; .any((it) => it.titleLink != null && it.type != 'giphy');
bool get showBottomRow => bool get showBottomRow =>
showThreadReplyIndicator || showThreadReplyIndicator ||
@@ -1156,7 +1157,9 @@ class _MessageWidgetState extends State<MessageWidget>
final attachmentGroups = <String, List<Attachment>>{}; final attachmentGroups = <String, List<Attachment>>{};
widget.message.attachments widget.message.attachments
.where((element) => element.titleLink == null && element.type != null) .where((element) =>
(element.titleLink == null && element.type != null) ||
element.type == 'giphy')
.forEach((e) { .forEach((e) {
if (attachmentGroups[e.type] == null) { if (attachmentGroups[e.type] == null) {
attachmentGroups[e.type!] = []; attachmentGroups[e.type!] = [];
@@ -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> 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,
);
}
}
@@ -100,7 +100,9 @@ class StreamChatState extends State<StreamChat> {
return Theme( return Theme(
data: materialTheme.copyWith( data: materialTheme.copyWith(
primaryIconTheme: streamTheme.primaryIconTheme, primaryIconTheme: streamTheme.primaryIconTheme,
accentColor: streamTheme.colorTheme.accentPrimary, colorScheme: materialTheme.colorScheme.copyWith(
secondary: streamTheme.colorTheme.accentPrimary,
),
), ),
child: StreamChatCore( child: StreamChatCore(
client: client, client: client,
@@ -131,7 +131,7 @@ class StreamChatThemeData {
final defaultTheme = StreamChatThemeData(brightness: theme.brightness); final defaultTheme = StreamChatThemeData(brightness: theme.brightness);
final customizedTheme = StreamChatThemeData.fromColorAndTextTheme( final customizedTheme = StreamChatThemeData.fromColorAndTextTheme(
defaultTheme.colorTheme.copyWith( defaultTheme.colorTheme.copyWith(
accentPrimary: theme.accentColor, accentPrimary: theme.colorScheme.secondary,
), ),
defaultTheme.textTheme, defaultTheme.textTheme,
); );
@@ -1,8 +1,9 @@
import 'package:flutter/material.dart'; 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/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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_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.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/thread_header_paint.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( return AppBar(
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
textTheme: Theme.of(context).textTheme, toolbarTextStyle: theme.textTheme.bodyText2,
brightness: Theme.of(context).brightness, titleTextStyle: theme.textTheme.headline6,
systemOverlayStyle: theme.brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
elevation: 1, elevation: 1,
leading: leading ?? leading: leading ??
(showBackButton (showBackButton
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:lottie/lottie.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/src/extension.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// Widget to show the current list of typing users /// Widget to show the current list of typing users
class TypingIndicator extends StatelessWidget { class TypingIndicator extends StatelessWidget {
@@ -66,6 +66,12 @@ class UserAvatar extends StatelessWidget {
final placeholder = final placeholder =
this.placeholder ?? streamChatTheme.placeholderUserImage; this.placeholder ?? streamChatTheme.placeholderUserImage;
final backupGradientAvatar = ClipRRect(
borderRadius: borderRadius ??
streamChatTheme.ownMessageTheme.avatarTheme?.borderRadius,
child: streamChatTheme.defaultUserImage(context, user),
);
Widget avatar = FittedBox( Widget avatar = FittedBox(
fit: BoxFit.cover, fit: BoxFit.cover,
child: Container( child: Container(
@@ -76,8 +82,7 @@ class UserAvatar extends StatelessWidget {
fit: BoxFit.cover, fit: BoxFit.cover,
filterQuality: FilterQuality.high, filterQuality: FilterQuality.high,
imageUrl: user.image!, imageUrl: user.image!,
errorWidget: (context, __, ___) => errorWidget: (context, __, ___) => backupGradientAvatar,
streamChatTheme.defaultUserImage(context, user),
placeholder: placeholder != null placeholder: placeholder != null
? (context, __) => placeholder(context, user) ? (context, __) => placeholder(context, user)
: null, : null,
@@ -91,11 +96,7 @@ class UserAvatar extends StatelessWidget {
), ),
), ),
) )
: ClipRRect( : backupGradientAvatar,
borderRadius: borderRadius ??
streamChatTheme.ownMessageTheme.avatarTheme?.borderRadius,
child: streamChatTheme.defaultUserImage(context, user),
),
), ),
); );
@@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.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/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/user_list_view.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/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.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. /// It shows the current [User] preview.
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; 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/src/theme/themes.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.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_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Callback called when tapping on a user /// Callback called when tapping on a user
typedef UserTapCallback = void Function(User, Widget?); typedef UserTapCallback = void Function(User, Widget?);
@@ -82,8 +82,7 @@ class UserListView extends StatefulWidget {
/// The query filters to use. /// The query filters to use.
/// You can query on any of the custom fields you've defined on the [Channel]. /// You can query on any of the custom fields you've defined on the [Channel].
/// You can also filter other built-in channel fields. /// 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. /// The sorting used for the channels matching the filters.
/// Sorting is based on field and direction, multiple sorting options can /// Sorting is based on field and direction, multiple sorting options can
@@ -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,
),
),
],
),
);
}
}
@@ -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<UserMentionsOverlay> {
late Future<List<User>> 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<List<User>>(
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<User> get membersAndWatchers {
final state = widget.channel.state!;
return {
...state.watchers,
...state.members.map((it) => it.user),
}.whereType<User>().toList(growable: false);
}
Future<List<User>> 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<User>()
.toList(growable: false);
}
Future<List<Member>> _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<List<User>> _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;
}
}
@@ -1,10 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart'; 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/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Launch URL /// Launch URL
Future<void> launchURL(BuildContext context, String url) async { Future<void> launchURL(BuildContext context, String url) async {
@@ -388,3 +389,39 @@ class Tuple2<T1, T2> {
@override @override
int get hashCode => item1.hashCode ^ item2.hashCode; 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<int>.filled(t.length + 1, 0);
final v1 = List<int>.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];
}
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; 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/stream_chat_theme.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Widget for displaying a footnote /// Widget for displaying a footnote
class VisibleFootnote extends StatelessWidget { class VisibleFootnote extends StatelessWidget {
@@ -44,5 +44,6 @@ export 'src/unread_indicator.dart';
export 'src/user_avatar.dart'; export 'src/user_avatar.dart';
export 'src/user_item.dart'; export 'src/user_item.dart';
export 'src/user_list_view.dart'; export 'src/user_list_view.dart';
export 'src/user_mention_tile.dart';
export 'src/utils.dart'; export 'src/utils.dart';
export 'src/visible_footnote.dart'; export 'src/visible_footnote.dart';
+3 -3
View File
@@ -1,7 +1,7 @@
name: stream_chat_flutter name: stream_chat_flutter
homepage: https://github.com/GetStream/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. 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 repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -14,12 +14,12 @@ dependencies:
characters: ^1.1.0 characters: ^1.1.0
chewie: ^1.2.0 chewie: ^1.2.0
collection: ^1.15.0 collection: ^1.15.0
diacritic: ^0.1.3
dio: ^4.0.0 dio: ^4.0.0
ezanimation: ^0.5.0 ezanimation: ^0.5.0
file_picker: ^3.0.1 file_picker: ^3.0.1
flutter: flutter:
sdk: flutter sdk: flutter
flutter_keyboard_visibility: ^5.0.1
flutter_markdown: ^0.6.1 flutter_markdown: ^0.6.1
flutter_portal: ^0.4.0 flutter_portal: ^0.4.0
flutter_slidable: ^0.6.0 flutter_slidable: ^0.6.0
@@ -37,7 +37,7 @@ dependencies:
scrollable_positioned_list: ^0.2.0-nullsafety.0 scrollable_positioned_list: ^0.2.0-nullsafety.0
share_plus: ^2.0.3 share_plus: ^2.0.3
shimmer: ^2.0.0 shimmer: ^2.0.0
stream_chat_flutter_core: ^2.2.1 stream_chat_flutter_core: ^3.0.0
substring_highlight: ^1.0.26 substring_highlight: ^1.0.26
synchronized: ^3.0.0 synchronized: ^3.0.0
url_launcher: ^6.0.3 url_launcher: ^6.0.3
@@ -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'
]);
@@ -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<User>.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]);
});
});
}
@@ -263,9 +263,11 @@ void main() {
final client = MockClient(); final client = MockClient();
final clientState = MockClientState(); final clientState = MockClientState();
final channel = MockChannel(); final channel = MockChannel();
final channelState = MockChannelState();
when(() => client.state).thenReturn(clientState); when(() => client.state).thenReturn(clientState);
when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id')); when(() => clientState.currentUser).thenReturn(OwnUser(id: 'user-id'));
when(() => channel.state).thenReturn(channelState);
final themeData = ThemeData(); final themeData = ThemeData();
final streamTheme = StreamChatThemeData.fromTheme(themeData); final streamTheme = StreamChatThemeData.fromTheme(themeData);
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart' hide TextTheme; import 'package:flutter/material.dart' hide TextTheme;
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class MockStreamChatClient extends Mock implements StreamChatClient {} class MockStreamChatClient extends Mock implements StreamChatClient {}
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:mocktail/mocktail.dart'; import 'package:mocktail/mocktail.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
class MockStreamChatClient extends Mock implements StreamChatClient {} class MockStreamChatClient extends Mock implements StreamChatClient {}
@@ -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`. - `MessageSearchListViewCore` `paginationParams` property is now deprecated in favor of `limit`.
```dart ```dart
@@ -10,7 +12,7 @@
// new // new
limit = 30 limit = 30
``` ```
- `UserListViewCore` `pagination` property is now deprecated in favor of `limit`. - `UserListCore` `pagination` property is now deprecated in favor of `limit`.
```dart ```dart
// previous // previous
pagination = const PaginationParams(limit: 30) pagination = const PaginationParams(limit: 30)
@@ -18,7 +20,7 @@
// new // new
limit = 30 limit = 30
``` ```
- `ChannelListViewCore` `pagination` property is now deprecated in favor of `limit`. - `ChannelListCore` `pagination` property is now deprecated in favor of `limit`.
```dart ```dart
// previous // previous
pagination = const PaginationParams(limit: 30) pagination = const PaginationParams(limit: 30)
@@ -27,9 +29,11 @@
limit = 30 limit = 30
``` ```
- `UserListCore` `filter` property now is non-nullable.
🔄 Changed 🔄 Changed
- `UserListViewCore` filter property now has a default value. - `UserListCore` filter property now has a default value.
```dart ```dart
filter = const Filter.empty() filter = const Filter.empty()
``` ```
@@ -56,7 +56,7 @@ import 'package:stream_chat_flutter_core/src/typedef.dart';
/// information about the channels. /// information about the channels.
class ChannelListCore extends StatefulWidget { class ChannelListCore extends StatefulWidget {
/// Instantiate a new ChannelListView /// Instantiate a new ChannelListView
ChannelListCore({ const ChannelListCore({
Key? key, Key? key,
required this.errorBuilder, required this.errorBuilder,
required this.emptyBuilder, required this.emptyBuilder,
@@ -69,15 +69,9 @@ class ChannelListCore extends StatefulWidget {
this.memberLimit, this.memberLimit,
this.messageLimit, this.messageLimit,
this.sort, 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, this.channelListController,
int? limit, this.limit = 25,
}) : limit = limit ?? pagination?.limit ?? 25, }) : super(key: key);
super(key: key);
/// A [ChannelListController] allows reloading and pagination. /// A [ChannelListController] allows reloading and pagination.
/// Use [ChannelListController.loadData] and /// Use [ChannelListController.loadData] and
@@ -124,16 +118,6 @@ class ChannelListCore extends StatefulWidget {
/// Number of messages to fetch in each channel /// Number of messages to fetch in each channel
final int? messageLimit; 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. /// The amount of channels requested per API call.
final int limit; final int limit;
@@ -20,7 +20,7 @@ import 'package:stream_chat_flutter_core/src/typedef.dart';
/// body: MessageSearchListCore( /// body: MessageSearchListCore(
/// messageQuery: _messageFilter, /// messageQuery: _messageFilter,
/// filters: _channelsFilter, /// filters: _channelsFilter,
/// paginationParams: PaginationParams(limit: 20), /// limit: 20,
/// ), /// ),
/// ); /// );
/// } /// }
@@ -152,13 +152,9 @@ class StreamChannelState extends State<StreamChannel> {
int limit = 20, int limit = 20,
}) { }) {
if (direction == QueryDirection.top) { if (direction == QueryDirection.top) {
return _queryTopMessages( return _queryTopMessages(limit: limit);
limit: limit,
);
} }
return _queryBottomMessages( return _queryBottomMessages(limit: limit);
limit: limit,
);
} }
/// Calls [channel.getReplies] updating [queryMessage] stream /// Calls [channel.getReplies] updating [queryMessage] stream
@@ -311,6 +307,20 @@ class StreamChannelState extends State<StreamChannel> {
return message; return message;
} }
/// Query channel members.
Future<List<Member>> queryMembers({
Filter? filter,
List<SortOption>? sort,
PaginationParams? pagination,
}) async {
final response = await channel.queryMembers(
filter: filter,
sort: sort,
pagination: pagination,
);
return response.members;
}
/// Reloads the channel with latest message /// Reloads the channel with latest message
Future<void> reloadChannel() => _queryAtMessage(before: 30); Future<void> reloadChannel() => _queryAtMessage(before: 30);
@@ -57,7 +57,7 @@ import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
/// [errorBuilder] must all be supplied and not null. /// [errorBuilder] must all be supplied and not null.
class UserListCore extends StatefulWidget { class UserListCore extends StatefulWidget {
/// Instantiate a new [UserListCore] /// Instantiate a new [UserListCore]
UserListCore({ const UserListCore({
required this.errorBuilder, required this.errorBuilder,
required this.emptyBuilder, required this.emptyBuilder,
required this.loadingBuilder, required this.loadingBuilder,
@@ -66,16 +66,10 @@ class UserListCore extends StatefulWidget {
this.filter = const Filter.empty(), this.filter = const Filter.empty(),
this.sort, this.sort,
this.presence, 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.groupAlphabetically = false,
this.userListController, this.userListController,
int? limit, this.limit = 30,
}) : limit = limit ?? pagination?.limit ?? 30, }) : super(key: key);
super(key: key);
/// A [UserListController] allows reloading and pagination. /// A [UserListController] allows reloading and pagination.
/// Use [UserListController.loadData] and [UserListController.paginateData] /// Use [UserListController.loadData] and [UserListController.paginateData]
@@ -97,8 +91,7 @@ class UserListCore extends StatefulWidget {
/// The query filters to use. /// The query filters to use.
/// You can query on any of the custom fields you've defined on the [Channel]. /// You can query on any of the custom fields you've defined on the [Channel].
/// You can also filter other built-in channel fields. /// 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. /// The sorting used for the channels matching the filters.
/// Sorting is based on field and direction, multiple sorting options can be /// Sorting is based on field and direction, multiple sorting options can be
@@ -109,16 +102,6 @@ class UserListCore extends StatefulWidget {
/// If true youll receive user presence updates via the websocket events /// If true youll receive user presence updates via the websocket events
final bool? presence; 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. /// The amount of users requested per API call.
final int limit; final int limit;
@@ -1,7 +1,7 @@
name: stream_chat_flutter_core name: stream_chat_flutter_core
homepage: https://github.com/GetStream/stream-chat-flutter homepage: https://github.com/GetStream/stream-chat-flutter
description: Stream Chat official Flutter SDK Core. Build your own chat experience using Dart and 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 repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -16,7 +16,7 @@ dependencies:
sdk: flutter sdk: flutter
meta: ^1.3.0 meta: ^1.3.0
rxdart: ^0.27.0 rxdart: ^0.27.0
stream_chat: ^2.2.1 stream_chat: ^3.0.0
dev_dependencies: dev_dependencies:
fake_async: ^1.2.0 fake_async: ^1.2.0
@@ -129,7 +129,7 @@ void main() {
emptyBuilder: (BuildContext context) => const Offstage(), emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => errorBuilder: (BuildContext context, Object error) =>
Container(key: errorWidgetKey), Container(key: errorWidgetKey),
pagination: pagination, limit: pagination.limit,
); );
final mockClient = MockClient(); final mockClient = MockClient();
@@ -186,7 +186,7 @@ void main() {
loadingBuilder: (BuildContext context) => const Offstage(), loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => Container(key: emptyWidgetKey), emptyBuilder: (BuildContext context) => Container(key: emptyWidgetKey),
errorBuilder: (BuildContext context, Object error) => const Offstage(), errorBuilder: (BuildContext context, Object error) => const Offstage(),
pagination: pagination, limit: pagination.limit,
); );
final mockClient = MockClient(); final mockClient = MockClient();
@@ -243,7 +243,7 @@ void main() {
loadingBuilder: (BuildContext context) => const Offstage(), loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(), emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => const Offstage(), errorBuilder: (BuildContext context, Object error) => const Offstage(),
pagination: pagination, limit: pagination.limit,
); );
final mockClient = MockClient(); final mockClient = MockClient();
@@ -306,7 +306,7 @@ void main() {
loadingBuilder: (BuildContext context) => const Offstage(), loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(), emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => const Offstage(), errorBuilder: (BuildContext context, Object error) => const Offstage(),
pagination: pagination, limit: pagination.limit,
); );
final mockClient = MockClient(); final mockClient = MockClient();
@@ -420,7 +420,7 @@ void main() {
emptyBuilder: (BuildContext context) => const Offstage(), emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => errorBuilder: (BuildContext context, Object error) =>
const Offstage(), const Offstage(),
pagination: pagination.copyWith(limit: limit), limit: limit,
); );
final mockClient = MockClient(); final mockClient = MockClient();
@@ -516,7 +516,7 @@ void main() {
loadingBuilder: (BuildContext context) => const Offstage(), loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(), emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => const Offstage(), errorBuilder: (BuildContext context, Object error) => const Offstage(),
pagination: pagination, limit: pagination.limit,
); );
expect(channelListCore.limit, pagination.limit); expect(channelListCore.limit, pagination.limit);
@@ -334,7 +334,7 @@ void main() {
loadingBuilder: (BuildContext context) => const Offstage(), loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(), emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => const Offstage(), errorBuilder: (BuildContext context, Object error) => const Offstage(),
paginationParams: pagination, limit: pagination.limit,
filters: testFilter, filters: testFilter,
messageFilters: testMessageFilter, messageFilters: testMessageFilter,
); );
@@ -457,7 +457,7 @@ void main() {
emptyBuilder: (BuildContext context) => const Offstage(), emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => errorBuilder: (BuildContext context, Object error) =>
const Offstage(), const Offstage(),
paginationParams: pagination.copyWith(limit: limit), limit: limit,
filters: testFilter, filters: testFilter,
messageFilters: testMessageFilter, messageFilters: testMessageFilter,
); );
@@ -561,7 +561,7 @@ void main() {
errorBuilder: (BuildContext context, Object? error) => const Offstage(), errorBuilder: (BuildContext context, Object? error) => const Offstage(),
filters: testFilter, filters: testFilter,
messageFilters: testMessageFilter, messageFilters: testMessageFilter,
paginationParams: pagination, limit: pagination.limit,
); );
expect(messageSearchListCore.limit, pagination.limit); expect(messageSearchListCore.limit, pagination.limit);
@@ -341,7 +341,7 @@ void main() {
loadingBuilder: (BuildContext context) => const Offstage(), loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(), emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => const Offstage(), errorBuilder: (BuildContext context, Object error) => const Offstage(),
pagination: pagination, limit: pagination.limit,
groupAlphabetically: true, groupAlphabetically: true,
); );
@@ -447,7 +447,7 @@ void main() {
emptyBuilder: (BuildContext context) => const Offstage(), emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => errorBuilder: (BuildContext context, Object error) =>
const Offstage(), const Offstage(),
pagination: pagination.copyWith(limit: limit), limit: limit,
groupAlphabetically: true, groupAlphabetically: true,
); );
@@ -522,15 +522,15 @@ void main() {
); );
test('`widget.limit` should match `widget.pagination.limit`', () { test('`widget.limit` should match `widget.pagination.limit`', () {
const pagination = PaginationParams(limit: 30); const limit = 20;
final userListCore = UserListCore( final userListCore = UserListCore(
listBuilder: (_, __) => const Offstage(), listBuilder: (_, __) => const Offstage(),
loadingBuilder: (BuildContext context) => const Offstage(), loadingBuilder: (BuildContext context) => const Offstage(),
emptyBuilder: (BuildContext context) => const Offstage(), emptyBuilder: (BuildContext context) => const Offstage(),
errorBuilder: (BuildContext context, Object error) => const Offstage(), errorBuilder: (BuildContext context, Object error) => const Offstage(),
pagination: pagination, limit: limit,
); );
expect(userListCore.limit, pagination.limit); expect(userListCore.limit, limit);
}); });
} }
@@ -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 🐞 Fixed
@@ -1,6 +1,6 @@
name: stream_chat_localizations name: stream_chat_localizations
description: The Official localizations for Stream Chat Flutter, a service for building chat applications 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 homepage: https://github.com/GetStream/stream-chat-flutter
repository: 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 issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -14,7 +14,7 @@ dependencies:
sdk: flutter sdk: flutter
flutter_localizations: flutter_localizations:
sdk: flutter sdk: flutter
stream_chat_flutter: ^2.2.0 stream_chat_flutter: ^3.0.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@@ -2,9 +2,9 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.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_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_localizations/src/stream_chat_localizations.dart'; import 'package:stream_chat_localizations/src/stream_chat_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
class FooStreamChatLocalizations extends StreamChatLocalizationsEn { class FooStreamChatLocalizations extends StreamChatLocalizationsEn {
FooStreamChatLocalizations( FooStreamChatLocalizations(
@@ -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 - [[#604]](https://github.com/GetStream/stream-chat-flutter/issues/604) Fix cascade deletion by
enabling `pragma foreign_keys`. enabling `pragma foreign_keys`.
- Added a new table `PinnedMessageReactions` and dao `PinnedMessageReactionDao` specifically for pinned messages. - Added a new table `PinnedMessageReactions` and dao `PinnedMessageReactionDao` specifically for pinned messages.
- Updated `stream_chat`: `^2.2.0` -> `TODO`
## 2.2.0 ## 2.2.0
@@ -7,11 +7,10 @@ import 'package:moor/isolate.dart';
import 'package:moor/moor.dart'; import 'package:moor/moor.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:path_provider/path_provider.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/src/stream_chat_persistence_client.dart';
import 'package:stream_chat_persistence/stream_chat_persistence.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 /// A Helper class to construct new instances of [MoorChatDatabase] specifically
/// for native platform applications /// for native platform applications
class SharedDB { class SharedDB {
@@ -1,8 +1,7 @@
// coverage:ignore-file // coverage:ignore-file
import 'package:moor/moor_web.dart'; 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/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 /// A Helper class to construct new instances of [MoorChatDatabase] specifically
/// for Web applications /// for Web applications
@@ -1,7 +1,7 @@
name: stream_chat_persistence name: stream_chat_persistence
homepage: https://github.com/GetStream/stream-chat-flutter homepage: https://github.com/GetStream/stream-chat-flutter
description: Official Stream Chat Persistence library. Build your own chat experience using Dart and 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 repository: https://github.com/GetStream/stream-chat-flutter
issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues issue_tracker: https://github.com/GetStream/stream-chat-flutter/issues
@@ -18,7 +18,7 @@ dependencies:
path: ^1.8.0 path: ^1.8.0
path_provider: ^2.0.1 path_provider: ^2.0.1
sqlite3_flutter_libs: ^0.5.0 sqlite3_flutter_libs: ^0.5.0
stream_chat: ^2.2.0 stream_chat: ^3.0.0
dev_dependencies: dev_dependencies:
build_runner: ^2.0.1 build_runner: ^2.0.1
@@ -1,7 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:test/test.dart';
import 'package:stream_chat_persistence/src/converter/list_converter.dart'; import 'package:stream_chat_persistence/src/converter/list_converter.dart';
import 'package:test/test.dart';
void main() { void main() {
group('mapToDart', () { group('mapToDart', () {
@@ -1,7 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:test/test.dart';
import 'package:stream_chat_persistence/src/converter/map_converter.dart'; import 'package:stream_chat_persistence/src/converter/map_converter.dart';
import 'package:test/test.dart';
void main() { void main() {
group('mapToDart', () { group('mapToDart', () {
@@ -1,6 +1,6 @@
import 'package:test/test.dart';
import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/stream_chat.dart';
import 'package:stream_chat_persistence/src/converter/message_sending_status_converter.dart'; import 'package:stream_chat_persistence/src/converter/message_sending_status_converter.dart';
import 'package:test/test.dart';
void main() { void main() {
group('mapToDart', () { group('mapToDart', () {
@@ -1,9 +1,9 @@
import 'dart:math' as math; 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/dao/dao.dart';
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:stream_chat/stream_chat.dart';
import '../../stream_chat_persistence_client_test.dart'; import '../../stream_chat_persistence_client_test.dart';
@@ -1,8 +1,8 @@
import 'package:test/test.dart';
import 'package:moor/ffi.dart'; import 'package:moor/ffi.dart';
import 'package:moor/isolate.dart'; import 'package:moor/isolate.dart';
import 'package:moor/moor.dart' hide isNotNull; import 'package:moor/moor.dart' hide isNotNull;
import 'package:stream_chat_persistence/src/db/moor_chat_database.dart'; import 'package:stream_chat_persistence/src/db/moor_chat_database.dart';
import 'package:test/test.dart';
DatabaseConnection _backgroundConnection() => DatabaseConnection _backgroundConnection() =>
DatabaseConnection.fromExecutor(VmDatabase.memory()); DatabaseConnection.fromExecutor(VmDatabase.memory());
@@ -1,9 +1,10 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:test/test.dart';
import 'package:stream_chat/stream_chat.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/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/mapper/channel_mapper.dart'; import 'package:stream_chat_persistence/src/mapper/channel_mapper.dart';
import 'package:test/test.dart';
import '../utils/date_matcher.dart'; import '../utils/date_matcher.dart';
void main() { void main() {
@@ -1,9 +1,9 @@
import 'dart:math' as math; import 'dart:math' as math;
import 'package:test/test.dart';
import 'package:stream_chat/stream_chat.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/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/mapper/member_mapper.dart'; import 'package:stream_chat_persistence/src/mapper/member_mapper.dart';
import 'package:test/test.dart';
import '../utils/date_matcher.dart'; import '../utils/date_matcher.dart';
@@ -1,10 +1,10 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:test/test.dart';
import 'package:stream_chat/stream_chat.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/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/mapper/message_mapper.dart'; import 'package:stream_chat_persistence/src/mapper/message_mapper.dart';
import 'package:test/test.dart';
import '../utils/date_matcher.dart'; import '../utils/date_matcher.dart';
@@ -1,10 +1,10 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math' as math; import 'dart:math' as math;
import 'package:test/test.dart';
import 'package:stream_chat/stream_chat.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/db/moor_chat_database.dart';
import 'package:stream_chat_persistence/src/mapper/pinned_message_mapper.dart'; import 'package:stream_chat_persistence/src/mapper/pinned_message_mapper.dart';
import 'package:test/test.dart';
import '../utils/date_matcher.dart'; import '../utils/date_matcher.dart';

Some files were not shown because too many files have changed in this diff Show More