style: Update for team lint (#297)

* Update for team lint

* fix tests

* remove pedantic and sort deps

* remove jiffy from system message

Co-authored-by: Salvatore Giordano <[email protected]>
This commit is contained in:
Neevash Ramdial (Nash)
2021-03-03 09:44:48 +01:00
committed by GitHub
co-authored by Salvatore Giordano
parent e13ce930a6
commit c5252ab4d4
38 changed files with 1051 additions and 979 deletions
+132 -48
View File
@@ -1,63 +1,147 @@
include: package:pedantic/analysis_options.yaml
analyzer:
exclude:
exclude:
- lib/**/*.g.dart
- lib/**/*.freezed.dart
- example/*
- test/*
linter:
rules:
# these rules are documented on and in the same order as
# the Dart Lint rules page to make maintenance easier
# https://github.com/dart-lang/linter/blob/master/example/all.yaml
# - always_declare_return_types
# - always_specify_types
# - annotate_overrides
# - avoid_as
linter:
rules:
- always_use_package_imports
- avoid_empty_else
- avoid_init_to_null
- avoid_return_types_on_setters
- avoid_web_libraries_in_flutter
- await_only_futures
- camel_case_types
- avoid_relative_lib_imports
- avoid_slow_async_io
- avoid_types_as_parameter_names
- cancel_subscriptions
- close_sinks
# - comment_references # we do not presume as to what people want to reference in their dartdocs
# - constant_identifier_names # https://github.com/dart-lang/linter/issues/204
- control_flow_in_finally
- empty_constructor_bodies
- diagnostic_describe_all_properties
- empty_statements
- hash_and_equals
- implementation_imports
# - invariant_booleans
# - iterable_contains_unrelated_type
- library_names
# - library_prefixes
# - list_remove_unrelated_type
# - literal_only_boolean_expressions
- non_constant_identifier_names
# - one_member_abstracts
# - only_throw_errors
# - overridden_fields
- package_api_docs
- package_names
- package_prefixed_library_names
- prefer_is_not_empty
# - prefer_mixin # https://github.com/dart-lang/language/issues/32
- public_member_api_docs
- slash_for_doc_comments
# - sort_constructors_first
# - sort_unnamed_constructors_first
# - super_goes_last # no longer needed w/ Dart 2
- invariant_booleans
- iterable_contains_unrelated_type
- list_remove_unrelated_type
- literal_only_boolean_expressions
- no_adjacent_strings_in_list
- no_duplicate_case_values
- no_logic_in_create_state
- prefer_void_to_null
- test_types_in_equals
- throw_in_finally
# - type_annotate_public_apis # subset of always_specify_types
- type_init_formals
# - unawaited_futures
- unnecessary_brace_in_string_interps
- unnecessary_getters_setters
- unnecessary_statements
- unrelated_type_equality_checks
- valid_regexps
- omit_local_variable_types
- use_key_in_widget_constructors
- valid_regexps
- always_declare_return_types
- always_put_required_named_parameters_first
- always_require_non_null_named_parameters
- annotate_overrides
- avoid_bool_literals_in_conditional_expressions
- avoid_catching_errors
- avoid_init_to_null
- avoid_null_checks_in_equality_operators
- avoid_positional_boolean_parameters
- avoid_private_typedef_functions
- avoid_redundant_argument_values
- avoid_return_types_on_setters
- avoid_returning_null
- avoid_returning_null_for_void
- avoid_shadowing_type_parameters
- avoid_single_cascade_in_expression_statements
- avoid_unnecessary_containers
- avoid_unused_constructor_parameters
- await_only_futures
- camel_case_extensions
- camel_case_types
- cascade_invocations
- constant_identifier_names
- curly_braces_in_flow_control_structures
- directives_ordering
- empty_catches
- empty_constructor_bodies
- exhaustive_cases
- file_names
- implementation_imports
- join_return_with_assignment
- leading_newlines_in_multiline_strings
- library_names
- library_prefixes
- lines_longer_than_80_chars
- missing_whitespace_between_adjacent_strings
- non_constant_identifier_names
- null_closures
- one_member_abstracts
- only_throw_errors
- package_api_docs
- package_prefixed_library_names
- parameter_assignments
- prefer_adjacent_string_concatenation
- prefer_asserts_in_initializer_lists
- prefer_asserts_with_message
- prefer_collection_literals
- prefer_conditional_assignment
- prefer_const_constructors
- prefer_const_constructors_in_immutables
- prefer_const_declarations
- prefer_const_literals_to_create_immutables
- prefer_constructors_over_static_methods
- prefer_contains
- prefer_equal_for_default_values
- prefer_expression_function_bodies
- prefer_final_fields
- prefer_final_in_for_each
- prefer_final_locals
- prefer_function_declarations_over_variables
- prefer_generic_function_type_aliases
- prefer_if_elements_to_conditional_expressions
- prefer_if_null_operators
- prefer_initializing_formals
- prefer_inlined_adds
- prefer_int_literals
- prefer_interpolation_to_compose_strings
- prefer_is_empty
- prefer_is_not_empty
- prefer_is_not_operator
- prefer_null_aware_operators
- prefer_single_quotes
- prefer_spread_collections
- prefer_typing_uninitialized_variables
- provide_deprecation_message
- public_member_api_docs
- recursive_getters
- sized_box_for_whitespace
- slash_for_doc_comments
- sort_child_properties_last
- sort_constructors_first
- sort_unnamed_constructors_first
- type_annotate_public_apis
- type_init_formals
- unnecessary_await_in_return
- unnecessary_brace_in_string_interps
- unnecessary_const
- unnecessary_getters_setters
- unnecessary_lambdas
- unnecessary_new
- unnecessary_null_aware_assignments
- unnecessary_null_in_if_null_operators
- unnecessary_nullable_for_final_variable_declarations
- unnecessary_parenthesis
- unnecessary_raw_strings
- unnecessary_string_escapes
- unnecessary_string_interpolations
- unnecessary_this
- use_is_even_rather_than_modulo
- use_late_for_private_fields_and_variables
- use_rethrow_when_possible
- use_setters_to_change_properties
- use_to_and_as_if_applicable
- package_names
- sort_pub_dependencies
# To be added when null-safe:
# - cast_nullable_to_non_nullable
#- unnecessary_null_checks
# - tighten_type_of_initializing_formals
# - null_check_on_nullable_type_parameter
+171 -162
View File
@@ -2,9 +2,9 @@ import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:pedantic/pedantic.dart' show unawaited;
import 'package:dio/dio.dart';
import 'package:logging/logging.dart';
import 'package:pedantic/pedantic.dart' show unawaited;
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/src/api/retry_queue.dart';
import 'package:stream_chat/src/debounce.dart';
@@ -14,13 +14,6 @@ import 'package:stream_chat/src/models/channel_state.dart';
import 'package:stream_chat/src/models/user.dart';
import 'package:stream_chat/stream_chat.dart';
import '../client.dart';
import '../models/event.dart';
import '../models/member.dart';
import '../models/message.dart';
import 'requests.dart';
import 'responses.dart';
/// This a the class that manages a specific channel.
class Channel {
/// Create a channel client instance.
@@ -57,7 +50,8 @@ class Channel {
set extraData(Map<String, dynamic> extraData) {
if (_initializedCompleter.isCompleted) {
throw Exception(
'Once the channel is initialized you should use channel.update to update channel data');
'Once the channel is initialized you should use channel.update '
'to update channel data');
}
_extraData = extraData;
}
@@ -168,14 +162,15 @@ class Channel {
final Completer<bool> _initializedCompleter = Completer();
/// True if this is initialized
/// Call [watch] to initialize the client or instantiate it using [Channel.fromState]
/// Call [watch] to initialize the client or instantiate it using
/// [Channel.fromState]
Future<bool> get initialized => _initializedCompleter.future;
final _cancelableAttachmentUploadRequest = <String, CancelToken>{};
final _messageAttachmentsUploadCompleter = <String, Completer>{};
/// Cancels [attachmentId] upload request. Throws exception if the request hasn't
/// even started yet, Already completed or Already cancelled.
/// Cancels [attachmentId] upload request. Throws exception if the request
/// hasn't even started yet, Already completed or Already cancelled.
///
/// Optionally, provide a [reason] for the cancellation.
void cancelAttachmentUpload(
@@ -185,7 +180,8 @@ class Channel {
final cancelToken = _cancelableAttachmentUploadRequest[attachmentId];
if (cancelToken == null) {
throw Exception(
"Upload request for this Attachment hasn't started yet or else Already completed",
"Upload request for this Attachment hasn't started yet or else "
'Already completed',
);
}
if (cancelToken.isCancelled) throw Exception('Already cancelled');
@@ -193,15 +189,14 @@ class Channel {
}
/// Retries the failed [attachmentId] upload request.
Future<void> retryAttachmentUpload(String messageId, String attachmentId) {
return _uploadAttachments(messageId, [attachmentId]);
}
Future<void> retryAttachmentUpload(String messageId, String attachmentId) =>
_uploadAttachments(messageId, [attachmentId]);
Future<void> _uploadAttachments(
String messageId,
Iterable<String> attachmentIds,
) {
var message = state.messages.firstWhere(
final message = state.messages.firstWhere(
(it) => it.id == messageId,
orElse: () => null,
);
@@ -228,9 +223,8 @@ class Channel {
client.logger.info('Uploading ${it.id} attachment...');
void updateAttachment(Attachment attachment) {
final index = message.attachments.indexWhere((it) {
return it.id == attachment.id;
});
final index =
message.attachments.indexWhere((it) => it.id == attachment.id);
if (index != -1) {
message.attachments[index] = attachment;
state?.addMessage(message);
@@ -239,7 +233,7 @@ class Channel {
void onSendProgress(int sent, int total) {
debounce(
timeout: Duration(seconds: 1),
timeout: const Duration(seconds: 1),
target: updateAttachment,
positionalArguments: [
it.copyWith(
@@ -270,11 +264,17 @@ class Channel {
client.logger.info('Attachment ${it.id} uploaded successfully...');
if (isImage) {
updateAttachment(
it.copyWith(imageUrl: url, uploadState: UploadState.success()),
it.copyWith(
imageUrl: url,
uploadState: const UploadState.success(),
),
);
} else {
updateAttachment(
it.copyWith(assetUrl: url, uploadState: UploadState.success()),
it.copyWith(
assetUrl: url,
uploadState: const UploadState.success(),
),
);
}
}).catchError((e, stk) {
@@ -305,6 +305,7 @@ class Channel {
(m) => m.id == message?.quotedMessageId,
orElse: () => null,
);
// ignore: parameter_assignments
message = message.copyWith(
createdAt: message.createdAt ?? DateTime.now(),
user: _client.state.user,
@@ -313,7 +314,7 @@ class Channel {
attachments: message.attachments?.map(
(it) {
if (it.uploadState.isSuccess) return it;
return it.copyWith(uploadState: UploadState.preparing());
return it.copyWith(uploadState: const UploadState.preparing());
},
)?.toList(),
);
@@ -340,6 +341,7 @@ class Channel {
message.attachments.map((it) => it.id),
));
// ignore: parameter_assignments
message = await attachmentsUploadCompleter.future;
}
@@ -364,13 +366,14 @@ class Channel {
.remove(message.id)
?.completeError('Message Cancelled');
// ignore: parameter_assignments
message = message.copyWith(
status: MessageSendingStatus.updating,
updatedAt: message.updatedAt ?? DateTime.now(),
attachments: message.attachments?.map(
(it) {
if (it.uploadState.isSuccess) return it;
return it.copyWith(uploadState: UploadState.preparing());
return it.copyWith(uploadState: const UploadState.preparing());
},
)?.toList(),
);
@@ -388,6 +391,7 @@ class Channel {
message.attachments.map((it) => it.id),
));
// ignore: parameter_assignments
message = await attachmentsUploadCompleter.future;
}
@@ -423,6 +427,7 @@ class Channel {
}
try {
// ignore: parameter_assignments
message = message.copyWith(
type: 'deleted',
status: MessageSendingStatus.deleting,
@@ -456,7 +461,7 @@ class Channel {
throw ArgumentError('Invalid timeout or Expiration date');
}
return true;
}());
}(), 'Check for invalid token or expiration date');
DateTime pinExpires;
if (timeoutOrExpirationDate is DateTime) {
@@ -475,39 +480,36 @@ class Channel {
}
/// Unpins provided message
Future<UpdateMessageResponse> unpinMessage(Message message) {
return updateMessage(message.copyWith(pinned: false));
}
Future<UpdateMessageResponse> unpinMessage(Message message) =>
updateMessage(message.copyWith(pinned: false));
/// Send a file to this channel
Future<SendFileResponse> sendFile(
AttachmentFile file, {
ProgressCallback onSendProgress,
CancelToken cancelToken,
}) {
return _client.sendFile(
file,
id,
type,
onSendProgress: onSendProgress,
cancelToken: cancelToken,
);
}
}) =>
_client.sendFile(
file,
id,
type,
onSendProgress: onSendProgress,
cancelToken: cancelToken,
);
/// Send an image to this channel
Future<SendImageResponse> sendImage(
AttachmentFile file, {
ProgressCallback onSendProgress,
CancelToken cancelToken,
}) {
return _client.sendImage(
file,
id,
type,
onSendProgress: onSendProgress,
cancelToken: cancelToken,
);
}
}) =>
_client.sendImage(
file,
id,
type,
onSendProgress: onSendProgress,
cancelToken: cancelToken,
);
/// A message search.
Future<SearchMessagesResponse> search({
@@ -515,35 +517,32 @@ class Channel {
Map<String, dynamic> messageFilters,
List<SortOption> sort,
PaginationParams paginationParams,
}) {
return _client.search(
{
'cid': {
r'$in': [cid],
}) =>
_client.search(
{
'cid': {
r'$in': [cid],
},
},
},
sort: sort,
query: query,
paginationParams: paginationParams,
messageFilters: messageFilters,
);
}
sort: sort,
query: query,
paginationParams: paginationParams,
messageFilters: messageFilters,
);
/// Delete a file from this channel
Future<EmptyResponse> deleteFile(
String url, {
CancelToken cancelToken,
}) {
return _client.deleteFile(url, id, type, cancelToken: cancelToken);
}
}) =>
_client.deleteFile(url, id, type, cancelToken: cancelToken);
/// Delete an image from this channel
Future<EmptyResponse> deleteImage(
String url, {
CancelToken cancelToken,
}) {
return _client.deleteImage(url, id, type, cancelToken: cancelToken);
}
}) =>
_client.deleteImage(url, id, type, cancelToken: cancelToken);
/// Send an event on this channel
Future<EmptyResponse> sendEvent(Event event) {
@@ -551,9 +550,7 @@ class Channel {
return _client.post(
'$_channelURL/event',
data: {'event': event.toJson()},
).then((res) {
return _client.decode(res.data, EmptyResponse.fromJson);
});
).then((res) => _client.decode(res.data, EmptyResponse.fromJson));
}
/// Send a reaction to this channel
@@ -643,11 +640,10 @@ class Channel {
}
final latestReactions = [...message.latestReactions ?? <Reaction>[]]
..removeWhere((r) {
return r.userId == reaction.userId &&
r.type == reaction.type &&
r.messageId == reaction.messageId;
});
..removeWhere((r) =>
r.userId == reaction.userId &&
r.type == reaction.type &&
r.messageId == reaction.messageId);
final ownReactions = [...latestReactions ?? <Reaction>[]]
..removeWhere((it) => it.userId != user.id);
@@ -825,7 +821,7 @@ class Channel {
})
..addAll(options);
var response;
ChannelState response;
try {
response = await query(options: watchOptions);
@@ -861,7 +857,8 @@ class Channel {
}
/// List the message replies for a parent message
/// Set [preferOffline] to true to avoid the api call if the data is already in the offline storage
/// Set [preferOffline] to true to avoid the api call if the data is already
/// in the offline storage
Future<QueryRepliesResponse> getReplies(
String parentId,
PaginationParams options, {
@@ -940,16 +937,15 @@ class Channel {
}
/// Creates a new channel
Future<ChannelState> create() async {
return query(options: {
'watch': false,
'state': false,
'presence': false,
});
}
Future<ChannelState> create() async => query(options: {
'watch': false,
'state': false,
'presence': false,
});
/// Query the API, get messages, members or other channel fields
/// Set [preferOffline] to true to avoid the api call if the data is already in the offline storage
/// Set [preferOffline] to true to avoid the api call if the data is already
/// in the offline storage
Future<ChannelState> query({
Map<String, dynamic> options = const {},
PaginationParams messagesPagination,
@@ -1112,8 +1108,9 @@ class Channel {
});
}
/// Hides the channel from [StreamChatClient.queryChannels] for the user until a message is added
/// If [clearHistory] is set to true - all messages will be removed for the user
/// Hides the channel from [StreamChatClient.queryChannels] for the user
/// until a message is added If [clearHistory] is set to true - all messages
/// will be removed for the user
Future<EmptyResponse> hide({bool clearHistory = false}) async {
_checkInitialized();
final response = await _client
@@ -1134,28 +1131,28 @@ class Channel {
return _client.decode(response.data, EmptyResponse.fromJson);
}
/// Stream of [Event] coming from websocket connection specific for the channel
/// Pass an eventType as parameter in order to filter just a type of event
/// Stream of [Event] coming from websocket connection specific for the
/// channel. Pass an eventType as parameter in order to filter just a type
/// of event
Stream<Event> on([
String eventType,
String eventType2,
String eventType3,
String eventType4,
]) {
return _client
.on(
eventType,
eventType2,
eventType3,
eventType4,
)
.where((e) => e.cid == cid);
}
]) =>
_client
.on(
eventType,
eventType2,
eventType3,
eventType4,
)
.where((e) => e.cid == cid);
DateTime _lastTypingEvent;
/// First of the [EventType.typingStart] and [EventType.typingStop] events based on the users keystrokes.
/// Call this on every keystroke.
/// First of the [EventType.typingStart] and [EventType.typingStop] events
/// based on the users keystrokes. Call this on every keystroke.
Future<void> keyStroke([String parentId]) async {
if (config?.typingEvents == false) {
return;
@@ -1196,15 +1193,14 @@ class Channel {
void _checkInitialized() {
if (!_initializedCompleter.isCompleted) {
throw Exception(
"Channel $cid hasn't been initialized yet. Make sure to call .watch() or to instantiate the client using [Channel.fromState]");
"Channel $cid hasn't been initialized yet. Make sure to call .watch()"
' or to instantiate the client using [Channel.fromState]');
}
}
}
/// The class that handles the state of the channel listening to the events
class ChannelClientState {
final _subscriptions = <StreamSubscription>[];
/// Creates a new instance listening to events and updating the state
ChannelClientState(this._channel, ChannelState channelState) {
retryQueue = RetryQueue(
@@ -1252,14 +1248,16 @@ class ChannelClientState {
_channel._client.chatPersistenceClient
?.getChannelStateByCid(_channel.cid)
?.then((state) {
// Replacing the persistence state members with the latest `channelState.members`
// as they may have changes over the time.
// Replacing the persistence state members with the latest
// `channelState.members` as they may have changes over the time.
updateChannelState(state.copyWith(members: channelState.members));
retryFailedMessages();
});
});
}
final _subscriptions = <StreamSubscription>[];
void _computeInitialUnread() {
final userRead = channelState?.read?.firstWhere(
(r) => r.user.id == _channel._client.state?.user?.id,
@@ -1338,8 +1336,8 @@ class ChannelClientState {
/// Flag which indicates if [ChannelClientState] contain latest/recent messages or not.
/// This flag should be managed by UI sdks.
/// When false, any new message (received by WebSocket event - [EventType.messageNew]) will not
/// be pushed on to message list.
/// When false, any new message (received by WebSocket event
/// - [EventType.messageNew]) will not be pushed on to message list.
bool get isUpToDate => _isUpToDateController.value;
set isUpToDate(bool isUpToDate) => _isUpToDateController.add(isUpToDate);
@@ -1357,12 +1355,18 @@ class ChannelClientState {
Future<void> retryFailedMessages() async {
final failedMessages =
<Message>[...messages, ...threads.values.expand((v) => v)]
.where((message) =>
message.status != null &&
message.status != MessageSendingStatus.sent &&
message.createdAt.isBefore(DateTime.now().subtract(Duration(
seconds: 1,
))))
.where(
(message) =>
message.status != null &&
message.status != MessageSendingStatus.sent &&
message.createdAt.isBefore(
DateTime.now().subtract(
const Duration(
seconds: 1,
),
),
),
)
.toList();
retryQueue.add(failedMessages);
@@ -1471,27 +1475,32 @@ class ChannelClientState {
return;
}
_subscriptions.add(_channel
.on(
EventType.messageRead,
EventType.notificationMarkRead,
)
.listen((event) {
final readList = List<Read>.from(_channelState?.read ?? []);
final userReadIndex = read?.indexWhere((r) => r.user.id == event.user.id);
_subscriptions.add(
_channel
.on(
EventType.messageRead,
EventType.notificationMarkRead,
)
.listen(
(event) {
final readList = List<Read>.from(_channelState?.read ?? []);
final userReadIndex =
read?.indexWhere((r) => r.user.id == event.user.id);
if (userReadIndex != null && userReadIndex != -1) {
final userRead = readList.removeAt(userReadIndex);
if (userRead.user?.id == _channel._client.state.user.id) {
_unreadCountController.add(0);
}
readList.add(Read(
user: event.user,
lastRead: event.createdAt,
));
_channelState = _channelState.copyWith(read: readList);
}
}));
if (userReadIndex != null && userReadIndex != -1) {
final userRead = readList.removeAt(userReadIndex);
if (userRead.user?.id == _channel._client.state.user.id) {
_unreadCountController.add(0);
}
readList.add(Read(
user: event.user,
lastRead: event.createdAt,
));
_channelState = _channelState.copyWith(read: readList);
}
},
),
);
}
/// Channel message list
@@ -1527,11 +1536,8 @@ class ChannelClientState {
List<Member>, Map<String, User>, List<Member>>(
channelStateStream.map((cs) => cs.members),
_channel.client.state.usersStream,
(members, users) {
return members
.map((e) => e.copyWith(user: users[e.user.id]))
.toList();
},
(members, users) =>
members.map((e) => e.copyWith(user: users[e.user.id])).toList(),
);
/// Channel watcher count
@@ -1551,9 +1557,7 @@ class ChannelClientState {
CombineLatestStream.combine2<List<User>, Map<String, User>, List<User>>(
channelStateStream.map((cs) => cs.watchers),
_channel.client.state.usersStream,
(watchers, users) {
return watchers.map((e) => users[e.id] ?? e).toList();
},
(watchers, users) => watchers.map((e) => users[e.id] ?? e).toList(),
);
/// Channel read list
@@ -1625,9 +1629,7 @@ class ChannelClientState {
true)
?.toList() ??
[],
];
newMessages.sort(_sortByCreatedAt);
]..sort(_sortByCreatedAt);
final newWatchers = <User>[
...updatedState?.watchers ?? [],
@@ -1692,10 +1694,9 @@ class ChannelClientState {
set _channelState(ChannelState v) {
_channelStateController.add(v);
if (_channel._client.persistenceEnabled) {
debounce(
timeout: Duration(milliseconds: 500),
timeout: const Duration(milliseconds: 500),
target: _channel._client.chatPersistenceClient?.updateChannelState,
positionalArguments: [v],
);
@@ -1735,28 +1736,37 @@ class ChannelClientState {
return;
}
_subscriptions.add(_channel.on(EventType.typingStart).listen((event) {
if (event.user.id != _channel.client.state.user.id) {
_typings[event.user] = DateTime.now();
_typingEventsController.add(_typings.keys.toList());
}
}));
_subscriptions.add(_channel.on(EventType.typingStop).listen((event) {
if (event.user.id != _channel.client.state.user.id) {
_typings.remove(event.user);
_typingEventsController.add(_typings.keys.toList());
}
}));
_subscriptions
..add(
_channel.on(EventType.typingStart).listen(
(event) {
if (event.user.id != _channel.client.state.user.id) {
_typings[event.user] = DateTime.now();
_typingEventsController.add(_typings.keys.toList());
}
},
),
)
..add(
_channel.on(EventType.typingStop).listen(
(event) {
if (event.user.id != _channel.client.state.user.id) {
_typings.remove(event.user);
_typingEventsController.add(_typings.keys.toList());
}
},
),
);
}
Timer _cleaningTimer;
void _startCleaning() {
if (_channel.config?.typingEvents == false) {
return;
}
_cleaningTimer = Timer.periodic(Duration(seconds: 1), (_) {
_cleaningTimer = Timer.periodic(const Duration(seconds: 1), (_) {
final now = DateTime.now();
if (_channel._lastTypingEvent != null &&
@@ -1769,8 +1779,9 @@ class ChannelClientState {
}
Timer _pinnedMessagesTimer;
void _startCleaningPinnedMessages() {
_pinnedMessagesTimer = Timer.periodic(Duration(seconds: 30), (_) {
_pinnedMessagesTimer = Timer.periodic(const Duration(seconds: 30), (_) {
final now = DateTime.now();
var expiredMessages = channelState.pinnedMessages
?.where((m) => m.pinExpires?.isBefore(now) == true)
@@ -1781,8 +1792,6 @@ class ChannelClientState {
.map((m) => m.copyWith(
pinExpires: null,
pinned: false,
pinnedAt: null,
pinnedBy: null,
))
.toList();
+37 -35
View File
@@ -5,22 +5,6 @@ part 'requests.g.dart';
/// Sorting options
@JsonSerializable(createFactory: false)
class SortOption<T> {
/// Ascending order
static const ASC = 1;
/// Descending order
static const DESC = -1;
/// A sorting field name
final String field;
/// A sorting direction
final int direction;
/// Sorting field Comparator required for offline sorting
@JsonKey(ignore: true)
final Comparator<T> comparator;
/// Creates a new SortOption instance
///
/// For example:
@@ -34,6 +18,24 @@ class SortOption<T> {
this.comparator,
});
/// Ascending order
// ignore: constant_identifier_names
static const ASC = 1;
/// Descending order
// ignore: constant_identifier_names
static const DESC = -1;
/// A sorting field name
final String field;
/// A sorting direction
final int direction;
/// Sorting field Comparator required for offline sorting
@JsonKey(ignore: true)
final Comparator<T> comparator;
/// Serialize model to json
Map<String, dynamic> toJson() => _$SortOptionToJson(this);
}
@@ -41,6 +43,25 @@ class SortOption<T> {
/// Pagination options.
@JsonSerializable(createFactory: false, includeIfNull: false)
class PaginationParams {
/// Creates a new PaginationParams instance
///
/// For example:
/// ```dart
/// // limit to 50
/// final paginationParams = PaginationParams(limit: 50);
///
/// // limit to 50 with offset
/// final paginationParams = PaginationParams(limit: 50, offset: 50);
/// ```
const PaginationParams({
this.limit = 10,
this.offset = 0,
this.greaterThan,
this.greaterThanOrEqual,
this.lessThan,
this.lessThanOrEqual,
});
/// The amount of items requested from the APIs.
final int limit;
@@ -63,25 +84,6 @@ class PaginationParams {
@JsonKey(name: 'id_lte')
final String lessThanOrEqual;
/// Creates a new PaginationParams instance
///
/// For example:
/// ```dart
/// // limit to 50
/// final paginationParams = PaginationParams(limit: 50);
///
/// // limit to 50 with offset
/// final paginationParams = PaginationParams(limit: 50, offset: 50);
/// ```
const PaginationParams({
this.limit = 10,
this.offset = 0,
this.greaterThan,
this.greaterThanOrEqual,
this.lessThan,
this.lessThanOrEqual,
});
/// Serialize model to json
Map<String, dynamic> toJson() => _$PaginationParamsToJson(this);
@@ -1,15 +1,14 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:stream_chat/src/client.dart';
import 'package:stream_chat/src/models/channel_model.dart';
import 'package:stream_chat/src/models/channel_state.dart';
import 'package:stream_chat/src/models/device.dart';
import 'package:stream_chat/src/models/event.dart';
import '../models/channel_model.dart';
import '../models/channel_state.dart';
import '../models/member.dart';
import '../models/message.dart';
import '../models/reaction.dart';
import '../models/read.dart';
import '../models/user.dart';
import 'package:stream_chat/src/models/member.dart';
import 'package:stream_chat/src/models/message.dart';
import 'package:stream_chat/src/models/reaction.dart';
import 'package:stream_chat/src/models/read.dart';
import 'package:stream_chat/src/models/user.dart';
part 'responses.g.dart';
@@ -18,7 +18,8 @@ class RetryPolicy {
final bool Function(StreamChatClient client, int attempt, ApiError apiError)
shouldRetry;
/// In the case that we want to retry a failed request the retryTimeout method is called to determine the timeout
/// In the case that we want to retry a failed request the retryTimeout
/// method is called to determine the timeout
final Duration Function(
StreamChatClient client, int attempt, ApiError apiError) retryTimeout;
@@ -1,8 +1,8 @@
import 'dart:async';
import 'package:collection/collection.dart';
import 'package:meta/meta.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:stream_chat/src/api/channel.dart';
import 'package:stream_chat/src/api/retry_policy.dart';
import 'package:stream_chat/src/event_type.dart';
@@ -12,12 +12,6 @@ import 'package:stream_chat/stream_chat.dart';
/// The retry queue associated to a channel
class RetryQueue {
/// The channel of this queue
final Channel channel;
/// The logger associated to this queue
final Logger logger;
/// Instantiate a new RetryQueue object
RetryQueue({
@required this.channel,
@@ -30,6 +24,12 @@ class RetryQueue {
_listenFailedEvents();
}
/// The channel of this queue
final Channel channel;
/// The logger associated to this queue
final Logger logger;
final _subscriptions = <StreamSubscription>[];
void _listenConnectionRecovered() {
+39 -38
View File
@@ -2,28 +2,29 @@ import 'dart:async';
import 'dart:convert';
import 'dart:math';
import 'package:meta/meta.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:rxdart/rxdart.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import '../models/event.dart';
import '../models/user.dart';
import 'connection_status.dart';
import 'web_socket_channel_stub.dart'
import 'package:stream_chat/src/api/connection_status.dart';
import 'package:stream_chat/src/api/web_socket_channel_stub.dart'
if (dart.library.html) 'web_socket_channel_html.dart'
if (dart.library.io) 'web_socket_channel_io.dart';
import 'package:stream_chat/src/models/event.dart';
import 'package:stream_chat/src/models/user.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
/// Typedef which exposes an [Event] as the only parameter.
typedef EventHandler = void Function(Event);
/// Typedef used for connecting to a websocket. Method returns a [WebSocketChannel]
/// and accepts a connection [url] and an optional [Iterable] of `protocols`.
/// Typedef used for connecting to a websocket. Method returns a
/// [WebSocketChannel] and accepts a connection [url] and an optional
/// [Iterable] of `protocols`.
typedef ConnectWebSocket = WebSocketChannel Function(String url,
{Iterable<String> protocols});
// TODO: parse error even
// TODO: if parsing an error into an event fails we should not hide the original error
// TODO: if parsing an error into an event fails we should not hide the
// TODO: original error
/// A WebSocket connection that reconnects upon failure.
class WebSocket {
/// Creates a new websocket
@@ -75,7 +76,8 @@ class WebSocket {
/// WS connection payload
final Map<String, dynamic> connectPayload;
/// Functions that will be called every time a new event is received from the connection
/// Functions that will be called every time a new event is received from the
/// connection
final EventHandler handler;
/// A WS specific logger instance
@@ -87,8 +89,9 @@ class WebSocket {
final ConnectWebSocket connectFunc;
/// Interval of the reconnection monitor timer
/// This checks that it received a new event in the last [reconnectionMonitorTimeout] seconds,
/// otherwise it considers the connection unhealthy and reconnects the WS
/// This checks that it received a new event in the last
/// [reconnectionMonitorTimeout] seconds, otherwise it considers the
/// connection unhealthy and reconnects the WS
final int reconnectionMonitorInterval;
/// Interval of the health event sending timer
@@ -96,7 +99,8 @@ class WebSocket {
/// make the server aware that the client is still listening
final int healthCheckInterval;
/// The timeout that uses the reconnection monitor timer to consider the connection unhealthy
/// The timeout that uses the reconnection monitor timer to consider the
/// connection unhealthy
final int reconnectionMonitorTimeout;
final _connectionStatusController =
@@ -121,9 +125,7 @@ class WebSocket {
_connecting = false,
_reconnecting = false;
Event _decodeEvent(String source) {
return Event.fromJson(json.decode(source));
}
Event _decodeEvent(String source) => Event.fromJson(json.decode(source));
Completer<Event> _connectionCompleter = Completer<Event>();
@@ -166,8 +168,8 @@ class WebSocket {
return;
}
logger.info(
'connection closed | closeCode: ${_channel.closeCode} | closedReason: ${_channel.closeReason}');
logger.info('connection closed | closeCode: ${_channel.closeCode} | '
'closedReason: ${_channel.closeReason}');
if (!_reconnecting) {
_reconnect();
@@ -200,8 +202,7 @@ class WebSocket {
}
Future<void> _onConnectionError(error, [stacktrace]) async {
logger.severe('error connecting');
logger.severe(error);
logger..severe('error connecting')..severe(error);
if (stacktrace != null) {
logger.severe(stacktrace);
}
@@ -219,21 +220,21 @@ class WebSocket {
}
}
void _startReconnectionMonitor() {
final reconnectionTimer = (_) {
final now = DateTime.now();
if (_lastEventAt != null &&
now.difference(_lastEventAt).inSeconds > reconnectionMonitorTimeout) {
_channel.sink.close();
}
};
void _reconnectionTimer(_) {
final now = DateTime.now();
if (_lastEventAt != null &&
now.difference(_lastEventAt).inSeconds > reconnectionMonitorTimeout) {
_channel.sink.close();
}
}
void _startReconnectionMonitor() {
_reconnectionMonitor = Timer.periodic(
Duration(seconds: reconnectionMonitorInterval),
reconnectionTimer,
_reconnectionTimer,
);
reconnectionTimer(_reconnectionMonitor);
_reconnectionTimer(_reconnectionMonitor);
}
void _reconnectTimer() async {
@@ -283,20 +284,20 @@ class WebSocket {
}
}
void _healthCheckTimer(_) {
logger.info('sending health.check');
_channel.sink.add("{'type': 'health.check'}");
}
void _startHealthCheck() {
logger.info('start health check monitor');
final healthCheckTimer = (_) {
logger.info('sending health.check');
_channel.sink.add("{'type': 'health.check'}");
};
_healthCheck = Timer.periodic(
Duration(seconds: healthCheckInterval),
healthCheckTimer,
_healthCheckTimer,
);
healthCheckTimer(_healthCheck);
_healthCheckTimer(_healthCheck);
}
/// Disconnects the WS and releases eventual resources
@@ -1,8 +1,8 @@
import 'package:dio/dio.dart';
import 'package:stream_chat/src/api/responses.dart';
import 'package:stream_chat/src/client.dart';
import 'package:stream_chat/src/models/attachment_file.dart';
import 'client.dart';
import 'extensions/string_extension.dart';
import 'package:stream_chat/src/extensions/string_extension.dart';
/// Class responsible for uploading images and files to a given channel
abstract class AttachmentFileUploader {
@@ -57,11 +57,11 @@ abstract class AttachmentFileUploader {
/// Stream's default implementation of [AttachmentFileUploader]
class StreamAttachmentFileUploader implements AttachmentFileUploader {
final StreamChatClient _client;
/// Creates a new [StreamAttachmentFileUploader] instance.
const StreamAttachmentFileUploader(this._client);
final StreamChatClient _client;
@override
Future<SendImageResponse> sendImage(
AttachmentFile file,
+142 -129
View File
@@ -1,36 +1,35 @@
import 'dart:async';
import 'dart:convert';
import 'package:stream_chat/src/extensions/map_extension.dart';
import 'package:dio/dio.dart';
import 'package:logging/logging.dart';
import 'package:meta/meta.dart';
import 'package:pedantic/pedantic.dart' show unawaited;
import 'package:rxdart/rxdart.dart';
import 'package:stream_chat/src/api/channel.dart';
import 'package:stream_chat/src/api/connection_status.dart';
import 'package:stream_chat/src/api/requests.dart';
import 'package:stream_chat/src/api/responses.dart';
import 'package:stream_chat/src/api/retry_policy.dart';
import 'package:stream_chat/src/api/websocket.dart';
import 'package:stream_chat/src/attachment_file_uploader.dart';
import 'package:stream_chat/src/db/chat_persistence_client.dart';
import 'package:stream_chat/src/event_type.dart';
import 'package:stream_chat/src/exceptions.dart';
import 'package:stream_chat/src/models/attachment_file.dart';
import 'package:stream_chat/src/models/channel_model.dart';
import 'package:stream_chat/src/models/channel_state.dart';
import 'package:stream_chat/src/models/event.dart';
import 'package:stream_chat/src/models/message.dart';
import 'package:stream_chat/src/models/own_user.dart';
import 'package:stream_chat/src/models/user.dart';
import 'package:stream_chat/src/platform_detector/platform_detector.dart';
import 'package:stream_chat/version.dart';
import 'package:uuid/uuid.dart';
import 'attachment_file_uploader.dart';
import 'api/channel.dart';
import 'api/connection_status.dart';
import 'api/requests.dart';
import 'api/responses.dart';
import 'api/websocket.dart';
import 'db/chat_persistence_client.dart';
import 'exceptions.dart';
import 'models/channel_state.dart';
import 'models/event.dart';
import 'models/message.dart';
import 'models/user.dart';
import 'extensions/map_extension.dart';
/// Handler function used for logging records. Function requires a single [LogRecord]
/// as the only parameter.
/// Handler function used for logging records. Function requires a single
/// [LogRecord] as the only parameter.
typedef LogHandlerFunction = void Function(LogRecord record);
/// Used for decoding [Map] data to a generic type `T`.
@@ -62,7 +61,9 @@ extension on PushProvider {
/// The official Dart client for Stream Chat,
/// a service for building chat applications.
/// This library can be used on any Dart project and on both mobile and web apps with Flutter.
/// This library can be used on any Dart project and on both mobile and web apps
/// with Flutter.
///
/// You can sign up for a Stream account at https://getstream.io/chat/
///
/// The Chat client will manage API call, event handling and manage the
@@ -73,7 +74,8 @@ extension on PushProvider {
/// ```
class StreamChatClient {
/// Create a client instance with default options.
/// You should only create the client once and re-use it across your application.
/// You should only create the client once and re-use it across your
/// application.
StreamChatClient(
this.apiKey, {
this.tokenProvider,
@@ -83,6 +85,7 @@ class StreamChatClient {
Duration connectTimeout = const Duration(seconds: 6),
Duration receiveTimeout = const Duration(seconds: 6),
Dio httpClient,
// ignore: avoid_unused_constructor_parameters
RetryPolicy retryPolicy,
this.attachmentFileUploader,
}) {
@@ -122,30 +125,39 @@ class StreamChatClient {
/// This client state
ClientState state;
/// By default the Chat client will write all messages with level Warn or Error to stdout.
/// During development you might want to enable more logging information, you can change the default log level when constructing the client.
/// By default the Chat client will write all messages with level Warn or
/// Error to stdout.
///
/// During development you might want to enable more logging information,
/// you can change the default log level when constructing the client.
///
/// ```dart
/// final client = StreamChatClient("stream-chat-api-key", logLevel: Level.INFO);
/// final client = StreamChatClient("stream-chat-api-key",
/// logLevel: Level.INFO);
/// ```
final Level logLevel;
/// Client specific logger instance.
/// Refer to the class [Logger] to learn more about the specific implementation.
/// Refer to the class [Logger] to learn more about the specific
/// implementation.
final Logger logger = Logger.detached('📡');
/// A function that has a parameter of type [LogRecord].
/// This is called on every new log record.
/// By default the client will use the handler returned by [_getDefaultLogHandler].
/// Setting it you can handle the log messages directly instead of have them written to stdout,
/// this is very convenient if you use an error tracking tool or if you want to centralize your logs into one facility.
/// By default the client will use the handler returned by
/// [_getDefaultLogHandler].
/// Setting it you can handle the log messages directly instead of have them
/// written to stdout,
/// this is very convenient if you use an error tracking tool or if you want
/// to centralize your logs into one facility.
///
/// ```dart
/// myLogHandlerFunction = (LogRecord record) {
/// // do something with the record (ie. send it to Sentry or Fabric)
/// }
///
/// final client = StreamChatClient("stream-chat-api-key", logHandlerFunction: myLogHandlerFunction);
/// final client = StreamChatClient("stream-chat-api-key",
/// logHandlerFunction: myLogHandlerFunction);
///```
LogHandlerFunction logHandlerFunction;
@@ -156,14 +168,18 @@ class StreamChatClient {
/// Your project Stream Chat base url.
final String baseURL;
/// A function in which you send a request to your own backend to get a Stream Chat API token.
/// A function in which you send a request to your own backend to get a Stream
/// Chat API token.
///
/// The token will be the return value of the function.
/// It's used by the client to refresh the token once expired or to connect the user without a predefined token using [connectUserWithProvider].
/// It's used by the client to refresh the token once expired or to connect
/// the user without a predefined token using [connectUserWithProvider].
final TokenProvider tokenProvider;
/// [Dio] httpClient
/// It's be chosen because it's easy to use and supports interesting features out of the box
/// (Interceptors, Global configuration, FormData, File downloading etc.)
/// It's be chosen because it's easy to use and supports interesting features
/// out of the box (Interceptors, Global configuration, FormData,
/// File downloading etc.)
@visibleForTesting
Dio httpClient = Dio();
@@ -234,7 +250,7 @@ class StreamChatClient {
(options.data is Map || options.data == null)) {
options.data = {
'connection_id': _connectionId,
...(options.data ?? {}),
...options.data ?? {},
};
}
@@ -278,7 +294,7 @@ class StreamChatClient {
await _disconnect();
final newToken = await tokenProvider(userId);
await Future.delayed(Duration(seconds: 4));
await Future.delayed(const Duration(seconds: 4));
token = newToken;
httpClient.unlock();
@@ -312,7 +328,10 @@ class StreamChatClient {
};
return (LogRecord record) {
print(
'(${record.time}) ${levelEmojiMapper[record.level.name] ?? record.level.name} ${record.loggerName} ${record.message}');
'(${record.time}) '
'${levelEmojiMapper[record.level.name] ?? record.level.name} '
'${record.loggerName} ${record.message}',
);
if (record.stackTrace != null) {
print(record.stackTrace);
}
@@ -321,11 +340,10 @@ class StreamChatClient {
Logger _detachedLogger(
String name,
) {
return Logger.detached(name)
..level = logLevel
..onRecord.listen(logHandlerFunction ?? _getDefaultLogHandler());
}
) =>
Logger.detached(name)
..level = logLevel
..onRecord.listen(logHandlerFunction ?? _getDefaultLogHandler());
void _setupLogger() {
logger.level = logLevel;
@@ -386,7 +404,8 @@ class StreamChatClient {
/// Set the current user using the [tokenProvider] to fetch the token.
/// It returns a [Future] that resolves when the connection is setup.
@Deprecated(
'Use `connectUserWithProvider` instead. Will be removed in Future releases')
'Use `connectUserWithProvider` instead. Will be removed in Future releases',
)
Future<Event> setUserWithProvider(User user) => connectUserWithProvider(user);
/// Connects the current user using the [tokenProvider] to fetch the token.
@@ -521,6 +540,7 @@ class StreamChatClient {
}).catchError((err, stacktrace) {
logger.severe('error connecting ws', err, stacktrace);
if (err is Map) {
// ignore: only_throw_errors
throw err;
}
});
@@ -558,13 +578,12 @@ class StreamChatClient {
res.events.sort((a, b) => a.createdAt.compareTo(b.createdAt));
res.events.forEach((element) {
logger.fine('element.type: ${element.type}');
logger.fine('element.message.text: ${element.message?.text}');
logger
..fine('element.type: ${element.type}')
..fine('element.message.text: ${element.message?.text}');
});
res.events.forEach((event) {
handleEvent(event);
});
res.events.forEach(handleEvent);
await chatPersistenceClient?.updateLastSyncAt(DateTime.now());
_synced = true;
@@ -573,9 +592,7 @@ class StreamChatClient {
}
}
String _asMap(sort) {
return sort?.map((s) => s.toJson().toString())?.join('');
}
String _asMap(sort) => sort?.map((s) => s.toJson().toString())?.join('');
final _queryChannelsStreams = <String, Future<List<Channel>>>{};
@@ -584,13 +601,14 @@ class StreamChatClient {
Map<String, dynamic> filter,
List<SortOption<ChannelModel>> sort,
Map<String, dynamic> options,
PaginationParams paginationParams = const PaginationParams(limit: 10),
PaginationParams paginationParams = const PaginationParams(),
int messageLimit,
bool preferOffline = false,
bool waitForConnect = true,
}) async* {
final hash = base64.encode(utf8.encode(
'$filter${_asMap(sort)}$options${paginationParams?.toJson()}$messageLimit$preferOffline',
'$filter${_asMap(sort)}$options${paginationParams?.toJson()}'
'$messageLimit$preferOffline',
));
if (_queryChannelsStreams.containsKey(hash)) {
@@ -627,7 +645,7 @@ class StreamChatClient {
List<SortOption<ChannelModel>> sort,
Map<String, dynamic> options,
int messageLimit,
PaginationParams paginationParams = const PaginationParams(limit: 10),
PaginationParams paginationParams = const PaginationParams(),
bool waitForConnect = true,
}) async {
if (waitForConnect) {
@@ -650,7 +668,7 @@ class StreamChatClient {
'presence': false,
};
var payload = <String, dynamic>{
final payload = <String, dynamic>{
'filter_conditions': filter,
'sort': sort,
};
@@ -682,9 +700,12 @@ class StreamChatClient {
);
if ((res.channels ?? []).isEmpty && (paginationParams?.offset ?? 0) == 0) {
logger.warning('''We could not find any channel for this query.
logger.warning(
'''
We could not find any channel for this query.
Please make sure to take a look at the Flutter tutorial: https://getstream.io/chat/flutter/tutorial
If your application already has users and channels, you might need to adjust your query channel as explained in the docs https://getstream.io/chat/docs/query_channels/?language=dart''');
If your application already has users and channels, you might need to adjust your query channel as explained in the docs https://getstream.io/chat/docs/query_channels/?language=dart''',
);
return <Channel>[];
}
@@ -715,7 +736,7 @@ class StreamChatClient {
Future<List<Channel>> queryChannelsOffline({
@required Map<String, dynamic> filter,
@required List<SortOption<ChannelModel>> sort,
PaginationParams paginationParams = const PaginationParams(limit: 10),
PaginationParams paginationParams = const PaginationParams(),
}) async {
final offlineChannels = await chatPersistenceClient?.getChannelStates(
filter: filter,
@@ -771,6 +792,7 @@ class StreamChatClient {
);
return response;
} on DioError catch (error) {
// ignore: only_throw_errors
throw _parseError(error);
}
}
@@ -791,6 +813,7 @@ class StreamChatClient {
);
return response;
} on DioError catch (error) {
// ignore: only_throw_errors
throw _parseError(error);
}
}
@@ -809,6 +832,7 @@ class StreamChatClient {
);
return response;
} on DioError catch (error) {
// ignore: only_throw_errors
throw _parseError(error);
}
}
@@ -827,6 +851,7 @@ class StreamChatClient {
);
return response;
} on DioError catch (error) {
// ignore: only_throw_errors
throw _parseError(error);
}
}
@@ -845,6 +870,7 @@ class StreamChatClient {
);
return response;
} on DioError catch (error) {
// ignore: only_throw_errors
throw _parseError(error);
}
}
@@ -864,8 +890,8 @@ class StreamChatClient {
String get _authType => _anonymous ? 'anonymous' : 'jwt';
String get _userAgent =>
'stream-chat-dart-client-${CurrentPlatform.name}-${PACKAGE_VERSION.split('+')[0]}';
String get _userAgent => 'stream-chat-dart-client-${CurrentPlatform.name}-'
'${PACKAGE_VERSION.split('+')[0]}';
Map<String, String> get _commonQueryParams => {
'user_id': state.user?.id,
@@ -873,14 +899,15 @@ class StreamChatClient {
'connection_id': _connectionId,
};
/// Set the current user with an anonymous id, this triggers a connection to the API.
/// It returns a [Future] that resolves when the connection is setup.
/// Set the current user with an anonymous id, this triggers a connection to
/// the API. It returns a [Future] that resolves when the connection is setup.
@Deprecated(
'Use `connectAnonymousUser` instead. Will be removed in Future releases')
Future<Event> setAnonymousUser() => connectAnonymousUser();
/// Connects the current user with an anonymous id, this triggers a connection to the API.
/// It returns a [Future] that resolves when the connection is setup.
/// Connects the current user with an anonymous id, this triggers a connection
/// to the API. It returns a [Future] that resolves when the connection is
/// setup.
Future<Event> connectAnonymousUser() async {
if (_connectCompleter != null && !_connectCompleter.isCompleted) {
logger.warning('Already connecting');
@@ -923,14 +950,14 @@ class StreamChatClient {
}
/// Closes the websocket connection and resets the client
/// If [flushChatPersistence] is true the client deletes all offline user's data
/// If [clearUser] is true the client unsets the current user
/// If [flushChatPersistence] is true the client deletes all offline
/// user's data. If [clearUser] is true the client unsets the current user
Future<void> disconnect({
bool flushChatPersistence = false,
bool clearUser = false,
}) async {
logger.info(
'Disconnecting flushOfflineStorage: $flushChatPersistence; clearUser: $clearUser');
logger.info('Disconnecting flushOfflineStorage: $flushChatPersistence; '
'clearUser: $clearUser');
await chatPersistenceClient?.disconnect(flush: flushChatPersistence);
chatPersistenceClient = null;
@@ -966,9 +993,7 @@ class StreamChatClient {
final payload = <String, dynamic>{
'filter_conditions': filter ?? {},
'sort': sort,
};
payload.addAll(defaultOptions);
}..addAll(defaultOptions);
if (pagination != null) {
payload.addAll(pagination.toJson());
@@ -1016,7 +1041,7 @@ class StreamChatClient {
);
}
return true;
}());
}(), 'Check incoming params.');
final payload = {
'filter_conditions': filters,
@@ -1041,15 +1066,14 @@ class StreamChatClient {
String channelType, {
ProgressCallback onSendProgress,
CancelToken cancelToken,
}) {
return attachmentFileUploader.sendFile(
file,
channelId,
channelType,
onSendProgress: onSendProgress,
cancelToken: cancelToken,
);
}
}) =>
attachmentFileUploader.sendFile(
file,
channelId,
channelType,
onSendProgress: onSendProgress,
cancelToken: cancelToken,
);
/// Send a [image] to the [channelId] of type [channelType]
Future<SendImageResponse> sendImage(
@@ -1058,15 +1082,14 @@ class StreamChatClient {
String channelType, {
ProgressCallback onSendProgress,
CancelToken cancelToken,
}) {
return attachmentFileUploader.sendImage(
image,
channelId,
channelType,
onSendProgress: onSendProgress,
cancelToken: cancelToken,
);
}
}) =>
attachmentFileUploader.sendImage(
image,
channelId,
channelType,
onSendProgress: onSendProgress,
cancelToken: cancelToken,
);
/// Delete a file from this channel
Future<EmptyResponse> deleteFile(
@@ -1074,14 +1097,13 @@ class StreamChatClient {
String channelId,
String channelType, {
CancelToken cancelToken,
}) {
return attachmentFileUploader.deleteFile(
url,
channelId,
channelType,
cancelToken: cancelToken,
);
}
}) =>
attachmentFileUploader.deleteFile(
url,
channelId,
channelType,
cancelToken: cancelToken,
);
/// Delete an image from this channel
Future<EmptyResponse> deleteImage(
@@ -1089,14 +1111,13 @@ class StreamChatClient {
String channelId,
String channelType, {
CancelToken cancelToken,
}) {
return attachmentFileUploader.deleteImage(
url,
channelId,
channelType,
cancelToken: cancelToken,
);
}
}) =>
attachmentFileUploader.deleteImage(
url,
channelId,
channelType,
cancelToken: cancelToken,
);
/// Add a device for Push Notifications.
Future<EmptyResponse> addDevice(String id, PushProvider pushProvider) async {
@@ -1146,9 +1167,8 @@ class StreamChatClient {
}
/// Update or Create the given user object.
Future<UpdateUsersResponse> updateUser(User user) async {
return updateUsers([user]);
}
Future<UpdateUsersResponse> updateUser(User user) async =>
updateUsers([user]);
/// Batch update a list of users
Future<UpdateUsersResponse> updateUsers(List<User> users) async {
@@ -1197,23 +1217,21 @@ class StreamChatClient {
Future<EmptyResponse> shadowBan(
String targetID, [
Map<String, dynamic> options = const {},
]) async {
return banUser(targetID, {
'shadow': true,
...options,
});
}
]) async =>
banUser(targetID, {
'shadow': true,
...options,
});
/// Removes shadow ban from a user
Future<EmptyResponse> removeShadowBan(
String targetID, [
Map<String, dynamic> options = const {},
]) async {
return unbanUser(targetID, {
'shadow': true,
...options,
});
}
]) async =>
unbanUser(targetID, {
'shadow': true,
...options,
});
/// Mutes a user
Future<EmptyResponse> muteUser(String targetID) async {
@@ -1312,7 +1330,7 @@ class StreamChatClient {
throw ArgumentError('Invalid timeout or Expiration date');
}
return true;
}());
}(), 'Check whether time out is valid');
DateTime pinExpires;
if (timeoutOrExpirationDate is DateTime) {
@@ -1328,15 +1346,12 @@ class StreamChatClient {
}
/// Unpins provided message
Future<UpdateMessageResponse> unpinMessage(Message message) {
return updateMessage(message.copyWith(pinned: false));
}
Future<UpdateMessageResponse> unpinMessage(Message message) =>
updateMessage(message.copyWith(pinned: false));
}
/// The class that handles the state of the channel listening to the events
class ClientState {
final _subscriptions = <StreamSubscription>[];
/// Creates a new instance listening to events and updating the state
ClientState(this._client) {
_subscriptions.addAll([
@@ -1358,16 +1373,12 @@ class ClientState {
.on()
.where((event) => event.unreadChannels != null)
.map((e) => e.unreadChannels)
.listen((unreadChannels) {
_unreadChannelsController.add(unreadChannels);
}),
.listen(_unreadChannelsController.add),
_client
.on()
.where((event) => event.totalUnreadCount != null)
.map((e) => e.totalUnreadCount)
.listen((totalUnreadCount) {
_totalUnreadCountController.add(totalUnreadCount);
}),
.listen(_totalUnreadCountController.add),
]);
_listenChannelDeleted();
@@ -1377,6 +1388,8 @@ class ClientState {
_listenUserUpdated();
}
final _subscriptions = <StreamSubscription>[];
/// Used internally for optimistic update of unread count
set totalUnreadCount(int unreadCount) {
_totalUnreadCountController?.add(unreadCount ?? 0);
@@ -101,18 +101,17 @@ abstract class ChatPersistenceClient {
Future<void> updateChannelQueries(
Map<String, dynamic> filter,
List<String> cids,
// ignore: avoid_positional_boolean_parameters
bool clearQueryCache,
);
/// Remove a message by [messageId]
Future<void> deleteMessageById(String messageId) {
return deleteMessageByIds([messageId]);
}
Future<void> deleteMessageById(String messageId) =>
deleteMessageByIds([messageId]);
/// Remove a pinned message by [messageId]
Future<void> deletePinnedMessageById(String messageId) {
return deletePinnedMessageByIds([messageId]);
}
Future<void> deletePinnedMessageById(String messageId) =>
deletePinnedMessageByIds([messageId]);
/// Remove a message by [messageIds]
Future<void> deleteMessageByIds(List<String> messageIds);
@@ -121,14 +120,11 @@ abstract class ChatPersistenceClient {
Future<void> deletePinnedMessageByIds(List<String> messageIds);
/// Remove a message by channel [cid]
Future<void> deleteMessageByCid(String cid) {
return deleteMessageByCids([cid]);
}
Future<void> deleteMessageByCid(String cid) => deleteMessageByCids([cid]);
/// Remove a pinned message by channel [cid]
Future<void> deletePinnedMessageByCid(String cid) {
return deletePinnedMessageByCids([cid]);
}
Future<void> deletePinnedMessageByCid(String cid) async =>
deletePinnedMessageByCids([cid]);
/// Remove a message by message [cids]
Future<void> deleteMessageByCids(List<String> cids);
@@ -175,9 +171,8 @@ abstract class ChatPersistenceClient {
Future<void> deleteMembersByCids(List<String> cids);
/// Update the channel state data using [channelState]
Future<void> updateChannelState(ChannelState channelState) {
return updateChannelStates([channelState]);
}
Future<void> updateChannelState(ChannelState channelState) =>
updateChannelStates([channelState]);
/// Update list of channel states
Future<void> updateChannelStates(List<ChannelState> channelStates) async {
@@ -195,31 +190,31 @@ abstract class ChatPersistenceClient {
deleteMembers,
]);
final channels = channelStates.map((it) {
return it.channel;
}).where((it) => it != null);
final channels =
channelStates.map((it) => it.channel).where((it) => it != null);
final reactions = channelStates.expand((it) => it.messages).expand((it) {
return [
if (it.ownReactions != null)
...it.ownReactions.where((r) => r.userId != null),
if (it.latestReactions != null)
...it.latestReactions.where((r) => r.userId != null)
];
}).where((it) => it != null);
final reactions = channelStates
.expand((it) => it.messages)
.expand((it) => [
if (it.ownReactions != null)
...it.ownReactions.where((r) => r.userId != null),
if (it.latestReactions != null)
...it.latestReactions.where((r) => r.userId != null)
])
.where((it) => it != null);
final users = channelStates
.map((cs) => [
cs.channel?.createdBy,
...cs.messages?.map((m) {
return [
m.user,
if (m.latestReactions != null)
...m.latestReactions.map((r) => r.user),
if (m.ownReactions != null)
...m.ownReactions.map((r) => r.user),
];
})?.expand((v) => v),
...cs.messages
?.map((m) => [
m.user,
if (m.latestReactions != null)
...m.latestReactions.map((r) => r.user),
if (m.ownReactions != null)
...m.ownReactions.map((r) => r.user),
])
?.expand((v) => v),
if (cs.read != null) ...cs.read.map((r) => r.user),
if (cs.members != null) ...cs.members.map((m) => m.user),
])
+9 -10
View File
@@ -2,6 +2,13 @@ import 'dart:convert';
/// Exception related to api calls
class ApiError extends Error {
/// Creates a new ApiError instance using the response body and status code
ApiError(this.body, this.status) : jsonData = _decode(body) {
if (jsonData != null && jsonData.containsKey('code')) {
_code = jsonData['code'];
}
}
/// Raw body of the response
final String body;
@@ -26,13 +33,6 @@ class ApiError extends Error {
}
}
/// Creates a new ApiError instance using the response body and status code
ApiError(this.body, this.status) : jsonData = _decode(body) {
if (jsonData != null && jsonData.containsKey('code')) {
_code = jsonData['code'];
}
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
@@ -48,7 +48,6 @@ class ApiError extends Error {
body.hashCode ^ jsonData.hashCode ^ status.hashCode ^ _code.hashCode;
@override
String toString() {
return 'ApiError{body: $body, jsonData: $jsonData, status: $status, code: $_code}';
}
String toString() => 'ApiError{body: $body, jsonData: $jsonData, '
'status: $status, code: $_code}';
}
@@ -1,7 +1,6 @@
/// Useful extension functions for [Map]
extension MapX on Map {
/// Returns a new map with null keys or values removed
Map<String, dynamic> get nullProtected {
return {...this}..removeWhere((key, value) => key == null || value == null);
}
Map<String, dynamic> get nullProtected =>
{...this}..removeWhere((key, value) => key == null || value == null);
}
@@ -5,6 +5,12 @@ part 'action.g.dart';
/// The class that contains the information about an action
@JsonSerializable()
class Action {
/// Constructor used for json serialization
Action({this.name, this.style, this.text, this.type, this.value});
/// Create a new instance from a json
factory Action.fromJson(Map<String, dynamic> json) => _$ActionFromJson(json);
/// The name of the action
final String name;
@@ -20,12 +26,6 @@ class Action {
/// The value of the action
final String value;
/// Constructor used for json serialization
Action({this.name, this.style, this.text, this.type, this.value});
/// Create a new instance from a json
factory Action.fromJson(Map<String, dynamic> json) => _$ActionFromJson(json);
/// Serialize to json
Map<String, dynamic> toJson() => _$ActionToJson(this);
}
@@ -1,18 +1,61 @@
// ignore_for_file: public_member_api_docs
import 'package:json_annotation/json_annotation.dart';
import 'package:stream_chat/src/models/action.dart';
import 'package:stream_chat/src/models/attachment_file.dart';
import 'package:stream_chat/src/models/serialization.dart';
import 'package:uuid/uuid.dart';
import 'action.dart';
import 'serialization.dart';
part 'attachment.g.dart';
/// The class that contains the information about an attachment
@JsonSerializable(includeIfNull: false)
class Attachment {
///The attachment type based on the URL resource. This can be: audio, image or video
/// Constructor used for json serialization
Attachment({
String id,
this.type,
this.titleLink,
String title,
this.thumbUrl,
this.text,
this.pretext,
this.ogScrapeUrl,
this.imageUrl,
this.footerIcon,
this.footer,
this.fields,
this.fallback,
this.color,
this.authorName,
this.authorLink,
this.authorIcon,
this.assetUrl,
this.actions,
this.extraData,
this.file,
UploadState uploadState,
}) : id = id ?? Uuid().v4(),
title = title ?? file?.name,
localUri = file?.path != null ? Uri.parse(file.path) : null {
this.uploadState = uploadState ??
((assetUrl != null || imageUrl != null)
? const UploadState.success()
: const UploadState.preparing());
}
/// Create a new instance from a json
factory Attachment.fromJson(Map<String, dynamic> json) =>
_$AttachmentFromJson(
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
/// Create a new instance from a db data
factory Attachment.fromData(Map<String, dynamic> json) =>
_$AttachmentFromJson(Serialization.moveToExtraDataFromRoot(
json, topLevelFields + dbSpecificTopLevelFields));
///The attachment type based on the URL resource. This can be: audio,
///image or video
final String type;
///The link to which the attachment message points to.
@@ -21,10 +64,12 @@ class Attachment {
/// The attachment title
final String title;
/// The URL to the attached file thumbnail. You can use this to represent the attached link.
/// The URL to the attached file thumbnail. You can use this to represent the
/// attached link.
final String thumbUrl;
/// The attachment text. It will be displayed in the channel next to the original message.
/// The attachment text. It will be displayed in the channel next to the
/// original message.
final String text;
/// Optional text that appears above the attachment block
@@ -33,7 +78,8 @@ class Attachment {
/// The original URL that was used to scrape this attachment.
final String ogScrapeUrl;
/// The URL to the attached image. This is present for URL pointing to an image article (eg. Unsplash)
/// The URL to the attached image. This is present for URL pointing to an
/// image article (eg. Unsplash)
final String imageUrl;
final String footerIcon;
final String footer;
@@ -100,56 +146,11 @@ class Attachment {
'file',
];
/// Constructor used for json serialization
Attachment({
String id,
this.type,
this.titleLink,
String title,
this.thumbUrl,
this.text,
this.pretext,
this.ogScrapeUrl,
this.imageUrl,
this.footerIcon,
this.footer,
this.fields,
this.fallback,
this.color,
this.authorName,
this.authorLink,
this.authorIcon,
this.assetUrl,
this.actions,
this.extraData,
this.file,
UploadState uploadState,
}) : id = id ?? Uuid().v4(),
title = title ?? file?.name,
localUri = file?.path != null ? Uri.parse(file.path) : null {
this.uploadState = uploadState ??
((assetUrl != null || imageUrl != null)
? UploadState.success()
: UploadState.preparing());
}
/// Create a new instance from a json
factory Attachment.fromJson(Map<String, dynamic> json) {
return _$AttachmentFromJson(
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
}
/// Serialize to json
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
_$AttachmentToJson(this), topLevelFields)
..removeWhere((key, value) => dbSpecificTopLevelFields.contains(key));
/// Create a new instance from a db data
factory Attachment.fromData(Map<String, dynamic> json) {
return _$AttachmentFromJson(Serialization.moveToExtraDataFromRoot(
json, topLevelFields + dbSpecificTopLevelFields));
}
/// Serialize to db data
Map<String, dynamic> toData() => Serialization.moveFromExtraDataToRoot(
_$AttachmentToJson(this), topLevelFields + dbSpecificTopLevelFields);
@@ -1,10 +1,9 @@
import 'dart:typed_data';
import 'package:meta/meta.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:meta/meta.dart';
part 'attachment_file.freezed.dart';
part 'attachment_file.g.dart';
/// Union class to hold various [UploadState] of a attachment.
@@ -57,8 +56,12 @@ class AttachmentFile {
this.size,
});
/// The absolute path for a cached copy of this file. It can be used to create a
/// file instance with a descriptor for the given path.
/// Create a new instance from a json
factory AttachmentFile.fromJson(Map<String, dynamic> json) =>
_$AttachmentFileFromJson(json);
/// The absolute path for a cached copy of this file. It can be used to
/// create a file instance with a descriptor for the given path.
/// ```
/// final File myFile = File(platformFile.path);
/// ```
@@ -67,8 +70,8 @@ class AttachmentFile {
/// File name including its extension.
final String name;
/// Byte data for this file. Particularly useful if you want to manipulate its data
/// or easily upload to somewhere else.
/// Byte data for this file. Particularly useful if you want to manipulate
/// its data or easily upload to somewhere else.
@JsonKey(toJson: _toString, fromJson: _fromString)
final Uint8List bytes;
@@ -78,11 +81,6 @@ class AttachmentFile {
/// File extension for this file.
String get extension => name?.split('.')?.last;
/// Create a new instance from a json
factory AttachmentFile.fromJson(Map<String, dynamic> json) {
return _$AttachmentFileFromJson(json);
}
/// Serialize to json
Map<String, dynamic> toJson() => _$AttachmentFileToJson(this);
}
@@ -1,12 +1,34 @@
import 'package:json_annotation/json_annotation.dart';
import 'command.dart';
import 'package:stream_chat/src/models/command.dart';
part 'channel_config.g.dart';
/// The class that contains the information about the configuration of a channel
@JsonSerializable()
class ChannelConfig {
/// Constructor used for json serialization
ChannelConfig({
this.automod,
this.commands,
this.connectEvents,
this.createdAt,
this.updatedAt,
this.maxMessageLength,
this.messageRetention,
this.mutes,
this.name,
this.reactions,
this.readEvents,
this.replies,
this.search,
this.typingEvents,
this.uploads,
this.urlEnrichment,
});
/// Create a new instance from a json
factory ChannelConfig.fromJson(Map<String, dynamic> json) =>
_$ChannelConfigFromJson(json);
/// Moderation configuration
final String automod;
@@ -55,30 +77,6 @@ class ChannelConfig {
/// True if urls appears as attachments
final bool urlEnrichment;
/// Constructor used for json serialization
ChannelConfig({
this.automod,
this.commands,
this.connectEvents,
this.createdAt,
this.updatedAt,
this.maxMessageLength,
this.messageRetention,
this.mutes,
this.name,
this.reactions,
this.readEvents,
this.replies,
this.search,
this.typingEvents,
this.uploads,
this.urlEnrichment,
});
/// Create a new instance from a json
factory ChannelConfig.fromJson(Map<String, dynamic> json) =>
_$ChannelConfigFromJson(json);
/// Serialize to json
Map<String, dynamic> toJson() => _$ChannelConfigToJson(this);
}
@@ -1,14 +1,35 @@
import 'package:json_annotation/json_annotation.dart';
import 'channel_config.dart';
import 'serialization.dart';
import 'user.dart';
import 'package:stream_chat/src/models/channel_config.dart';
import 'package:stream_chat/src/models/serialization.dart';
import 'package:stream_chat/src/models/user.dart';
part 'channel_model.g.dart';
/// The class that contains the information about a channel
@JsonSerializable()
class ChannelModel {
/// Constructor used for json serialization
ChannelModel({
this.id,
this.type,
this.cid,
this.config,
this.createdBy,
this.frozen,
this.lastMessageAt,
this.createdAt,
this.updatedAt,
this.deletedAt,
this.memberCount,
this.extraData,
this.team,
});
/// Create a new instance from a json
factory ChannelModel.fromJson(Map<String, dynamic> json) =>
_$ChannelModelFromJson(
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
/// The id of this channel
final String id;
@@ -76,40 +97,15 @@ class ChannelModel {
'team',
];
/// Constructor used for json serialization
ChannelModel({
this.id,
this.type,
this.cid,
this.config,
this.createdBy,
this.frozen,
this.lastMessageAt,
this.createdAt,
this.updatedAt,
this.deletedAt,
this.memberCount,
this.extraData,
this.team,
});
/// Shortcut for channel name
String get name =>
extraData?.containsKey('name') == true ? extraData['name'] : cid;
/// Create a new instance from a json
factory ChannelModel.fromJson(Map<String, dynamic> json) {
return _$ChannelModelFromJson(
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
}
/// Serialize to json
Map<String, dynamic> toJson() {
return Serialization.moveFromExtraDataToRoot(
_$ChannelModelToJson(this),
topLevelFields,
);
}
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
_$ChannelModelToJson(this),
topLevelFields,
);
/// Creates a copy of [ChannelModel] with specified attributes overridden.
ChannelModel copyWith({
@@ -143,8 +139,8 @@ class ChannelModel {
team: team ?? this.team,
);
/// Returns a new [ChannelModel] that is a combination of this channelModel and the given
/// [other] channelModel.
/// Returns a new [ChannelModel] that is a combination of this channelModel
/// and the given [other] channelModel.
ChannelModel merge(ChannelModel other) {
if (other == null) return this;
return copyWith(
@@ -1,16 +1,26 @@
import 'package:json_annotation/json_annotation.dart';
import '../models/read.dart';
import '../models/user.dart';
import 'channel_model.dart';
import 'member.dart';
import 'message.dart';
import 'package:stream_chat/src/models/channel_model.dart';
import 'package:stream_chat/src/models/member.dart';
import 'package:stream_chat/src/models/message.dart';
import 'package:stream_chat/src/models/read.dart';
import 'package:stream_chat/src/models/user.dart';
part 'channel_state.g.dart';
/// The class that contains the information about a channel
@JsonSerializable()
class ChannelState {
/// Constructor used for json serialization
ChannelState({
this.channel,
this.messages = const [],
this.members = const [],
this.pinnedMessages = const [],
this.watcherCount,
this.watchers = const [],
this.read = const [],
});
/// The channel to which this state belongs
final ChannelModel channel;
@@ -32,17 +42,6 @@ class ChannelState {
/// The list of channel reads
final List<Read> read;
/// Constructor used for json serialization
ChannelState({
this.channel,
this.messages = const [],
this.members = const [],
this.pinnedMessages = const [],
this.watcherCount,
this.watchers = const [],
this.read = const [],
});
/// Create a new instance from a json
static ChannelState fromJson(Map<String, dynamic> json) =>
_$ChannelStateFromJson(json);
@@ -5,15 +5,6 @@ part 'command.g.dart';
/// The class that contains the information about a command
@JsonSerializable()
class Command {
/// The name of the command
final String name;
/// The description explaining the command
final String description;
/// The arguments of the command
final String args;
/// Constructor used for json serialization
Command({
this.name,
@@ -25,6 +16,15 @@ class Command {
factory Command.fromJson(Map<String, dynamic> json) =>
_$CommandFromJson(json);
/// The name of the command
final String name;
/// The description explaining the command
final String description;
/// The arguments of the command
final String args;
/// Serialize to json
Map<String, dynamic> toJson() => _$CommandToJson(this);
}
@@ -5,12 +5,6 @@ part 'device.g.dart';
/// The class that contains the information about a device
@JsonSerializable()
class Device {
/// The id of the device
final String id;
/// The notification push provider
final String pushProvider;
/// Constructor used for json serialization
Device({
this.id,
@@ -20,6 +14,12 @@ class Device {
/// Create a new instance from a json
factory Device.fromJson(Map<String, dynamic> json) => _$DeviceFromJson(json);
/// The id of the device
final String id;
/// The notification push provider
final String pushProvider;
/// Serialize to json
Map<String, dynamic> toJson() => _$DeviceToJson(this);
}
+44 -52
View File
@@ -4,17 +4,40 @@ import 'package:stream_chat/src/models/message.dart';
import 'package:stream_chat/src/models/serialization.dart';
import 'package:stream_chat/stream_chat.dart';
import '../event_type.dart';
import 'member.dart';
import 'own_user.dart';
import 'reaction.dart';
import 'user.dart';
part 'event.g.dart';
/// The class that contains the information about an event
@JsonSerializable()
class Event {
/// Constructor used for json serialization
Event({
this.type,
this.cid,
this.connectionId,
this.createdAt,
this.me,
this.user,
this.message,
this.totalUnreadCount,
this.unreadChannels,
this.reaction,
this.online,
this.channel,
this.member,
this.channelId,
this.channelType,
this.parentId,
this.extraData,
}) : isLocal = true;
/// Create a new instance from a json
factory Event.fromJson(Map<String, dynamic> json) =>
_$EventFromJson(Serialization.moveToExtraDataFromRoot(
json,
topLevelFields,
))
..isLocal = false;
/// The type of the event
/// [EventType] contains some predefined constant types
final String type;
@@ -71,27 +94,6 @@ class Event {
@JsonKey(includeIfNull: false)
final Map<String, dynamic> extraData;
/// Constructor used for json serialization
Event({
this.type,
this.cid,
this.connectionId,
this.createdAt,
this.me,
this.user,
this.message,
this.totalUnreadCount,
this.unreadChannels,
this.reaction,
this.online,
this.channel,
this.member,
this.channelId,
this.channelType,
this.parentId,
this.extraData,
}) : isLocal = true;
/// Known top level fields.
/// Useful for [Serialization] methods.
static final topLevelFields = [
@@ -114,15 +116,6 @@ class Event {
'is_local',
];
/// Create a new instance from a json
factory Event.fromJson(Map<String, dynamic> json) {
return _$EventFromJson(Serialization.moveToExtraDataFromRoot(
json,
topLevelFields,
))
..isLocal = false;
}
/// Serialize to json
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
_$EventToJson(this),
@@ -133,16 +126,6 @@ class Event {
/// The channel embedded in the event object
@JsonSerializable()
class EventChannel extends ChannelModel {
/// A paginated list of channel members
final List<Member> members;
/// Known top level fields.
/// Useful for [Serialization] methods.
static final topLevelFields = [
'members',
...ChannelModel.topLevelFields,
];
/// Constructor used for json serialization
EventChannel({
this.members,
@@ -174,12 +157,21 @@ class EventChannel extends ChannelModel {
);
/// Create a new instance from a json
factory EventChannel.fromJson(Map<String, dynamic> json) {
return _$EventChannelFromJson(Serialization.moveToExtraDataFromRoot(
json,
topLevelFields,
));
}
factory EventChannel.fromJson(Map<String, dynamic> json) =>
_$EventChannelFromJson(Serialization.moveToExtraDataFromRoot(
json,
topLevelFields,
));
/// A paginated list of channel members
final List<Member> members;
/// Known top level fields.
/// Useful for [Serialization] methods.
static final topLevelFields = [
'members',
...ChannelModel.topLevelFields,
];
/// Serialize to json
@override
+26 -26
View File
@@ -1,12 +1,35 @@
import 'package:json_annotation/json_annotation.dart';
import '../models/user.dart';
import 'package:stream_chat/src/models/user.dart';
part 'member.g.dart';
/// The class that contains the information about the user membership in a channel
/// The class that contains the information about the user membership
/// in a channel
@JsonSerializable()
class Member {
/// Constructor used for json serialization
Member({
this.user,
this.inviteAcceptedAt,
this.inviteRejectedAt,
this.invited,
this.role,
this.userId,
this.isModerator,
this.createdAt,
this.updatedAt,
this.banned,
this.shadowBanned,
});
/// Create a new instance from a json
factory Member.fromJson(Map<String, dynamic> json) {
final member = _$MemberFromJson(json);
return member.copyWith(
userId: member.user?.id,
);
}
/// The interested user
final User user;
@@ -40,29 +63,6 @@ class Member {
/// The last date of update
final DateTime updatedAt;
/// Constructor used for json serialization
Member({
this.user,
this.inviteAcceptedAt,
this.inviteRejectedAt,
this.invited,
this.role,
this.userId,
this.isModerator,
this.createdAt,
this.updatedAt,
this.banned,
this.shadowBanned,
});
/// Create a new instance from a json
factory Member.fromJson(Map<String, dynamic> json) {
final member = _$MemberFromJson(json);
return member.copyWith(
userId: member.user?.id,
);
}
/// Creates a copy of [Member] with specified attributes overridden.
Member copyWith({
User user,
@@ -1,11 +1,10 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:stream_chat/src/models/attachment.dart';
import 'package:stream_chat/src/models/reaction.dart';
import 'package:stream_chat/src/models/serialization.dart';
import 'package:stream_chat/src/models/user.dart';
import 'package:uuid/uuid.dart';
import 'attachment.dart';
import 'reaction.dart';
import 'serialization.dart';
import 'user.dart';
part 'message.g.dart';
class _PinExpires {
@@ -29,9 +28,11 @@ enum MessageSendingStatus {
failed,
/// Message failed to updated
// ignore: constant_identifier_names
failed_update,
/// Message failed to delete
// ignore: constant_identifier_names
failed_delete,
/// Message correctly sent
@@ -41,7 +42,45 @@ enum MessageSendingStatus {
/// The class that contains the information about a message
@JsonSerializable()
class Message {
/// The message ID. This is either created by Stream or set client side when the message is added.
/// Constructor used for json serialization
Message({
String id,
this.text,
this.type,
this.attachments,
this.mentionedUsers,
this.silent,
this.shadowed,
this.reactionCounts,
this.reactionScores,
this.latestReactions,
this.ownReactions,
this.parentId,
this.quotedMessage,
this.quotedMessageId,
this.replyCount = 0,
this.threadParticipants,
this.showInChannel,
this.command,
this.createdAt,
this.updatedAt,
this.user,
this.pinned = false,
this.pinnedAt,
DateTime pinExpires,
this.pinnedBy,
this.extraData,
this.deletedAt,
this.status = MessageSendingStatus.sent,
}) : id = id ?? Uuid().v4(),
pinExpires = pinExpires?.toUtc();
/// Create a new instance from a json
factory Message.fromJson(Map<String, dynamic> json) => _$MessageFromJson(
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
/// The message ID. This is either created by Stream or set client side when
/// the message is added.
final String id;
/// The text of this message
@@ -55,7 +94,8 @@ class Message {
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
final String type;
/// The list of attachments, either provided by the user or generated from a command or as a result of URL scraping.
/// The list of attachments, either provided by the user or generated from a
/// command or as a result of URL scraping.
@JsonKey(includeIfNull: false)
final List<Attachment> attachments;
@@ -188,43 +228,6 @@ class Message {
'pinned_by',
];
/// Constructor used for json serialization
Message({
String id,
this.text,
this.type,
this.attachments,
this.mentionedUsers,
this.silent,
this.shadowed,
this.reactionCounts,
this.reactionScores,
this.latestReactions,
this.ownReactions,
this.parentId,
this.quotedMessage,
this.quotedMessageId,
this.replyCount = 0,
this.threadParticipants,
this.showInChannel,
this.command,
this.createdAt,
this.updatedAt,
this.user,
this.pinned = false,
this.pinnedAt,
DateTime pinExpires,
this.pinnedBy,
this.extraData,
this.deletedAt,
this.status = MessageSendingStatus.sent,
}) : id = id ?? Uuid().v4(),
pinExpires = pinExpires?.toUtc();
/// Create a new instance from a json
factory Message.fromJson(Map<String, dynamic> json) => _$MessageFromJson(
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
/// Serialize to json
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
_$MessageToJson(this), topLevelFields);
@@ -267,7 +270,7 @@ class Message {
throw ArgumentError('`pinExpires` can only be set as DateTime or null');
}
return true;
}());
}(), 'Validate type for pinExpires');
return Message(
id: id ?? this.id,
text: text ?? this.text,
@@ -300,8 +303,8 @@ class Message {
);
}
/// Returns a new [Message] that is a combination of this message and the given
/// [other] message.
/// Returns a new [Message] that is a combination of this message and the
/// given [other] message.
Message merge(Message other) {
if (other == null) return this;
return copyWith(
@@ -344,6 +347,12 @@ class TranslatedMessage extends Message {
/// Constructor used for json serialization
TranslatedMessage(this.i18n);
/// Create a new instance from a json
factory TranslatedMessage.fromJson(Map<String, dynamic> json) =>
_$TranslatedMessageFromJson(
Serialization.moveToExtraDataFromRoot(json, topLevelFields),
);
/// A Map of
final Map<String, String> i18n;
@@ -354,13 +363,6 @@ class TranslatedMessage extends Message {
...Message.topLevelFields,
];
/// Create a new instance from a json
factory TranslatedMessage.fromJson(Map<String, dynamic> json) {
return _$TranslatedMessageFromJson(
Serialization.moveToExtraDataFromRoot(json, topLevelFields),
);
}
/// Serialize to json
@override
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
@@ -1,14 +1,19 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:stream_chat/src/models/channel_model.dart';
import 'serialization.dart';
import 'user.dart';
import 'package:stream_chat/src/models/serialization.dart';
import 'package:stream_chat/src/models/user.dart';
part 'mute.g.dart';
/// The class that contains the information about a muted user
@JsonSerializable()
class Mute {
/// Constructor used for json serialization
Mute({this.user, this.channel, this.createdAt, this.updatedAt});
/// Create a new instance from a json
factory Mute.fromJson(Map<String, dynamic> json) => _$MuteFromJson(json);
/// The user that performed the muting action
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
final User user;
@@ -25,12 +30,6 @@ class Mute {
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
final DateTime updatedAt;
/// Constructor used for json serialization
Mute({this.user, this.channel, this.createdAt, this.updatedAt});
/// Create a new instance from a json
factory Mute.fromJson(Map<String, dynamic> json) => _$MuteFromJson(json);
/// Serialize to json
Map<String, dynamic> toJson() => _$MuteToJson(this);
}
@@ -1,9 +1,8 @@
import 'package:json_annotation/json_annotation.dart';
import 'device.dart';
import 'mute.dart';
import 'serialization.dart';
import 'user.dart';
import 'package:stream_chat/src/models/device.dart';
import 'package:stream_chat/src/models/mute.dart';
import 'package:stream_chat/src/models/serialization.dart';
import 'package:stream_chat/src/models/user.dart';
part 'own_user.g.dart';
@@ -11,6 +10,36 @@ part 'own_user.g.dart';
/// This object can be found in [Event]
@JsonSerializable()
class OwnUser extends User {
/// Constructor used for json serialization
OwnUser({
this.devices,
this.mutes,
this.totalUnreadCount,
this.unreadChannels,
this.channelMutes,
String id,
String role,
DateTime createdAt,
DateTime updatedAt,
DateTime lastActive,
bool online,
Map<String, dynamic> extraData,
bool banned,
}) : super(
id: id,
role: role,
createdAt: createdAt,
updatedAt: updatedAt,
lastActive: lastActive,
online: online,
extraData: extraData,
banned: banned,
);
/// Create a new instance from a json
factory OwnUser.fromJson(Map<String, dynamic> json) => _$OwnUserFromJson(
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
/// List of user devices
@JsonKey(includeIfNull: false, toJson: Serialization.readOnly)
final List<Device> devices;
@@ -42,42 +71,8 @@ class OwnUser extends User {
...User.topLevelFields,
];
/// Constructor used for json serialization
OwnUser({
this.devices,
this.mutes,
this.totalUnreadCount,
this.unreadChannels,
this.channelMutes,
String id,
String role,
DateTime createdAt,
DateTime updatedAt,
DateTime lastActive,
bool online,
Map<String, dynamic> extraData,
bool banned,
}) : super(
id: id,
role: role,
createdAt: createdAt,
updatedAt: updatedAt,
lastActive: lastActive,
online: online,
extraData: extraData,
banned: banned,
);
/// Create a new instance from a json
factory OwnUser.fromJson(Map<String, dynamic> json) {
return _$OwnUserFromJson(
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
}
/// Serialize to json
@override
Map<String, dynamic> toJson() {
return Serialization.moveFromExtraDataToRoot(
_$OwnUserToJson(this), topLevelFields);
}
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
_$OwnUserToJson(this), topLevelFields);
}
@@ -1,13 +1,27 @@
import 'package:json_annotation/json_annotation.dart';
import 'serialization.dart';
import 'user.dart';
import 'package:stream_chat/src/models/serialization.dart';
import 'package:stream_chat/src/models/user.dart';
part 'reaction.g.dart';
/// The class that defines a reaction
@JsonSerializable()
class Reaction {
/// Constructor used for json serialization
Reaction({
this.messageId,
this.createdAt,
this.type,
this.user,
String userId,
this.score,
this.extraData,
}) : userId = userId ?? user?.id;
/// Create a new instance from a json
factory Reaction.fromJson(Map<String, dynamic> json) => _$ReactionFromJson(
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
/// The messageId to which the reaction belongs
final String messageId;
@@ -43,28 +57,9 @@ class Reaction {
'score',
];
/// Constructor used for json serialization
Reaction({
this.messageId,
this.createdAt,
this.type,
this.user,
String userId,
this.score,
this.extraData,
}) : userId = userId ?? user?.id;
/// Create a new instance from a json
factory Reaction.fromJson(Map<String, dynamic> json) {
return _$ReactionFromJson(
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
}
/// Serialize to json
Map<String, dynamic> toJson() {
return Serialization.moveFromExtraDataToRoot(
_$ReactionToJson(this), topLevelFields);
}
Map<String, dynamic> toJson() => Serialization.moveFromExtraDataToRoot(
_$ReactionToJson(this), topLevelFields);
/// Creates a copy of [Reaction] with specified attributes overridden.
Reaction copyWith({
@@ -75,20 +70,19 @@ class Reaction {
String userId,
int score,
Map<String, dynamic> extraData,
}) {
return Reaction(
messageId: messageId ?? this.messageId,
createdAt: createdAt ?? this.createdAt,
type: type ?? this.type,
user: user ?? this.user,
userId: userId ?? this.userId,
score: score ?? this.score,
extraData: extraData ?? this.extraData,
);
}
}) =>
Reaction(
messageId: messageId ?? this.messageId,
createdAt: createdAt ?? this.createdAt,
type: type ?? this.type,
user: user ?? this.user,
userId: userId ?? this.userId,
score: score ?? this.score,
extraData: extraData ?? this.extraData,
);
/// Returns a new [Reaction] that is a combination of this reaction and the given
/// [other] reaction.
/// Returns a new [Reaction] that is a combination of this reaction and the
/// given [other] reaction.
Reaction merge(Reaction other) {
if (other == null) return this;
return copyWith(
+10 -11
View File
@@ -1,21 +1,11 @@
import 'package:json_annotation/json_annotation.dart';
import 'user.dart';
import 'package:stream_chat/src/models/user.dart';
part 'read.g.dart';
/// The class that defines a read event
@JsonSerializable()
class Read {
/// Date of the read event
final DateTime lastRead;
/// User who sent the event
final User user;
/// Number of unread messages
final int unreadMessages;
/// Constructor used for json serialization
Read({
this.lastRead,
@@ -26,6 +16,15 @@ class Read {
/// Create a new instance from a json
factory Read.fromJson(Map<String, dynamic> json) => _$ReadFromJson(json);
/// Date of the read event
final DateTime lastRead;
/// User who sent the event
final User user;
/// Number of unread messages
final int unreadMessages;
/// Serialize to json
Map<String, dynamic> toJson() => _$ReadToJson(this);
}
@@ -1,6 +1,7 @@
import 'user.dart';
import 'package:stream_chat/src/models/user.dart';
/// Used to avoid to serialize properties to json
// ignore: prefer_void_to_null
Null readonly(_) => null;
/// Helper class for serialization to and from json
@@ -9,9 +10,8 @@ class Serialization {
static const Function readOnly = readonly;
/// List of users to list of userIds
static List<String> userIds(List<User> users) {
return users?.map((u) => u.id)?.toList();
}
static List<String> userIds(List<User> users) =>
users?.map((u) => u.id)?.toList();
/// Takes unknown json keys and puts them in the `extra_data` key
static Map<String, dynamic> moveToExtraDataFromRoot(
@@ -34,7 +34,8 @@ class Serialization {
});
}
/// Takes values in `extra_data` key and puts them on the root level of the json map
/// Takes values in `extra_data` key and puts them on the root level of
/// the json map
static Map<String, dynamic> moveFromExtraDataToRoot(
Map<String, dynamic> json,
List<String> topLevelFields,
+48 -53
View File
@@ -1,12 +1,53 @@
import 'package:json_annotation/json_annotation.dart';
import 'serialization.dart';
import 'package:stream_chat/src/models/serialization.dart';
part 'user.g.dart';
/// The class that defines the user model
@JsonSerializable()
class User {
/// Constructor used for json serialization
User({
this.id,
this.role,
this.createdAt,
this.updatedAt,
this.lastActive,
this.online,
this.extraData,
this.banned,
this.teams,
});
/// Create a new instance from a json
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
/// Use this named constructor to create a new user instance
User.init(
this.id, {
this.online,
this.extraData,
}) : createdAt = null,
updatedAt = null,
lastActive = null,
banned = null,
teams = null,
role = null;
/// Known top level fields.
/// Useful for [Serialization] methods.
static const topLevelFields = [
'id',
'role',
'created_at',
'updated_at',
'last_active',
'online',
'banned',
'teams',
];
/// User id
final String id;
@@ -42,43 +83,8 @@ class User {
@JsonKey(includeIfNull: false)
final Map<String, dynamic> extraData;
/// Known top level fields.
/// Useful for [Serialization] methods.
static const topLevelFields = [
'id',
'role',
'created_at',
'updated_at',
'last_active',
'online',
'banned',
'teams',
];
/// Use this named constructor to create a new user instance
User.init(
this.id, {
this.online,
this.extraData,
}) : createdAt = null,
updatedAt = null,
lastActive = null,
banned = null,
teams = null,
role = null;
/// Constructor used for json serialization
User({
this.id,
this.role,
this.createdAt,
this.updatedAt,
this.lastActive,
this.online,
this.extraData,
this.banned,
this.teams,
});
@override
int get hashCode => id.hashCode;
/// Shortcut for user name
String get name =>
@@ -86,23 +92,12 @@ class User {
? extraData['name']
: id;
/// Create a new instance from a json
factory User.fromJson(Map<String, dynamic> json) {
return _$UserFromJson(
Serialization.moveToExtraDataFromRoot(json, topLevelFields));
}
/// Serialize to json
Map<String, dynamic> toJson() {
return Serialization.moveFromExtraDataToRoot(
_$UserToJson(this), topLevelFields);
}
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is User && runtimeType == other.runtimeType && id == other.id;
@override
int get hashCode => id.hashCode;
/// Serialize to json
Map<String, dynamic> toJson() =>
Serialization.moveFromExtraDataToRoot(_$UserToJson(this), topLevelFields);
}
@@ -1,29 +1,29 @@
import 'platform_detector_stub.dart'
import 'package:stream_chat/src/platform_detector/platform_detector_stub.dart'
if (dart.library.html) 'platform_detector_web.dart'
if (dart.library.io) 'platform_detector_io.dart';
/// Possible platforms
enum PlatformType {
///
Android,
android,
///
Ios,
ios,
///
Web,
web,
///
MacOS,
macOS,
///
Windows,
windows,
///
Linux,
linux,
///
Fuchsia,
fuchsia,
}
/// Utility class that provides information on the current platform
@@ -31,42 +31,42 @@ class CurrentPlatform {
CurrentPlatform._();
/// True if the app is running on android
static bool get isAndroid => type == PlatformType.Android;
static bool get isAndroid => type == PlatformType.android;
/// True if the app is running on ios
static bool get isIos => type == PlatformType.Ios;
static bool get isIos => type == PlatformType.ios;
/// True if the app is running on web
static bool get isWeb => type == PlatformType.Web;
static bool get isWeb => type == PlatformType.web;
/// True if the app is running on macos
static bool get isMacOS => type == PlatformType.MacOS;
static bool get isMacOS => type == PlatformType.macOS;
/// True if the app is running on windows
static bool get isWindows => type == PlatformType.Windows;
static bool get isWindows => type == PlatformType.windows;
/// True if the app is running on linux
static bool get isLinux => type == PlatformType.Linux;
static bool get isLinux => type == PlatformType.linux;
/// True if the app is running on fuchsia
static bool get isFuchsia => type == PlatformType.Fuchsia;
static bool get isFuchsia => type == PlatformType.fuchsia;
/// Returns a string version of the platform
static String get name {
switch (type) {
case PlatformType.Android:
case PlatformType.android:
return 'android';
case PlatformType.Ios:
case PlatformType.ios:
return 'ios';
case PlatformType.Web:
case PlatformType.web:
return 'web';
case PlatformType.MacOS:
case PlatformType.macOS:
return 'macos';
case PlatformType.Windows:
case PlatformType.windows:
return 'windows';
case PlatformType.Linux:
case PlatformType.linux:
return 'linux';
case PlatformType.Fuchsia:
case PlatformType.fuchsia:
return 'fuchsia';
default:
return '';
@@ -1,12 +1,12 @@
import 'dart:io';
import 'platform_detector.dart';
import 'package:stream_chat/src/platform_detector/platform_detector.dart';
/// Version running on native systems
PlatformType get currentPlatform {
if (Platform.isWindows) return PlatformType.Windows;
if (Platform.isFuchsia) return PlatformType.Fuchsia;
if (Platform.isMacOS) return PlatformType.MacOS;
if (Platform.isLinux) return PlatformType.Linux;
if (Platform.isIOS) return PlatformType.Ios;
return PlatformType.Android;
if (Platform.isWindows) return PlatformType.windows;
if (Platform.isFuchsia) return PlatformType.fuchsia;
if (Platform.isMacOS) return PlatformType.macOS;
if (Platform.isLinux) return PlatformType.linux;
if (Platform.isIOS) return PlatformType.ios;
return PlatformType.android;
}
@@ -1,4 +1,4 @@
import 'platform_detector.dart';
import 'package:stream_chat/src/platform_detector/platform_detector.dart';
/// Stub implementation
PlatformType get currentPlatform {
@@ -1,4 +1,4 @@
import 'platform_detector.dart';
import 'package:stream_chat/src/platform_detector/platform_detector.dart';
/// Version running on web
PlatformType get currentPlatform => PlatformType.Web;
PlatformType get currentPlatform => PlatformType.web;
+2 -2
View File
@@ -12,7 +12,9 @@ export './src/api/requests.dart';
export './src/api/responses.dart';
export './src/attachment_file_uploader.dart' show AttachmentFileUploader;
export './src/client.dart';
export './src/db/chat_persistence_client.dart';
export './src/event_type.dart';
export './src/extensions/string_extension.dart';
export './src/models/action.dart';
export './src/models/attachment.dart';
export './src/models/attachment_file.dart';
@@ -29,5 +31,3 @@ export './src/models/own_user.dart';
export './src/models/reaction.dart';
export './src/models/read.dart';
export './src/models/user.dart';
export './src/extensions/string_extension.dart';
export './src/db/chat_persistence_client.dart';
+1
View File
@@ -2,4 +2,5 @@ import 'package:stream_chat/src/client.dart';
/// Current package version
/// Used in [StreamChatClient] to build the `x-stream-client` header
// ignore: constant_identifier_names
const PACKAGE_VERSION = '1.3.2+1-beta';
+13 -12
View File
@@ -9,23 +9,24 @@ environment:
sdk: ">=2.7.0 <3.0.0"
dependencies:
json_annotation: ^3.0.1
logging: ^0.11.4
dio: ^3.0.10
web_socket_channel: ^1.1.0
uuid: ^2.2.2
async: ^2.4.2
rxdart: ^0.25.0
collection: ^1.14.13
pedantic: ^1.9.2
meta: ^1.2.4
mime: ^0.9.7
dio: ^3.0.10
freezed_annotation: ^0.12.0
http_parser: ^3.1.4
json_annotation: ^3.0.1
logging: ^0.11.4
meta: ^1.2.4
mime: ^0.9.7
rxdart: ^0.25.0
uuid: ^2.2.2
web_socket_channel: ^1.1.0
dev_dependencies:
build_runner: ^1.10.0
json_serializable: ^3.3.0
test: ^1.15.7
mockito: ^4.1.1
freezed: ^0.12.7
json_serializable: ^3.3.0
mockito: ^4.1.1
test: ^1.15.7
@@ -188,7 +188,7 @@ void main() {
}
};
final query = 'hello';
const query = 'hello';
final queryParams = {
'payload': json.encode({
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:jiffy/jiffy.dart';
import 'package:stream_chat_flutter/stream_chat_flutter.dart';
/// It shows a date divider depending on the date difference