Skip to content

Commit bd17255

Browse files
committed
Merge remote-tracking branch 'origin/master' into v10.0.0
# Conflicts: # melos.yaml # packages/stream_chat/CHANGELOG.md # packages/stream_chat/example/pubspec.yaml # packages/stream_chat/lib/src/client/channel.dart # packages/stream_chat/lib/src/core/models/channel_config.dart # packages/stream_chat/lib/src/core/models/channel_config.g.dart # packages/stream_chat/lib/src/core/models/message.dart # packages/stream_chat/lib/src/core/models/message.g.dart # packages/stream_chat/lib/version.dart # packages/stream_chat/pubspec.yaml # packages/stream_chat_flutter/CHANGELOG.md # packages/stream_chat_flutter/example/pubspec.yaml # packages/stream_chat_flutter/pubspec.yaml # packages/stream_chat_flutter_core/CHANGELOG.md # packages/stream_chat_flutter_core/example/pubspec.yaml # packages/stream_chat_flutter_core/pubspec.yaml # packages/stream_chat_localizations/CHANGELOG.md # packages/stream_chat_localizations/example/pubspec.yaml # packages/stream_chat_localizations/pubspec.yaml # packages/stream_chat_persistence/CHANGELOG.md # packages/stream_chat_persistence/example/pubspec.yaml # packages/stream_chat_persistence/lib/src/db/drift_chat_database.dart # packages/stream_chat_persistence/pubspec.yaml # sample_app/pubspec.yaml
2 parents 7ba65c2 + fe326e7 commit bd17255

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+921
-166
lines changed

.github/workflows/check_db_entities.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ jobs:
4848
4949
- name: 🔍 Find Comment
5050
if: steps.check_entities.outputs.has_changes == 'true'
51-
uses: peter-evans/find-comment@v3
51+
uses: peter-evans/find-comment@v4
5252
id: find_comment
5353
with:
5454
issue-number: ${{ github.event.pull_request.number }}
@@ -57,7 +57,7 @@ jobs:
5757

5858
- name: 💬 Create or Update Comment
5959
if: steps.check_entities.outputs.has_changes == 'true'
60-
uses: peter-evans/create-or-update-comment@v4
60+
uses: peter-evans/create-or-update-comment@v5
6161
with:
6262
comment-id: ${{ steps.find_comment.outputs.comment-id }}
6363
issue-number: ${{ github.event.pull_request.number }}

.github/workflows/stream_flutter_workflow.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ on:
88
pull_request:
99
paths:
1010
- 'packages/**'
11+
- 'sample_app/**'
1112
- '.github/workflows/stream_flutter_workflow.yml'
1213
types:
1314
- opened

.github/workflows/update_goldens.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
run: melos run update:goldens
3131

3232
- name: 📤 Commit Changes
33-
uses: stefanzweifel/git-auto-commit-action@v6
33+
uses: stefanzweifel/git-auto-commit-action@v7
3434
with:
3535
commit_message: "chore: Update Goldens"
3636
file_pattern: "**/test/**/goldens/*.png"

