Release/1.4.0 beta (#318)

* fix: fixed long system messages overflowing

* fix: fixed long system messages overflowing

* [UI-Kit] Refactor, add support for downloading attachments.

Signed-off-by: Sahil Kumar <[email protected]>

* fix: header casing

* [UI-Kit] Disable attachment download dialog pop using back button

Signed-off-by: Sahil Kumar <[email protected]>

* stream_chat_flutter fix svg on web

* [UI-Kit] Pop attachment download dialog in case of any error.

Signed-off-by: Sahil Kumar <[email protected]>

* restore svg

* feat: add pr title linter

* fix test

* fix client headers

* update system message design

* update gh token env var

* add scopes

* expose systemMessageBuilder in messagelistview

* fix(ui): fix button size

* add dispatch workflow

* add dispatch workflow

* update dispatch acton

* add runson

* fix ref

* fix ref

* update secret

* specify repo

* fix: list scroll keyboard behaviour  (#304)

* fix: Keyboard now closes by clicking on space or scrolling.

* fix: Exposed system and normal message tap builders

* fmt: dartfmt

* feat: Add support for messages filter in `MessageListView` and `MessageListCore` (#303)

* [MessageListView, MessageListCore] Add support for message filter

Signed-off-by: Sahil Kumar <[email protected]>

* [UI-Kit -> Pubspec] Remove `stream_chat_flutter_core` relative import

Signed-off-by: Sahil Kumar <[email protected]>

* add relative import

* add relative import

* remove svg for web

* remove badge logic on web

* 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]>

* add section highlighting sample app repo (#307)

Co-authored-by: Salvatore Giordano <[email protected]>

* add null check to debounce

* fix messagelistview loading errors

* remove core relative import

* fix: fixes message newline issue (#308)

* fix: minor

* fix: generate image thumbnails and use transparent image as placeholder for cached network images (#310)

* update cdn handling

* fix image attachment with new cdn

* fix(ui): Fix message input permission request crash (#311)

Signed-off-by: Sahil Kumar <[email protected]>

* fix(llc): update member presence and add skip_push to message (#314)

* update member presence

* add skip_push to message

* fix tests

* fix: Fixed shimmer overflow (#316)

* fix dialogs

* fix(llc): Fix attachment upload state uneven progress. (#315)

* feat(llc): Add rate limiter functions.
fix(llc): Fix attachment upload state uneven progress.

Signed-off-by: Sahil Kumar <[email protected]>

* feat(llc): Export `async.dart` and `rate_limit.dart`

Signed-off-by: Sahil Kumar <[email protected]>

* refactor(llc): Replace our `rate_limit` implementation with the dart port of js `lodash` to better handlw `cancel` and `flush` functions.

Signed-off-by: Sahil Kumar <[email protected]>

* fix(UI-Kit): Make android example run on real devices

Signed-off-by: Sahil Kumar <[email protected]>

* fix offline behaviour

Co-authored-by: Salvatore Giordano <[email protected]>

* feat: Introduce onAttachmentTap on messages attachment (#309)

* introdce onAttachmentTap on message

* correct indent

* sdfsd

Co-authored-by: Salvatore Giordano <[email protected]>

* feat: added customization options in main widgets (#312)

* polish channelpreview customization options

* polish channelheader customization options

* polish messageinput customization options

* polish threadheader customization options

* polish channellistheader customization options

* show the parent message if no messages in thread

* fix edit message

* fix channelinfo textstyle

* fix review

* fix useravatar

* add ontitle tap to thread header

* add custom message actions

* add borderradius and button builder

* add ontap to messageinput custom send button

* fix bottom sheet

* add doc

* use placeholder image

* add doc

* fix listtile density

* remove subtitle from ChannelListHeaderTheme.copyWith

* add InputDecoration.merge extension

* fix: use shimmer in images

* fix: remove notification badge logic from the sdk and move it to sample app

* fix import

* fix: Method being called during build (#317)

* method call bug fix

* dartfmt

* fix: show error messages as system and keep them in the message input (#319)

* fix: pre release (#324)

* use share_plus plugin

* bump dependency versions

* fix ui for web

* unfocus messageinput only on commands

* fix default error for messagesearchlistview

* fix thumbnail animation

* fix lint

* ignore example in linter

* update changelogs

* fix lint

* fix lint

* update stream_chat pana min

* add doc

Co-authored-by: Sahil Kumar <[email protected]>
Co-authored-by: Deven Joshi <[email protected]>
Co-authored-by: Neevash Ramdial (Nash) <[email protected]>
Co-authored-by: Nelson Nunes <[email protected]>
This commit is contained in:
Salvatore Giordano
2021-03-09 18:58:23 +01:00
committed by GitHub
co-authored by Sahil Kumar Deven Joshi Neevash Ramdial Nelson Nunes
parent 3332a48a0e
commit 4488fa6c3a
121 changed files with 3768 additions and 2376 deletions
+235 -190
View File
@@ -2,24 +2,16 @@ 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:rxdart/rxdart.dart';
import 'package:stream_chat/src/api/retry_queue.dart';
import 'package:stream_chat/src/debounce.dart';
import 'package:stream_chat/src/event_type.dart';
import 'package:stream_chat/src/models/attachment_file.dart';
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';
import 'package:stream_chat/src/extensions/rate_limit.dart';
/// This a the class that manages a specific channel.
class Channel {
@@ -57,7 +49,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 +161,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 +179,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 +188,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,
);
@@ -224,29 +218,29 @@ class Channel {
}
client.logger.info('Found ${attachments.length} attachments');
void updateAttachment(Attachment attachment) {
final index =
message.attachments.indexWhere((it) => it.id == attachment.id);
if (index != -1) {
message.attachments[index] = attachment;
state?.addMessage(message);
}
}
return Future.wait(attachments.map((it) {
client.logger.info('Uploading ${it.id} attachment...');
void updateAttachment(Attachment attachment) {
final index = message.attachments.indexWhere((it) {
return it.id == attachment.id;
});
if (index != -1) {
message.attachments[index] = attachment;
state?.addMessage(message);
}
}
final throttledUpdateAttachment = updateAttachment.throttled(
const Duration(milliseconds: 500),
);
void onSendProgress(int sent, int total) {
debounce(
timeout: Duration(seconds: 1),
target: updateAttachment,
positionalArguments: [
it.copyWith(
uploadState: UploadState.inProgress(uploaded: sent, total: total),
),
],
);
throttledUpdateAttachment([
it.copyWith(
uploadState: UploadState.inProgress(uploaded: sent, total: total),
),
]);
}
final isImage = it.type == 'image';
@@ -270,18 +264,26 @@ 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) {
client.logger.severe('error uploading the attachment', e, stk);
updateAttachment(
it.copyWith(uploadState: UploadState.failed(error: e.toString())),
);
}).whenComplete(() {
throttledUpdateAttachment?.cancel();
_cancelableAttachmentUploadRequest.remove(it.id);
});
})).whenComplete(() {
@@ -305,6 +307,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 +316,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(),
);
@@ -335,11 +338,13 @@ class Channel {
_messageAttachmentsUploadCompleter[message.id] =
attachmentsUploadCompleter;
unawaited(_uploadAttachments(
// ignore: unawaited_futures
_uploadAttachments(
message.id,
message.attachments.map((it) => it.id),
));
);
// ignore: parameter_assignments
message = await attachmentsUploadCompleter.future;
}
@@ -364,13 +369,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(),
);
@@ -383,11 +389,13 @@ class Channel {
_messageAttachmentsUploadCompleter[message.id] =
attachmentsUploadCompleter;
unawaited(_uploadAttachments(
// ignore: unawaited_futures
_uploadAttachments(
message.id,
message.attachments.map((it) => it.id),
));
);
// ignore: parameter_assignments
message = await attachmentsUploadCompleter.future;
}
@@ -423,6 +431,7 @@ class Channel {
}
try {
// ignore: parameter_assignments
message = message.copyWith(
type: 'deleted',
status: MessageSendingStatus.deleting,
@@ -456,7 +465,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 +484,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 +521,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 +554,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 +644,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 +825,7 @@ class Channel {
})
..addAll(options);
var response;
ChannelState response;
try {
response = await query(options: watchOptions);
@@ -861,7 +861,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 +941,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 +1112,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 +1135,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,17 +1197,21 @@ 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) {
ChannelClientState(
this._channel,
ChannelState channelState,
) : _debouncedUpdatePersistenceChannelState = _channel
?._client?.chatPersistenceClient?.updateChannelState
?.debounced(const Duration(seconds: 1)) {
retryQueue = RetryQueue(
channel: _channel,
logger: Logger('RETRY QUEUE ${_channel.cid}'),
@@ -1252,14 +1257,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,
@@ -1277,11 +1284,16 @@ class ChannelClientState {
m.attachments?.isNotEmpty == true &&
m.attachments?.any((e) {
final url = e.imageUrl ?? e.assetUrl;
if (url == null || !url.contains('stream-io-cdn.com')) {
if (url == null || !url.contains('')) {
return false;
}
final uri = Uri.parse(url);
if (uri.host != 'stream-io-cdn.com' ||
uri.queryParameters['Expires'] == null) {
return false;
}
final expiration =
DateTime.parse(Uri.parse(url).queryParameters['Expires']);
DateTime.parse(uri.queryParameters['Expires']);
return expiration.isBefore(DateTime.now());
}) ==
true)
@@ -1338,8 +1350,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 +1369,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 +1489,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 +1550,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 +1571,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 +1643,7 @@ class ChannelClientState {
true)
?.toList() ??
[],
];
newMessages.sort(_sortByCreatedAt);
]..sort(_sortByCreatedAt);
final newWatchers = <User>[
...updatedState?.watchers ?? [],
@@ -1690,16 +1706,11 @@ class ChannelClientState {
ChannelState get channelState => _channelStateController.value;
BehaviorSubject<ChannelState> _channelStateController;
final Debounce _debouncedUpdatePersistenceChannelState;
set _channelState(ChannelState v) {
_channelStateController.add(v);
if (_channel._client.persistenceEnabled) {
debounce(
timeout: Duration(milliseconds: 500),
target: _channel._client.chatPersistenceClient?.updateChannelState,
positionalArguments: [v],
);
}
_debouncedUpdatePersistenceChannelState?.call([v]);
}
/// The channel threads related to this channel
@@ -1735,28 +1746,62 @@ 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());
}
},
),
)
..add(
_channel
.on()
.where((event) =>
event.user != null &&
members?.any((m) => m.userId == event.user.id) == true)
.listen(
(event) {
final newMembers = List<Member>.from(members);
final oldMemberIndex =
newMembers.indexWhere((m) => m.userId == event.user.id);
if (oldMemberIndex > -1) {
final oldMember = newMembers.removeAt(oldMemberIndex);
updateChannelState(ChannelState(
members: [
...newMembers,
oldMember.copyWith(
user: event.user,
),
],
));
}
},
),
);
}
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 +1814,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 +1827,6 @@ class ChannelClientState {
.map((m) => m.copyWith(
pinExpires: null,
pinned: false,
pinnedAt: null,
pinnedBy: null,
))
.toList();
@@ -1811,6 +1855,7 @@ class ChannelClientState {
/// Call this method to dispose this object
void dispose() {
_debouncedUpdatePersistenceChannelState?.cancel();
_unreadCountController.close();
retryQueue.dispose();
_subscriptions.forEach((s) => s.cancel());