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

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