Merge branch 'develop' into feat/positioned-list-experiment

This commit is contained in:
Gordon Hayes
2021-10-06 15:11:11 +02:00
175 changed files with 3780 additions and 2112 deletions
@@ -2,7 +2,7 @@ name: stream_flutter_workflow
env: env:
ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true' ACTIONS_ALLOW_UNSECURE_COMMANDS: 'true'
flutter_version: "2.2.2" flutter_version: "2.5.1"
on: on:
pull_request: pull_request:
+8
View File
@@ -177,6 +177,14 @@ Develop is merged into master after the team performs various automated and QA t
--- ---
# Versioning Policy
All of the Stream Chat packages follow [semantic versioning (semver)](https://semver.org/).
See our [versioning policy documentation](https://getstream.io/chat/docs/sdk/flutter/basics/versioning_policy/) for more information.
---
# Styleguides 💅 # Styleguides 💅
![image](https://user-images.githubusercontent.com/20601437/124241186-d17a8680-db1b-11eb-9a21-3df305674ca9.png) ![image](https://user-images.githubusercontent.com/20601437/124241186-d17a8680-db1b-11eb-9a21-3df305674ca9.png)
+5
View File
@@ -62,6 +62,11 @@ Every package folder includes a fully functional example with setup instructions
We also provide a set of sample apps created using the Stream Flutter SDK at [this location](https://github.com/GetStream/flutter-samples). We also provide a set of sample apps created using the Stream Flutter SDK at [this location](https://github.com/GetStream/flutter-samples).
## Versioning Policy
All of the Stream Chat packages follow [semantic versioning (semver)](https://semver.org/).
See our [versioning policy documentation](https://getstream.io/chat/docs/sdk/flutter/basics/versioning_policy/) for more information.
## We are hiring ## We are hiring
@@ -0,0 +1,20 @@
---
id: versioning_policy
sidebar_position: 3
title: Versioning Policy
---
All of the Stream Chat packages follow [semantic versioning (semver)](https://semver.org/).
That means that with a version number x.y.z (major.minor.patch):
- When releasing bug fixes (backwards compatible), we make a patch release by changing the z number (ex: 3.6.2 to 3.6.3). A bug fix is defined as an internal change that fixes incorrect behavior.
- When releasing new features or non-critical fixes, we make a minor release by changing the y number (ex: 3.6.2 to 3.7.0).
- When releasing breaking changes (backward incompatible), we make a major release by changing the x number (ex: 3.6.2 to 4.0.0).
See the [semantic versioning](https://dart.dev/tools/pub/versioning#semantic-versions) section from the Dart docs for more information.
This versioning policy does not apply to prerelease packages (below major version of 1). See this
[StackOverflow thread](https://stackoverflow.com/questions/66201337/how-do-dart-package-versions-work-how-should-i-version-my-flutter-plugins)
for more information on Dart package versioning.
Whenever possible, we will add deprecation warnings in preparation for future breaking changes.
@@ -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,
), ),
), ),
); );
@@ -28,9 +28,9 @@ class MessageSearchPage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: MessageSearchListCore( body: MessageSearchListCore(
messageQuery: _messageFilter, messageQuery: _messageFilter,
filters: _channelsFilter, filters: _channelsFilter,
paginationParams: PaginationParams(limit: 20), limit: 20,
), ),
); );
} }
+25 -1
View File
@@ -1,4 +1,15 @@
## Upcoming ## 3.1.1
✅ Added
- Added `Filter.notExists`.
🐞 Fixed
- [[#710]](https://github.com/GetStream/stream-chat-flutter/issues/710) Fixed JWT requiring using `String` as id.
- Fixed expired CDN attachment links not updating correctly.
## 3.0.0
🛑️ Breaking Changes from `2.2.1` 🛑️ Breaking Changes from `2.2.1`
@@ -13,6 +24,19 @@
✅ Added ✅ Added
- Added `Filter.contains` and `Filter.empty` - Added `Filter.contains` and `Filter.empty`
- Added support for `next`, `previous` value pagination in `client.search`
, [read more.](https://getstream.io/chat/docs/other-rest/search/#pagination)
- `Attachment` class now has a `fileSize` and `mimeType` property. Setting a `file` will also set the `file_size`
, `mime_type` key on `extraData`, so `attachment.fileSize`, `attachment.mimetype`
and `attachment.extraData['file_size']`
, `attachment.extraData['mime_type]` is same respectively.
🐞 Fixed
- [[#659]](https://github.com/GetStream/stream-chat-flutter/issues/659) Fixed unread count not updating correctly.
- Fix `Filter.empty()` json encoding.
- [[#700]](https://github.com/GetStream/stream-chat-flutter/issues/700) Connecting user without providing `name`
uses `id` instead for setting `user.name`.
## 2.2.1 ## 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;
@@ -1521,7 +1497,7 @@ class ChannelClientState {
} }
} }
void _checkExpiredAttachmentMessages(ChannelState channelState) { void _checkExpiredAttachmentMessages(ChannelState channelState) async {
final expiredAttachmentMessagesId = channelState.messages final expiredAttachmentMessagesId = channelState.messages
.where((m) => .where((m) =>
!_updatedMessagesIds.contains(m.id) && !_updatedMessagesIds.contains(m.id) &&
@@ -1532,20 +1508,24 @@ class ChannelClientState {
return false; return false;
} }
final uri = Uri.parse(url); final uri = Uri.parse(url);
if (uri.host != 'stream-io-cdn.com' || if (!uri.host.endsWith('stream-io-cdn.com') ||
uri.queryParameters['Expires'] == null) { uri.queryParameters['Expires'] == null) {
return false; return false;
} }
final expiration = final secondsFromEpoch =
DateTime.parse(uri.queryParameters['Expires']!); int.parse(uri.queryParameters['Expires']!);
final expiration = DateTime.fromMillisecondsSinceEpoch(
secondsFromEpoch * 1000);
return expiration.isBefore(DateTime.now()); return expiration.isBefore(DateTime.now());
}) == }) ==
true) true)
.map((e) => e.id) .map((e) => e.id)
.toList(); .toList();
if (expiredAttachmentMessagesId.isNotEmpty == true) { if (expiredAttachmentMessagesId.isNotEmpty == true) {
_channel.getMessagesById(expiredAttachmentMessagesId); await _channel._initializedCompleter.future;
_updatedMessagesIds.addAll(expiredAttachmentMessagesId); _updatedMessagesIds.addAll(expiredAttachmentMessagesId);
_channel.getMessagesById(expiredAttachmentMessagesId);
} }
} }
@@ -1704,7 +1684,7 @@ class ChannelClientState {
})); }));
} }
/// Add a message to this channel. /// Add a [message] to this [channelState].
void addMessage(Message message) { 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 +1715,35 @@ class ChannelClientState {
} }
} }
/// Remove a [message] from this [channelState].
void removeMessage(Message message) {
final parentId = message.parentId;
// i.e. it's a thread message
// 1. Remove the thread message
// 2. Reduce total reply count of parent message
if (parentId != null) {
final allMessages = [...messages];
final parentMessage = allMessages.firstWhereOrNull(
(it) => it.id == parentId,
);
// return if message not available in the memory
if (parentMessage == null) return;
final replyCount = parentMessage.replyCount;
// return if reply count is null or zero
if (replyCount == null || replyCount == 0) return;
addMessage(parentMessage.copyWith(replyCount: replyCount - 1));
updateThreadInfo(parentId, threads[parentId]!..remove(message));
} else {
// Remove regular message
final allMessages = [...messages];
if (allMessages.remove(message)) {
_channelState = _channelState.copyWith(messages: allMessages);
}
}
}
void _listenReadEvents() { void _listenReadEvents() {
if (_channelState.channel?.config.readEvents == false) { if (_channelState.channel?.config.readEvents == false) {
return; return;
@@ -1862,15 +1871,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;
} }
+21 -25
View File
@@ -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();
@@ -1366,26 +1354,21 @@ class ClientState {
.where((event) => .where((event) =>
event.me != null && event.type != EventType.healthCheck) event.me != null && event.type != EventType.healthCheck)
.map((e) => e.me!) .map((e) => e.me!)
.listen((user) { .listen((user) => currentUser = currentUser?.merge(user) ?? user),
currentUser = currentUser?.merge(user) ?? user;
final totalUnreadCount = user.totalUnreadCount;
_totalUnreadCountController.add(totalUnreadCount);
final unreadChannels = user.unreadChannels;
if (unreadChannels != null) {
_unreadChannelsController.add(unreadChannels);
}
}),
_client _client
.on() .on()
.map((event) => event.unreadChannels) .map((event) => event.unreadChannels)
.whereType<int>() .whereType<int>()
.listen(_unreadChannelsController.add), .listen((count) {
currentUser = currentUser?.copyWith(unreadChannels: count);
}),
_client _client
.on() .on()
.map((event) => event.totalUnreadCount) .map((event) => event.totalUnreadCount)
.whereType<int>() .whereType<int>()
.listen(_totalUnreadCountController.add), .listen((count) {
currentUser = currentUser?.copyWith(totalUnreadCount: count);
}),
]); ]);
_listenChannelDeleted(); _listenChannelDeleted();
@@ -1441,6 +1424,7 @@ class ClientState {
/// Sets the user currently interacting with the client /// Sets the user currently interacting with the client
/// note: this fully overrides the [currentUser] /// note: this fully overrides the [currentUser]
set currentUser(OwnUser? user) { set currentUser(OwnUser? user) {
_computeUnreadCounts(user);
_currentUserController.add(user); _currentUserController.add(user);
} }
@@ -1506,6 +1490,18 @@ class ClientState {
_channelsController.add(newChannels); _channelsController.add(newChannels);
} }
void _computeUnreadCounts(OwnUser? user) {
final totalUnreadCount = user?.totalUnreadCount;
if (totalUnreadCount != null) {
_totalUnreadCountController.add(totalUnreadCount);
}
final unreadChannels = user?.unreadChannels;
if (unreadChannels != null) {
_unreadChannelsController.add(unreadChannels);
}
}
final _channelsController = BehaviorSubject<Map<String, Channel>>.seeded({}); final _channelsController = BehaviorSubject<Map<String, Channel>>.seeded({});
final _currentUserController = BehaviorSubject<OwnUser?>(); final _currentUserController = BehaviorSubject<OwnUser?>();
final _usersController = BehaviorSubject<Map<String, User>>.seeded({}); final _usersController = BehaviorSubject<Map<String, User>>.seeded({});
@@ -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
@@ -36,6 +36,10 @@ class GeneralApi {
PaginationParams? pagination, PaginationParams? pagination,
Filter? messageFilters, Filter? messageFilters,
}) async { }) async {
assert(
pagination?.offset == null || pagination?.offset == 0 || sort == null,
'Cannot specify `offset` with `sort` parameter',
);
assert(() { assert(() {
if (query == null && messageFilters == null) { if (query == null && messageFilters == null) {
throw ArgumentError('Provide at least `query` or `messageFilters`'); throw ArgumentError('Provide at least `query` or `messageFilters`');
@@ -15,7 +15,7 @@ class SortOption<T> {
/// ``` /// ```
const SortOption( const SortOption(
this.field, { this.field, {
this.direction = DESC, this.direction = SortOption.DESC,
this.comparator, this.comparator,
}); });
@@ -60,12 +60,16 @@ class PaginationParams extends Equatable {
/// ``` /// ```
const PaginationParams({ const PaginationParams({
this.limit = 10, this.limit = 10,
this.offset = 0, this.offset,
this.next,
this.greaterThan, this.greaterThan,
this.greaterThanOrEqual, this.greaterThanOrEqual,
this.lessThan, this.lessThan,
this.lessThanOrEqual, this.lessThanOrEqual,
}); }) : assert(
offset == null || offset == 0 || next == null,
'Cannot specify non-zero `offset` with `next` parameter',
);
/// Create a new instance from a json /// Create a new instance from a json
factory PaginationParams.fromJson(Map<String, dynamic> json) => factory PaginationParams.fromJson(Map<String, dynamic> json) =>
@@ -75,7 +79,10 @@ class PaginationParams extends Equatable {
final int limit; final int limit;
/// The offset of requesting items. /// The offset of requesting items.
final int offset; final int? offset;
/// A key used to paginate.
final String? next;
/// Filter on ids greater than the given value. /// Filter on ids greater than the given value.
@JsonKey(name: 'id_gt') @JsonKey(name: 'id_gt')
@@ -100,6 +107,7 @@ class PaginationParams extends Equatable {
PaginationParams copyWith({ PaginationParams copyWith({
int? limit, int? limit,
int? offset, int? offset,
String? next,
String? greaterThan, String? greaterThan,
String? greaterThanOrEqual, String? greaterThanOrEqual,
String? lessThan, String? lessThan,
@@ -108,6 +116,7 @@ class PaginationParams extends Equatable {
PaginationParams( PaginationParams(
limit: limit ?? this.limit, limit: limit ?? this.limit,
offset: offset ?? this.offset, offset: offset ?? this.offset,
next: next ?? this.next,
greaterThan: greaterThan ?? this.greaterThan, greaterThan: greaterThan ?? this.greaterThan,
greaterThanOrEqual: greaterThanOrEqual ?? this.greaterThanOrEqual, greaterThanOrEqual: greaterThanOrEqual ?? this.greaterThanOrEqual,
lessThan: lessThan ?? this.lessThan, lessThan: lessThan ?? this.lessThan,
@@ -118,6 +127,7 @@ class PaginationParams extends Equatable {
List<Object?> get props => [ List<Object?> get props => [
limit, limit,
offset, offset,
next,
greaterThan, greaterThan,
greaterThanOrEqual, greaterThanOrEqual,
lessThan, lessThan,
@@ -6,12 +6,11 @@ part of 'requests.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
SortOption<T> _$SortOptionFromJson<T>(Map<String, dynamic> json) { SortOption<T> _$SortOptionFromJson<T>(Map<String, dynamic> json) =>
return SortOption<T>( SortOption<T>(
json['field'] as String, json['field'] as String,
direction: json['direction'] as int, direction: json['direction'] as int? ?? SortOption.DESC,
); );
}
Map<String, dynamic> _$SortOptionToJson<T>(SortOption<T> instance) => Map<String, dynamic> _$SortOptionToJson<T>(SortOption<T> instance) =>
<String, dynamic>{ <String, dynamic>{
@@ -19,21 +18,20 @@ Map<String, dynamic> _$SortOptionToJson<T>(SortOption<T> instance) =>
'direction': instance.direction, 'direction': instance.direction,
}; };
PaginationParams _$PaginationParamsFromJson(Map<String, dynamic> json) { PaginationParams _$PaginationParamsFromJson(Map<String, dynamic> json) =>
return PaginationParams( PaginationParams(
limit: json['limit'] as int, limit: json['limit'] as int? ?? 10,
offset: json['offset'] as int, offset: json['offset'] as int?,
greaterThan: json['id_gt'] as String?, next: json['next'] as String?,
greaterThanOrEqual: json['id_gte'] as String?, greaterThan: json['id_gt'] as String?,
lessThan: json['id_lt'] as String?, greaterThanOrEqual: json['id_gte'] as String?,
lessThanOrEqual: json['id_lte'] as String?, lessThan: json['id_lt'] as String?,
); lessThanOrEqual: json['id_lte'] as String?,
} );
Map<String, dynamic> _$PaginationParamsToJson(PaginationParams instance) { Map<String, dynamic> _$PaginationParamsToJson(PaginationParams instance) {
final val = <String, dynamic>{ final val = <String, dynamic>{
'limit': instance.limit, 'limit': instance.limit,
'offset': instance.offset,
}; };
void writeNotNull(String key, dynamic value) { void writeNotNull(String key, dynamic value) {
@@ -42,6 +40,8 @@ Map<String, dynamic> _$PaginationParamsToJson(PaginationParams instance) {
} }
} }
writeNotNull('offset', instance.offset);
writeNotNull('next', instance.next);
writeNotNull('id_gt', instance.greaterThan); writeNotNull('id_gt', instance.greaterThan);
writeNotNull('id_gte', instance.greaterThanOrEqual); writeNotNull('id_gte', instance.greaterThanOrEqual);
writeNotNull('id_lt', instance.lessThan); writeNotNull('id_lt', instance.lessThan);
@@ -253,6 +253,12 @@ class SearchMessagesResponse extends _BaseResponse {
@JsonKey(defaultValue: []) @JsonKey(defaultValue: [])
late List<GetMessageResponse> results; late List<GetMessageResponse> results;
/// Message id of where to start searching from for next [results]
late String? next;
/// Message id of where to start searching from for previous [results]
late String? previous;
/// Create a new instance from a json /// Create a new instance from a json
static SearchMessagesResponse fromJson(Map<String, dynamic> json) => static SearchMessagesResponse fromJson(Map<String, dynamic> json) =>
_$SearchMessagesResponseFromJson(json); _$SearchMessagesResponseFromJson(json);
@@ -6,14 +6,13 @@ part of 'responses.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
ErrorResponse _$ErrorResponseFromJson(Map<String, dynamic> json) { ErrorResponse _$ErrorResponseFromJson(Map<String, dynamic> json) =>
return ErrorResponse() ErrorResponse()
..duration = json['duration'] as String? ..duration = json['duration'] as String?
..code = json['code'] as int? ..code = json['code'] as int?
..message = json['message'] as String? ..message = json['message'] as String?
..statusCode = json['StatusCode'] as int? ..statusCode = json['StatusCode'] as int?
..moreInfo = json['more_info'] as String?; ..moreInfo = json['more_info'] as String?;
}
Map<String, dynamic> _$ErrorResponseToJson(ErrorResponse instance) => Map<String, dynamic> _$ErrorResponseToJson(ErrorResponse instance) =>
<String, dynamic>{ <String, dynamic>{
@@ -24,273 +23,253 @@ Map<String, dynamic> _$ErrorResponseToJson(ErrorResponse instance) =>
'more_info': instance.moreInfo, 'more_info': instance.moreInfo,
}; };
SyncResponse _$SyncResponseFromJson(Map<String, dynamic> json) { SyncResponse _$SyncResponseFromJson(Map<String, dynamic> json) => SyncResponse()
return SyncResponse() ..duration = json['duration'] as String?
..duration = json['duration'] as String? ..events = (json['events'] as List<dynamic>?)
..events = (json['events'] as List<dynamic>?) ?.map((e) => Event.fromJson(e as Map<String, dynamic>))
?.map((e) => Event.fromJson(e as Map<String, dynamic>)) .toList() ??
.toList() ?? [];
[];
}
QueryChannelsResponse _$QueryChannelsResponseFromJson( QueryChannelsResponse _$QueryChannelsResponseFromJson(
Map<String, dynamic> json) { Map<String, dynamic> json) =>
return QueryChannelsResponse() QueryChannelsResponse()
..duration = json['duration'] as String? ..duration = json['duration'] as String?
..channels = (json['channels'] as List<dynamic>?) ..channels = (json['channels'] as List<dynamic>?)
?.map((e) => ChannelState.fromJson(e as Map<String, dynamic>)) ?.map((e) => ChannelState.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList() ??
[]; [];
}
TranslateMessageResponse _$TranslateMessageResponseFromJson( TranslateMessageResponse _$TranslateMessageResponseFromJson(
Map<String, dynamic> json) { Map<String, dynamic> json) =>
return TranslateMessageResponse() TranslateMessageResponse()
..duration = json['duration'] as String? ..duration = json['duration'] as String?
..message = Message.fromJson(json['message'] as Map<String, dynamic>); ..message = Message.fromJson(json['message'] as Map<String, dynamic>);
}
QueryMembersResponse _$QueryMembersResponseFromJson(Map<String, dynamic> json) { QueryMembersResponse _$QueryMembersResponseFromJson(
return QueryMembersResponse() Map<String, dynamic> json) =>
..duration = json['duration'] as String? QueryMembersResponse()
..members = (json['members'] as List<dynamic>?) ..duration = json['duration'] as String?
?.map((e) => Member.fromJson(e as Map<String, dynamic>)) ..members = (json['members'] as List<dynamic>?)
.toList() ?? ?.map((e) => Member.fromJson(e as Map<String, dynamic>))
[]; .toList() ??
} [];
QueryUsersResponse _$QueryUsersResponseFromJson(Map<String, dynamic> json) { QueryUsersResponse _$QueryUsersResponseFromJson(Map<String, dynamic> json) =>
return QueryUsersResponse() QueryUsersResponse()
..duration = json['duration'] as String? ..duration = json['duration'] as String?
..users = (json['users'] as List<dynamic>?) ..users = (json['users'] as List<dynamic>?)
?.map((e) => User.fromJson(e as Map<String, dynamic>)) ?.map((e) => User.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList() ??
[]; [];
}
QueryReactionsResponse _$QueryReactionsResponseFromJson( QueryReactionsResponse _$QueryReactionsResponseFromJson(
Map<String, dynamic> json) { Map<String, dynamic> json) =>
return QueryReactionsResponse() QueryReactionsResponse()
..duration = json['duration'] as String? ..duration = json['duration'] as String?
..reactions = (json['reactions'] as List<dynamic>?) ..reactions = (json['reactions'] as List<dynamic>?)
?.map((e) => Reaction.fromJson(e as Map<String, dynamic>)) ?.map((e) => Reaction.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList() ??
[]; [];
}
QueryRepliesResponse _$QueryRepliesResponseFromJson(Map<String, dynamic> json) { QueryRepliesResponse _$QueryRepliesResponseFromJson(
return QueryRepliesResponse() Map<String, dynamic> json) =>
..duration = json['duration'] as String? QueryRepliesResponse()
..messages = (json['messages'] as List<dynamic>?) ..duration = json['duration'] as String?
?.map((e) => Message.fromJson(e as Map<String, dynamic>)) ..messages = (json['messages'] as List<dynamic>?)
.toList() ?? ?.map((e) => Message.fromJson(e as Map<String, dynamic>))
[]; .toList() ??
} [];
ListDevicesResponse _$ListDevicesResponseFromJson(Map<String, dynamic> json) { ListDevicesResponse _$ListDevicesResponseFromJson(Map<String, dynamic> json) =>
return ListDevicesResponse() ListDevicesResponse()
..duration = json['duration'] as String? ..duration = json['duration'] as String?
..devices = (json['devices'] as List<dynamic>?) ..devices = (json['devices'] as List<dynamic>?)
?.map((e) => Device.fromJson(e as Map<String, dynamic>)) ?.map((e) => Device.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList() ??
[]; [];
}
SendFileResponse _$SendFileResponseFromJson(Map<String, dynamic> json) { SendFileResponse _$SendFileResponseFromJson(Map<String, dynamic> json) =>
return SendFileResponse() SendFileResponse()
..duration = json['duration'] as String? ..duration = json['duration'] as String?
..file = json['file'] as String; ..file = json['file'] as String;
}
SendImageResponse _$SendImageResponseFromJson(Map<String, dynamic> json) { SendImageResponse _$SendImageResponseFromJson(Map<String, dynamic> json) =>
return SendImageResponse() SendImageResponse()
..duration = json['duration'] as String? ..duration = json['duration'] as String?
..file = json['file'] as String; ..file = json['file'] as String;
}
SendReactionResponse _$SendReactionResponseFromJson(Map<String, dynamic> json) { SendReactionResponse _$SendReactionResponseFromJson(
return SendReactionResponse() Map<String, dynamic> json) =>
..duration = json['duration'] as String? SendReactionResponse()
..message = Message.fromJson(json['message'] as Map<String, dynamic>) ..duration = json['duration'] as String?
..reaction = Reaction.fromJson(json['reaction'] as Map<String, dynamic>); ..message = Message.fromJson(json['message'] as Map<String, dynamic>)
} ..reaction = Reaction.fromJson(json['reaction'] as Map<String, dynamic>);
ConnectGuestUserResponse _$ConnectGuestUserResponseFromJson( ConnectGuestUserResponse _$ConnectGuestUserResponseFromJson(
Map<String, dynamic> json) { Map<String, dynamic> json) =>
return ConnectGuestUserResponse() ConnectGuestUserResponse()
..duration = json['duration'] as String? ..duration = json['duration'] as String?
..accessToken = json['access_token'] as String ..accessToken = json['access_token'] as String
..user = User.fromJson(json['user'] as Map<String, dynamic>); ..user = User.fromJson(json['user'] as Map<String, dynamic>);
}
UpdateUsersResponse _$UpdateUsersResponseFromJson(Map<String, dynamic> json) { UpdateUsersResponse _$UpdateUsersResponseFromJson(Map<String, dynamic> json) =>
return UpdateUsersResponse() UpdateUsersResponse()
..duration = json['duration'] as String? ..duration = json['duration'] as String?
..users = (json['users'] as Map<String, dynamic>?)?.map( ..users = (json['users'] as Map<String, dynamic>?)?.map(
(k, e) => MapEntry(k, User.fromJson(e as Map<String, dynamic>)), (k, e) => MapEntry(k, User.fromJson(e as Map<String, dynamic>)),
) ?? ) ??
{}; {};
}
UpdateMessageResponse _$UpdateMessageResponseFromJson( UpdateMessageResponse _$UpdateMessageResponseFromJson(
Map<String, dynamic> json) { Map<String, dynamic> json) =>
return UpdateMessageResponse() UpdateMessageResponse()
..duration = json['duration'] as String? ..duration = json['duration'] as String?
..message = Message.fromJson(json['message'] as Map<String, dynamic>); ..message = Message.fromJson(json['message'] as Map<String, dynamic>);
}
SendMessageResponse _$SendMessageResponseFromJson(Map<String, dynamic> json) { SendMessageResponse _$SendMessageResponseFromJson(Map<String, dynamic> json) =>
return SendMessageResponse() SendMessageResponse()
..duration = json['duration'] as String? ..duration = json['duration'] as String?
..message = Message.fromJson(json['message'] as Map<String, dynamic>); ..message = Message.fromJson(json['message'] as Map<String, dynamic>);
}
GetMessageResponse _$GetMessageResponseFromJson(Map<String, dynamic> json) { GetMessageResponse _$GetMessageResponseFromJson(Map<String, dynamic> json) =>
return GetMessageResponse() GetMessageResponse()
..duration = json['duration'] as String? ..duration = json['duration'] as String?
..message = Message.fromJson(json['message'] as Map<String, dynamic>) ..message = Message.fromJson(json['message'] as Map<String, dynamic>)
..channel = json['channel'] == null ..channel = json['channel'] == null
? null ? null
: ChannelModel.fromJson(json['channel'] as Map<String, dynamic>); : ChannelModel.fromJson(json['channel'] as Map<String, dynamic>);
}
SearchMessagesResponse _$SearchMessagesResponseFromJson( SearchMessagesResponse _$SearchMessagesResponseFromJson(
Map<String, dynamic> json) { Map<String, dynamic> json) =>
return SearchMessagesResponse() SearchMessagesResponse()
..duration = json['duration'] as String? ..duration = json['duration'] as String?
..results = (json['results'] as List<dynamic>?) ..results = (json['results'] as List<dynamic>?)
?.map((e) => GetMessageResponse.fromJson(e as Map<String, dynamic>)) ?.map(
.toList() ?? (e) => GetMessageResponse.fromJson(e as Map<String, dynamic>))
[]; .toList() ??
} []
..next = json['next'] as String?
..previous = json['previous'] as String?;
GetMessagesByIdResponse _$GetMessagesByIdResponseFromJson( GetMessagesByIdResponse _$GetMessagesByIdResponseFromJson(
Map<String, dynamic> json) { Map<String, dynamic> json) =>
return GetMessagesByIdResponse() GetMessagesByIdResponse()
..duration = json['duration'] as String? ..duration = json['duration'] as String?
..messages = (json['messages'] as List<dynamic>?) ..messages = (json['messages'] as List<dynamic>?)
?.map((e) => Message.fromJson(e as Map<String, dynamic>)) ?.map((e) => Message.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList() ??
[]; [];
}
UpdateChannelResponse _$UpdateChannelResponseFromJson( UpdateChannelResponse _$UpdateChannelResponseFromJson(
Map<String, dynamic> json) { Map<String, dynamic> json) =>
return UpdateChannelResponse() UpdateChannelResponse()
..duration = json['duration'] as String? ..duration = json['duration'] as String?
..channel = ChannelModel.fromJson(json['channel'] as Map<String, dynamic>) ..channel = ChannelModel.fromJson(json['channel'] as Map<String, dynamic>)
..members = (json['members'] as List<dynamic>?) ..members = (json['members'] as List<dynamic>?)
?.map((e) => Member.fromJson(e as Map<String, dynamic>)) ?.map((e) => Member.fromJson(e as Map<String, dynamic>))
.toList() .toList()
..message = json['message'] == null ..message = json['message'] == null
? null ? null
: Message.fromJson(json['message'] as Map<String, dynamic>); : Message.fromJson(json['message'] as Map<String, dynamic>);
}
PartialUpdateChannelResponse _$PartialUpdateChannelResponseFromJson( PartialUpdateChannelResponse _$PartialUpdateChannelResponseFromJson(
Map<String, dynamic> json) { Map<String, dynamic> json) =>
return PartialUpdateChannelResponse() PartialUpdateChannelResponse()
..duration = json['duration'] as String? ..duration = json['duration'] as String?
..channel = ChannelModel.fromJson(json['channel'] as Map<String, dynamic>) ..channel = ChannelModel.fromJson(json['channel'] as Map<String, dynamic>)
..members = (json['members'] as List<dynamic>?) ..members = (json['members'] as List<dynamic>?)
?.map((e) => Member.fromJson(e as Map<String, dynamic>)) ?.map((e) => Member.fromJson(e as Map<String, dynamic>))
.toList(); .toList();
}
InviteMembersResponse _$InviteMembersResponseFromJson( InviteMembersResponse _$InviteMembersResponseFromJson(
Map<String, dynamic> json) { Map<String, dynamic> json) =>
return InviteMembersResponse() InviteMembersResponse()
..duration = json['duration'] as String? ..duration = json['duration'] as String?
..channel = ChannelModel.fromJson(json['channel'] as Map<String, dynamic>) ..channel = ChannelModel.fromJson(json['channel'] as Map<String, dynamic>)
..members = (json['members'] as List<dynamic>?) ..members = (json['members'] as List<dynamic>?)
?.map((e) => Member.fromJson(e as Map<String, dynamic>)) ?.map((e) => Member.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList() ??
[] []
..message = json['message'] == null ..message = json['message'] == null
? null ? null
: Message.fromJson(json['message'] as Map<String, dynamic>); : Message.fromJson(json['message'] as Map<String, dynamic>);
}
RemoveMembersResponse _$RemoveMembersResponseFromJson( RemoveMembersResponse _$RemoveMembersResponseFromJson(
Map<String, dynamic> json) { Map<String, dynamic> json) =>
return RemoveMembersResponse() RemoveMembersResponse()
..duration = json['duration'] as String? ..duration = json['duration'] as String?
..channel = ChannelModel.fromJson(json['channel'] as Map<String, dynamic>) ..channel = ChannelModel.fromJson(json['channel'] as Map<String, dynamic>)
..members = (json['members'] as List<dynamic>?) ..members = (json['members'] as List<dynamic>?)
?.map((e) => Member.fromJson(e as Map<String, dynamic>)) ?.map((e) => Member.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList() ??
[] []
..message = json['message'] == null ..message = json['message'] == null
? null ? null
: Message.fromJson(json['message'] as Map<String, dynamic>); : Message.fromJson(json['message'] as Map<String, dynamic>);
}
SendActionResponse _$SendActionResponseFromJson(Map<String, dynamic> json) { SendActionResponse _$SendActionResponseFromJson(Map<String, dynamic> json) =>
return SendActionResponse() SendActionResponse()
..duration = json['duration'] as String? ..duration = json['duration'] as String?
..message = json['message'] == null ..message = json['message'] == null
? null ? null
: Message.fromJson(json['message'] as Map<String, dynamic>); : Message.fromJson(json['message'] as Map<String, dynamic>);
}
AddMembersResponse _$AddMembersResponseFromJson(Map<String, dynamic> json) { AddMembersResponse _$AddMembersResponseFromJson(Map<String, dynamic> json) =>
return AddMembersResponse() AddMembersResponse()
..duration = json['duration'] as String? ..duration = json['duration'] as String?
..channel = ChannelModel.fromJson(json['channel'] as Map<String, dynamic>) ..channel = ChannelModel.fromJson(json['channel'] as Map<String, dynamic>)
..members = (json['members'] as List<dynamic>?) ..members = (json['members'] as List<dynamic>?)
?.map((e) => Member.fromJson(e as Map<String, dynamic>)) ?.map((e) => Member.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList() ??
[] []
..message = json['message'] == null ..message = json['message'] == null
? null ? null
: Message.fromJson(json['message'] as Map<String, dynamic>); : Message.fromJson(json['message'] as Map<String, dynamic>);
}
AcceptInviteResponse _$AcceptInviteResponseFromJson(Map<String, dynamic> json) { AcceptInviteResponse _$AcceptInviteResponseFromJson(
return AcceptInviteResponse() Map<String, dynamic> json) =>
..duration = json['duration'] as String? AcceptInviteResponse()
..channel = ChannelModel.fromJson(json['channel'] as Map<String, dynamic>) ..duration = json['duration'] as String?
..members = (json['members'] as List<dynamic>?) ..channel = ChannelModel.fromJson(json['channel'] as Map<String, dynamic>)
?.map((e) => Member.fromJson(e as Map<String, dynamic>)) ..members = (json['members'] as List<dynamic>?)
.toList() ?? ?.map((e) => Member.fromJson(e as Map<String, dynamic>))
[] .toList() ??
..message = json['message'] == null []
? null ..message = json['message'] == null
: Message.fromJson(json['message'] as Map<String, dynamic>); ? null
} : Message.fromJson(json['message'] as Map<String, dynamic>);
RejectInviteResponse _$RejectInviteResponseFromJson(Map<String, dynamic> json) { RejectInviteResponse _$RejectInviteResponseFromJson(
return RejectInviteResponse() Map<String, dynamic> json) =>
..duration = json['duration'] as String? RejectInviteResponse()
..channel = ChannelModel.fromJson(json['channel'] as Map<String, dynamic>) ..duration = json['duration'] as String?
..members = (json['members'] as List<dynamic>?) ..channel = ChannelModel.fromJson(json['channel'] as Map<String, dynamic>)
?.map((e) => Member.fromJson(e as Map<String, dynamic>)) ..members = (json['members'] as List<dynamic>?)
.toList() ?? ?.map((e) => Member.fromJson(e as Map<String, dynamic>))
[] .toList() ??
..message = json['message'] == null []
? null ..message = json['message'] == null
: Message.fromJson(json['message'] as Map<String, dynamic>); ? null
} : Message.fromJson(json['message'] as Map<String, dynamic>);
EmptyResponse _$EmptyResponseFromJson(Map<String, dynamic> json) { EmptyResponse _$EmptyResponseFromJson(Map<String, dynamic> json) =>
return EmptyResponse()..duration = json['duration'] as String?; EmptyResponse()..duration = json['duration'] as String?;
}
ChannelStateResponse _$ChannelStateResponseFromJson(Map<String, dynamic> json) { ChannelStateResponse _$ChannelStateResponseFromJson(
return ChannelStateResponse() Map<String, dynamic> json) =>
..duration = json['duration'] as String? ChannelStateResponse()
..channel = ChannelModel.fromJson(json['channel'] as Map<String, dynamic>) ..duration = json['duration'] as String?
..messages = (json['messages'] as List<dynamic>?) ..channel = ChannelModel.fromJson(json['channel'] as Map<String, dynamic>)
?.map((e) => Message.fromJson(e as Map<String, dynamic>)) ..messages = (json['messages'] as List<dynamic>?)
.toList() ?? ?.map((e) => Message.fromJson(e as Map<String, dynamic>))
[] .toList() ??
..members = (json['members'] as List<dynamic>?) []
?.map((e) => Member.fromJson(e as Map<String, dynamic>)) ..members = (json['members'] as List<dynamic>?)
.toList() ?? ?.map((e) => Member.fromJson(e as Map<String, dynamic>))
[] .toList() ??
..watcherCount = json['watcher_count'] as int? ?? 0 []
..read = (json['read'] as List<dynamic>?) ..watcherCount = json['watcher_count'] as int? ?? 0
?.map((e) => Read.fromJson(e as Map<String, dynamic>)) ..read = (json['read'] as List<dynamic>?)
.toList() ?? ?.map((e) => Read.fromJson(e as Map<String, dynamic>))
[]; .toList() ??
} [];
@@ -38,7 +38,9 @@ class AuthInterceptor extends Interceptor {
'Authorization': token.rawValue, 'Authorization': token.rawValue,
'stream-auth-type': token.authType.raw, 'stream-auth-type': token.authType.raw,
}; };
options..queryParameters.addAll(params)..headers.addAll(headers); options
..queryParameters.addAll(params)
..headers.addAll(headers);
return handler.next(options); return handler.next(options);
} }
@@ -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.
@@ -21,7 +21,6 @@ class Action {
final String name; final String name;
/// The style of the action /// The style of the action
@JsonKey(defaultValue: 'default')
final String style; final String style;
/// The test of the action /// The test of the action
@@ -6,15 +6,13 @@ part of 'action.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
Action _$ActionFromJson(Map<String, dynamic> json) { Action _$ActionFromJson(Map<String, dynamic> json) => Action(
return Action( name: json['name'] as String,
name: json['name'] as String, style: json['style'] as String? ?? 'default',
style: json['style'] as String? ?? 'default', text: json['text'] as String,
text: json['text'] as String, type: json['type'] as String,
type: json['type'] as String, value: json['value'] as String?,
value: json['value'] as String?, );
);
}
Map<String, dynamic> _$ActionToJson(Action instance) => <String, dynamic>{ Map<String, dynamic> _$ActionToJson(Action instance) => <String, dynamic>{
'name': instance.name, 'name': instance.name,
@@ -33,13 +33,20 @@ class Attachment extends Equatable {
this.authorIcon, this.authorIcon,
this.assetUrl, this.assetUrl,
List<Action>? actions, List<Action>? actions,
this.extraData = const {}, Map<String, Object?> extraData = const {},
this.file, this.file,
UploadState? uploadState, UploadState? uploadState,
}) : id = id ?? const Uuid().v4(), }) : id = id ?? const Uuid().v4(),
title = title ?? file?.name, title = title ?? file?.name,
localUri = file?.path != null ? Uri.parse(file!.path!) : null, localUri = file?.path != null ? Uri.parse(file!.path!) : null,
actions = actions ?? [] { actions = actions ?? [],
// For backwards compatibility,
// set 'file_size', 'mime_type' in [extraData].
extraData = {
...extraData,
if (file?.size != null) 'file_size': file?.size,
if (file?.mimeType != null) 'mime_type': file?.mimeType?.mimeType,
} {
this.uploadState = uploadState ?? this.uploadState = uploadState ??
((assetUrl != null || imageUrl != null) ((assetUrl != null || imageUrl != null)
? const UploadState.success() ? const UploadState.success()
@@ -110,10 +117,7 @@ class Attachment extends Equatable {
late final UploadState uploadState; late final UploadState uploadState;
/// Map of custom channel extraData /// Map of custom channel extraData
@JsonKey( @JsonKey(includeIfNull: false)
includeIfNull: false,
defaultValue: {},
)
final Map<String, Object?> extraData; final Map<String, Object?> extraData;
/// The attachment ID. /// The attachment ID.
@@ -121,6 +125,18 @@ class Attachment extends Equatable {
/// This is created locally for uniquely identifying a attachment. /// This is created locally for uniquely identifying a attachment.
final String id; final String id;
/// Shortcut for file size.
///
/// {@macro fileSize}
@JsonKey(ignore: true)
int? get fileSize => extraData['file_size'] as int?;
/// Shortcut for file mimeType.
///
/// {@macro mimeType}
@JsonKey(ignore: true)
String? get mimeType => extraData['mime_type'] as String?;
/// Known top level fields. /// Known top level fields.
/// Useful for [Serializer] methods. /// Useful for [Serializer] methods.
static const topLevelFields = [ static const topLevelFields = [
@@ -6,39 +6,37 @@ part of 'attachment.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
Attachment _$AttachmentFromJson(Map<String, dynamic> json) { Attachment _$AttachmentFromJson(Map<String, dynamic> json) => Attachment(
return Attachment( id: json['id'] as String?,
id: json['id'] as String?, type: json['type'] as String?,
type: json['type'] as String?, titleLink: json['title_link'] as String?,
titleLink: json['title_link'] as String?, title: json['title'] as String?,
title: json['title'] as String?, thumbUrl: json['thumb_url'] as String?,
thumbUrl: json['thumb_url'] as String?, text: json['text'] as String?,
text: json['text'] as String?, pretext: json['pretext'] as String?,
pretext: json['pretext'] as String?, ogScrapeUrl: json['og_scrape_url'] as String?,
ogScrapeUrl: json['og_scrape_url'] as String?, imageUrl: json['image_url'] as String?,
imageUrl: json['image_url'] as String?, footerIcon: json['footer_icon'] as String?,
footerIcon: json['footer_icon'] as String?, footer: json['footer'] as String?,
footer: json['footer'] as String?, fields: json['fields'],
fields: json['fields'], fallback: json['fallback'] as String?,
fallback: json['fallback'] as String?, color: json['color'] as String?,
color: json['color'] as String?, authorName: json['author_name'] as String?,
authorName: json['author_name'] as String?, authorLink: json['author_link'] as String?,
authorLink: json['author_link'] as String?, authorIcon: json['author_icon'] as String?,
authorIcon: json['author_icon'] as String?, assetUrl: json['asset_url'] as String?,
assetUrl: json['asset_url'] as String?, actions: (json['actions'] as List<dynamic>?)
actions: (json['actions'] as List<dynamic>?) ?.map((e) => Action.fromJson(e as Map<String, dynamic>))
?.map((e) => Action.fromJson(e as Map<String, dynamic>)) .toList() ??
.toList() ?? [],
[], extraData: json['extra_data'] as Map<String, dynamic>? ?? const {},
extraData: json['extra_data'] as Map<String, dynamic>? ?? {}, file: json['file'] == null
file: json['file'] == null ? null
? null : AttachmentFile.fromJson(json['file'] as Map<String, dynamic>),
: AttachmentFile.fromJson(json['file'] as Map<String, dynamic>), uploadState: json['upload_state'] == null
uploadState: json['upload_state'] == null ? null
? null : UploadState.fromJson(json['upload_state'] as Map<String, dynamic>),
: UploadState.fromJson(json['upload_state'] as Map<String, dynamic>), );
);
}
Map<String, dynamic> _$AttachmentToJson(Attachment instance) { Map<String, dynamic> _$AttachmentToJson(Attachment instance) {
final val = <String, dynamic>{}; final val = <String, dynamic>{};
@@ -2,6 +2,7 @@ import 'dart:typed_data';
import 'package:dio/dio.dart' show MultipartFile; import 'package:dio/dio.dart' show MultipartFile;
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:http_parser/http_parser.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:stream_chat/src/core/platform_detector/platform_detector.dart'; import 'package:stream_chat/src/core/platform_detector/platform_detector.dart';
import 'package:stream_chat/src/core/util/extension.dart'; import 'package:stream_chat/src/core/util/extension.dart';
@@ -65,7 +66,7 @@ class AttachmentFile {
AttachmentFile({ AttachmentFile({
required this.size, required this.size,
this.path, this.path,
this.name, String? name,
this.bytes, this.bytes,
}) : assert( }) : assert(
path != null || bytes != null, path != null || bytes != null,
@@ -74,7 +75,12 @@ class AttachmentFile {
assert( assert(
!CurrentPlatform.isWeb || bytes != null, !CurrentPlatform.isWeb || bytes != null,
'File by path is not supported in web, Please provide bytes', 'File by path is not supported in web, Please provide bytes',
); ),
assert(
name?.contains('.') ?? true,
'Invalid file name, should also contain file extension',
),
_name = name;
/// Create a new instance from a json /// Create a new instance from a json
factory AttachmentFile.fromJson(Map<String, dynamic> json) => factory AttachmentFile.fromJson(Map<String, dynamic> json) =>
@@ -87,8 +93,10 @@ class AttachmentFile {
/// ``` /// ```
final String? path; final String? path;
final String? _name;
/// File name including its extension. /// File name including its extension.
final String? name; String? get name => _name ?? path?.split('/').last;
/// Byte data for this file. Particularly useful if you want to manipulate /// Byte data for this file. Particularly useful if you want to manipulate
/// its data or easily upload to somewhere else. /// its data or easily upload to somewhere else.
@@ -101,26 +109,26 @@ class AttachmentFile {
/// File extension for this file. /// File extension for this file.
String? get extension => name?.split('.').last; String? get extension => name?.split('.').last;
/// The mime type of this file.
MediaType? get mimeType => name?.mimeType;
/// Serialize to json /// Serialize to json
Map<String, dynamic> toJson() => _$AttachmentFileToJson(this); Map<String, dynamic> toJson() => _$AttachmentFileToJson(this);
/// Converts this into a [MultipartFile] /// Converts this into a [MultipartFile]
Future<MultipartFile> toMultipartFile() async { Future<MultipartFile> toMultipartFile() async {
final filename = path?.split('/').last ?? name; MultipartFile multiPartFile;
final mimeType = filename?.mimeType;
late MultipartFile multiPartFile;
if (CurrentPlatform.isWeb) { if (CurrentPlatform.isWeb) {
multiPartFile = MultipartFile.fromBytes( multiPartFile = MultipartFile.fromBytes(
bytes!, bytes!,
filename: filename, filename: name,
contentType: mimeType, contentType: mimeType,
); );
} else { } else {
multiPartFile = await MultipartFile.fromFile( multiPartFile = await MultipartFile.fromFile(
path!, path!,
filename: filename, filename: name,
contentType: mimeType, contentType: mimeType,
); );
} }
@@ -1,5 +1,6 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND // GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides // ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target
part of 'attachment_file.dart'; part of 'attachment_file.dart';
@@ -13,7 +14,7 @@ final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more informations: https://github.com/rrousselGit/freezed#custom-getters-and-methods');
UploadState _$UploadStateFromJson(Map<String, dynamic> json) { UploadState _$UploadStateFromJson(Map<String, dynamic> json) {
switch (json['runtimeType'] as String) { switch (json['runtimeType'] as String?) {
case 'preparing': case 'preparing':
return Preparing.fromJson(json); return Preparing.fromJson(json);
case 'inProgress': case 'inProgress':
@@ -24,7 +25,8 @@ UploadState _$UploadStateFromJson(Map<String, dynamic> json) {
return Failed.fromJson(json); return Failed.fromJson(json);
default: default:
throw FallThroughError(); throw CheckedFromJsonException(json, 'runtimeType', 'UploadState',
'Invalid union type "${json['runtimeType']}"!');
} }
} }
@@ -72,6 +74,14 @@ mixin _$UploadState {
}) => }) =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@optionalTypeArgs @optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult Function()? preparing,
TResult Function(int uploaded, int total)? inProgress,
TResult Function()? success,
TResult Function(String error)? failed,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
TResult Function()? preparing, TResult Function()? preparing,
TResult Function(int uploaded, int total)? inProgress, TResult Function(int uploaded, int total)? inProgress,
@@ -89,6 +99,14 @@ mixin _$UploadState {
}) => }) =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@optionalTypeArgs @optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult Function(Preparing value)? preparing,
TResult Function(InProgress value)? inProgress,
TResult Function(Success value)? success,
TResult Function(Failed value)? failed,
}) =>
throw _privateConstructorUsedError;
@optionalTypeArgs
TResult maybeMap<TResult extends Object?>({ TResult maybeMap<TResult extends Object?>({
TResult Function(Preparing value)? preparing, TResult Function(Preparing value)? preparing,
TResult Function(InProgress value)? inProgress, TResult Function(InProgress value)? inProgress,
@@ -138,7 +156,7 @@ class _$Preparing implements Preparing {
const _$Preparing(); const _$Preparing();
factory _$Preparing.fromJson(Map<String, dynamic> json) => factory _$Preparing.fromJson(Map<String, dynamic> json) =>
_$_$PreparingFromJson(json); _$$PreparingFromJson(json);
@override @override
String toString() { String toString() {
@@ -164,6 +182,17 @@ class _$Preparing implements Preparing {
return preparing(); return preparing();
} }
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult Function()? preparing,
TResult Function(int uploaded, int total)? inProgress,
TResult Function()? success,
TResult Function(String error)? failed,
}) {
return preparing?.call();
}
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
@@ -190,6 +219,17 @@ class _$Preparing implements Preparing {
return preparing(this); return preparing(this);
} }
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult Function(Preparing value)? preparing,
TResult Function(InProgress value)? inProgress,
TResult Function(Success value)? success,
TResult Function(Failed value)? failed,
}) {
return preparing?.call(this);
}
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeMap<TResult extends Object?>({ TResult maybeMap<TResult extends Object?>({
@@ -207,7 +247,7 @@ class _$Preparing implements Preparing {
@override @override
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return _$_$PreparingToJson(this)..['runtimeType'] = 'preparing'; return _$$PreparingToJson(this)..['runtimeType'] = 'preparing';
} }
} }
@@ -258,7 +298,7 @@ class _$InProgress implements InProgress {
const _$InProgress({required this.uploaded, required this.total}); const _$InProgress({required this.uploaded, required this.total});
factory _$InProgress.fromJson(Map<String, dynamic> json) => factory _$InProgress.fromJson(Map<String, dynamic> json) =>
_$_$InProgressFromJson(json); _$$InProgressFromJson(json);
@override @override
final int uploaded; final int uploaded;
@@ -303,6 +343,17 @@ class _$InProgress implements InProgress {
return inProgress(uploaded, total); return inProgress(uploaded, total);
} }
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult Function()? preparing,
TResult Function(int uploaded, int total)? inProgress,
TResult Function()? success,
TResult Function(String error)? failed,
}) {
return inProgress?.call(uploaded, total);
}
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
@@ -329,6 +380,17 @@ class _$InProgress implements InProgress {
return inProgress(this); return inProgress(this);
} }
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult Function(Preparing value)? preparing,
TResult Function(InProgress value)? inProgress,
TResult Function(Success value)? success,
TResult Function(Failed value)? failed,
}) {
return inProgress?.call(this);
}
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeMap<TResult extends Object?>({ TResult maybeMap<TResult extends Object?>({
@@ -346,7 +408,7 @@ class _$InProgress implements InProgress {
@override @override
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return _$_$InProgressToJson(this)..['runtimeType'] = 'inProgress'; return _$$InProgressToJson(this)..['runtimeType'] = 'inProgress';
} }
} }
@@ -386,7 +448,7 @@ class _$Success implements Success {
const _$Success(); const _$Success();
factory _$Success.fromJson(Map<String, dynamic> json) => factory _$Success.fromJson(Map<String, dynamic> json) =>
_$_$SuccessFromJson(json); _$$SuccessFromJson(json);
@override @override
String toString() { String toString() {
@@ -412,6 +474,17 @@ class _$Success implements Success {
return success(); return success();
} }
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult Function()? preparing,
TResult Function(int uploaded, int total)? inProgress,
TResult Function()? success,
TResult Function(String error)? failed,
}) {
return success?.call();
}
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
@@ -438,6 +511,17 @@ class _$Success implements Success {
return success(this); return success(this);
} }
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult Function(Preparing value)? preparing,
TResult Function(InProgress value)? inProgress,
TResult Function(Success value)? success,
TResult Function(Failed value)? failed,
}) {
return success?.call(this);
}
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeMap<TResult extends Object?>({ TResult maybeMap<TResult extends Object?>({
@@ -455,7 +539,7 @@ class _$Success implements Success {
@override @override
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return _$_$SuccessToJson(this)..['runtimeType'] = 'success'; return _$$SuccessToJson(this)..['runtimeType'] = 'success';
} }
} }
@@ -500,7 +584,7 @@ class _$Failed implements Failed {
const _$Failed({required this.error}); const _$Failed({required this.error});
factory _$Failed.fromJson(Map<String, dynamic> json) => factory _$Failed.fromJson(Map<String, dynamic> json) =>
_$_$FailedFromJson(json); _$$FailedFromJson(json);
@override @override
final String error; final String error;
@@ -538,6 +622,17 @@ class _$Failed implements Failed {
return failed(error); return failed(error);
} }
@override
@optionalTypeArgs
TResult? whenOrNull<TResult extends Object?>({
TResult Function()? preparing,
TResult Function(int uploaded, int total)? inProgress,
TResult Function()? success,
TResult Function(String error)? failed,
}) {
return failed?.call(error);
}
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeWhen<TResult extends Object?>({ TResult maybeWhen<TResult extends Object?>({
@@ -564,6 +659,17 @@ class _$Failed implements Failed {
return failed(this); return failed(this);
} }
@override
@optionalTypeArgs
TResult? mapOrNull<TResult extends Object?>({
TResult Function(Preparing value)? preparing,
TResult Function(InProgress value)? inProgress,
TResult Function(Success value)? success,
TResult Function(Failed value)? failed,
}) {
return failed?.call(this);
}
@override @override
@optionalTypeArgs @optionalTypeArgs
TResult maybeMap<TResult extends Object?>({ TResult maybeMap<TResult extends Object?>({
@@ -581,7 +687,7 @@ class _$Failed implements Failed {
@override @override
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return _$_$FailedToJson(this)..['runtimeType'] = 'failed'; return _$$FailedToJson(this)..['runtimeType'] = 'failed';
} }
} }
@@ -6,14 +6,13 @@ part of 'attachment_file.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
AttachmentFile _$AttachmentFileFromJson(Map<String, dynamic> json) { AttachmentFile _$AttachmentFileFromJson(Map<String, dynamic> json) =>
return AttachmentFile( AttachmentFile(
size: json['size'] as int?, size: json['size'] as int?,
path: json['path'] as String?, path: json['path'] as String?,
name: json['name'] as String?, name: json['name'] as String?,
bytes: _fromString(json['bytes'] as String?), bytes: _fromString(json['bytes'] as String?),
); );
}
Map<String, dynamic> _$AttachmentFileToJson(AttachmentFile instance) => Map<String, dynamic> _$AttachmentFileToJson(AttachmentFile instance) =>
<String, dynamic>{ <String, dynamic>{
@@ -23,39 +22,31 @@ Map<String, dynamic> _$AttachmentFileToJson(AttachmentFile instance) =>
'size': instance.size, 'size': instance.size,
}; };
_$Preparing _$_$PreparingFromJson(Map<String, dynamic> json) { _$Preparing _$$PreparingFromJson(Map<String, dynamic> json) => _$Preparing();
return _$Preparing();
}
Map<String, dynamic> _$_$PreparingToJson(_$Preparing instance) => Map<String, dynamic> _$$PreparingToJson(_$Preparing instance) =>
<String, dynamic>{}; <String, dynamic>{};
_$InProgress _$_$InProgressFromJson(Map<String, dynamic> json) { _$InProgress _$$InProgressFromJson(Map<String, dynamic> json) => _$InProgress(
return _$InProgress( uploaded: json['uploaded'] as int,
uploaded: json['uploaded'] as int, total: json['total'] as int,
total: json['total'] as int, );
);
}
Map<String, dynamic> _$_$InProgressToJson(_$InProgress instance) => Map<String, dynamic> _$$InProgressToJson(_$InProgress instance) =>
<String, dynamic>{ <String, dynamic>{
'uploaded': instance.uploaded, 'uploaded': instance.uploaded,
'total': instance.total, 'total': instance.total,
}; };
_$Success _$_$SuccessFromJson(Map<String, dynamic> json) { _$Success _$$SuccessFromJson(Map<String, dynamic> json) => _$Success();
return _$Success();
}
Map<String, dynamic> _$_$SuccessToJson(_$Success instance) => Map<String, dynamic> _$$SuccessToJson(_$Success instance) =>
<String, dynamic>{}; <String, dynamic>{};
_$Failed _$_$FailedFromJson(Map<String, dynamic> json) { _$Failed _$$FailedFromJson(Map<String, dynamic> json) => _$Failed(
return _$Failed( error: json['error'] as String,
error: json['error'] as String, );
);
}
Map<String, dynamic> _$_$FailedToJson(_$Failed instance) => <String, dynamic>{ Map<String, dynamic> _$$FailedToJson(_$Failed instance) => <String, dynamic>{
'error': instance.error, 'error': instance.error,
}; };
@@ -31,15 +31,12 @@ class ChannelConfig {
_$ChannelConfigFromJson(json); _$ChannelConfigFromJson(json);
/// Moderation configuration /// Moderation configuration
@JsonKey(defaultValue: 'flag')
final String automod; final String automod;
/// List of available commands /// List of available commands
@JsonKey(defaultValue: [])
final List<Command> commands; final List<Command> commands;
/// True if the channel should send connect events /// True if the channel should send connect events
@JsonKey(defaultValue: false)
final bool connectEvents; final bool connectEvents;
/// Date of channel creation /// Date of channel creation
@@ -49,43 +46,33 @@ class ChannelConfig {
final DateTime updatedAt; final DateTime updatedAt;
/// Max channel message length /// Max channel message length
@JsonKey(defaultValue: 0)
final int maxMessageLength; final int maxMessageLength;
/// Duration of message retention /// Duration of message retention
@JsonKey(defaultValue: '')
final String messageRetention; final String messageRetention;
/// True if users can be muted /// True if users can be muted
@JsonKey(defaultValue: false)
final bool mutes; final bool mutes;
/// True if reaction are active for this channel /// True if reaction are active for this channel
@JsonKey(defaultValue: false)
final bool reactions; final bool reactions;
/// True if readEvents are active for this channel /// True if readEvents are active for this channel
@JsonKey(defaultValue: false)
final bool readEvents; final bool readEvents;
/// True if reply message are active for this channel /// True if reply message are active for this channel
@JsonKey(defaultValue: false)
final bool replies; final bool replies;
/// True if it's possible to perform a search in this channel /// True if it's possible to perform a search in this channel
@JsonKey(defaultValue: false)
final bool search; final bool search;
/// True if typing events should be sent for this channel /// True if typing events should be sent for this channel
@JsonKey(defaultValue: false)
final bool typingEvents; final bool typingEvents;
/// True if it's possible to upload files to this channel /// True if it's possible to upload files to this channel
@JsonKey(defaultValue: false)
final bool uploads; final bool uploads;
/// True if urls appears as attachments /// True if urls appears as attachments
@JsonKey(defaultValue: false)
final bool urlEnrichment; final bool urlEnrichment;
/// Serialize to json /// Serialize to json
@@ -6,32 +6,31 @@ part of 'channel_config.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
ChannelConfig _$ChannelConfigFromJson(Map<String, dynamic> json) { ChannelConfig _$ChannelConfigFromJson(Map<String, dynamic> json) =>
return ChannelConfig( ChannelConfig(
automod: json['automod'] as String? ?? 'flag', automod: json['automod'] as String? ?? 'flag',
commands: (json['commands'] as List<dynamic>?) commands: (json['commands'] as List<dynamic>?)
?.map((e) => Command.fromJson(e as Map<String, dynamic>)) ?.map((e) => Command.fromJson(e as Map<String, dynamic>))
.toList() ?? .toList() ??
[], const [],
connectEvents: json['connect_events'] as bool? ?? false, connectEvents: json['connect_events'] as bool? ?? false,
createdAt: json['created_at'] == null createdAt: json['created_at'] == null
? null ? null
: DateTime.parse(json['created_at'] as String), : DateTime.parse(json['created_at'] as String),
updatedAt: json['updated_at'] == null updatedAt: json['updated_at'] == null
? null ? null
: DateTime.parse(json['updated_at'] as String), : DateTime.parse(json['updated_at'] as String),
maxMessageLength: json['max_message_length'] as int? ?? 0, maxMessageLength: json['max_message_length'] as int? ?? 0,
messageRetention: json['message_retention'] as String? ?? '', messageRetention: json['message_retention'] as String? ?? '',
mutes: json['mutes'] as bool? ?? false, mutes: json['mutes'] as bool? ?? false,
reactions: json['reactions'] as bool? ?? false, reactions: json['reactions'] as bool? ?? false,
readEvents: json['read_events'] as bool? ?? false, readEvents: json['read_events'] as bool? ?? false,
replies: json['replies'] as bool? ?? false, replies: json['replies'] as bool? ?? false,
search: json['search'] as bool? ?? false, search: json['search'] as bool? ?? false,
typingEvents: json['typing_events'] as bool? ?? false, typingEvents: json['typing_events'] as bool? ?? false,
uploads: json['uploads'] as bool? ?? false, uploads: json['uploads'] as bool? ?? false,
urlEnrichment: json['url_enrichment'] as bool? ?? false, urlEnrichment: json['url_enrichment'] as bool? ?? false,
); );
}
Map<String, dynamic> _$ChannelConfigToJson(ChannelConfig instance) => Map<String, dynamic> _$ChannelConfigToJson(ChannelConfig instance) =>
<String, dynamic>{ <String, dynamic>{
@@ -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';
@@ -59,7 +59,7 @@ class ChannelModel {
final User? createdBy; final User? createdBy;
/// True if this channel is frozen /// True if this channel is frozen
@JsonKey(includeIfNull: false, defaultValue: false) @JsonKey(includeIfNull: false)
final bool frozen; final bool frozen;
/// The date of the last message /// The date of the last message
@@ -79,18 +79,15 @@ class ChannelModel {
final DateTime? deletedAt; final DateTime? deletedAt;
/// The count of this channel members /// The count of this channel members
@JsonKey(includeIfNull: false, toJson: Serializer.readOnly, defaultValue: 0) @JsonKey(includeIfNull: false, toJson: Serializer.readOnly)
final int memberCount; final int memberCount;
/// The number of seconds in a cooldown /// The number of seconds in a cooldown
@JsonKey(includeIfNull: false, defaultValue: 0) @JsonKey(includeIfNull: false)
final int cooldown; final int cooldown;
/// Map of custom channel extraData /// Map of custom channel extraData
@JsonKey( @JsonKey(includeIfNull: false)
includeIfNull: false,
defaultValue: {},
)
final Map<String, Object?> extraData; final Map<String, Object?> extraData;
/// The team the channel belongs to /// The team the channel belongs to
@@ -6,36 +6,34 @@ part of 'channel_model.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
ChannelModel _$ChannelModelFromJson(Map<String, dynamic> json) { ChannelModel _$ChannelModelFromJson(Map<String, dynamic> json) => ChannelModel(
return ChannelModel( id: json['id'] as String?,
id: json['id'] as String?, type: json['type'] as String?,
type: json['type'] as String?, cid: json['cid'] as String?,
cid: json['cid'] as String?, config: json['config'] == null
config: json['config'] == null ? null
? null : ChannelConfig.fromJson(json['config'] as Map<String, dynamic>),
: ChannelConfig.fromJson(json['config'] as Map<String, dynamic>), createdBy: json['created_by'] == null
createdBy: json['created_by'] == null ? null
? null : User.fromJson(json['created_by'] as Map<String, dynamic>),
: User.fromJson(json['created_by'] as Map<String, dynamic>), frozen: json['frozen'] as bool? ?? false,
frozen: json['frozen'] as bool? ?? false, lastMessageAt: json['last_message_at'] == null
lastMessageAt: json['last_message_at'] == null ? null
? null : DateTime.parse(json['last_message_at'] as String),
: DateTime.parse(json['last_message_at'] as String), createdAt: json['created_at'] == null
createdAt: json['created_at'] == null ? null
? null : DateTime.parse(json['created_at'] as String),
: DateTime.parse(json['created_at'] as String), updatedAt: json['updated_at'] == null
updatedAt: json['updated_at'] == null ? null
? null : DateTime.parse(json['updated_at'] as String),
: DateTime.parse(json['updated_at'] as String), deletedAt: json['deleted_at'] == null
deletedAt: json['deleted_at'] == null ? null
? null : DateTime.parse(json['deleted_at'] as String),
: DateTime.parse(json['deleted_at'] as String), memberCount: json['member_count'] as int? ?? 0,
memberCount: json['member_count'] as int? ?? 0, extraData: json['extra_data'] as Map<String, dynamic>? ?? const {},
extraData: json['extra_data'] as Map<String, dynamic>? ?? {}, team: json['team'] as String?,
team: json['team'] as String?, cooldown: json['cooldown'] as int? ?? 0,
cooldown: json['cooldown'] as int? ?? 0, );
);
}
Map<String, dynamic> _$ChannelModelToJson(ChannelModel instance) { Map<String, dynamic> _$ChannelModelToJson(ChannelModel instance) {
final val = <String, dynamic>{ final val = <String, dynamic>{
@@ -25,26 +25,21 @@ class ChannelState {
final ChannelModel? channel; final ChannelModel? channel;
/// A paginated list of channel messages /// A paginated list of channel messages
@JsonKey(defaultValue: <Message>[])
final List<Message> messages; final List<Message> messages;
/// A paginated list of channel members /// A paginated list of channel members
@JsonKey(defaultValue: <Member>[])
final List<Member> members; final List<Member> members;
/// A paginated list of pinned messages /// A paginated list of pinned messages
@JsonKey(defaultValue: <Message>[])
final List<Message> pinnedMessages; final List<Message> pinnedMessages;
/// The count of users watching the channel /// The count of users watching the channel
final int? watcherCount; final int? watcherCount;
/// A paginated list of users watching the channel /// A paginated list of users watching the channel
@JsonKey(defaultValue: <User>[])
final List<User> watchers; final List<User> watchers;
/// The list of channel reads /// The list of channel reads
@JsonKey(defaultValue: <Read>[])
final List<Read> read; final List<Read> read;
/// Create a new instance from a json /// Create a new instance from a json
@@ -6,34 +6,32 @@ part of 'channel_state.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
ChannelState _$ChannelStateFromJson(Map<String, dynamic> json) { ChannelState _$ChannelStateFromJson(Map<String, dynamic> json) => ChannelState(
return ChannelState( channel: json['channel'] == null
channel: json['channel'] == null ? null
? null : ChannelModel.fromJson(json['channel'] as Map<String, dynamic>),
: ChannelModel.fromJson(json['channel'] as Map<String, dynamic>), messages: (json['messages'] as List<dynamic>?)
messages: (json['messages'] as List<dynamic>?) ?.map((e) => Message.fromJson(e as Map<String, dynamic>))
?.map((e) => Message.fromJson(e as Map<String, dynamic>)) .toList() ??
.toList() ?? const [],
[], members: (json['members'] as List<dynamic>?)
members: (json['members'] as List<dynamic>?) ?.map((e) => Member.fromJson(e as Map<String, dynamic>))
?.map((e) => Member.fromJson(e as Map<String, dynamic>)) .toList() ??
.toList() ?? const [],
[], pinnedMessages: (json['pinned_messages'] as List<dynamic>?)
pinnedMessages: (json['pinned_messages'] as List<dynamic>?) ?.map((e) => Message.fromJson(e as Map<String, dynamic>))
?.map((e) => Message.fromJson(e as Map<String, dynamic>)) .toList() ??
.toList() ?? const [],
[], watcherCount: json['watcher_count'] as int?,
watcherCount: json['watcher_count'] as int?, watchers: (json['watchers'] as List<dynamic>?)
watchers: (json['watchers'] as List<dynamic>?) ?.map((e) => User.fromJson(e as Map<String, dynamic>))
?.map((e) => User.fromJson(e as Map<String, dynamic>)) .toList() ??
.toList() ?? const [],
[], read: (json['read'] as List<dynamic>?)
read: (json['read'] as List<dynamic>?) ?.map((e) => Read.fromJson(e as Map<String, dynamic>))
?.map((e) => Read.fromJson(e as Map<String, dynamic>)) .toList() ??
.toList() ?? const [],
[], );
);
}
Map<String, dynamic> _$ChannelStateToJson(ChannelState instance) => Map<String, dynamic> _$ChannelStateToJson(ChannelState instance) =>
<String, dynamic>{ <String, dynamic>{
@@ -6,13 +6,11 @@ part of 'command.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
Command _$CommandFromJson(Map<String, dynamic> json) { Command _$CommandFromJson(Map<String, dynamic> json) => Command(
return Command( name: json['name'] as String,
name: json['name'] as String, description: json['description'] as String,
description: json['description'] as String, args: json['args'] as String,
args: json['args'] as String, );
);
}
Map<String, dynamic> _$CommandToJson(Command instance) => <String, dynamic>{ Map<String, dynamic> _$CommandToJson(Command instance) => <String, dynamic>{
'name': instance.name, 'name': instance.name,
@@ -6,12 +6,10 @@ part of 'device.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
Device _$DeviceFromJson(Map<String, dynamic> json) { Device _$DeviceFromJson(Map<String, dynamic> json) => Device(
return Device( id: json['id'] as String,
id: json['id'] as String, pushProvider: json['push_provider'] as String,
pushProvider: json['push_provider'] as String, );
);
}
Map<String, dynamic> _$DeviceToJson(Device instance) => <String, dynamic>{ Map<String, dynamic> _$DeviceToJson(Device instance) => <String, dynamic>{
'id': instance.id, 'id': instance.id,
@@ -92,7 +92,6 @@ class Event {
final bool isLocal; final bool isLocal;
/// Map of custom channel extraData /// Map of custom channel extraData
@JsonKey(defaultValue: {})
final Map<String, Object?> extraData; final Map<String, Object?> extraData;
/// Known top level fields. /// Known top level fields.
@@ -184,7 +183,7 @@ class EventChannel extends ChannelModel {
DateTime? deletedAt, DateTime? deletedAt,
required int memberCount, required int memberCount,
Map<String, Object?>? extraData, Map<String, Object?>? extraData,
required int cooldown, int cooldown = 0,
String? team, String? team,
}) : super( }) : super(
id: id, id: id,
@@ -6,42 +6,40 @@ part of 'event.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
Event _$EventFromJson(Map<String, dynamic> json) { Event _$EventFromJson(Map<String, dynamic> json) => Event(
return Event( type: json['type'] as String? ?? 'local.event',
type: json['type'] as String, cid: json['cid'] as String?,
cid: json['cid'] as String?, connectionId: json['connection_id'] as String?,
connectionId: json['connection_id'] as String?, createdAt: json['created_at'] == null
createdAt: json['created_at'] == null ? null
? null : DateTime.parse(json['created_at'] as String),
: DateTime.parse(json['created_at'] as String), me: json['me'] == null
me: json['me'] == null ? null
? null : OwnUser.fromJson(json['me'] as Map<String, dynamic>),
: OwnUser.fromJson(json['me'] as Map<String, dynamic>), user: json['user'] == null
user: json['user'] == null ? null
? null : User.fromJson(json['user'] as Map<String, dynamic>),
: User.fromJson(json['user'] as Map<String, dynamic>), message: json['message'] == null
message: json['message'] == null ? null
? null : Message.fromJson(json['message'] as Map<String, dynamic>),
: Message.fromJson(json['message'] as Map<String, dynamic>), totalUnreadCount: json['total_unread_count'] as int?,
totalUnreadCount: json['total_unread_count'] as int?, unreadChannels: json['unread_channels'] as int?,
unreadChannels: json['unread_channels'] as int?, reaction: json['reaction'] == null
reaction: json['reaction'] == null ? null
? null : Reaction.fromJson(json['reaction'] as Map<String, dynamic>),
: Reaction.fromJson(json['reaction'] as Map<String, dynamic>), online: json['online'] as bool?,
online: json['online'] as bool?, channel: json['channel'] == null
channel: json['channel'] == null ? null
? null : EventChannel.fromJson(json['channel'] as Map<String, dynamic>),
: EventChannel.fromJson(json['channel'] as Map<String, dynamic>), member: json['member'] == null
member: json['member'] == null ? null
? null : Member.fromJson(json['member'] as Map<String, dynamic>),
: Member.fromJson(json['member'] as Map<String, dynamic>), channelId: json['channel_id'] as String?,
channelId: json['channel_id'] as String?, channelType: json['channel_type'] as String?,
channelType: json['channel_type'] as String?, parentId: json['parent_id'] as String?,
parentId: json['parent_id'] as String?, extraData: json['extra_data'] as Map<String, dynamic>? ?? const {},
extraData: json['extra_data'] as Map<String, dynamic>? ?? {}, isLocal: json['is_local'] as bool? ?? false,
isLocal: json['is_local'] as bool? ?? false, );
);
}
Map<String, dynamic> _$EventToJson(Event instance) => <String, dynamic>{ Map<String, dynamic> _$EventToJson(Event instance) => <String, dynamic>{
'type': instance.type, 'type': instance.type,
@@ -64,30 +62,28 @@ Map<String, dynamic> _$EventToJson(Event instance) => <String, dynamic>{
'extra_data': instance.extraData, 'extra_data': instance.extraData,
}; };
EventChannel _$EventChannelFromJson(Map<String, dynamic> json) { EventChannel _$EventChannelFromJson(Map<String, dynamic> json) => EventChannel(
return EventChannel( members: (json['members'] as List<dynamic>?)
members: (json['members'] as List<dynamic>?) ?.map((e) => Member.fromJson(e as Map<String, dynamic>))
?.map((e) => Member.fromJson(e as Map<String, dynamic>)) .toList(),
.toList(), id: json['id'] as String?,
id: json['id'] as String?, type: json['type'] as String?,
type: json['type'] as String?, cid: json['cid'] as String,
cid: json['cid'] as String, config: ChannelConfig.fromJson(json['config'] as Map<String, dynamic>),
config: ChannelConfig.fromJson(json['config'] as Map<String, dynamic>), createdBy: json['created_by'] == null
createdBy: json['created_by'] == null ? null
? null : User.fromJson(json['created_by'] as Map<String, dynamic>),
: User.fromJson(json['created_by'] as Map<String, dynamic>), frozen: json['frozen'] as bool? ?? false,
frozen: json['frozen'] as bool? ?? false, lastMessageAt: json['last_message_at'] == null
lastMessageAt: json['last_message_at'] == null ? null
? null : DateTime.parse(json['last_message_at'] as String),
: DateTime.parse(json['last_message_at'] as String), createdAt: DateTime.parse(json['created_at'] as String),
createdAt: DateTime.parse(json['created_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), deletedAt: json['deleted_at'] == null
deletedAt: json['deleted_at'] == null ? null
? null : DateTime.parse(json['deleted_at'] as String),
: DateTime.parse(json['deleted_at'] as String), memberCount: json['member_count'] as int,
memberCount: json['member_count'] as int? ?? 0, extraData: json['extra_data'] as Map<String, dynamic>?,
extraData: json['extra_data'] as Map<String, dynamic>? ?? {}, cooldown: json['cooldown'] as int? ?? 0,
cooldown: json['cooldown'] as int? ?? 0, team: json['team'] as String?,
team: json['team'] as String?, );
);
}
@@ -102,6 +102,12 @@ class Filter extends Equatable {
this.key, this.key,
}) : operator = operator.rawValue; }) : operator = operator.rawValue;
/// An empty filter
const Filter.empty()
: value = const <String, Object?>{},
operator = null,
key = null;
/// Combines the provided filters and matches the values /// Combines the provided filters and matches the values
/// matched by all filters. /// matched by all filters.
factory Filter.and(List<Filter> filters) => factory Filter.and(List<Filter> filters) =>
@@ -161,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);
@@ -172,9 +181,6 @@ class Filter extends Equatable {
String? key, String? key,
}) = Filter.__; }) = Filter.__;
/// An empty filter
factory Filter.empty() => const Filter.raw(value: {});
/// Creates a custom [Filter] from a raw map value /// Creates a custom [Filter] from a raw map value
/// ///
/// ```dart /// ```dart
@@ -42,7 +42,6 @@ class Member extends Equatable {
final DateTime? inviteRejectedAt; final DateTime? inviteRejectedAt;
/// True if the user has been invited to the channel /// True if the user has been invited to the channel
@JsonKey(defaultValue: false)
final bool invited; final bool invited;
/// The role of the user in the channel /// The role of the user in the channel
@@ -52,15 +51,12 @@ class Member extends Equatable {
final String? userId; final String? userId;
/// True if the user is a moderator of the channel /// True if the user is a moderator of the channel
@JsonKey(defaultValue: false)
final bool isModerator; final bool isModerator;
/// True if the member is banned from the channel /// True if the member is banned from the channel
@JsonKey(defaultValue: false)
final bool banned; final bool banned;
/// True if the member is shadow banned from the channel /// True if the member is shadow banned from the channel
@JsonKey(defaultValue: false)
final bool shadowBanned; final bool shadowBanned;
/// The date of creation /// The date of creation
@@ -6,31 +6,29 @@ part of 'member.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
Member _$MemberFromJson(Map<String, dynamic> json) { Member _$MemberFromJson(Map<String, dynamic> json) => Member(
return Member( user: json['user'] == null
user: json['user'] == null ? null
? null : User.fromJson(json['user'] as Map<String, dynamic>),
: User.fromJson(json['user'] as Map<String, dynamic>), inviteAcceptedAt: json['invite_accepted_at'] == null
inviteAcceptedAt: json['invite_accepted_at'] == null ? null
? null : DateTime.parse(json['invite_accepted_at'] as String),
: DateTime.parse(json['invite_accepted_at'] as String), inviteRejectedAt: json['invite_rejected_at'] == null
inviteRejectedAt: json['invite_rejected_at'] == null ? null
? null : DateTime.parse(json['invite_rejected_at'] as String),
: DateTime.parse(json['invite_rejected_at'] as String), invited: json['invited'] as bool? ?? false,
invited: json['invited'] as bool? ?? false, role: json['role'] as String?,
role: json['role'] as String?, userId: json['user_id'] as String?,
userId: json['user_id'] as String?, isModerator: json['is_moderator'] as bool? ?? false,
isModerator: json['is_moderator'] as bool? ?? false, createdAt: json['created_at'] == null
createdAt: json['created_at'] == null ? null
? null : DateTime.parse(json['created_at'] as String),
: DateTime.parse(json['created_at'] as String), updatedAt: json['updated_at'] == null
updatedAt: json['updated_at'] == null ? null
? null : DateTime.parse(json['updated_at'] as String),
: DateTime.parse(json['updated_at'] as String), banned: json['banned'] as bool? ?? false,
banned: json['banned'] as bool? ?? false, shadowBanned: json['shadow_banned'] as bool? ?? false,
shadowBanned: json['shadow_banned'] as bool? ?? false, );
);
}
Map<String, dynamic> _$MemberToJson(Member instance) => <String, dynamic>{ Map<String, dynamic> _$MemberToJson(Member instance) => <String, dynamic>{
'user': instance.user?.toJson(), 'user': instance.user?.toJson(),
@@ -98,23 +98,16 @@ class Message extends Equatable {
@JsonKey( @JsonKey(
includeIfNull: false, includeIfNull: false,
toJson: Serializer.readOnly, toJson: Serializer.readOnly,
defaultValue: 'regular',
) )
final String type; final String type;
/// The list of attachments, either provided by the user or generated from a /// The list of attachments, either provided by the user or generated from a
/// command or as a result of URL scraping. /// command or as a result of URL scraping.
@JsonKey( @JsonKey(includeIfNull: false)
includeIfNull: false,
defaultValue: [],
)
final List<Attachment> attachments; final List<Attachment> attachments;
/// The list of user mentioned in the message /// The list of user mentioned in the message
@JsonKey( @JsonKey(toJson: User.toIds)
toJson: User.toIds,
defaultValue: [],
)
final List<User> mentionedUsers; final List<User> mentionedUsers;
/// A map describing the count of number of every reaction /// A map describing the count of number of every reaction
@@ -155,14 +148,12 @@ class Message extends Equatable {
final bool? showInChannel; final bool? showInChannel;
/// If true the message is silent /// If true the message is silent
@JsonKey(defaultValue: false)
final bool silent; final bool silent;
/// If true the message is shadowed /// If true the message is shadowed
@JsonKey( @JsonKey(
includeIfNull: false, includeIfNull: false,
toJson: Serializer.readOnly, toJson: Serializer.readOnly,
defaultValue: false,
) )
final bool shadowed; final bool shadowed;
@@ -183,7 +174,6 @@ class Message extends Equatable {
final User? user; final User? user;
/// If true the message is pinned /// If true the message is pinned
@JsonKey(defaultValue: false)
final bool pinned; final bool pinned;
/// Reserved field indicating when the message was pinned /// Reserved field indicating when the message was pinned
@@ -200,10 +190,7 @@ class Message extends Equatable {
final User? pinnedBy; final User? pinnedBy;
/// Message custom extraData /// Message custom extraData
@JsonKey( @JsonKey(includeIfNull: false)
includeIfNull: false,
defaultValue: {},
)
final Map<String, Object?> extraData; final Map<String, Object?> extraData;
/// True if the message is a system info /// True if the message is a system info
@@ -6,72 +6,70 @@ part of 'message.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
Message _$MessageFromJson(Map<String, dynamic> json) { Message _$MessageFromJson(Map<String, dynamic> json) => Message(
return Message( id: json['id'] as String?,
id: json['id'] as String?, text: json['text'] as String?,
text: json['text'] as String?, type: json['type'] as String? ?? 'regular',
type: json['type'] as String? ?? 'regular', attachments: (json['attachments'] as List<dynamic>?)
attachments: (json['attachments'] as List<dynamic>?) ?.map((e) => Attachment.fromJson(e as Map<String, dynamic>))
?.map((e) => Attachment.fromJson(e as Map<String, dynamic>)) .toList() ??
.toList() ?? const [],
[], mentionedUsers: (json['mentioned_users'] as List<dynamic>?)
mentionedUsers: (json['mentioned_users'] as List<dynamic>?) ?.map((e) => User.fromJson(e as Map<String, dynamic>))
?.map((e) => User.fromJson(e as Map<String, dynamic>)) .toList() ??
.toList() ?? const [],
[], silent: json['silent'] as bool? ?? false,
silent: json['silent'] as bool? ?? false, shadowed: json['shadowed'] as bool? ?? false,
shadowed: json['shadowed'] as bool? ?? false, reactionCounts: (json['reaction_counts'] as Map<String, dynamic>?)?.map(
reactionCounts: (json['reaction_counts'] as Map<String, dynamic>?)?.map( (k, e) => MapEntry(k, e as int),
(k, e) => MapEntry(k, e as int), ),
), reactionScores: (json['reaction_scores'] as Map<String, dynamic>?)?.map(
reactionScores: (json['reaction_scores'] as Map<String, dynamic>?)?.map( (k, e) => MapEntry(k, e as int),
(k, e) => MapEntry(k, e as int), ),
), latestReactions: (json['latest_reactions'] as List<dynamic>?)
latestReactions: (json['latest_reactions'] as List<dynamic>?) ?.map((e) => Reaction.fromJson(e as Map<String, dynamic>))
?.map((e) => Reaction.fromJson(e as Map<String, dynamic>)) .toList(),
.toList(), ownReactions: (json['own_reactions'] as List<dynamic>?)
ownReactions: (json['own_reactions'] as List<dynamic>?) ?.map((e) => Reaction.fromJson(e as Map<String, dynamic>))
?.map((e) => Reaction.fromJson(e as Map<String, dynamic>)) .toList(),
.toList(), parentId: json['parent_id'] as String?,
parentId: json['parent_id'] as String?, quotedMessage: json['quoted_message'] == null
quotedMessage: json['quoted_message'] == null ? null
? null : Message.fromJson(json['quoted_message'] as Map<String, dynamic>),
: Message.fromJson(json['quoted_message'] as Map<String, dynamic>), quotedMessageId: json['quoted_message_id'] as String?,
quotedMessageId: json['quoted_message_id'] as String?, replyCount: json['reply_count'] as int? ?? 0,
replyCount: json['reply_count'] as int?, threadParticipants: (json['thread_participants'] as List<dynamic>?)
threadParticipants: (json['thread_participants'] as List<dynamic>?) ?.map((e) => User.fromJson(e as Map<String, dynamic>))
?.map((e) => User.fromJson(e as Map<String, dynamic>)) .toList(),
.toList(), showInChannel: json['show_in_channel'] as bool?,
showInChannel: json['show_in_channel'] as bool?, command: json['command'] as String?,
command: json['command'] as String?, createdAt: json['created_at'] == null
createdAt: json['created_at'] == null ? null
? null : DateTime.parse(json['created_at'] as String),
: DateTime.parse(json['created_at'] as String), updatedAt: json['updated_at'] == null
updatedAt: json['updated_at'] == null ? null
? null : DateTime.parse(json['updated_at'] as String),
: DateTime.parse(json['updated_at'] as String), user: json['user'] == null
user: json['user'] == null ? null
? null : User.fromJson(json['user'] as Map<String, dynamic>),
: User.fromJson(json['user'] as Map<String, dynamic>), pinned: json['pinned'] as bool? ?? false,
pinned: json['pinned'] as bool? ?? false, pinnedAt: json['pinned_at'] == null
pinnedAt: json['pinned_at'] == null ? null
? null : DateTime.parse(json['pinned_at'] as String),
: DateTime.parse(json['pinned_at'] as String), pinExpires: json['pin_expires'] == null
pinExpires: json['pin_expires'] == null ? null
? null : DateTime.parse(json['pin_expires'] as String),
: DateTime.parse(json['pin_expires'] as String), pinnedBy: json['pinned_by'] == null
pinnedBy: json['pinned_by'] == null ? null
? null : User.fromJson(json['pinned_by'] as Map<String, dynamic>),
: User.fromJson(json['pinned_by'] as Map<String, dynamic>), extraData: json['extra_data'] as Map<String, dynamic>? ?? const {},
extraData: json['extra_data'] as Map<String, dynamic>? ?? {}, deletedAt: json['deleted_at'] == null
deletedAt: json['deleted_at'] == null ? null
? null : DateTime.parse(json['deleted_at'] as String),
: DateTime.parse(json['deleted_at'] as String), i18n: (json['i18n'] as Map<String, dynamic>?)?.map(
i18n: (json['i18n'] as Map<String, dynamic>?)?.map( (k, e) => MapEntry(k, e as String),
(k, e) => MapEntry(k, e as String), ),
), );
);
}
Map<String, dynamic> _$MessageToJson(Message instance) { Map<String, dynamic> _$MessageToJson(Message instance) {
final val = <String, dynamic>{ final val = <String, dynamic>{
@@ -6,11 +6,9 @@ part of 'mute.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
Mute _$MuteFromJson(Map<String, dynamic> json) { Mute _$MuteFromJson(Map<String, dynamic> json) => Mute(
return Mute( user: User.fromJson(json['user'] as Map<String, dynamic>),
user: User.fromJson(json['user'] as Map<String, dynamic>), channel: ChannelModel.fromJson(json['channel'] as Map<String, dynamic>),
channel: ChannelModel.fromJson(json['channel'] as Map<String, dynamic>), createdAt: DateTime.parse(json['created_at'] as String),
createdAt: DateTime.parse(json['created_at'] as String), updatedAt: DateTime.parse(json['updated_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String), );
);
}
@@ -17,7 +17,7 @@ class OwnUser extends User {
this.devices = const [], this.devices = const [],
this.mutes = const [], this.mutes = const [],
this.totalUnreadCount = 0, this.totalUnreadCount = 0,
this.unreadChannels, this.unreadChannels = 0,
this.channelMutes = const [], this.channelMutes = const [],
required String id, required String id,
String? role, String? role,
@@ -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,
@@ -135,24 +131,24 @@ class OwnUser extends User {
} }
/// List of user devices. /// List of user devices.
@JsonKey(includeIfNull: false, defaultValue: <Device>[]) @JsonKey(includeIfNull: false)
final List<Device> devices; final List<Device> devices;
/// List of users muted by the user. /// List of users muted by the user.
@JsonKey(includeIfNull: false, defaultValue: <Mute>[]) @JsonKey(includeIfNull: false)
final List<Mute> mutes; final List<Mute> mutes;
/// List of users muted by the user. /// List of users muted by the user.
@JsonKey(includeIfNull: false, defaultValue: <Mute>[]) @JsonKey(includeIfNull: false)
final List<Mute> channelMutes; final List<Mute> channelMutes;
/// Total unread messages by the user. /// Total unread messages by the user.
@JsonKey(includeIfNull: false, defaultValue: 0) @JsonKey(includeIfNull: false)
final int totalUnreadCount; final int totalUnreadCount;
/// Total unread channels by the user. /// Total unread channels by the user.
@JsonKey(includeIfNull: false) @JsonKey(includeIfNull: false)
final int? unreadChannels; final int unreadChannels;
/// Known top level fields. /// Known top level fields.
/// ///
@@ -6,39 +6,37 @@ part of 'own_user.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
OwnUser _$OwnUserFromJson(Map<String, dynamic> json) { OwnUser _$OwnUserFromJson(Map<String, dynamic> json) => OwnUser(
return OwnUser( devices: (json['devices'] as List<dynamic>?)
devices: (json['devices'] as List<dynamic>?) ?.map((e) => Device.fromJson(e as Map<String, dynamic>))
?.map((e) => Device.fromJson(e as Map<String, dynamic>)) .toList() ??
.toList() ?? const [],
[], mutes: (json['mutes'] as List<dynamic>?)
mutes: (json['mutes'] as List<dynamic>?) ?.map((e) => Mute.fromJson(e as Map<String, dynamic>))
?.map((e) => Mute.fromJson(e as Map<String, dynamic>)) .toList() ??
.toList() ?? const [],
[], totalUnreadCount: json['total_unread_count'] as int? ?? 0,
totalUnreadCount: json['total_unread_count'] as int? ?? 0, unreadChannels: json['unread_channels'] as int? ?? 0,
unreadChannels: json['unread_channels'] as int?, channelMutes: (json['channel_mutes'] as List<dynamic>?)
channelMutes: (json['channel_mutes'] as List<dynamic>?) ?.map((e) => Mute.fromJson(e as Map<String, dynamic>))
?.map((e) => Mute.fromJson(e as Map<String, dynamic>)) .toList() ??
.toList() ?? const [],
[], id: json['id'] as String,
id: json['id'] as String, role: json['role'] as String?,
role: json['role'] as String?, createdAt: json['created_at'] == null
createdAt: json['created_at'] == null ? null
? null : DateTime.parse(json['created_at'] as String),
: DateTime.parse(json['created_at'] as String), updatedAt: json['updated_at'] == null
updatedAt: json['updated_at'] == null ? null
? null : DateTime.parse(json['updated_at'] as String),
: DateTime.parse(json['updated_at'] as String), lastActive: json['last_active'] == null
lastActive: json['last_active'] == null ? null
? null : DateTime.parse(json['last_active'] as String),
: DateTime.parse(json['last_active'] as String), online: json['online'] as bool? ?? false,
online: json['online'] as bool? ?? false, extraData: json['extra_data'] as Map<String, dynamic>? ?? const {},
extraData: json['extra_data'] as Map<String, dynamic>? ?? {}, banned: json['banned'] as bool? ?? false,
banned: json['banned'] as bool? ?? false, teams:
teams: (json['teams'] as List<dynamic>?)?.map((e) => e as String).toList() ??
(json['teams'] as List<dynamic>?)?.map((e) => e as String).toList() ?? const [],
[], language: json['language'] as String?,
language: json['language'] as String?, );
);
}
@@ -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';
@@ -41,7 +41,6 @@ class Reaction {
final User? user; final User? user;
/// The score of the reaction (ie. number of reactions sent) /// The score of the reaction (ie. number of reactions sent)
@JsonKey(defaultValue: 0)
final int score; final int score;
/// The userId that sent the reaction /// The userId that sent the reaction
@@ -49,10 +48,7 @@ class Reaction {
final String? userId; final String? userId;
/// Reaction custom extraData /// Reaction custom extraData
@JsonKey( @JsonKey(includeIfNull: false)
includeIfNull: false,
defaultValue: {},
)
final Map<String, Object?> extraData; final Map<String, Object?> extraData;
/// Map of custom user extraData /// Map of custom user extraData
@@ -6,21 +6,19 @@ part of 'reaction.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
Reaction _$ReactionFromJson(Map<String, dynamic> json) { Reaction _$ReactionFromJson(Map<String, dynamic> json) => Reaction(
return Reaction( messageId: json['message_id'] as String?,
messageId: json['message_id'] as String?, createdAt: json['created_at'] == null
createdAt: json['created_at'] == null ? null
? null : DateTime.parse(json['created_at'] as String),
: DateTime.parse(json['created_at'] as String), type: json['type'] as String,
type: json['type'] as String, user: json['user'] == null
user: json['user'] == null ? null
? null : User.fromJson(json['user'] as Map<String, dynamic>),
: User.fromJson(json['user'] as Map<String, dynamic>), userId: json['user_id'] as String?,
userId: json['user_id'] as String?, score: json['score'] as int? ?? 0,
score: json['score'] as int? ?? 0, extraData: json['extra_data'] as Map<String, dynamic>? ?? const {},
extraData: json['extra_data'] as Map<String, dynamic>? ?? {}, );
);
}
Map<String, dynamic> _$ReactionToJson(Reaction instance) { Map<String, dynamic> _$ReactionToJson(Reaction instance) {
final val = <String, dynamic>{ final val = <String, dynamic>{
@@ -23,7 +23,6 @@ class Read {
final User user; final User user;
/// Number of unread messages /// Number of unread messages
@JsonKey(defaultValue: 0)
final int unreadMessages; final int unreadMessages;
/// Serialize to json /// Serialize to json
@@ -6,13 +6,11 @@ part of 'read.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
Read _$ReadFromJson(Map<String, dynamic> json) { Read _$ReadFromJson(Map<String, dynamic> json) => Read(
return Read( lastRead: DateTime.parse(json['last_read'] as String),
lastRead: DateTime.parse(json['last_read'] as String), user: User.fromJson(json['user'] as Map<String, dynamic>),
user: User.fromJson(json['user'] as Map<String, dynamic>), unreadMessages: json['unread_messages'] as int? ?? 0,
unreadMessages: json['unread_messages'] as int? ?? 0, );
);
}
Map<String, dynamic> _$ReadToJson(Read instance) => <String, dynamic>{ Map<String, dynamic> _$ReadToJson(Read instance) => <String, dynamic>{
'last_read': instance.lastRead.toIso8601String(), 'last_read': instance.lastRead.toIso8601String(),
@@ -100,7 +100,6 @@ class User extends Equatable {
@JsonKey( @JsonKey(
includeIfNull: false, includeIfNull: false,
toJson: Serializer.readOnly, toJson: Serializer.readOnly,
defaultValue: <String>[],
) )
final List<String> teams; final List<String> teams;
@@ -118,19 +117,20 @@ class User extends Equatable {
/// True if user is online. /// True if user is online.
@JsonKey( @JsonKey(
includeIfNull: false, toJson: Serializer.readOnly, defaultValue: false) includeIfNull: false,
toJson: Serializer.readOnly,
)
final bool online; final bool online;
/// True if user is banned from the chat. /// True if user is banned from the chat.
@JsonKey( @JsonKey(
includeIfNull: false, toJson: Serializer.readOnly, defaultValue: false) includeIfNull: false,
toJson: Serializer.readOnly,
)
final bool banned; final bool banned;
/// Map of custom user extraData. /// Map of custom user extraData.
@JsonKey( @JsonKey(includeIfNull: false)
includeIfNull: false,
defaultValue: {},
)
final Map<String, Object?> extraData; final Map<String, Object?> extraData;
/// The language this user prefers. /// The language this user prefers.
@@ -6,28 +6,26 @@ part of 'user.dart';
// JsonSerializableGenerator // JsonSerializableGenerator
// ************************************************************************** // **************************************************************************
User _$UserFromJson(Map<String, dynamic> json) { User _$UserFromJson(Map<String, dynamic> json) => User(
return User( id: json['id'] as String,
id: json['id'] as String, role: json['role'] as String?,
role: json['role'] as String?, createdAt: json['created_at'] == null
createdAt: json['created_at'] == null ? null
? null : DateTime.parse(json['created_at'] as String),
: DateTime.parse(json['created_at'] as String), updatedAt: json['updated_at'] == null
updatedAt: json['updated_at'] == null ? null
? null : DateTime.parse(json['updated_at'] as String),
: DateTime.parse(json['updated_at'] as String), lastActive: json['last_active'] == null
lastActive: json['last_active'] == null ? null
? null : DateTime.parse(json['last_active'] as String),
: DateTime.parse(json['last_active'] as String), extraData: json['extra_data'] as Map<String, dynamic>? ?? const {},
extraData: json['extra_data'] as Map<String, dynamic>? ?? {}, online: json['online'] as bool? ?? false,
online: json['online'] as bool? ?? false, banned: json['banned'] as bool? ?? false,
banned: json['banned'] as bool? ?? false, teams:
teams: (json['teams'] as List<dynamic>?)?.map((e) => e as String).toList() ??
(json['teams'] as List<dynamic>?)?.map((e) => e as String).toList() ?? const [],
[], language: json['language'] as String?,
language: json['language'] as String?, );
);
}
Map<String, dynamic> _$UserToJson(User instance) { Map<String, dynamic> _$UserToJson(User instance) {
final val = <String, dynamic>{ final val = <String, dynamic>{
@@ -6,15 +6,15 @@ import 'package:logging/logging.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/src/core/error/error.dart'; import 'package:stream_chat/src/core/error/error.dart';
import 'package:stream_chat/src/ws/connection_status.dart';
import 'package:stream_chat/src/core/http/token.dart'; import 'package:stream_chat/src/core/http/token.dart';
import 'package:stream_chat/src/core/http/token_manager.dart'; import 'package:stream_chat/src/core/http/token_manager.dart';
import 'package:stream_chat/src/core/models/event.dart'; import 'package:stream_chat/src/core/models/event.dart';
import 'package:stream_chat/src/core/models/user.dart'; import 'package:stream_chat/src/core/models/user.dart';
import 'package:stream_chat/src/event_type.dart'; import 'package:stream_chat/src/event_type.dart';
import 'package:stream_chat/src/ws/connection_status.dart';
import 'package:stream_chat/src/ws/timer_helper.dart'; import 'package:stream_chat/src/ws/timer_helper.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:web_socket_channel/status.dart' as status; import 'package:web_socket_channel/status.dart' as status;
import 'package:web_socket_channel/web_socket_channel.dart';
/// Typedef which exposes an [Event] as the only parameter. /// Typedef which exposes an [Event] as the only parameter.
typedef EventHandler = void Function(Event); typedef EventHandler = void Function(Event);
+1 -1
View File
@@ -3,4 +3,4 @@ import 'package:stream_chat/src/client/client.dart';
/// Current package version /// Current package version
/// Used in [StreamChatClient] to build the `x-stream-client` header /// Used in [StreamChatClient] to build the `x-stream-client` header
// ignore: constant_identifier_names // ignore: constant_identifier_names
const PACKAGE_VERSION = '2.2.1'; const PACKAGE_VERSION = '3.1.1';
+3 -3
View File
@@ -1,7 +1,7 @@
name: stream_chat name: stream_chat
homepage: https://getstream.io/ homepage: https://getstream.io/
description: The official Dart client for Stream Chat, a service for building chat applications. description: The official Dart client for Stream Chat, a service for building chat applications.
version: 2.2.1 version: 3.1.1
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
@@ -28,6 +28,6 @@ dependencies:
dev_dependencies: dev_dependencies:
build_runner: ^2.0.1 build_runner: ^2.0.1
freezed: ^0.14.1+3 freezed: ^0.14.1+3
json_serializable: ^4.1.0 json_serializable: ^5.0.2
mocktail: ^0.1.1 mocktail: ^0.1.1
test: ^1.17.7 test: ^1.18.2
@@ -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(
[ [
isSameMessageAs( // preparing attachments to upload
message.copyWith(status: MessageSendingStatus.sending), [
matchSendingStatus: true, isSameMessageAs(
), message.copyWith(
status: MessageSendingStatus.sending,
attachments: [
...attachments.map((it) => it.copyWith(
uploadState: const UploadState.preparing()))
],
),
matchSendingStatus: true,
matchAttachments: true,
matchAttachmentsUploadState: true,
),
],
// 0th attachment is successfully uploaded
[
isSameMessageAs(
message.copyWith(
status: MessageSendingStatus.sending,
attachments: [...attachments]..[0] =
attachments[0].copyWith(
uploadState: const UploadState.success(),
),
),
matchSendingStatus: true,
matchAttachments: true,
matchAttachmentsUploadState: true,
),
],
// 0th and 1st attachment is successfully uploaded
[
isSameMessageAs(
message.copyWith(
status: MessageSendingStatus.sending,
attachments: [...attachments]
..[0] = attachments[0].copyWith(
uploadState: const UploadState.success(),
)
..[1] = attachments[1].copyWith(
uploadState: const UploadState.success(),
),
),
matchSendingStatus: true,
matchAttachments: true,
matchAttachmentsUploadState: true,
),
],
// all the attachments are successfully uploaded
[
isSameMessageAs(
message.copyWith(
status: MessageSendingStatus.sending,
attachments: [
...attachments.map((it) =>
it.copyWith(uploadState: const UploadState.success()))
],
),
matchSendingStatus: true,
matchAttachments: true,
matchAttachmentsUploadState: true,
),
],
[
isSameMessageAs(
message.copyWith(
status: MessageSendingStatus.sent,
attachments: [
...attachments.map((it) =>
it.copyWith(uploadState: const UploadState.success()))
],
),
matchSendingStatus: true,
matchAttachments: true,
matchAttachmentsUploadState: true,
),
],
], ],
[ ),
isSameMessageAs(
message.copyWith(status: MessageSendingStatus.sent),
matchSendingStatus: true,
),
],
]),
); );
final res = await channel.sendMessage(message); 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(
[ [
isSameMessageAs( // preparing attachments to upload
message.copyWith(status: MessageSendingStatus.updating), [
matchSendingStatus: true, isSameMessageAs(
), message.copyWith(
status: MessageSendingStatus.updating,
attachments: [
...attachments.map((it) => it.copyWith(
uploadState: const UploadState.preparing()))
],
),
matchSendingStatus: true,
matchAttachments: true,
matchAttachmentsUploadState: true,
),
],
// 0th attachment is successfully uploaded
[
isSameMessageAs(
message.copyWith(
status: MessageSendingStatus.updating,
attachments: [...attachments]..[0] =
attachments[0].copyWith(
uploadState: const UploadState.success(),
),
),
matchSendingStatus: true,
matchAttachments: true,
matchAttachmentsUploadState: true,
),
],
// 0th and 1st attachment is successfully uploaded
[
isSameMessageAs(
message.copyWith(
status: MessageSendingStatus.updating,
attachments: [...attachments]
..[0] = attachments[0].copyWith(
uploadState: const UploadState.success(),
)
..[1] = attachments[1].copyWith(
uploadState: const UploadState.success(),
),
),
matchSendingStatus: true,
matchAttachments: true,
matchAttachmentsUploadState: true,
),
],
// all the attachments are successfully uploaded
[
isSameMessageAs(
message.copyWith(
status: MessageSendingStatus.updating,
attachments: [
...attachments.map((it) =>
it.copyWith(uploadState: const UploadState.success()))
],
),
matchSendingStatus: true,
matchAttachments: true,
matchAttachmentsUploadState: true,
),
],
[
isSameMessageAs(
message.copyWith(
status: MessageSendingStatus.sent,
attachments: [
...attachments.map((it) =>
it.copyWith(uploadState: const UploadState.success()))
],
),
matchSendingStatus: true,
matchAttachments: true,
matchAttachmentsUploadState: true,
),
],
], ],
[ ),
isSameMessageAs(
message.copyWith(status: MessageSendingStatus.sent),
matchSendingStatus: true,
),
],
]),
); );
final res = await channel.updateMessage(message); final res = await channel.updateMessage(message);
@@ -2313,5 +2313,27 @@ void main() {
)).called(1); )).called(1);
verifyNoMoreInteractions(api.message); verifyNoMoreInteractions(api.message);
}); });
test(
'''setting the `currentUser` should also compute and update the unreadCounts''',
() {
final state = client.state;
final initialUser = OwnUser.fromUser(user);
expect(state.currentUser, initialUser);
expect(state.totalUnreadCount, 0);
expect(state.unreadChannels, 0);
final updateUser = initialUser.copyWith(
totalUnreadCount: 33,
unreadChannels: 33,
);
state.currentUser = updateUser;
expect(state.currentUser, updateUser);
expect(state.totalUnreadCount, 33);
expect(state.unreadChannels, 33);
},
);
}); });
} }
@@ -86,6 +86,24 @@ void main() {
}, },
); );
test(
'should throw if `pagination.offset` and `sort` both are provided',
() async {
final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']);
const sort = [SortOption<ChannelModel>('test-field')];
const pagination = PaginationParams(offset: 10);
try {
await generalApi.searchMessages(
filter,
sort: sort,
pagination: pagination,
);
} catch (e) {
expect(e, isA<AssertionError>());
}
},
);
test('should run successfully with `query`', () async { test('should run successfully with `query`', () async {
final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']); final filter = Filter.in_('cid', const ['test-cid-1', 'test-cid-2']);
const query = 'test-query'; const query = 'test-query';
@@ -9,11 +9,23 @@ void main() {
expect(j, {'field': 'name', 'direction': -1}); expect(j, {'field': 'name', 'direction': -1});
}); });
test('PaginationParams', () { group('PaginationParams', () {
const option = PaginationParams(); test('default', () {
final j = option.toJson(); const option = PaginationParams();
expect(j, containsPair('limit', 10)); final j = option.toJson();
expect(j, containsPair('offset', 0)); expect(j, containsPair('limit', 10));
});
test(
'should throw if non-zero `offset` and `next` both are provided',
() {
try {
PaginationParams(offset: 10, next: 'next-message-id');
} catch (e) {
expect(e, isA<AssertionError>());
}
},
);
}); });
}); });
} }
@@ -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,5 +1,6 @@
import 'package:stream_chat/src/core/models/action.dart'; import 'package:stream_chat/src/core/models/action.dart';
import 'package:stream_chat/src/core/models/attachment.dart'; import 'package:stream_chat/src/core/models/attachment.dart';
import 'package:stream_chat/src/core/models/attachment_file.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import '../../utils.dart'; import '../../utils.dart';
@@ -41,5 +42,59 @@ void main() {
}, },
); );
}); });
test('fileName, mimeType property and extraData manipulation', () {
final file = AttachmentFile(size: 3, path: 'myfolder/myfile.txt');
final attachment = Attachment(file: file);
expect(attachment.fileSize, 3);
expect(attachment.mimeType, 'text/plain');
expect(attachment.toJson(), {
'title': 'myfile.txt',
'actions': [],
'file_size': 3,
'mime_type': 'text/plain'
});
expect(Attachment.fromJson(attachment.toJson()).toJson(), {
'title': 'myfile.txt',
'actions': [],
'file_size': 3,
'mime_type': 'text/plain'
});
// Setting the size and mimeType using extraData should work fine
var newAttachment = Attachment(
extraData: const {
'file_size': 6,
'mime_type': 'application/pdf',
},
);
expect(newAttachment.extraData['file_size'], 6);
expect(newAttachment.extraData['mime_type'], 'application/pdf');
expect(newAttachment.fileSize, 6);
expect(newAttachment.mimeType, 'application/pdf');
// switching a new file should update size and mimeType
final fileTwo = AttachmentFile(size: 12, path: 'myfolder/fileTwo.pdf');
newAttachment = attachment.copyWith(file: fileTwo);
expect(newAttachment.extraData['file_size'], 12);
expect(newAttachment.extraData['mime_type'], 'application/pdf');
expect(newAttachment.fileSize, 12);
expect(newAttachment.mimeType, 'application/pdf');
// if file is available, should override size and mimeType.
final fileThree = AttachmentFile(size: 9, path: 'myfolder/fileThree.png');
newAttachment = attachment.copyWith(file: fileThree, extraData: {
'file_size': 88,
'mime_type': 'application/pdf',
});
expect(newAttachment.extraData['file_size'], 9);
expect(newAttachment.extraData['mime_type'], 'image/png');
expect(newAttachment.fileSize, 9);
expect(newAttachment.mimeType, 'image/png');
});
}); });
} }
@@ -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);
@@ -139,7 +139,7 @@ void main() {
}); });
test('empty', () { test('empty', () {
final filter = Filter.empty(); const filter = Filter.empty();
expect(filter.value, {}); expect(filter.value, {});
}); });
@@ -226,6 +226,12 @@ void main() {
json.encode(value), json.encode(value),
); );
}); });
test('empty', () {
const filter = Filter.empty();
final encoded = json.encode(filter);
expect(encoded, '{}');
});
}); });
test('groupedFilter', () { test('groupedFilter', () {
@@ -175,5 +175,26 @@ void main() {
expect(newUser.teams, ['team1', 'team2']); expect(newUser.teams, ['team1', 'team2']);
expect(newUser.language, 'fr'); expect(newUser.language, 'fr');
}); });
test(
'fromUser should not override name with id if not available in extraData',
() {
final user = User(id: 'test-id');
expect(user.id, 'test-id');
expect(user.name, 'test-id');
final encodedUser = user.toJson();
expect(encodedUser['id'], 'test-id');
expect(encodedUser['name'], null);
final ownUser = OwnUser.fromUser(user);
expect(user.id, 'test-id');
expect(user.name, 'test-id');
final encodedOwnUser = ownUser.toJson();
expect(encodedOwnUser['id'], 'test-id');
expect(encodedOwnUser['name'], null);
},
);
}); });
} }
@@ -1,5 +1,5 @@
import 'package:test/test.dart';
import 'package:stream_chat/src/core/util/serializer.dart'; import 'package:stream_chat/src/core/util/serializer.dart';
import 'package:test/test.dart';
void main() { void main() {
group('src/models/serialization', () { group('src/models/serialization', () {
@@ -1,5 +1,5 @@
import 'package:test/test.dart';
import 'package:stream_chat/src/core/util/extension.dart'; import 'package:stream_chat/src/core/util/extension.dart';
import 'package:test/test.dart';
void main() { void main() {
test('`.withNullifyer` converts the type into non-nullable', () { test('`.withNullifyer` converts the type into non-nullable', () {
+1 -1
View File
@@ -6,11 +6,11 @@ import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/src/core/api/channel_api.dart'; import 'package:stream_chat/src/core/api/channel_api.dart';
import 'package:stream_chat/src/core/api/device_api.dart'; import 'package:stream_chat/src/core/api/device_api.dart';
import 'package:stream_chat/src/core/api/general_api.dart'; import 'package:stream_chat/src/core/api/general_api.dart';
import 'package:stream_chat/src/core/api/guest_api.dart';
import 'package:stream_chat/src/core/api/message_api.dart'; import 'package:stream_chat/src/core/api/message_api.dart';
import 'package:stream_chat/src/core/api/moderation_api.dart'; import 'package:stream_chat/src/core/api/moderation_api.dart';
import 'package:stream_chat/src/core/api/stream_chat_api.dart'; import 'package:stream_chat/src/core/api/stream_chat_api.dart';
import 'package:stream_chat/src/core/api/user_api.dart'; import 'package:stream_chat/src/core/api/user_api.dart';
import 'package:stream_chat/src/core/api/guest_api.dart';
import 'package:stream_chat/src/core/http/token.dart'; import 'package:stream_chat/src/core/http/token.dart';
import 'package:stream_chat/src/core/http/token_manager.dart'; import 'package:stream_chat/src/core/http/token_manager.dart';
import 'package:stream_chat/src/ws/websocket.dart'; import 'package:stream_chat/src/ws/websocket.dart';
@@ -1,6 +1,7 @@
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:dio/dio.dart' show MultipartFile; import 'package:dio/dio.dart' show MultipartFile;
import 'package:stream_chat/src/client/channel.dart'; import 'package:stream_chat/src/client/channel.dart';
import 'package:stream_chat/src/core/models/attachment.dart';
import 'package:stream_chat/src/core/models/channel_state.dart'; import 'package:stream_chat/src/core/models/channel_state.dart';
import 'package:stream_chat/src/core/models/event.dart'; import 'package:stream_chat/src/core/models/event.dart';
import 'package:stream_chat/src/core/models/message.dart'; import 'package:stream_chat/src/core/models/message.dart';
@@ -46,12 +47,16 @@ Matcher isSameMessageAs(
bool matchText = false, bool matchText = false,
bool matchReactions = false, bool matchReactions = false,
bool matchSendingStatus = false, bool matchSendingStatus = false,
bool matchAttachments = false,
bool matchAttachmentsUploadState = false,
}) => }) =>
_IsSameMessageAs( _IsSameMessageAs(
targetMessage: targetMessage, targetMessage: targetMessage,
matchText: matchText, matchText: matchText,
matchReactions: matchReactions, matchReactions: matchReactions,
matchSendingStatus: matchSendingStatus, matchSendingStatus: matchSendingStatus,
matchAttachments: matchAttachments,
matchAttachmentsUploadState: matchAttachmentsUploadState,
); );
class _IsSameMessageAs extends Matcher { class _IsSameMessageAs extends Matcher {
@@ -60,12 +65,16 @@ class _IsSameMessageAs extends Matcher {
this.matchText = false, this.matchText = false,
this.matchReactions = false, this.matchReactions = false,
this.matchSendingStatus = false, this.matchSendingStatus = false,
this.matchAttachments = false,
this.matchAttachmentsUploadState = false,
}); });
final Message targetMessage; final Message targetMessage;
final bool matchText; final bool matchText;
final bool matchReactions; final bool matchReactions;
final bool matchSendingStatus; final bool matchSendingStatus;
final bool matchAttachments;
final bool matchAttachmentsUploadState;
@override @override
Description describe(Description description) => Description describe(Description description) =>
@@ -96,6 +105,56 @@ class _IsSameMessageAs extends Matcher {
?.map((it) => '${it.type}-${it.messageId}') ?.map((it) => '${it.type}-${it.messageId}')
.toList()); .toList());
} }
if (matchAttachments) {
bool matchAttachments() {
final attachments = message.attachments;
final targetAttachments = targetMessage.attachments;
if (identical(attachments, targetAttachments)) return true;
final length = attachments.length;
if (length != targetAttachments.length) return false;
for (var i = 0; i < length; i++) {
if (!isSameAttachmentAs(
attachments[i],
matchUploadState: matchAttachmentsUploadState,
).matches(targetAttachments[i], matchState)) return false;
}
return true;
}
matches &= matchAttachments();
}
return matches;
}
}
Matcher isSameAttachmentAs(
Attachment targetAttachment, {
bool matchUploadState = false,
}) =>
_IsSameAttachmentAs(
targetAttachment: targetAttachment,
matchUploadState: matchUploadState,
);
class _IsSameAttachmentAs extends Matcher {
const _IsSameAttachmentAs({
required this.targetAttachment,
this.matchUploadState = false,
});
final Attachment targetAttachment;
final bool matchUploadState;
@override
Description describe(Description description) =>
description.add('is same attachment as $targetAttachment');
@override
bool matches(covariant Attachment attachment, Map matchState) {
var matches = attachment.id == targetAttachment.id;
if (matchUploadState) {
matches &= attachment.uploadState == targetAttachment.uploadState;
}
return matches; return matches;
} }
} }
@@ -1,11 +1,11 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:mocktail/mocktail.dart';
import 'package:stream_chat/src/core/http/token_manager.dart'; import 'package:stream_chat/src/core/http/token_manager.dart';
import 'package:stream_chat/src/ws/websocket.dart';
import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/stream_chat.dart';
import 'package:test/test.dart'; import 'package:test/test.dart';
import 'package:mocktail/mocktail.dart';
import 'package:stream_chat/src/ws/websocket.dart';
import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:web_socket_channel/web_socket_channel.dart';
import '../fakes.dart'; import '../fakes.dart';
+130 -48
View File
@@ -1,3 +1,98 @@
## Upcoming
- Updated Dart SDK constraints to `>=2.14.0 <3.0.0`
## 3.1.1
- Updated `stream_chat_flutter_core` dependency to [`3.1.1`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
- Updated `file_picker`, `image_gallery_saver`, and `video_thumbnail` to the latest versions.
🐞 Fixed
- [[#687]](https://github.com/GetStream/stream-chat-flutter/issues/687): Fix Users losing their place in the conversation after replying in threads.
- Fixed floating date stream subscription causing "Bad state: stream has already been listened.” error.
- Fixed `String` capitalize extension not working on empty strings.
✅ Added
- Added `MessageInput.customOverlays` property to add custom overlays to the message input.
- Added `MessageInput.mentionAllAppUsers` property to mention all app users in the message input.
- The `MessageInput` now supports local search for channels with less than 100 members.
- Added `MessageListView.paginationLoadingIndicatorBuilder` to override the default loading indicator shown while paginating the message list.
- Added new `linkBackgroundColor` in `MessageTheme` for setting background colors of link attachments.
⚠️ Deprecated
- `MessageInput.mentionsTileBuilder` is now deprecated in favor of `MessageInput.userMentionsTileBuilder`.
- `MentionTile` is now deprecated in favor of `UserMentionsTile`.
## 3.0.0
- Updated `stream_chat_flutter_core` dependency to [`3.0.0`](https://pub.dev/packages/stream_chat_flutter_core/changelog).
🛑️ Breaking Changes from `2.2.1`
- `UserListView` `filter` property now is non-nullable.
🐞 Fixed
- [[#668]](https://github.com/GetStream/stream-chat-flutter/issues/668): Fix `MessageInput` rendering errors in case
there are no actions available to show.
- [[#349]](https://github.com/GetStream/stream-chat-flutter/issues/349): Fix `MessageInput` attachment render overflow error.
- `MessageInput` overlays now follow the `MessageInput` focus.
- [[#674]](https://github.com/GetStream/stream-chat-flutter/issues/674): Check scrollController is attached before calling jump in MessageListView.
- Fixed `MessageListView` header and footer when `reverse: false`.
🔄 Changed
- Animation curves changed from default `Curves.linear` to `Curves.easeOut` and `Curves.easeIn` for attachment controls.
- Removed default padding in `DateDivider` in `MessageListView`
✅ Added
- Added `MessageInput.customPortalOptions` property to add custom overlays to the `MessageInput`.
## 2.2.1
⚠️ Deprecated
- `MessageSearchListView` `paginationParams` property is now deprecated in favor of `limit`.
```dart
// previous
paginationParams = const PaginationParams(limit: 30)
// new
limit = 30
```
- `UserListView` `pagination` property is now deprecated in favor of `limit`.
```dart
// previous
pagination = const PaginationParams(limit: 30)
// new
limit = 30
```
- `ChannelListView` `pagination` property is now deprecated in favor of `limit`.
```dart
// previous
pagination = const PaginationParams(limit: 30)
// new
limit = 30
```
🔄 Changed
- `UserListViewCore` filter property now has a default value.
```dart
filter = const Filter.empty()
```
🐞 Fixed
- Fixed `MessageSearchListView` pagination.
- Fixed `MessageWidget` attachment tap callbacks.
## 2.2.1 ## 2.2.1
- Updated `stream_chat_flutter_core` dependency to 2.2.1 - Updated `stream_chat_flutter_core` dependency to 2.2.1
@@ -7,14 +102,13 @@
✅ Added ✅ Added
- [#516](https://github.com/GetStream/stream-chat-flutter/issues/516): - [#516](https://github.com/GetStream/stream-chat-flutter/issues/516):
Added `StreamChatThemeData.placeholderUserImage` for building a widget when the `UserAvatar` image Added `StreamChatThemeData.placeholderUserImage` for building a widget when the `UserAvatar` image is loading
is loading
- Added a `backgroundColor` property to the following widgets: - Added a `backgroundColor` property to the following widgets:
- `ChannelHeader` - `ChannelHeader`
- `ChannelListHeader` - `ChannelListHeader`
- `GalleryHeader` - `GalleryHeader`
- `GalleryFooter` - `GalleryFooter`
- `ThreadHeader` - `ThreadHeader`
- Added `MessageInput.attachmentLimit` in order to limit the no. of attachments that can be sent with a single message. - Added `MessageInput.attachmentLimit` in order to limit the no. of attachments that can be sent with a single message.
- Added `MessageInput.onAttachmentLimitExceed` callback which will be called when the `attachmentLimit` is exceeded. - Added `MessageInput.onAttachmentLimitExceed` callback which will be called when the `attachmentLimit` is exceeded.
This will override the default error alert behaviour. This will override the default error alert behaviour.
@@ -34,9 +128,8 @@ You can call `.copyWith` to customize just a subset of properties.
🔄 Changed 🔄 Changed
Theming has been upgraded! Most theme classes now have `InheritedTheme` classes associated with Theming has been upgraded! Most theme classes now have `InheritedTheme` classes associated with them, and have been
them, and have been upgraded with some goodies like `lerp` functions. Here's the full naming upgraded with some goodies like `lerp` functions. Here's the full naming breakdown:
breakdown:
* `AvatarTheme` is now `AvatarThemeData` * `AvatarTheme` is now `AvatarThemeData`
* `ChannelHeaderTheme` is now `ChannelHeaderThemeData` * `ChannelHeaderTheme` is now `ChannelHeaderThemeData`
@@ -53,18 +146,18 @@ breakdown:
🐞 Fixed 🐞 Fixed
- Fixed `MessageInput` textField case where `input` is not enabled if the file picked from the - Fixed `MessageInput` textField case where `input` is not enabled if the file picked from the camera is null.
camera is null.
- Fixed date dividers position/alignment in non reversed `MessageListView`. - Fixed date dividers position/alignment in non reversed `MessageListView`.
- Fixed `MessageListView` not opening to the right initialMessage if `StreamChannel.initialMessageId` is set. - Fixed `MessageListView` not opening to the right initialMessage if `StreamChannel.initialMessageId` is set.
- Fixed null check errors when accessing `message.text` in `MessageWidget` and `MessageListView`; this occurred when sending a message with no text. - Fixed null check errors when accessing `message.text` in `MessageWidget` and `MessageListView`; this occurred when
sending a message with no text.
## 2.1.2 ## 2.1.2
🐞 Fixed 🐞 Fixed
- [#590](https://github.com/GetStream/stream-chat-flutter/issues/590): livestream use case, no - [#590](https://github.com/GetStream/stream-chat-flutter/issues/590): livestream use case, no members when sending
members when sending message message
## 2.1.1 ## 2.1.1
@@ -82,8 +175,7 @@ breakdown:
🔄 Changed 🔄 Changed
- `StreamChat.of(context).user` is now deprecated in favor of `StreamChat.of(context).currentUser`. - `StreamChat.of(context).user` is now deprecated in favor of `StreamChat.of(context).currentUser`.
- `StreamChat.of(context).userStream` is now deprecated in favor - `StreamChat.of(context).userStream` is now deprecated in favor of `StreamChat.of(context).currentUserStream`.
of `StreamChat.of(context).currentUserStream`.
🐞 Fixed 🐞 Fixed
@@ -136,8 +228,7 @@ You can call `.copyWith` to customize just a subset of properties
- Added video compress options (frame and quality) to `MessageInput` - Added video compress options (frame and quality) to `MessageInput`
- TypingIndicator now has a property called `parentId` to show typing indicator specific to threads - TypingIndicator now has a property called `parentId` to show typing indicator specific to threads
- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView - [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView header/footer
header/footer
- `MessageWidget` accepts a `userAvatarBuilder` - `MessageWidget` accepts a `userAvatarBuilder`
- Added pinMessage ui support - Added pinMessage ui support
- Added `MessageListView.threadSeparatorBuilder` property - Added `MessageListView.threadSeparatorBuilder` property
@@ -146,12 +237,10 @@ You can call `.copyWith` to customize just a subset of properties
🐞 Fixed 🐞 Fixed
- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text - [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing
box when editing message message
- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator - Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case
use case - [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without a reload
- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without
a reload
- `MessageListView` not rendering if the user is not a member of the channel - `MessageListView` not rendering if the user is not a member of the channel
- Fix `MessageInput` overflow when there are no actions - Fix `MessageInput` overflow when there are no actions
- Minor fixes and improvements - Minor fixes and improvements
@@ -204,18 +293,15 @@ You can call `.copyWith` to customize just a subset of properties.
✅ Added ✅ Added
- TypingIndicator now has a property called `parentId` to show typing indicator specific to threads - TypingIndicator now has a property called `parentId` to show typing indicator specific to threads
- [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView - [#493](https://github.com/GetStream/stream-chat-flutter/pull/493): add support for messageListView header/footer
header/footer
- `MessageWidget` accepts a `userAvatarBuilder` - `MessageWidget` accepts a `userAvatarBuilder`
🐞 Fixed 🐞 Fixed
- [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text - [#483](https://github.com/GetStream/stream-chat-flutter/issues/483): Keyboard covers input text box when editing
box when editing message message
- Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator - Modals are shown using the nearest `Navigator` to make using the SDK easier in a nested navigator use case
use case - [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without a reload
- [#484](https://github.com/GetStream/stream-chat-flutter/issues/484): messages don't update without
a reload
- `MessageListView` not rendering if the user is not a member of the channel - `MessageListView` not rendering if the user is not a member of the channel
## 2.0.0-nullsafety.7 ## 2.0.0-nullsafety.7
@@ -285,8 +371,7 @@ You can call `.copyWith` to customize just a subset of properties.
- Show error messages as system and keep them in the message input - Show error messages as system and keep them in the message input
- Remove notification badge logic - Remove notification badge logic
- Use shimmer while loading images - Use shimmer while loading images
- Polished `StreamChatTheme` adding more options and a new `MessageInputTheme` dedicated - Polished `StreamChatTheme` adding more options and a new `MessageInputTheme` dedicated to `MessageInput`
to `MessageInput`
- Add possibility to specify custom message actions using `MessageWidget.customActions` - Add possibility to specify custom message actions using `MessageWidget.customActions`
- Added `MessageListView.onAttachmentTap` callback - Added `MessageListView.onAttachmentTap` callback
- Fixed message newline issue - Fixed message newline issue
@@ -343,8 +428,7 @@ You can call `.copyWith` to customize just a subset of properties.
- Improved api documentation - Improved api documentation
- Updated `stream_chat` dependency to `^1.0.0-beta` - Updated `stream_chat` dependency to `^1.0.0-beta`
- Extracted sample app into dedicated [repo](https://github.com/GetStream/flutter-samples) - Extracted sample app into dedicated [repo](https://github.com/GetStream/flutter-samples)
- Reimplemented existing widgets - Reimplemented existing widgets using [stream_chat_flutter_core](https://pub.dev/packages/stream_chat_flutter_core)
using [stream_chat_flutter_core](https://pub.dev/packages/stream_chat_flutter_core)
## 0.2.21 ## 0.2.21
@@ -361,8 +445,8 @@ You can call `.copyWith` to customize just a subset of properties.
## 0.2.20+2 ## 0.2.20+2
- Added `shouldAddChannel` to ChannelsBloc in order to check if a channel has to be added to the - Added `shouldAddChannel` to ChannelsBloc in order to check if a channel has to be added to the list when a new message
list when a new message arrives arrives
## 0.2.20+1 ## 0.2.20+1
@@ -396,8 +480,7 @@ You can call `.copyWith` to customize just a subset of properties.
## 0.2.16 ## 0.2.16
- Do not wrap channel preview builder. Users will have to implement they're custom onTap/onLongPress - Do not wrap channel preview builder. Users will have to implement they're custom onTap/onLongPress implementation
implementation
- Make public autofocus field of the TextField of message_input - Make public autofocus field of the TextField of message_input
## 0.2.15 ## 0.2.15
@@ -582,11 +665,10 @@ You can call `.copyWith` to customize just a subset of properties.
## 0.2.1-alpha+1 ## 0.2.1-alpha+1
- Removed the additional `Navigator` in `StreamChat` widget. It was added to make the app have - Removed the additional `Navigator` in `StreamChat` widget. It was added to make the app have the `StreamChat` widget
the `StreamChat` widget as ancestor in every route. Now the recommended way to add `StreamChat` to as ancestor in every route. Now the recommended way to add `StreamChat` to your app is using the `builder` property of
your app is using the `builder` property of your `MaterialApp` widget. Otherwise you can use it in your `MaterialApp` widget. Otherwise you can use it in the usual way, but you need to add a `StreamChat` widget to
the usual way, but you need to add a `StreamChat` widget to every route of your app. every route of your app. Read [this issue](https://github.com/GetStream/stream-chat-flutter/issues/47) for more
Read [this issue](https://github.com/GetStream/stream-chat-flutter/issues/47) for more
information. information.
```dart ```dart
@@ -688,8 +770,8 @@ Widget build(BuildContext context) {
- Add gesture (vertical drag down) to close the keyboard - Add gesture (vertical drag down) to close the keyboard
- Add keyboard type parameters (set it to TextInputType.text to show the submit button that will - Add keyboard type parameters (set it to TextInputType.text to show the submit button that will even close the
even close the keyboard) keyboard)
The property showVideoFullScreen was added mainly because of this issue brianegan/chewie#261 The property showVideoFullScreen was added mainly because of this issue brianegan/chewie#261
@@ -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,9 +106,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,
),
), ),
), ),
); );
@@ -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(),
), ),
), ),
@@ -34,7 +34,7 @@ Future<void> main() async {
await client.connectUser( await client.connectUser(
User(id: 'super-band-9'), User(id: 'super-band-9'),
'''eyJ0eXAiO«iJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''', '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoic3VwZXItYmFuZC05In0.0L6lGoeLwkz0aZRUcpZKsvaXtNEDHBcezVTZ0oPq40A''',
); );
runApp( runApp(
@@ -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(),
), ),
), ),
@@ -19,42 +19,36 @@ class AttachmentTitle extends StatelessWidget {
final Attachment attachment; final Attachment attachment;
@override @override
Widget build(BuildContext context) => GestureDetector( Widget build(BuildContext context) {
onTap: () { final normalizedTitleLink = attachment.titleLink?.replaceFirst(
if (attachment.titleLink != null) { RegExp(r'https?://(www\.)?'),
launchURL(context, attachment.titleLink); '',
} );
}, return GestureDetector(
child: Padding( onTap: () {
padding: const EdgeInsets.all(8), final titleLink = attachment.titleLink;
child: Column( if (titleLink != null) launchURL(context, titleLink);
mainAxisSize: MainAxisSize.min, },
crossAxisAlignment: CrossAxisAlignment.stretch, child: Padding(
children: <Widget>[ padding: const EdgeInsets.all(8),
if (attachment.title != null) child: Column(
Text( mainAxisSize: MainAxisSize.min,
attachment.title!, crossAxisAlignment: CrossAxisAlignment.stretch,
overflow: TextOverflow.ellipsis, children: <Widget>[
style: messageTheme.messageTextStyle?.copyWith( if (attachment.title != null)
color: StreamChatTheme.of(context).colorTheme.accentPrimary, Text(
fontWeight: FontWeight.bold, attachment.title!,
), overflow: TextOverflow.ellipsis,
style: messageTheme.messageTextStyle?.copyWith(
color: StreamChatTheme.of(context).colorTheme.accentPrimary,
fontWeight: FontWeight.bold,
), ),
if (attachment.titleLink != null || ),
attachment.ogScrapeUrl != null) if (normalizedTitleLink != null)
Text( Text(normalizedTitleLink, style: messageTheme.messageTextStyle),
Uri.parse(attachment.titleLink ?? attachment.ogScrapeUrl!) ],
.authority
.split('.')
.reversed
.take(2)
.toList()
.reversed
.join('.'),
style: messageTheme.messageTextStyle,
),
],
),
), ),
); ),
);
}
} }
@@ -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 {
@@ -258,7 +257,8 @@ class FileAttachment extends AttachmentWidget {
visualDensity: VisualDensity.compact, visualDensity: VisualDensity.compact,
splashRadius: 16, splashRadius: 16,
onPressed: () { onPressed: () {
launchURL(context, attachment.assetUrl); final assetUrl = attachment.assetUrl;
if (assetUrl != null) launchURL(context, assetUrl);
}, },
); );
} }
@@ -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 {
@@ -98,7 +98,13 @@ class GiphyAttachment extends AttachmentWidget {
Padding( Padding(
padding: const EdgeInsets.all(2), padding: const EdgeInsets.all(2),
child: GestureDetector( child: GestureDetector(
onTap: () => onAttachmentTap ?? _onImageTap(context), onTap: () {
if (onAttachmentTap != null) {
onAttachmentTap?.call();
} else {
_onImageTap(context);
}
},
child: CachedNetworkImage( child: CachedNetworkImage(
height: size?.height, height: size?.height,
width: size?.width, width: size?.width,
@@ -253,21 +259,12 @@ class GiphyAttachment extends AttachmentWidget {
Widget _buildSentAttachment(BuildContext context, String imageUrl) => Widget _buildSentAttachment(BuildContext context, String imageUrl) =>
SizedBox( SizedBox(
child: GestureDetector( child: GestureDetector(
onTap: () async { onTap: () {
final res = if (onAttachmentTap != null) {
await Navigator.push(context, MaterialPageRoute(builder: (_) { onAttachmentTap?.call();
final channel = StreamChannel.of(context).channel; } else {
return StreamChannel( _onImageTap(context);
channel: channel, }
child: FullScreenMedia(
mediaAttachments: [attachment],
userName: message.user?.name,
message: message,
onShowMessage: onShowMessage,
),
);
}));
if (res != null) onReturnAction!(res);
}, },
child: Stack( child: Stack(
children: [ children: [
@@ -10,6 +10,7 @@ class UrlAttachment extends StatelessWidget {
Key? key, Key? key,
required this.urlAttachment, required this.urlAttachment,
required this.hostDisplayName, required this.hostDisplayName,
required this.messageTheme,
this.textPadding = const EdgeInsets.symmetric( this.textPadding = const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
vertical: 8, vertical: 8,
@@ -25,15 +26,16 @@ class UrlAttachment extends StatelessWidget {
/// Padding for text /// Padding for text
final EdgeInsets textPadding; final EdgeInsets textPadding;
/// [MessageThemeData] for showing image title
final MessageThemeData messageTheme;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final chatThemeData = StreamChatTheme.of(context); final chatThemeData = StreamChatTheme.of(context);
return GestureDetector( return GestureDetector(
onTap: () { onTap: () {
launchURL( final titleLink = urlAttachment.titleLink;
context, if (titleLink != null) launchURL(context, titleLink);
urlAttachment.ogScrapeUrl,
);
}, },
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -60,7 +62,7 @@ class UrlAttachment extends StatelessWidget {
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topRight: Radius.circular(16), topRight: Radius.circular(16),
), ),
color: chatThemeData.colorTheme.linkBg, color: messageTheme.linkBackgroundColor,
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:stream_chat_flutter/src/channel_info.dart'; import 'package:stream_chat_flutter/src/channel_info.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Bottom Sheet with options /// Bottom Sheet with options
class ChannelBottomSheet extends StatefulWidget { class ChannelBottomSheet extends StatefulWidget {
@@ -1,12 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:stream_chat_flutter/src/back_button.dart'; import 'package:stream_chat_flutter/src/back_button.dart';
import 'package:stream_chat_flutter/src/channel_info.dart'; import 'package:stream_chat_flutter/src/channel_info.dart';
import 'package:stream_chat_flutter/src/channel_name.dart'; import 'package:stream_chat_flutter/src/channel_name.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/info_tile.dart'; import 'package:stream_chat_flutter/src/info_tile.dart';
import 'package:stream_chat_flutter/src/stream_chat_theme.dart'; import 'package:stream_chat_flutter/src/stream_chat_theme.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header.png)
/// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header_paint.png) /// ![screenshot](https://raw.githubusercontent.com/GetStream/stream-chat-flutter/master/packages/stream_chat_flutter/screenshots/channel_header_paint.png)
@@ -137,12 +138,17 @@ class ChannelHeader extends StatelessWidget implements PreferredSizeWidget {
break; break;
} }
final theme = Theme.of(context);
return InfoTile( return InfoTile(
showMessage: showConnectionStateTile && showStatus, showMessage: showConnectionStateTile && showStatus,
message: statusString, message: statusString,
child: AppBar( child: AppBar(
textTheme: Theme.of(context).textTheme, toolbarTextStyle: theme.textTheme.bodyText2,
brightness: Theme.of(context).brightness, titleTextStyle: theme.textTheme.headline6,
systemOverlayStyle: theme.brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
elevation: 1, elevation: 1,
leading: leadingWidget, leading: leadingWidget,
backgroundColor: backgroundColor ?? channelHeaderTheme.color, backgroundColor: backgroundColor ?? channelHeaderTheme.color,
@@ -1,8 +1,8 @@
import 'package:collection/collection.dart' show IterableExtension; import 'package:collection/collection.dart' show IterableExtension;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Widget which shows channel info /// Widget which shows channel info
class ChannelInfo extends StatelessWidget { class ChannelInfo extends StatelessWidget {
@@ -1,6 +1,7 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/stream_neumorphic_button.dart'; import 'package:stream_chat_flutter/src/stream_neumorphic_button.dart';
@@ -121,12 +122,16 @@ class ChannelListHeader extends StatelessWidget implements PreferredSizeWidget {
final chatThemeData = StreamChatTheme.of(context); final chatThemeData = StreamChatTheme.of(context);
final channelListHeaderThemeData = ChannelListHeaderTheme.of(context); final channelListHeaderThemeData = ChannelListHeaderTheme.of(context);
final theme = Theme.of(context);
return InfoTile( return InfoTile(
showMessage: showConnectionStateTile && showStatus, showMessage: showConnectionStateTile && showStatus,
message: statusString, message: statusString,
child: AppBar( child: AppBar(
textTheme: Theme.of(context).textTheme, toolbarTextStyle: theme.textTheme.bodyText2,
brightness: Theme.of(context).brightness, titleTextStyle: theme.textTheme.headline6,
systemOverlayStyle: theme.brightness == Brightness.dark
? SystemUiOverlayStyle.light
: SystemUiOverlayStyle.dark,
elevation: 1, elevation: 1,
backgroundColor: backgroundColor:
backgroundColor ?? channelListHeaderThemeData.color, backgroundColor ?? channelListHeaderThemeData.color,
@@ -4,11 +4,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:flutter_slidable/flutter_slidable.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.dart';
import 'package:stream_chat_flutter/src/channel_bottom_sheet.dart'; import 'package:stream_chat_flutter/src/channel_bottom_sheet.dart';
import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/stream_svg_icon.dart'; import 'package:stream_chat_flutter/src/stream_svg_icon.dart';
import 'package:stream_chat_flutter/src/theme/themes.dart'; import 'package:stream_chat_flutter/src/theme/themes.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart'; import 'package:stream_chat_flutter_core/stream_chat_flutter_core.dart';
import 'package:stream_chat_flutter/src/extension.dart';
/// Callback called when tapping on a channel /// Callback called when tapping on a channel
typedef ChannelTapCallback = void Function(Channel, Widget?); typedef ChannelTapCallback = void Function(Channel, Widget?);
@@ -59,7 +59,7 @@ typedef ViewInfoCallback = void Function(Channel);
/// Modify it to change the widget appearance. /// Modify it to change the widget appearance.
class ChannelListView extends StatefulWidget { class ChannelListView extends StatefulWidget {
/// Instantiate a new ChannelListView /// Instantiate a new ChannelListView
const ChannelListView({ ChannelListView({
Key? key, Key? key,
this.filter, this.filter,
this.sort, this.sort,
@@ -68,9 +68,12 @@ class ChannelListView extends StatefulWidget {
this.presence = false, this.presence = false,
this.memberLimit, this.memberLimit,
this.messageLimit, this.messageLimit,
this.pagination = const PaginationParams( @Deprecated(
limit: 25, "'pagination' is deprecated and shouldn't be used. "
), "This property is no longer used, Please use 'limit' instead",
)
this.pagination,
int? limit,
this.onChannelTap, this.onChannelTap,
this.onChannelLongPress, this.onChannelLongPress,
this.channelWidget, this.channelWidget,
@@ -92,7 +95,8 @@ class ChannelListView extends StatefulWidget {
this.onDeletePressed, this.onDeletePressed,
this.swipeActions, this.swipeActions,
this.channelListController, this.channelListController,
}) : super(key: key); }) : limit = limit ?? pagination?.limit ?? 25,
super(key: key);
/// If true a default swipe to action behaviour will be added to this widget /// If true a default swipe to action behaviour will be added to this widget
final bool swipeToAction; final bool swipeToAction;
@@ -129,7 +133,14 @@ class ChannelListView extends StatefulWidget {
/// limit: the number of channels to return (max is 30) /// limit: the number of channels to return (max is 30)
/// offset: the offset (max is 1000) /// offset: the offset (max is 1000)
/// message_limit: how many messages should be included to each channel /// message_limit: how many messages should be included to each channel
final PaginationParams pagination; @Deprecated(
"'pagination' is deprecated and shouldn't be used. "
"This property is no longer used, Please use 'limit' instead",
)
final PaginationParams? pagination;
/// The amount of channels requested per API call.
final int limit;
/// Function called when tapping on a channel /// Function called when tapping on a channel
/// By default it calls [Navigator.push] building a [MaterialPageRoute] /// By default it calls [Navigator.push] building a [MaterialPageRoute]
@@ -218,7 +229,7 @@ class _ChannelListViewState extends State<ChannelListView> {
presence: widget.presence, presence: widget.presence,
memberLimit: widget.memberLimit, memberLimit: widget.memberLimit,
messageLimit: widget.messageLimit, messageLimit: widget.messageLimit,
pagination: widget.pagination, limit: widget.limit,
channelListController: _channelListController, channelListController: _channelListController,
listBuilder: widget.listBuilder ?? _buildListView, listBuilder: widget.listBuilder ?? _buildListView,
emptyBuilder: widget.emptyBuilder ?? _buildEmptyWidget, emptyBuilder: widget.emptyBuilder ?? _buildEmptyWidget,
@@ -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';
@@ -11,7 +12,7 @@ final _emojiChars = Emoji.chars();
extension StringExtension on String { extension StringExtension on String {
/// Returns the capitalized string /// Returns the capitalized string
String capitalize() => String capitalize() =>
'${this[0].toUpperCase()}${substring(1).toLowerCase()}'; isNotEmpty ? '${this[0].toUpperCase()}${substring(1).toLowerCase()}' : '';
/// Returns whether the string contains only emoji's or not. /// Returns whether the string contains only emoji's or not.
/// ///
@@ -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(
@@ -15,6 +15,7 @@ class ImageGroup extends StatelessWidget {
required this.size, required this.size,
this.onReturnAction, this.onReturnAction,
this.onShowMessage, this.onShowMessage,
this.onAttachmentTap,
}) : super(key: key); }) : super(key: key);
/// List of attachments to show /// List of attachments to show
@@ -23,6 +24,9 @@ class ImageGroup extends StatelessWidget {
/// Callback when attachment is returned to from other screens /// Callback when attachment is returned to from other screens
final ValueChanged<ReturnActionType>? onReturnAction; final ValueChanged<ReturnActionType>? onReturnAction;
/// Callback when attachment is tapped
final void Function(Message message, Attachment attachment)? onAttachmentTap;
/// Message which images are attached to /// Message which images are attached to
final Message message; final Message message;
@@ -117,6 +121,10 @@ class ImageGroup extends StatelessWidget {
BuildContext context, BuildContext context,
int index, int index,
) async { ) async {
if (onAttachmentTap != null) {
return onAttachmentTap!(message, images[index]);
}
final channel = StreamChannel.of(context).channel; final channel = StreamChannel.of(context).channel;
final res = await Navigator.push( final res = await Navigator.push(
@@ -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(
File diff suppressed because it is too large Load Diff
@@ -169,6 +169,7 @@ class MessageListView extends StatefulWidget {
this.messageListController, this.messageListController,
this.reverse = true, this.reverse = true,
this.paginationLimit = 20, this.paginationLimit = 20,
this.paginationLoadingIndicatorBuilder,
}) : super(key: key); }) : super(key: key);
/// Function used to build a custom message widget /// Function used to build a custom message widget
@@ -284,6 +285,9 @@ class MessageListView extends StatefulWidget {
/// Use [ChannelListController.paginateData] pagination. /// Use [ChannelListController.paginateData] pagination.
final MessageListController? messageListController; final MessageListController? messageListController;
/// Builder used to build the loading indicator shown while paginating.
final WidgetBuilder? paginationLoadingIndicatorBuilder;
@override @override
_MessageListViewState createState() => _MessageListViewState(); _MessageListViewState createState() => _MessageListViewState();
} }
@@ -293,7 +297,6 @@ class _MessageListViewState extends State<MessageListView> {
void Function(Message)? _onThreadTap; void Function(Message)? _onThreadTap;
bool _showScrollToBottom = false; bool _showScrollToBottom = false;
late final ItemPositionsListener _itemPositionListener; late final ItemPositionsListener _itemPositionListener;
late final Stream<Iterable<ItemPosition>> _itemPositionStream;
int? _messageListLength; int? _messageListLength;
StreamChannelState? streamChannel; StreamChannelState? streamChannel;
late StreamChatThemeData _streamTheme; late StreamChatThemeData _streamTheme;
@@ -499,14 +502,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);
@@ -530,13 +537,13 @@ class _MessageListViewState extends State<MessageListView> {
? widget.dateDividerBuilder!( ? widget.dateDividerBuilder!(
nextMessage.createdAt.toLocal(), nextMessage.createdAt.toLocal(),
) )
: DateDivider( : Padding(
dateTime: nextMessage.createdAt.toLocal(), padding: const EdgeInsets.symmetric(vertical: 12),
child: DateDivider(
dateTime: nextMessage.createdAt.toLocal(),
),
); );
return Padding( return divider;
padding: const EdgeInsets.symmetric(vertical: 12),
child: divider,
);
} }
final timeDiff = final timeDiff =
Jiffy(nextMessage.createdAt.toLocal()).diff( Jiffy(nextMessage.createdAt.toLocal()).diff(
@@ -565,27 +572,42 @@ class _MessageListViewState extends State<MessageListView> {
} }
if (i == itemCount - 2) { if (i == itemCount - 2) {
return widget.headerBuilder?.call(context) ?? if (widget.reverse) {
const Offstage(); return widget.headerBuilder?.call(context) ??
const Offstage();
} else {
return widget.footerBuilder?.call(context) ??
const Offstage();
}
} }
final indicatorBuilder =
widget.paginationLoadingIndicatorBuilder;
if (i == itemCount - 3) { if (i == itemCount - 3) {
return _buildLoadingIndicator( return _loadingIndicator(
streamChannel!, streamChannel!,
QueryDirection.top, QueryDirection.top,
indicatorBuilder: indicatorBuilder,
); );
} }
if (i == 1) { if (i == 1) {
return _buildLoadingIndicator( return _loadingIndicator(
streamChannel!, streamChannel!,
QueryDirection.bottom, QueryDirection.bottom,
indicatorBuilder: indicatorBuilder,
); );
} }
if (i == 0) { if (i == 0) {
return widget.footerBuilder?.call(context) ?? if (widget.reverse) {
const Offstage(); return widget.footerBuilder?.call(context) ??
const Offstage();
} else {
return widget.headerBuilder?.call(context) ??
const Offstage();
}
} }
const bottomMessageIndex = 2; // 1 -> loader // 0 -> footer const bottomMessageIndex = 2; // 1 -> loader // 0 -> footer
@@ -657,7 +679,8 @@ class _MessageListViewState extends State<MessageListView> {
right: 0, right: 0,
child: BetterStreamBuilder<Iterable<ItemPosition>>( child: BetterStreamBuilder<Iterable<ItemPosition>>(
initialData: _itemPositionListener.itemPositions.value, initialData: _itemPositionListener.itemPositions.value,
stream: _itemPositionStream, stream: _valueListenableToStreamAdapter(
_itemPositionListener.itemPositions),
comparator: (a, b) { comparator: (a, b) {
if (a == null || b == null) { if (a == null || b == null) {
return false; return false;
@@ -808,15 +831,17 @@ class _MessageListViewState extends State<MessageListView> {
}, },
); );
Widget _buildLoadingIndicator( Widget _loadingIndicator(
StreamChannelState streamChannel, StreamChannelState streamChannel,
QueryDirection direction, QueryDirection direction, {
) => WidgetBuilder? indicatorBuilder,
}) =>
_LoadingIndicator( _LoadingIndicator(
direction: direction, direction: direction,
streamTheme: _streamTheme, streamTheme: _streamTheme,
streamChannel: streamChannel, streamChannel: streamChannel,
isThreadConversation: _isThreadConversation, isThreadConversation: _isThreadConversation,
indicatorBuilder: indicatorBuilder,
); );
Widget _buildBottomMessage( Widget _buildBottomMessage(
@@ -996,7 +1021,7 @@ class _MessageListViewState extends State<MessageListView> {
final isOnlyEmoji = message.text?.isOnlyEmoji ?? false; final isOnlyEmoji = message.text?.isOnlyEmoji ?? false;
final hasUrlAttachment = final hasUrlAttachment =
message.attachments.any((it) => it.ogScrapeUrl != null) == true; message.attachments.any((it) => it.titleLink != null) == true;
final borderSide = final borderSide =
isOnlyEmoji || hasUrlAttachment || (isMyMessage && !hasFileAttachment) isOnlyEmoji || hasUrlAttachment || (isMyMessage && !hasFileAttachment)
@@ -1185,8 +1210,6 @@ class _MessageListViewState extends State<MessageListView> {
_scrollController = widget.scrollController ?? ItemScrollController(); _scrollController = widget.scrollController ?? ItemScrollController();
_itemPositionListener = _itemPositionListener =
widget.itemPositionListener ?? ItemPositionsListener.create(); widget.itemPositionListener ?? ItemPositionsListener.create();
_itemPositionStream =
_valueListenableToStreamAdapter(_itemPositionListener.itemPositions);
_getOnThreadTap(); _getOnThreadTap();
super.initState(); super.initState();
@@ -1204,10 +1227,12 @@ class _MessageListViewState extends State<MessageListView> {
initialAlignment = _initialAlignment; initialAlignment = _initialAlignment;
WidgetsBinding.instance!.addPostFrameCallback((timeStamp) { WidgetsBinding.instance!.addPostFrameCallback((timeStamp) {
_scrollController?.jumpTo( if (_scrollController?.isAttached == true) {
index: initialIndex, _scrollController?.jumpTo(
alignment: initialAlignment, index: initialIndex,
); alignment: initialAlignment,
);
}
}); });
_messageNewListener = _messageNewListener =
@@ -1216,8 +1241,9 @@ class _MessageListViewState extends State<MessageListView> {
_bottomPaginationActive = false; _bottomPaginationActive = false;
_topPaginationActive = false; _topPaginationActive = false;
} }
if (event.message!.user!.id == if (event.message?.parentId == widget.parentMessage?.id &&
streamChannel!.channel.client.state.currentUser!.id) { event.message!.user!.id ==
streamChannel!.channel.client.state.currentUser!.id) {
WidgetsBinding.instance!.addPostFrameCallback((_) { WidgetsBinding.instance!.addPostFrameCallback((_) {
_scrollController?.jumpTo( _scrollController?.jumpTo(
index: 0, index: 0,
@@ -1280,12 +1306,14 @@ class _LoadingIndicator extends StatelessWidget {
required this.isThreadConversation, required this.isThreadConversation,
required this.direction, required this.direction,
required this.streamChannel, required this.streamChannel,
this.indicatorBuilder,
}) : super(key: key); }) : super(key: key);
final StreamChatThemeData streamTheme; final StreamChatThemeData streamTheme;
final bool isThreadConversation; final bool isThreadConversation;
final QueryDirection direction; final QueryDirection direction;
final StreamChannelState streamChannel; final StreamChannelState streamChannel;
final WidgetBuilder? indicatorBuilder;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -1304,12 +1332,13 @@ class _LoadingIndicator extends StatelessWidget {
), ),
builder: (context, data) { builder: (context, data) {
if (!data) return const Offstage(); if (!data) return const Offstage();
return const Center( return indicatorBuilder?.call(context) ??
child: Padding( const Center(
padding: EdgeInsets.all(8), child: Padding(
child: CircularProgressIndicator(), padding: EdgeInsets.all(8),
), child: CircularProgressIndicator(),
); ),
);
}, },
); );
} }
@@ -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);
@@ -30,13 +30,13 @@ typedef EmptyMessageSearchBuilder = Widget Function(
/// Widget build(BuildContext context) { /// Widget build(BuildContext context) {
/// return Scaffold( /// return Scaffold(
/// body: MessageSearchListView( /// body: MessageSearchListView(
/// messageQuery: _channelQuery, /// messageQuery: _channelQuery,
/// filters: { /// filters: {
/// 'members': { /// 'members': {
/// r'$in': [user.id] /// r'$in': [user.id]
/// } /// }
/// }, /// },
/// paginationParams: PaginationParams(limit: 20), /// limit: 20,
/// ), /// ),
/// ); /// );
/// } /// }
@@ -58,7 +58,7 @@ class MessageSearchListView extends StatefulWidget {
required this.filters, required this.filters,
this.messageQuery, this.messageQuery,
this.sortOptions, this.sortOptions,
this.paginationParams, this.limit = 30,
this.messageFilters, this.messageFilters,
this.separatorBuilder, this.separatorBuilder,
this.itemBuilder, this.itemBuilder,
@@ -89,11 +89,8 @@ 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 /// The amount of messages requested per API call.
/// limit: the number of users to return (max is 30) final int limit;
/// offset: the offset (max is 1000)
/// message_limit: how many messages should be included to each channel
final PaginationParams? paginationParams;
/// The message query filters to use. /// The message 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].
@@ -152,7 +149,7 @@ class _MessageSearchListViewState extends State<MessageSearchListView> {
filters: widget.filters, filters: widget.filters,
sortOptions: widget.sortOptions, sortOptions: widget.sortOptions,
messageQuery: widget.messageQuery, messageQuery: widget.messageQuery,
paginationParams: widget.paginationParams, limit: widget.limit,
messageFilters: widget.messageFilters, messageFilters: widget.messageFilters,
messageSearchListController: _messageSearchListController, messageSearchListController: _messageSearchListController,
emptyBuilder: widget.emptyBuilder ?? emptyBuilder: widget.emptyBuilder ??
@@ -7,6 +7,7 @@ import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_portal/flutter_portal.dart'; import 'package:flutter_portal/flutter_portal.dart';
import 'package:jiffy/jiffy.dart'; import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/src/attachment/url_attachment.dart';
import 'package:stream_chat_flutter/src/extension.dart'; import 'package:stream_chat_flutter/src/extension.dart';
import 'package:stream_chat_flutter/src/image_group.dart'; import 'package:stream_chat_flutter/src/image_group.dart';
import 'package:stream_chat_flutter/src/message_action.dart'; import 'package:stream_chat_flutter/src/message_action.dart';
@@ -15,7 +16,6 @@ import 'package:stream_chat_flutter/src/message_reactions_modal.dart';
import 'package:stream_chat_flutter/src/quoted_message_widget.dart'; import 'package:stream_chat_flutter/src/quoted_message_widget.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/theme/themes.dart'; import 'package:stream_chat_flutter/src/theme/themes.dart';
import 'package:stream_chat_flutter/src/url_attachment.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart'; import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// Widget builder for building attachments /// Widget builder for building attachments
@@ -96,7 +96,7 @@ class MessageWidget extends StatefulWidget {
this.bottomRowBuilder, this.bottomRowBuilder,
this.deletedBottomRowBuilder, this.deletedBottomRowBuilder,
this.onReturnAction, this.onReturnAction,
Map<String, AttachmentBuilder>? customAttachmentBuilders, this.customAttachmentBuilders,
this.readList, this.readList,
this.padding, this.padding,
this.textPadding = const EdgeInsets.symmetric( this.textPadding = const EdgeInsets.symmetric(
@@ -133,6 +133,7 @@ class MessageWidget extends StatefulWidget {
messageTheme: messageTheme, messageTheme: messageTheme,
onShowMessage: onShowMessage, onShowMessage: onShowMessage,
onReturnAction: onReturnAction, onReturnAction: onReturnAction,
onAttachmentTap: onAttachmentTap,
), ),
), ),
border, border,
@@ -214,6 +215,11 @@ class MessageWidget extends StatefulWidget {
), ),
onShowMessage: onShowMessage, onShowMessage: onShowMessage,
onReturnAction: onReturnAction, onReturnAction: onReturnAction,
onAttachmentTap: onAttachmentTap != null
? () {
onAttachmentTap(message, attachment);
}
: null,
); );
}).toList(), }).toList(),
), ),
@@ -243,6 +249,11 @@ class MessageWidget extends StatefulWidget {
mediaQueryData.size.width * 0.8, mediaQueryData.size.width * 0.8,
mediaQueryData.size.height * 0.3, mediaQueryData.size.height * 0.3,
), ),
onAttachmentTap: onAttachmentTap != null
? () {
onAttachmentTap(message, attachment);
}
: null,
), ),
border, border,
reverse, reverse,
@@ -395,6 +406,9 @@ class MessageWidget extends StatefulWidget {
/// Builder for respective attachment types /// Builder for respective attachment types
final Map<String, AttachmentBuilder> attachmentBuilders; final Map<String, AttachmentBuilder> attachmentBuilders;
/// Builder for respective attachment types (user facing builder)
final Map<String, AttachmentBuilder>? customAttachmentBuilders;
/// Center user avatar with bottom of the message /// Center user avatar with bottom of the message
final bool translateUserAvatar; final bool translateUserAvatar;
@@ -519,7 +533,7 @@ class MessageWidget extends StatefulWidget {
showPinButton: showPinButton ?? this.showPinButton, showPinButton: showPinButton ?? this.showPinButton,
showPinHighlight: showPinHighlight ?? this.showPinHighlight, showPinHighlight: showPinHighlight ?? this.showPinHighlight,
customAttachmentBuilders: customAttachmentBuilders:
customAttachmentBuilders ?? attachmentBuilders, customAttachmentBuilders ?? this.customAttachmentBuilders,
translateUserAvatar: translateUserAvatar ?? this.translateUserAvatar, translateUserAvatar: translateUserAvatar ?? this.translateUserAvatar,
onQuotedMessageTap: onQuotedMessageTap ?? this.onQuotedMessageTap, onQuotedMessageTap: onQuotedMessageTap ?? this.onQuotedMessageTap,
onMessageTap: onMessageTap ?? this.onMessageTap, onMessageTap: onMessageTap ?? this.onMessageTap,
@@ -567,11 +581,11 @@ class _MessageWidgetState extends State<MessageWidget>
bool get isOnlyEmoji => widget.message.text?.isOnlyEmoji == true; bool get isOnlyEmoji => widget.message.text?.isOnlyEmoji == true;
bool get hasNonUrlAttachments => widget.message.attachments bool get hasNonUrlAttachments => widget.message.attachments
.where((it) => it.ogScrapeUrl == null) .where((it) => it.titleLink == null || it.type == 'giphy')
.isNotEmpty; .isNotEmpty;
bool get hasUrlAttachments => bool get hasUrlAttachments => widget.message.attachments
widget.message.attachments.any((it) => it.ogScrapeUrl != null) == true; .any((it) => it.titleLink != null && it.type != 'giphy');
bool get showBottomRow => bool get showBottomRow =>
showThreadReplyIndicator || showThreadReplyIndicator ||
@@ -984,9 +998,9 @@ class _MessageWidgetState extends State<MessageWidget>
Widget _buildUrlAttachment() { Widget _buildUrlAttachment() {
final urlAttachment = widget.message.attachments final urlAttachment = widget.message.attachments
.firstWhere((element) => element.ogScrapeUrl != null); .firstWhere((element) => element.titleLink != null);
final host = Uri.parse(urlAttachment.ogScrapeUrl!).host; final host = Uri.parse(urlAttachment.titleLink!).host;
final splitList = host.split('.'); final splitList = host.split('.');
final hostName = splitList.length == 3 ? splitList[1] : splitList[0]; final hostName = splitList.length == 3 ? splitList[1] : splitList[0];
final hostDisplayName = urlAttachment.authorName?.capitalize() ?? final hostDisplayName = urlAttachment.authorName?.capitalize() ??
@@ -997,6 +1011,7 @@ class _MessageWidgetState extends State<MessageWidget>
urlAttachment: urlAttachment, urlAttachment: urlAttachment,
hostDisplayName: hostDisplayName, hostDisplayName: hostDisplayName,
textPadding: widget.textPadding, textPadding: widget.textPadding,
messageTheme: widget.messageTheme,
); );
} }
@@ -1155,7 +1170,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.ogScrapeUrl == 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!] = [];
@@ -1335,7 +1352,7 @@ class _MessageWidgetState extends State<MessageWidget>
} }
if (hasUrlAttachments) { if (hasUrlAttachments) {
return _streamChatTheme.colorTheme.linkBg; return widget.messageTheme.linkBackgroundColor;
} }
if (isOnlyEmoji) { if (isOnlyEmoji) {

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