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