packages/stream_chat/CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,26 @@ For more details, please refer to the [migration guide](../../migrations/v10-mig
2525
- `StreamChatClient.removeImage()` - Remove an image from the Stream CDN
2626
- `StreamChatClient.removeFile()` - Remove a file from the Stream CDN
2727

28+
## 9.18.0
29+
30+
🐞 Fixed
31+
32+
- Improved sync reliability and error handling with enhanced `lastSyncAt` initialization, 400
33+
error recovery, and automatic flushing of stale persistence data after 30 days of inactivity.
34+
35+
✅ Added
36+
37+
- Added support for `Message.channelRole` field to provide access to the sender's channel role.
38+
- Added support for `Channel.messageCount` field.
39+
- Added support for Pending Messages. Pending messages can be accessed via
40+
`ChannelState.pendingMessages` or `ChannelState.pendingMessagesStream`.
41+
42+
🐞 Fixed
43+
44+
- Fixed thread messages increasing the unread count in the main channel.
45+
- Fixed `ChannelState.memberCount`, `ChannelState.config` and `ChannelState.extraData` getting reset
46+
on first load.
47+
2848
## 10.0.0-beta.6
2949

3050
- Included the changes from version [`9.17.0`](https://pub.dev/packages/stream_chat/changelog).

packages/stream_chat/lib/src/client/channel.dart

Lines changed: 89 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,24 @@ class Channel {
407407
return state!.channelStateStream.map((cs) => cs.channel?.memberCount);
408408
}
409409

410+
/// Channel message count.
411+
///
412+
/// Note: This field is only populated if the `count_messages` option is
413+
/// enabled for your app.
414+
int? get messageCount {
415+
_checkInitialized();
416+
return state!._channelState.channel?.messageCount;
417+
}
418+
419+
/// Channel message count as a stream.
420+
///
421+
/// Note: This field is only populated if the `count_messages` option is
422+
/// enabled for your app.
423+
Stream<int?> get messageCountStream {
424+
_checkInitialized();
425+
return state!.channelStateStream.map((cs) => cs.channel?.messageCount);
426+
}
427+
410428
/// Channel id.
411429
String? get id => state?._channelState.channel?.id ?? _id;
412430

@@ -2256,23 +2274,19 @@ class ChannelClientState {
22562274
ChannelClientState(
22572275
this._channel,
22582276
ChannelState channelState,
2259-
) : _debouncedUpdatePersistenceChannelState = debounce(
2260-
(ChannelState state) {
2261-
final persistenceClient = _channel._client.chatPersistenceClient;
2262-
return persistenceClient?.updateChannelState(state);
2263-
},
2264-
const Duration(seconds: 1),
2265-
) {
2277+
) {
22662278
_retryQueue = RetryQueue(
22672279
channel: _channel,
22682280
logger: _channel.client.detachedLogger(
22692281
'⟳ (${generateHash([_channel.cid])})',
22702282
),
22712283
);
22722284

2273-
_checkExpiredAttachmentMessages(channelState);
2274-
22752285
_channelStateController = BehaviorSubject.seeded(channelState);
2286+
// Update the persistence storage with the seeded channel state.
2287+
_debouncedUpdatePersistenceChannelState.call([channelState]);
2288+
2289+
_checkExpiredAttachmentMessages(channelState);
22762290

22772291
// region TYPING EVENTS
22782292
_listenTypingEvents();
@@ -2313,6 +2327,7 @@ class ChannelClientState {
23132327
// region CHANNEL EVENTS
23142328
_listenChannelTruncated();
23152329
_listenChannelUpdated();
2330+
_listenChannelMessageCount();
23162331
// endregion
23172332

23182333
// region MEMBER EVENTS
@@ -2348,20 +2363,11 @@ class ChannelClientState {
23482363

23492364
_listenChannelPushPreferenceUpdated();
23502365

2351-
_channel._client.chatPersistenceClient
2352-
?.getChannelThreads(_channel.cid!)
2353-
.then((threads) {
2354-
_threads = threads;
2355-
}).then((_) {
2356-
_channel._client.chatPersistenceClient
2357-
?.getChannelStateByCid(_channel.cid!)
2358-
.then((state) {
2359-
// Replacing the persistence state members with the latest
2360-
// `channelState.members` as they may have changes over the time.
2361-
updateChannelState(state.copyWith(members: channelState.members));
2362-
retryFailedMessages();
2363-
});
2364-
});
2366+
final persistenceClient = _channel.client.chatPersistenceClient;
2367+
persistenceClient?.getChannelThreads(_channel.cid!).then((threads) {
2368+
// Load all the threads for the channel from the offline storage.
2369+
if (threads.isNotEmpty) _threads = threads;
2370+
}).then((_) => retryFailedMessages());
23652371
}
23662372

23672373
final Channel _channel;
@@ -2496,6 +2502,23 @@ class ChannelClientState {
24962502
}));
24972503
}
24982504

2505+
void _listenChannelMessageCount() {
2506+
_subscriptions.add(_channel.on().listen(
2507+
(Event e) {
2508+
final messageCount = e.channelMessageCount;
2509+
if (messageCount == null) return;
2510+
2511+
updateChannelState(
2512+
channelState.copyWith(
2513+
channel: channelState.channel?.copyWith(
2514+
messageCount: messageCount,
2515+
),
2516+
),
2517+
);
2518+
},
2519+
));
2520+
}
2521+
24992522
void _listenChannelTruncated() {
25002523
_subscriptions.add(_channel
25012524
.on(EventType.channelTruncated, EventType.notificationChannelTruncated)
@@ -3478,6 +3501,15 @@ class ChannelClientState {
34783501
.map((cs) => cs.pinnedMessages ?? <Message>[])
34793502
.distinct(const ListEquality().equals);
34803503

3504+
/// Channel pending message list.
3505+
List<Message> get pendingMessages =>
3506+
_channelState.pendingMessages ?? <Message>[];
3507+
3508+
/// Channel pending message list as a stream.
3509+
Stream<List<Message>> get pendingMessagesStream => channelStateStream
3510+
.map((cs) => cs.pendingMessages ?? <Message>[])
3511+
.distinct(const ListEquality().equals);
3512+
34813513
/// Get channel last message.
34823514
Message? get lastMessage =>
34833515
_channelState.messages != null && _channelState.messages!.isNotEmpty
@@ -3602,7 +3634,7 @@ class ChannelClientState {
36023634
if (message.isEphemeral) return false;
36033635

36043636
// Don't count thread replies which are not shown in the channel as unread.
3605-
if (message.parentId != null && message.showInChannel == false) {
3637+
if (message.parentId != null && message.showInChannel != true) {
36063638
return false;
36073639
}
36083640

@@ -3624,6 +3656,18 @@ class ChannelClientState {
36243656
final isMuted = currentUser.mutes.any((it) => it.user.id == messageUser.id);
36253657
if (isMuted) return false;
36263658

3659+
final lastRead = currentUserRead?.lastRead;
3660+
// Don't count messages created before the last read time as unread.
3661+
if (lastRead case final read? when message.createdAt.isBefore(read)) {
3662+
return false;
3663+
}
3664+
3665+
final lastReadMessageId = currentUserRead?.lastReadMessageId;
3666+
// Don't count if the last read message id is the same as the message id.
3667+
if (lastReadMessageId case final id? when message.id == id) {
3668+
return false;
3669+
}
3670+
36273671
// If we've passed all checks, count the message as unread.
36283672
return true;
36293673
}
@@ -3700,6 +3744,7 @@ class ChannelClientState {
37003744
read: newReads,
37013745
draft: updatedState.draft,
37023746
pinnedMessages: updatedState.pinnedMessages,
3747+
pendingMessages: updatedState.pendingMessages,
37033748
pushPreferences: updatedState.pushPreferences,
37043749
activeLiveLocations: updatedState.activeLiveLocations,
37053750
);
@@ -3718,13 +3763,30 @@ class ChannelClientState {
37183763
ChannelState get channelState => _channelStateController.value;
37193764
late BehaviorSubject<ChannelState> _channelStateController;
37203765

3721-
final Debounce _debouncedUpdatePersistenceChannelState;
3766+
late final _debouncedUpdatePersistenceChannelState = debounce(
3767+
(ChannelState state) {
3768+
final persistenceClient = _channel._client.chatPersistenceClient;
3769+
return persistenceClient?.updateChannelState(state);
3770+
},
3771+
const Duration(seconds: 1),
3772+
);
37223773

37233774
set _channelState(ChannelState v) {
37243775
_channelStateController.safeAdd(v);
37253776
_debouncedUpdatePersistenceChannelState.call([v]);
37263777
}
37273778

3779+
late final _debouncedUpdatePersistenceChannelThreads = debounce(
3780+
(Map<String, List<Message>> threads) async {
3781+
final channelCid = _channel.cid;
3782+
if (channelCid == null) return;
3783+
3784+
final persistenceClient = _channel._client.chatPersistenceClient;
3785+
return persistenceClient?.updateChannelThreads(channelCid, threads);
3786+
},
3787+
const Duration(seconds: 1),
3788+
);
3789+
37283790
/// The channel threads related to this channel.
37293791
Map<String, List<Message>> get threads => {..._threadsController.value};
37303792

@@ -3733,10 +3795,7 @@ class ChannelClientState {
37333795
final _threadsController = BehaviorSubject.seeded(<String, List<Message>>{});
37343796
set _threads(Map<String, List<Message>> threads) {
37353797
_threadsController.safeAdd(threads);
3736-
_channel.client.chatPersistenceClient?.updateChannelThreads(
3737-
_channel.cid!,
3738-
threads,
3739-
);
3798+
_debouncedUpdatePersistenceChannelThreads.call([threads]);
37403799
}
37413800

37423801
/// Clears all the replies in the thread identified by [parentId].
@@ -3934,6 +3993,7 @@ class ChannelClientState {
39343993

39353994
/// Call this method to dispose this object.
39363995
void dispose() {
3996+
_debouncedUpdatePersistenceChannelThreads.cancel();
39373997
_debouncedUpdatePersistenceChannelState.cancel();
39383998
_retryQueue.dispose();
39393999
_subscriptions.cancel();

packages/stream_chat/lib/src/client/client.dart

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'dart:async';
22

3+
import 'package:collection/collection.dart';
34
import 'package:dio/dio.dart';
45
import 'package:logging/logging.dart';
56
import 'package:meta/meta.dart';
@@ -192,9 +193,6 @@ class StreamChatClient {
192193

193194
late final RetryPolicy _retryPolicy;
194195

195-
/// the last dateTime at the which all the channels were synced
196-
DateTime? _lastSyncedAt;
197-
198196
/// The retry policy options getter
199197
RetryPolicy get retryPolicy => _retryPolicy;
200198

@@ -523,25 +521,17 @@ class StreamChatClient {
523521

524522
if (connectionRecovered) {
525523
// connection recovered
526-
final cids = state.channels.keys.toList(growable: false);
524+
final cids = [...state.channels.keys.toSet()];
527525
if (cids.isNotEmpty) {
528526
await queryChannelsOnline(
529527
filter: Filter.in_('cid', cids),
530528
paginationParams: const PaginationParams(limit: 30),
531529
);
532-
if (persistenceEnabled) {
533-
await sync(cids: cids, lastSyncAt: _lastSyncedAt);
534-
}
535-
} else {
536-
// channels are empty, assuming it's a fresh start
537-
// and making sure `lastSyncAt` is initialized
538-
if (persistenceEnabled) {
539-
final lastSyncAt = await chatPersistenceClient?.getLastSyncAt();
540-
if (lastSyncAt == null) {
541-
await chatPersistenceClient?.updateLastSyncAt(DateTime.now());
542-
}
543-
}
530+
531+
// Sync the persistence client if available
532+
if (persistenceEnabled) await sync(cids: cids);
544533
}
534+
545535
handleEvent(Event(
546536
type: EventType.connectionRecovered,
547537
online: true,
@@ -573,34 +563,45 @@ class StreamChatClient {
573563
Future<void> sync({List<String>? cids, DateTime? lastSyncAt}) {
574564
return _syncLock.synchronized(() async {
575565
final channels = cids ?? await chatPersistenceClient?.getChannelCids();
576-
if (channels == null || channels.isEmpty) {
577-
return;
578-
}
566+
if (channels == null || channels.isEmpty) return;
579567

580568
final syncAt = lastSyncAt ?? await chatPersistenceClient?.getLastSyncAt();
581569
if (syncAt == null) {
582-
return;
570+
logger.info('Fresh sync start: lastSyncAt initialized to now.');
571+
return chatPersistenceClient?.updateLastSyncAt(DateTime.now());
583572
}
584573

585574
try {
575+
logger.info('Syncing events since $syncAt for channels: $channels');
576+
586577
final res = await _chatApi.general.sync(channels, syncAt);
587-
final events = res.events
588-
..sort((a, b) => a.createdAt.compareTo(b.createdAt));
578+
final events = res.events.sorted(
579+
(a, b) => a.createdAt.compareTo(b.createdAt),
580+
);
589581

590582
for (final event in events) {
591-
logger.fine('event.type: ${event.type}');
592-
final messageText = event.message?.text;
593-
if (messageText != null) {
594-
logger.fine('event.message.text: $messageText');
595-
}
583+
logger.fine('Syncing event: ${event.type}');
596584
handleEvent(event);
597585
}
598586

599-
final now = DateTime.now();
600-
_lastSyncedAt = now;
601-
chatPersistenceClient?.updateLastSyncAt(now);
602-
} catch (e, stk) {
603-
logger.severe('Error during sync', e, stk);
587+
final updatedSyncAt = events.lastOrNull?.createdAt ?? DateTime.now();
588+
return chatPersistenceClient?.updateLastSyncAt(updatedSyncAt);
589+
} catch (error, stk) {
590+
// If we got a 400 error, it means that either the sync time is too
591+
// old or the channel list is too long or too many events need to be
592+
// synced. In this case, we should just flush the persistence client
593+
// and start over.
594+
if (error is StreamChatNetworkError && error.statusCode == 400) {
595+
logger.warning(
596+
'Failed to sync events due to stale or oversized state. '
597+
'Resetting the persistence client to enable a fresh start.',
598+
);
599+
600+
await chatPersistenceClient?.flush();
601+
return chatPersistenceClient?.updateLastSyncAt(DateTime.now());
602+
}
603+
604+
logger.warning('Error syncing events', error, stk);
604605
}
605606
});
606607
}
@@ -2193,7 +2194,6 @@ class StreamChatClient {
21932194
// resetting state.
21942195
state.dispose();
21952196
state = ClientState(this);
2196-
_lastSyncedAt = null;
21972197

21982198
// resetting credentials.
21992199
_tokenManager.reset();

0 commit comments

Comments
 (0)