Skip to content

Commit b9f1333

Browse files
Merge pull request #147 from sendbird/v4.5.2
Add 4.5.2.
2 parents fbfd3e8 + 3d5c200 commit b9f1333

22 files changed

+629
-120
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
## v4.5.2 (Sep 15, 2025)
2+
3+
### Improvements
4+
- Fixed a bug where the failed `FileMessage` does not load on iOS when restarting the app
5+
- Fixed a bug regarding offline login
6+
- Updated regarding statistics
7+
18
## v4.5.1 (Jul 24, 2025)
29

310
### Improvements

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ Before installing Sendbird Chat SDK, you need to create a Sendbird application o
5050

5151
```yaml
5252
dependencies:
53-
sendbird_chat_sdk: ^4.5.1
53+
sendbird_chat_sdk: ^4.5.2
5454
```
5555
5656
- Run `flutter pub get` command in your project directory.

lib/src/internal/db/db.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,20 @@ class DB {
337337
);
338338
}
339339

340+
Future<FileMessage?> getFailedFileMessage({
341+
required ChannelType channelType,
342+
required String channelUrl,
343+
required String requestId,
344+
}) async {
345+
return await CChannelMessage.getFailedFileMessage(
346+
_chat,
347+
_isar,
348+
channelType,
349+
channelUrl,
350+
requestId,
351+
);
352+
}
353+
340354
Future<void> removeFailedMessages({
341355
required ChannelType channelType,
342356
required String channelUrl,

lib/src/internal/db/schema/message/meta/c_channel_message.dart

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,33 @@ class CChannelMessage {
342342
return messages;
343343
}
344344

345+
static Future<FileMessage?> getFailedFileMessage(
346+
Chat chat,
347+
Isar isar,
348+
ChannelType channelType,
349+
String channelUrl,
350+
String requestId,
351+
) async {
352+
final cChannelMessages = await isar.cChannelMessages
353+
.where()
354+
.channelTypeChannelUrlEqualTo(channelType, channelUrl)
355+
.filter()
356+
.sendingStatusEqualTo(SendingStatus.failed)
357+
.findAll();
358+
359+
FileMessage? fileMessage;
360+
for (final cChannelMessage in cChannelMessages) {
361+
final message = await cChannelMessage.toChannelMessage(chat, isar);
362+
if (message != null && message.message is FileMessage) {
363+
if ((message.message as FileMessage).requestId == requestId) {
364+
fileMessage = message.message as FileMessage;
365+
break;
366+
}
367+
}
368+
}
369+
return fileMessage;
370+
}
371+
345372
static Future<void> removeFailedMessages(
346373
Chat chat,
347374
Isar isar,

lib/src/internal/main/chat/chat.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import 'package:sendbird_chat_sdk/src/internal/main/chat_manager/db_manager.dart
1818
import 'package:sendbird_chat_sdk/src/internal/main/chat_manager/device_token_manager.dart';
1919
import 'package:sendbird_chat_sdk/src/internal/main/chat_manager/event_dispatcher.dart';
2020
import 'package:sendbird_chat_sdk/src/internal/main/chat_manager/event_manager.dart';
21+
import 'package:sendbird_chat_sdk/src/internal/main/chat_manager/file_cache_manager.dart';
2122
import 'package:sendbird_chat_sdk/src/internal/main/chat_manager/session_manager.dart';
2223
import 'package:sendbird_chat_sdk/src/internal/main/logger/sendbird_logger.dart';
2324
import 'package:sendbird_chat_sdk/src/internal/main/model/delivery_status.dart';
@@ -53,7 +54,6 @@ import 'package:sendbird_chat_sdk/src/internal/network/http/http_client/request/
5354
import 'package:sendbird_chat_sdk/src/internal/network/http/http_client/request/user/preference/user_snooze_request.dart';
5455
import 'package:sendbird_chat_sdk/src/internal/network/http/http_client/request/user/push/user_push_register_request.dart';
5556
import 'package:sendbird_chat_sdk/src/internal/network/http/http_client/request/user/push/user_push_unregister_request.dart';
56-
import 'package:sendbird_chat_sdk/src/public/main/params/channel/feed_channel_total_unread_message_count_params.dart';
5757
import 'package:universal_io/io.dart';
5858

5959
part 'chat_auth.dart';
@@ -66,7 +66,7 @@ part 'chat_notifications.dart';
6666
part 'chat_push.dart';
6767
part 'chat_user.dart';
6868

69-
const sdkVersion = '4.5.1';
69+
const sdkVersion = '4.5.2';
7070

7171
// Internal implementation for main class. Do not directly access this class.
7272
class Chat with WidgetsBindingObserver {
@@ -124,6 +124,7 @@ class Chat with WidgetsBindingObserver {
124124
late StatManager statManager;
125125
late DBManager dbManager;
126126
late DeviceTokenManager deviceTokenManager;
127+
late FileCacheManager fileCacheManager;
127128

128129
final int chatId;
129130

@@ -148,6 +149,7 @@ class Chat with WidgetsBindingObserver {
148149
collectionManager = CollectionManager(chat: this);
149150
dbManager = DBManager(chat: this);
150151
deviceTokenManager = DeviceTokenManager();
152+
fileCacheManager = FileCacheManager(chat: this);
151153

152154
_listenConnectivityChangedEvent();
153155
}

lib/src/internal/main/chat_manager/connection_manager.dart

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,8 @@ class ConnectionManager {
135135
}) async {
136136
sbLog.i(StackTrace.current, 'userId: $userId');
137137

138+
chat.connectionManager.changeState(ConnectingState(chat: chat));
139+
138140
setLoginInfo(
139141
fromWebSocket: true,
140142
userId: userId,
@@ -259,10 +261,12 @@ class ConnectionManager {
259261
'clear: $clear, logout: $logout, userId: ${chat.chatContext.currentUserId}',
260262
);
261263

262-
if (chat.chatContext.loginCompleter != null &&
263-
!chat.chatContext.loginCompleter!.isCompleted) {
264-
chat.chatContext.loginCompleter
265-
?.completeError(ConnectionCanceledException());
264+
if (chat.dbManager.isEnabled() == false) {
265+
if (chat.chatContext.loginCompleter != null &&
266+
!chat.chatContext.loginCompleter!.isCompleted) {
267+
chat.chatContext.loginCompleter
268+
?.completeError(ConnectionCanceledException());
269+
}
266270
}
267271

268272
if (isReconnecting()) {
@@ -496,8 +500,6 @@ class ConnectionManager {
496500
await disconnect(logout: false);
497501
await reconnect(reset: true);
498502
}
499-
} else {
500-
throw WebSocketFailedException(message: e.toString());
501503
}
502504
}
503505

@@ -519,6 +521,7 @@ class ConnectionManager {
519521
'SB-User-Agent': _sbUserAgentHeader,
520522
'SB-SDK-USER-AGENT': _sbSdkUserAgentHeader,
521523
'SendBird': _sendbirdHeader,
524+
'Connection': 'keep-alive',
522525
};
523526
}
524527

lib/src/internal/main/chat_manager/db_manager.dart

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ class DBManager {
124124
if (preDbVersion == null || _dbVersion > preDbVersion) {
125125
await clear();
126126
await prefs.setInt(_dbVersionKey, _dbVersion);
127+
} else {
128+
_chat.fileCacheManager.removeOldCachedFilesForIOS(); // Async
127129
}
128130
}
129131

@@ -167,6 +169,7 @@ class DBManager {
167169
try {
168170
sbLog.d(StackTrace.current);
169171
await _db.clear();
172+
await _chat.fileCacheManager.clearCacheForIOS();
170173
} catch (e) {
171174
sbLog.e(StackTrace.current, e.toString());
172175
}
@@ -508,6 +511,57 @@ class DBManager {
508511
return [];
509512
}
510513

514+
Future<FileMessage?> getFailedFileMessage({
515+
required ChannelType channelType,
516+
required String channelUrl,
517+
required String requestId,
518+
}) async {
519+
if (isEnabled()) {
520+
return await _db.getFailedFileMessage(
521+
channelType: channelType,
522+
channelUrl: channelUrl,
523+
requestId: requestId,
524+
);
525+
}
526+
return null;
527+
}
528+
529+
Future<void> removeOldFailedMessages(
530+
List<String> filePathList,
531+
) async {
532+
if (isEnabled()) {
533+
final channels = await getGroupChannels(query: GroupChannelListQuery());
534+
for (final channel in channels) {
535+
final failedMessages = await getFailedMessages(
536+
channelType: ChannelType.group,
537+
channelUrl: channel.channelUrl,
538+
);
539+
540+
List<BaseMessage> messagesToRemove = [];
541+
542+
for (final message in failedMessages) {
543+
if (message is FileMessage) {
544+
for (final filePath in filePathList) {
545+
if (message.messageCreateParams?.fileInfo.file?.path ==
546+
filePath) {
547+
messagesToRemove.add(message);
548+
break;
549+
}
550+
}
551+
}
552+
}
553+
554+
if (messagesToRemove.isNotEmpty) {
555+
await removeFailedMessages(
556+
channelType: ChannelType.group,
557+
channelUrl: channel.channelUrl,
558+
messages: messagesToRemove,
559+
);
560+
}
561+
}
562+
}
563+
}
564+
511565
Future<void> removeFailedMessages({
512566
required ChannelType channelType,
513567
required String channelUrl,
@@ -523,6 +577,15 @@ class DBManager {
523577
channelUrl: channelUrl,
524578
messages: failedMessages,
525579
);
580+
581+
for (final message in failedMessages) {
582+
if (message is FileMessage) {
583+
await _chat.fileCacheManager.removeCachedFileForIOS(
584+
requestId: message.requestId,
585+
file: message.messageCreateParams?.fileInfo.file,
586+
);
587+
}
588+
}
526589
}
527590

528591
// Event
@@ -551,6 +614,7 @@ class DBManager {
551614
channelType: channelType,
552615
channelUrl: channelUrl,
553616
);
617+
await _chat.fileCacheManager.clearCacheForIOS();
554618
}
555619

556620
// Event
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
// Copyright (c) 2025 Sendbird, Inc. All rights reserved.
2+
3+
import 'dart:io';
4+
5+
import 'package:path_provider/path_provider.dart';
6+
import 'package:sendbird_chat_sdk/sendbird_chat_sdk.dart';
7+
import 'package:sendbird_chat_sdk/src/internal/main/chat/chat.dart';
8+
9+
import '../logger/sendbird_logger.dart';
10+
11+
class FileCacheManager {
12+
static const String _folderName = 'sendbird_chat_file_cache';
13+
14+
final Chat _chat;
15+
int retentionMinutes = 3 * 24 * 60; // 3 days
16+
17+
FileCacheManager({required Chat chat}) : _chat = chat;
18+
19+
bool _isForIOSCaching() {
20+
return Platform.isIOS && _chat.chatContext.options.useCollectionCaching;
21+
}
22+
23+
Future<Directory> _getCacheDir() async {
24+
final appSupportDir = await getApplicationSupportDirectory();
25+
final cacheDir = Directory('${appSupportDir.path}/$_folderName');
26+
if (!cacheDir.existsSync()) {
27+
cacheDir.createSync();
28+
}
29+
return cacheDir;
30+
}
31+
32+
Future<String> _getCachedFilePath({
33+
required String requestId,
34+
required File file,
35+
}) async {
36+
final cacheDir = await _getCacheDir();
37+
String cachedFilePath =
38+
'${cacheDir.path}/${requestId}_${file.path.split('/').last}';
39+
return cachedFilePath;
40+
}
41+
42+
Future<void> copyFileForIOS({
43+
required GroupChannel channel,
44+
required String? requestId,
45+
required File? originalFile,
46+
}) async {
47+
if (_isForIOSCaching() && requestId != null && originalFile != null) {
48+
try {
49+
final cachedFilePath = await _getCachedFilePath(
50+
requestId: requestId,
51+
file: originalFile,
52+
);
53+
File cachedFile = File(cachedFilePath);
54+
if (cachedFile.existsSync()) {
55+
cachedFile.deleteSync();
56+
}
57+
cachedFile = await originalFile.copy(cachedFilePath);
58+
59+
final failedFileMessage = await _chat.dbManager.getFailedFileMessage(
60+
channelType: ChannelType.group,
61+
channelUrl: channel.channelUrl,
62+
requestId: requestId,
63+
);
64+
65+
if (failedFileMessage != null) {
66+
failedFileMessage.file = cachedFile;
67+
failedFileMessage.messageCreateParams?.fileInfo.file = cachedFile;
68+
69+
await _chat.dbManager.upsertMessages([failedFileMessage]);
70+
}
71+
} catch (e) {
72+
sbLog.e(StackTrace.current, 'Failed to write cache file for iOS: $e');
73+
}
74+
}
75+
}
76+
77+
Future<void> removeCachedFileForIOS({
78+
required String? requestId,
79+
required File? file,
80+
}) async {
81+
if (_isForIOSCaching() && requestId != null && file != null) {
82+
try {
83+
final cachedFilePath = await _getCachedFilePath(
84+
requestId: requestId,
85+
file: file,
86+
);
87+
final cachedFile = File(cachedFilePath);
88+
if (cachedFile.existsSync()) {
89+
cachedFile.deleteSync();
90+
}
91+
} catch (e) {
92+
sbLog.e(StackTrace.current, 'Failed to delete cache file for iOS: $e');
93+
}
94+
}
95+
}
96+
97+
Future<void> removeOldCachedFilesForIOS() async {
98+
if (_isForIOSCaching()) {
99+
try {
100+
final cacheDir = await _getCacheDir();
101+
List<String> filePathList = [];
102+
final now = DateTime.now();
103+
104+
for (final fileEntity in cacheDir.listSync()) {
105+
if (fileEntity is File) {
106+
final stat = await fileEntity.stat();
107+
if (now.difference(stat.accessed).inMinutes > retentionMinutes) {
108+
filePathList.add(fileEntity.path);
109+
}
110+
}
111+
}
112+
113+
if (filePathList.isNotEmpty) {
114+
try {
115+
await _chat.dbManager.removeOldFailedMessages(filePathList);
116+
} catch (e) {
117+
sbLog.e(StackTrace.current,
118+
'Failed to remove failed messages with old cache files: $e');
119+
}
120+
}
121+
} catch (e) {
122+
sbLog.e(StackTrace.current, 'Failed to clean old cache files: $e');
123+
}
124+
}
125+
}
126+
127+
Future<void> clearCacheForIOS() async {
128+
if (_isForIOSCaching()) {
129+
try {
130+
final cacheDir = await _getCacheDir();
131+
if (cacheDir.existsSync()) {
132+
cacheDir.deleteSync(recursive: true);
133+
}
134+
} catch (e) {
135+
sbLog.e(StackTrace.current, 'Failed to clear cache for iOS: $e');
136+
}
137+
}
138+
}
139+
}

0 commit comments

Comments
 (0)