diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dd7e18..4860624 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +## v4.2.26 (Oct 28, 2024) + +### Features +- Added `useAutoResend` in `SendbirdChatOptions` + +### Improvements +- Fixed a bug regarding `onReconnectFailed()` event in `ConnectionHandler` + ## v4.2.25 (Oct 21, 2024) ### Improvements diff --git a/README.md b/README.md index 8fafb29..e5368a3 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Before installing Sendbird Chat SDK, you need to create a Sendbird application o ```yaml dependencies: - sendbird_chat_sdk: ^4.2.25 + sendbird_chat_sdk: ^4.2.26 ``` - Run `flutter pub get` command in your project directory. diff --git a/lib/src/internal/main/chat/chat.dart b/lib/src/internal/main/chat/chat.dart index 2fc9c1f..465a810 100644 --- a/lib/src/internal/main/chat/chat.dart +++ b/lib/src/internal/main/chat/chat.dart @@ -62,7 +62,7 @@ part 'chat_notifications.dart'; part 'chat_push.dart'; part 'chat_user.dart'; -const sdkVersion = '4.2.25'; +const sdkVersion = '4.2.26'; // Internal implementation for main class. Do not directly access this class. class Chat with WidgetsBindingObserver { diff --git a/lib/src/internal/main/chat_manager/collection_manager/auto_resend_manager.dart b/lib/src/internal/main/chat_manager/collection_manager/auto_resend_manager.dart new file mode 100644 index 0000000..1b77901 --- /dev/null +++ b/lib/src/internal/main/chat_manager/collection_manager/auto_resend_manager.dart @@ -0,0 +1,107 @@ +// Copyright (c) 2024 Sendbird, Inc. All rights reserved. + +import 'dart:async'; + +import 'package:sendbird_chat_sdk/sendbird_chat_sdk.dart'; +import 'package:sendbird_chat_sdk/src/internal/main/chat/chat.dart'; +import 'package:sendbird_chat_sdk/src/internal/main/logger/sendbird_logger.dart'; + +class AutoResendManager { + AutoResendManager._(); + + static final AutoResendManager _instance = AutoResendManager._(); + + factory AutoResendManager() => _instance; + + static const int _delayForRateLimit = 200; // Check + bool _isAutoResending = false; + bool _stopAutoResending = false; + + void startAutoResend(Chat chat) async { + if (!chat.chatContext.options.useAutoResend) { + sbLog.i(StackTrace.current, 'Returned because of useAutoResend == false'); + return; + } + + if (_isAutoResending) { + sbLog.i( + StackTrace.current, 'Returned because of _isAutoResending == true'); + return; + } + + sbLog.i(StackTrace.current, 'Started'); + _isAutoResending = true; + + try { + for (final collection in chat.collectionManager.baseMessageCollections) { + if (collection is MessageCollection) { + if (collection.channel.isFrozen) { + sbLog.i(StackTrace.current, + 'Skipped because of collection.channel.isFrozen == true'); + continue; + } + + final failedMessages = await collection.getFailedMessages(); + + for (final failedMessage in failedMessages) { + if (failedMessage.isAutoResendable()) { + // Resend a message + Completer completer = Completer(); + SendbirdException? exception; + if (failedMessage is UserMessage) { + collection.channel.resendUserMessage( + failedMessage, + handler: (UserMessage message, SendbirdException? e) { + exception = e; + completer.complete(); + }, + ); + } else if (failedMessage is FileMessage) { + collection.channel.resendFileMessage( + failedMessage, + handler: (FileMessage message, SendbirdException? e) { + exception = e; + completer.complete(); + }, + ); + } else { + // Defensive code + completer.complete(); + } + await completer.future; + + if (exception != null) { + sbLog.i( + StackTrace.current, 'Stopped because of exception != null'); + break; + } + + if (_stopAutoResending) break; + + // Delay to avoid the rate limit + await Future.delayed( + const Duration(milliseconds: _delayForRateLimit)); + } + + if (_stopAutoResending) break; + } + } + + if (_stopAutoResending) break; + } + } catch (e) { + sbLog.e(StackTrace.current, e.toString()); + } + + _stopAutoResending = false; + _isAutoResending = false; + sbLog.i(StackTrace.current, 'Stopped'); + } + + void stopAutoResend() { + if (_isAutoResending) { + sbLog.i(StackTrace.current); + _stopAutoResending = true; + } + } +} diff --git a/lib/src/internal/main/chat_manager/collection_manager/collection_manager.dart b/lib/src/internal/main/chat_manager/collection_manager/collection_manager.dart index 31c3987..74747b2 100644 --- a/lib/src/internal/main/chat_manager/collection_manager/collection_manager.dart +++ b/lib/src/internal/main/chat_manager/collection_manager/collection_manager.dart @@ -7,6 +7,7 @@ import 'package:sendbird_chat_sdk/sendbird_chat_sdk.dart'; import 'package:sendbird_chat_sdk/src/internal/db/schema/channel/meta/channel_info.dart'; import 'package:sendbird_chat_sdk/src/internal/db/schema/message/meta/message_changelog_info.dart'; import 'package:sendbird_chat_sdk/src/internal/main/chat/chat.dart'; +import 'package:sendbird_chat_sdk/src/internal/main/chat_manager/collection_manager/auto_resend_manager.dart'; import 'package:sendbird_chat_sdk/src/internal/main/logger/sendbird_logger.dart'; import 'package:sendbird_chat_sdk/src/internal/network/http/http_client/request/channel/message/channel_messages_gap_request.dart'; import 'package:sendbird_chat_sdk/src/internal/network/http/http_client/request/channel/message/channel_messages_get_request.dart'; @@ -75,12 +76,22 @@ class CollectionManager { await _chat.dbManager.checkDBFileSize(); } //- [DBManager] + + AutoResendManager().startAutoResend(_chat); + } + + Future onDisconnected() async { + sbLog.d(StackTrace.current); + + AutoResendManager().stopAutoResend(); } Future onReconnected() async { sbLog.d(StackTrace.current); await _refresh(); + + AutoResendManager().startAutoResend(_chat); } Future _refresh() async { diff --git a/lib/src/internal/main/chat_manager/connection_manager.dart b/lib/src/internal/main/chat_manager/connection_manager.dart index 86ab431..cd00404 100644 --- a/lib/src/internal/main/chat_manager/connection_manager.dart +++ b/lib/src/internal/main/chat_manager/connection_manager.dart @@ -268,8 +268,6 @@ class ConnectionManager { final disconnectedUserId = chat.chatContext.currentUserId ?? ''; if (clear || logout) { - await chat.eventDispatcher.onLogout(); - chat.messageQueueMap.forEach((key, q) => q.cleanUp()); chat.messageQueueMap.clear(); // chat.uploads.forEach((key, value) => _api.cancelUploadingFile(key)); @@ -281,6 +279,8 @@ class ConnectionManager { chat.apiClient.cleanUp(); if (logout) { + await chat.eventDispatcher.onLogout(); + chat.chatContext.cleanUp(); chat.collectionManager.cleanUpGroupChannelCollections(); chat.collectionManager.cleanUpMessageCollections(); @@ -311,8 +311,10 @@ class ConnectionManager { if (chat.chatContext.currentUser == null || chat.chatContext.sessionKey == null) { + if (isReconnecting()) { + chat.eventManager.notifyReconnectFailed(); + } changeState(DisconnectedState(chat: chat)); - chat.eventManager.notifyReconnectFailed(); return false; } diff --git a/lib/src/internal/main/chat_manager/event_dispatcher.dart b/lib/src/internal/main/chat_manager/event_dispatcher.dart index aa418e8..e8af42a 100644 --- a/lib/src/internal/main/chat_manager/event_dispatcher.dart +++ b/lib/src/internal/main/chat_manager/event_dispatcher.dart @@ -22,6 +22,7 @@ class EventDispatcher { Future onDisconnected() async { sbLog.d(StackTrace.current); + _chat.collectionManager.onDisconnected(); } Future onReconnecting() async { diff --git a/lib/src/public/core/message/base_message.dart b/lib/src/public/core/message/base_message.dart index f336a90..2f1f983 100644 --- a/lib/src/public/core/message/base_message.dart +++ b/lib/src/public/core/message/base_message.dart @@ -232,6 +232,17 @@ class BaseMessage extends RootMessage { return result; } + bool isAutoResendable() { + if (errorCode == SendbirdError.connectionRequired || + errorCode == SendbirdError.webSocketConnectionClosed || + errorCode == SendbirdError.webSocketConnectionFailed || + errorCode == SendbirdError.requestFailed || // Check + errorCode == SendbirdError.socketChannelFrozen) { + return true; + } + return false; + } + /// Returns [MessageMetaArray] list which is filtered by given metaArrayKeys. List getMetaArrays(List keys) { sbLog.i(StackTrace.current, 'keys: $keys'); diff --git a/lib/src/public/main/chat/sendbird_chat_options.dart b/lib/src/public/main/chat/sendbird_chat_options.dart index acdf805..03d85d6 100644 --- a/lib/src/public/main/chat/sendbird_chat_options.dart +++ b/lib/src/public/main/chat/sendbird_chat_options.dart @@ -11,6 +11,7 @@ class SendbirdChatOptions { static const defaultFileTransferTimeout = 30; static const defaultTypingIndicatorThrottle = 1000; static const defaultUseMemberInfoInMessage = true; + static const defaultUseAutoResend = false; bool _useCollectionCaching = defaultUseCollectionCaching; int _connectionTimeout = defaultConnectionTimeout; @@ -18,6 +19,7 @@ class SendbirdChatOptions { int _fileTransferTimeout = defaultFileTransferTimeout; int _typingIndicatorThrottle = defaultTypingIndicatorThrottle; bool _useMemberInfoInMessage = defaultUseMemberInfoInMessage; + bool _useAutoResend = defaultUseAutoResend; SendbirdChatOptions({ bool? useCollectionCaching = defaultUseCollectionCaching, @@ -26,6 +28,7 @@ class SendbirdChatOptions { int? fileTransferTimeout = defaultFileTransferTimeout, int? typingIndicatorThrottle = defaultTypingIndicatorThrottle, bool? useMemberInfoInMessage = defaultUseMemberInfoInMessage, + bool? useAutoResend = defaultUseAutoResend, }) { this.useCollectionCaching = useCollectionCaching; this.connectionTimeout = connectionTimeout; @@ -33,6 +36,8 @@ class SendbirdChatOptions { this.fileTransferTimeout = fileTransferTimeout; this.typingIndicatorThrottle = typingIndicatorThrottle; this.useMemberInfoInMessage = useMemberInfoInMessage; + this.useAutoResend = useAutoResend; + this.useAutoResend = _useCollectionCaching ? useAutoResend : false; } bool get useCollectionCaching => _useCollectionCaching; @@ -105,4 +110,24 @@ class SendbirdChatOptions { set useMemberInfoInMessage(value) { _useMemberInfoInMessage = value; } + + bool get useAutoResend => _useAutoResend; + + /// If set `true` and `useCollectionCaching` is `true`, + /// the failed messages will be resent automatically + /// when the WebSocket is reconnected + /// + /// With local caching, you can temporarily keep an unsent message in the local cache + /// if the WebSocket connection is lost. + /// The Chat SDK with local caching marks the failed message as pending, stores it locally, + /// and automatically resends the pending message when the WebSocket is reconnected. + /// This is called auto resend. + /// The default value is `false`. + /// + /// @since 4.2.26 + set useAutoResend(value) { + if (_useCollectionCaching) { + _useAutoResend = value; + } + } } diff --git a/lib/src/public/main/collection/group_channel_message_collection/base_message_collection.dart b/lib/src/public/main/collection/group_channel_message_collection/base_message_collection.dart index 2fc03c1..3f29a8b 100644 --- a/lib/src/public/main/collection/group_channel_message_collection/base_message_collection.dart +++ b/lib/src/public/main/collection/group_channel_message_collection/base_message_collection.dart @@ -6,6 +6,7 @@ import 'package:collection/collection.dart'; import 'package:sendbird_chat_sdk/sendbird_chat_sdk.dart'; import 'package:sendbird_chat_sdk/src/internal/main/chat/chat.dart'; import 'package:sendbird_chat_sdk/src/internal/main/chat_cache/cache_service.dart'; +import 'package:sendbird_chat_sdk/src/internal/main/chat_manager/collection_manager/auto_resend_manager.dart'; import 'package:sendbird_chat_sdk/src/internal/main/chat_manager/collection_manager/collection_manager.dart'; import 'package:sendbird_chat_sdk/src/internal/main/chat_manager/db_manager.dart'; import 'package:sendbird_chat_sdk/src/internal/main/logger/sendbird_logger.dart'; @@ -397,6 +398,8 @@ abstract class BaseMessageCollection { throw exception; } } + + AutoResendManager().startAutoResend(chat); } void _setValuesForInitialize({ diff --git a/lib/src/public/main/collection/group_channel_message_collection/message_collection.dart b/lib/src/public/main/collection/group_channel_message_collection/message_collection.dart index 7887b02..c75ba9e 100644 --- a/lib/src/public/main/collection/group_channel_message_collection/message_collection.dart +++ b/lib/src/public/main/collection/group_channel_message_collection/message_collection.dart @@ -53,11 +53,12 @@ class MessageCollection extends BaseMessageCollection { /// Gets all failed messages of this MessageCollection /// @since 4.2.0 Future> getFailedMessages() async { - sbLog.i(StackTrace.current, 'getFailedMessages()'); - return await chat.dbManager.getFailedMessages( + final failedMessages = await chat.dbManager.getFailedMessages( channelType: ChannelType.group, channelUrl: channel.channelUrl, ); + sbLog.i(StackTrace.current, '${failedMessages.length}'); + return failedMessages; } /// Removes specific failed messages of this MessageCollection. @@ -65,7 +66,7 @@ class MessageCollection extends BaseMessageCollection { Future removeFailedMessages({ required List messages, }) async { - sbLog.i(StackTrace.current, 'removeFailedMessages()'); + sbLog.i(StackTrace.current, '${messages.length}'); await chat.dbManager.removeFailedMessages( channelType: ChannelType.group, channelUrl: channel.channelUrl, @@ -76,7 +77,7 @@ class MessageCollection extends BaseMessageCollection { /// Removes all failed messages of this MessageCollection. /// @since 4.2.0 Future removeAllFailedMessages() async { - sbLog.i(StackTrace.current, 'removeAllFailedMessages()'); + sbLog.i(StackTrace.current); await chat.dbManager.removeAllFailedMessages( channelType: ChannelType.group, channelUrl: channel.channelUrl, diff --git a/pubspec.yaml b/pubspec.yaml index 0bd448a..e16d7a1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: sendbird_chat_sdk description: With Sendbird Chat for Flutter, you can easily build an in-app chat with all the essential messaging features. -version: 4.2.25 +version: 4.2.26 homepage: https://sendbird.com repository: https://github.com/sendbird/sendbird-chat-sdk-flutter documentation: https://sendbird.com/docs/chat/sdk/v4/flutter/getting-started/send-first-message