Skip to content

Commit 5e0e7e7

Browse files
Merge pull request #142 from sendbird/v4.5.0
Add 4.5.0.
2 parents 2637cce + d00be82 commit 5e0e7e7

File tree

19 files changed

+289
-94
lines changed

19 files changed

+289
-94
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
## v4.5.0 (Jul 23, 2025)
2+
3+
### Features
4+
- Added `getTotalUnreadMessageCountWithParams({GroupChannelTotalUnreadMessageCountParams? groupChannelParams, FeedChannelTotalUnreadMessageCountParams? feedChannelParams})` and deprecated `getTotalUnreadMessageCountWithFeedChannel([GroupChannelTotalUnreadMessageCountParams? params])`
5+
6+
### Improvements
7+
- Added `logViewed(List<NotificationMessage> messages)` and deprecated `logImpression(List<NotificationMessage> messages)` in `FeedChannel`
8+
- Added API timeout for 10 seconds
9+
- Added reconnection timeout for `connectionTimeout` in `SendbirdChatOptions`
10+
- Updated regarding statistics
11+
112
## v4.4.1 (Jun 27, 2025)
213

314
### 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.4.1
53+
sendbird_chat_sdk: ^4.5.0
5454
```
5555
5656
- Run `flutter pub get` command in your project directory.

lib/sendbird_chat_sdk.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ export 'src/public/main/model/thread/thread_info.dart';
7575
export 'src/public/main/model/thread/thread_info_updated_event.dart';
7676
export 'src/public/main/model/thread/threaded_messages.dart';
7777
export 'src/public/main/params/channel/feed_channel_change_logs_params.dart';
78+
export 'src/public/main/params/channel/feed_channel_total_unread_message_count_params.dart';
7879
export 'src/public/main/params/channel/group_channel_change_logs_params.dart';
7980
export 'src/public/main/params/channel/group_channel_create_params.dart';
8081
export 'src/public/main/params/channel/group_channel_total_unread_channel_count_params.dart';

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ import 'package:sendbird_chat_sdk/src/internal/network/http/http_client/request/
5353
import 'package:sendbird_chat_sdk/src/internal/network/http/http_client/request/user/preference/user_snooze_request.dart';
5454
import 'package:sendbird_chat_sdk/src/internal/network/http/http_client/request/user/push/user_push_register_request.dart';
5555
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';
5657
import 'package:universal_io/io.dart';
5758

5859
part 'chat_auth.dart';
@@ -65,7 +66,7 @@ part 'chat_notifications.dart';
6566
part 'chat_push.dart';
6667
part 'chat_user.dart';
6768

68-
const sdkVersion = '4.4.1';
69+
const sdkVersion = '4.5.0';
6970

7071
// Internal implementation for main class. Do not directly access this class.
7172
class Chat with WidgetsBindingObserver {

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

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,10 +152,16 @@ extension ChatChannel on Chat {
152152
return result;
153153
}
154154

155-
Future<UnreadMessageCount> getTotalUnreadMessageCount(
156-
[GroupChannelTotalUnreadMessageCountParams? params]) async {
157-
final result = await apiClient.send<UnreadMessageCount>(
158-
UserTotalUnreadMessageCountGetRequest(this, params: params));
155+
Future<UnreadMessageCount> getTotalUnreadMessageCount({
156+
GroupChannelTotalUnreadMessageCountParams? groupChannelParams,
157+
FeedChannelTotalUnreadMessageCountParams? feedChannelParams,
158+
}) async {
159+
final result = await apiClient
160+
.send<UnreadMessageCount>(UserTotalUnreadMessageCountGetRequest(
161+
this,
162+
groupChannelParams: groupChannelParams,
163+
feedChannelParams: feedChannelParams,
164+
));
159165
sbLog.i(StackTrace.current, 'return: $result');
160166
return result;
161167
}

lib/src/internal/main/chat_context/chat_context.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,12 +90,12 @@ class ChatContext {
9090
UnreadMessageCountInfo unreadMessageCountInfo =
9191
UnreadMessageCountInfo(all: 0, customTypes: {}, ts: 0);
9292

93-
void resetReconnectTask() {
94-
final config = reconnectConfig;
95-
if (config != null) {
96-
reconnectTask = ReconnectTask(config);
97-
}
98-
}
93+
// void resetReconnectTask() {
94+
// final config = reconnectConfig;
95+
// if (config != null) {
96+
// reconnectTask = ReconnectTask(config);
97+
// }
98+
// }
9999

100100
bool get isChatConnected {
101101
return (services.isEmpty || services.contains(Service.chat)); // Check

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import 'package:sendbird_chat_sdk/src/public/main/model/poll/poll_update_event.d
4343
import 'package:sendbird_chat_sdk/src/public/main/model/poll/poll_vote_event.dart';
4444
import 'package:sendbird_chat_sdk/src/public/main/model/reaction/reaction_event.dart';
4545
import 'package:sendbird_chat_sdk/src/public/main/model/thread/thread_info_updated_event.dart';
46+
import 'package:uuid/uuid.dart';
4647

4748
class CommandManager {
4849
final Map<String, Completer<Command?>> _completerMap = {};
@@ -357,12 +358,24 @@ class CommandManager {
357358
//- [DBManager]
358359

359360
if (fromWebSocket) {
361+
final wasReconnecting = _chat.connectionManager.isReconnecting();
362+
if (wasReconnecting) {
363+
_chat.statManager.endWsConnectStat(
364+
hostUrl: _chat.chatContext.reconnectTask?.url ?? '',
365+
success: true,
366+
connectedTs: _chat.connectionManager.webSocketClient.connectedTs,
367+
logiTs: _chat.commandManager.logiTs,
368+
accumTrial: _chat.chatContext.reconnectTask?.retryCount ?? 1,
369+
connectionId:
370+
_chat.chatContext.reconnectTask?.id ?? const Uuid().v1(),
371+
);
372+
}
373+
360374
_chat.chatContext.reconnectTask =
361375
ReconnectTask(event.reconnectConfiguration);
362376
_chat.chatContext.setPingInterval(event.pingInterval);
363377
_chat.chatContext.setWatchdogInterval(event.watchdogInterval);
364378

365-
final wasReconnecting = _chat.connectionManager.isReconnecting();
366379
if (wasReconnecting) {
367380
await _chat.eventDispatcher.onReconnected(event);
368381
} else {

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

Lines changed: 81 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import 'package:sendbird_chat_sdk/src/public/core/user/user.dart';
2020
import 'package:sendbird_chat_sdk/src/public/main/define/exceptions.dart';
2121
import 'package:sendbird_chat_sdk/src/public/main/define/sendbird_error.dart';
2222
import 'package:universal_io/io.dart';
23+
import 'package:uuid/uuid.dart';
2324

2425
class ConnectionManager {
2526
Timer? reconnectTimer;
@@ -344,72 +345,101 @@ class ConnectionManager {
344345
}
345346

346347
changeState(ReconnectingState(chat: chat));
347-
348-
if (reset) {
349-
chat.chatContext.resetReconnectTask();
350-
} else {
351-
chat.chatContext.reconnectTask?.increaseRetryCount();
352-
}
348+
chat.chatContext.reconnectTask?.increaseRetryCount(reset: reset); // Check
353349

354350
sbLog.i(
355351
StackTrace.current,
356352
'[Timer()] ${chat.chatContext.reconnectTask?.backOffPeriod}sec, ${chat.chatContext.reconnectTask?.retryCount}/${chat.chatContext.reconnectTask?.config.maximumRetryCount}',
357353
);
358354

359355
reconnectTimer?.cancel();
360-
reconnectTimer =
361-
Timer(Duration(seconds: chat.chatContext.reconnectTask!.backOffPeriod),
362-
() async {
363-
sbLog.i(
364-
StackTrace.current,
365-
'[Timer() => RUN] ${chat.chatContext.reconnectTask?.backOffPeriod}sec, ${chat.chatContext.reconnectTask?.retryCount}/${chat.chatContext.reconnectTask?.config.maximumRetryCount}',
366-
);
356+
reconnectTimer = Timer(
357+
Duration(seconds: chat.chatContext.reconnectTask!.backOffPeriod),
358+
() async => await _reconnect(doNotCallReconnectStartedEvent),
359+
);
360+
return true;
361+
}
367362

368-
if (chat.chatContext.reconnectTask?.retryCount == 1) {
369-
if (!doNotCallReconnectStartedEvent) {
370-
await chat.eventDispatcher.onReconnecting();
371-
chat.eventManager.notifyReconnectStarted();
372-
}
363+
Future<void> _reconnect(bool doNotCallReconnectStartedEvent) async {
364+
sbLog.i(
365+
StackTrace.current,
366+
'[Timer() => RUN] ${chat.chatContext.reconnectTask?.backOffPeriod}sec, ${chat.chatContext.reconnectTask?.retryCount}/${chat.chatContext.reconnectTask?.config.maximumRetryCount}',
367+
);
368+
369+
if (chat.chatContext.reconnectTask?.retryCount == 1) {
370+
if (!doNotCallReconnectStartedEvent) {
371+
await chat.eventDispatcher.onReconnecting();
372+
chat.eventManager.notifyReconnectStarted();
373373
}
374+
}
374375

375-
// ===== Reconnect =====
376-
final sessionKey = await chat.sessionManager.getSessionKey();
377-
final params = {
378-
if (sessionKey == null) 'user_id': chat.chatContext.currentUserId,
379-
'SB-User-Agent': _sbUserAgentHeader,
380-
'SB-SDK-USER-AGENT': _sbSdkUserAgentHeader,
381-
'expiring_session':
382-
chat.eventManager.getSessionHandler() != null ? '1' : '0',
383-
'include_extra_data': chat.extraData.join(','),
384-
'include_poll_details': '1',
385-
};
386-
params.addAll(await _getWebSocketParams(
387-
userId: chat.chatContext.currentUser?.userId ?? ''));
388-
389-
final url =
390-
'${chat.chatContext.wsHost}/?${Uri(queryParameters: params).query}';
391-
392-
runZonedGuarded(() {
393-
sbLog.d(StackTrace.current, 'webSocketClient?.connect()');
394-
webSocketClient.connect(
395-
url: url,
396-
accessToken: chat.chatContext.accessToken,
397-
sessionKey: sessionKey,
398-
);
376+
// ===== Reconnect =====
377+
final sessionKey = await chat.sessionManager.getSessionKey();
378+
final params = {
379+
if (sessionKey == null) 'user_id': chat.chatContext.currentUserId,
380+
'SB-User-Agent': _sbUserAgentHeader,
381+
'SB-SDK-USER-AGENT': _sbSdkUserAgentHeader,
382+
'expiring_session':
383+
chat.eventManager.getSessionHandler() != null ? '1' : '0',
384+
'include_extra_data': chat.extraData.join(','),
385+
'include_poll_details': '1',
386+
};
387+
params.addAll(await _getWebSocketParams(
388+
userId: chat.chatContext.currentUser?.userId ?? ''));
399389

400-
reconnectTimer?.cancel();
401-
reconnectTimer = null;
402-
}, (e, s) {
403-
sbLog.e(StackTrace.current, 'e: $e');
404-
});
390+
final url =
391+
'${chat.chatContext.wsHost}/?${Uri(queryParameters: params).query}';
392+
393+
runZonedGuarded(() {
394+
sbLog.d(StackTrace.current, 'webSocketClient?.connect()');
395+
396+
chat.statManager.startWsConnectStat(hostUrl: url);
397+
if (chat.chatContext.reconnectTask != null) {
398+
chat.chatContext.reconnectTask?.url = url;
399+
}
400+
401+
webSocketClient.connect(
402+
url: url,
403+
accessToken: chat.chatContext.accessToken,
404+
sessionKey: sessionKey,
405+
reconnect: true,
406+
);
407+
408+
reconnectTimer?.cancel();
409+
reconnectTimer = null;
410+
}, (e, s) {
411+
sbLog.e(StackTrace.current, 'e: $e');
412+
413+
if (e is SendbirdException) {
414+
chat.statManager.endWsConnectStat(
415+
hostUrl: url,
416+
success: false,
417+
errorCode: e.code,
418+
errorDescription: e.message,
419+
accumTrial: chat.chatContext.reconnectTask?.retryCount ?? 1,
420+
connectionId: chat.chatContext.reconnectTask?.id ?? const Uuid().v1(),
421+
);
422+
} else {
423+
final exception = WebSocketFailedException(message: e.toString());
424+
425+
chat.statManager.endWsConnectStat(
426+
hostUrl: url,
427+
success: false,
428+
errorCode: exception.code,
429+
errorDescription: exception.message,
430+
accumTrial: chat.chatContext.reconnectTask?.retryCount ?? 1,
431+
connectionId: chat.chatContext.reconnectTask?.id ?? const Uuid().v1(),
432+
);
433+
}
405434
});
406-
return true;
407435
}
408436

409437
//------------------------------//
410438
// WebSocket Event Listener
411439
//------------------------------//
412-
void _onWebSocketConnected() {}
440+
void _onWebSocketConnected() {
441+
// Nothing to do here.
442+
}
413443

414444
void _onWebSocketClosed() {
415445
chat.commandManager.clearCompleterMap();
@@ -459,6 +489,8 @@ class ConnectionManager {
459489
Future<void> _onWebSocketError(Object e) async {
460490
if (chat.chatContext.currentUser != null) {
461491
if (isReconnecting()) {
492+
await Future.delayed(const Duration(
493+
milliseconds: 1)); // [Timing issue] Because of endWsConnectStat()
462494
await reconnect(reset: false);
463495
} else {
464496
await disconnect(logout: false);

lib/src/internal/main/model/reconnect_task.dart

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,21 @@
33
import 'dart:math';
44

55
import 'package:sendbird_chat_sdk/src/internal/main/model/reconnect_configuration.dart';
6+
import 'package:uuid/uuid.dart';
67

78
class ReconnectTask {
9+
final String _id = const Uuid().v1();
10+
String? url;
811
int _backOffPeriod = 0;
9-
int _retryCount = 1;
12+
int _retryCount = 0;
1013
ReconnectConfiguration config;
1114

1215
ReconnectTask(this.config);
1316

17+
String get id => _id;
18+
1419
int get backOffPeriod => _backOffPeriod;
20+
1521
int get retryCount => _retryCount;
1622

1723
bool get exceedRetryCount {
@@ -25,12 +31,24 @@ class ReconnectTask {
2531
return _retryCount == config.maximumRetryCount;
2632
}
2733

28-
void increaseRetryCount() {
29-
if (_backOffPeriod < config.maxInterval) {
30-
final newBackOff =
31-
config.interval * pow(config.multiplier, (_retryCount - 1));
34+
void increaseRetryCount({bool reset = false}) {
35+
if (reset) {
36+
if (_retryCount == 0) {
37+
_retryCount = 1;
38+
}
39+
40+
_backOffPeriod = 0;
41+
} else if (_backOffPeriod < config.maxInterval) {
42+
_retryCount++;
43+
44+
double newBackOff;
45+
if (_retryCount <= 2) {
46+
newBackOff = config.interval;
47+
} else {
48+
newBackOff =
49+
config.interval * pow(config.multiplier, (_retryCount - 2));
50+
}
3251
_backOffPeriod = min(newBackOff.toInt(), config.maxInterval.toInt());
3352
}
34-
_retryCount++;
3553
}
3654
}

lib/src/internal/main/stats/stat_manager.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,7 @@ class StatManager {
492492
int? logiTs,
493493
int? errorCode,
494494
String? errorDescription,
495+
String? connectionId,
495496
}) async {
496497
sbLog.d(StackTrace.current);
497498

@@ -515,7 +516,7 @@ class StatManager {
515516
'logi_latency': logiLatency,
516517
'error_code': errorCode,
517518
'error_description': errorDescription,
518-
'connection_id': const Uuid().v1(),
519+
'connection_id': connectionId ?? const Uuid().v1(),
519520
},
520521
);
521522

@@ -550,7 +551,7 @@ class StatManager {
550551
}) {
551552
sbLog.d(StackTrace.current);
552553

553-
if (_wsConnectStartTsMap[endpoint] == null) {
554+
if (_apiResultStartTsMap[endpoint] == null) {
554555
_apiResultStartTsMap[endpoint] = DateTime.now().millisecondsSinceEpoch;
555556
}
556557
}

0 commit comments

Comments
 (0)