diff --git a/CHANGELOG.md b/CHANGELOG.md index cb9945b..e952e78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +## v1.2.0 (Oct 29, 2025) + +### Features +- Added a `navigatorKey` parameter in `SendbirdUIKit.init()` to show the delayed connecting dialog + ## v1.1.0 (Jul 30, 2025) ### Features diff --git a/README.md b/README.md index c6cc096..3c56bf6 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,8 @@ Add following dependencies and fonts for `SendbirdIcons` in `pubspec.yaml`. ```yaml dependencies: - sendbird_uikit: ^1.1.0 - sendbird_chat_sdk: ^4.5.1 + sendbird_uikit: ^1.2.0 + sendbird_chat_sdk: ^4.7.0 flutter: fonts: diff --git a/lib/fonts/SendbirdIcons.ttf b/lib/fonts/SendbirdIcons.ttf index b0283a0..6a03236 100644 Binary files a/lib/fonts/SendbirdIcons.ttf and b/lib/fonts/SendbirdIcons.ttf differ diff --git a/lib/src/internal/component/basic/sbu_bottom_sheet_user_component.dart b/lib/src/internal/component/basic/sbu_bottom_sheet_user_component.dart index 88a7106..dbb5c6e 100644 --- a/lib/src/internal/component/basic/sbu_bottom_sheet_user_component.dart +++ b/lib/src/internal/component/basic/sbu_bottom_sheet_user_component.dart @@ -81,7 +81,9 @@ class SBUBottomSheetUserComponentState ..name = '' ..isDistinct = false, ).then((channel) { - Navigator.pop(context); + if (context.mounted) { + Navigator.pop(context); + } on1On1ChannelCreated(channel); }); diff --git a/lib/src/internal/component/dialog/sbu_delayed_connecting_dialog.dart b/lib/src/internal/component/dialog/sbu_delayed_connecting_dialog.dart new file mode 100644 index 0000000..529e1e5 --- /dev/null +++ b/lib/src/internal/component/dialog/sbu_delayed_connecting_dialog.dart @@ -0,0 +1,181 @@ +// Copyright (c) 2025 Sendbird, Inc. All rights reserved. + +import 'dart:async'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:sendbird_uikit/sendbird_uikit.dart'; +import 'package:sendbird_uikit/src/internal/component/base/sbu_base_component.dart'; +import 'package:sendbird_uikit/src/internal/component/basic/sbu_text_button_component.dart'; +import 'package:sendbird_uikit/src/internal/component/basic/sbu_text_component.dart'; +import 'package:sendbird_uikit/src/internal/resource/sbu_text_styles.dart'; +import 'package:sendbird_uikit/src/internal/utils/sbu_time_extensions.dart'; + +class SBUDelayedConnectingDialog extends SBUStatefulComponent { + final int retryAfter; + final bool showCloseButton; + final void Function()? onCloseButtonClicked; + final String? closeButtonText; + + const SBUDelayedConnectingDialog({ + required this.retryAfter, + this.showCloseButton = false, + this.onCloseButtonClicked, + this.closeButtonText, + super.key, + }); + + @override + State createState() => SBUDelayedConnectingDialogState(); +} + +class SBUDelayedConnectingDialogState + extends State { + late int currentRetryAfter; + late final DateTime startTime; + Timer? _timer; + + @override + void initState() { + super.initState(); + currentRetryAfter = widget.retryAfter; + startTime = DateTime.now(); + _startTimer(); + } + + void _startTimer() { + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + updateRetryAfter(); + if (currentRetryAfter <= 0) { + _timer?.cancel(); + } + }); + } + + @override + void dispose() { + _timer?.cancel(); + super.dispose(); + } + + void updateRetryAfter() { + final now = DateTime.now(); + final elapsedTime = now.difference(startTime).inMilliseconds; + final updatedRetryAfter = widget.retryAfter - elapsedTime / 1000; + + setState(() { + currentRetryAfter = updatedRetryAfter > 0 ? updatedRetryAfter.ceil() : 0; + }); + } + + @override + Widget build(BuildContext context) { + final isLightTheme = context.watch().isLight(); + final strings = context.watch().strings; + + return PopScope( + canPop: false, + child: Container( + width: double.maxFinite, + height: double.maxFinite, + color: SBUColors.overlayLight, + child: Center( + child: Container( + width: 280, + decoration: BoxDecoration( + color: isLightTheme + ? SBUColors.background50 + : SBUColors.background500, + borderRadius: const BorderRadius.all(Radius.circular(4)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 24), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: DefaultTextStyle.merge( + style: kIsWeb + ? const TextStyle(decoration: TextDecoration.none) + : null, + child: SBUTextComponent( + text: strings.youWillBeReconnectedShortly, + textType: SBUTextType.heading1, + textColorType: SBUTextColorType.text01, + maxLines: 3, + ), + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: DefaultTextStyle.merge( + style: kIsWeb + ? const TextStyle(decoration: TextDecoration.none) + : null, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + SBUTextComponent( + text: currentRetryAfter > 0 + ? strings.estimatedWaitingTime + : '', + textType: SBUTextType.body3, + textColorType: SBUTextColorType.text02, + ), + const SizedBox(width: 4), + SBUTextComponent( + text: currentRetryAfter > 0 + ? currentRetryAfter.toTimeString() + : '', + textType: SBUTextType.body3Bold, + textColorType: SBUTextColorType.text02, + ), + ], + ), + ), + ), + const SizedBox(height: 24), + if (widget.showCloseButton) + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container( + margin: const EdgeInsets.only(right: 8, bottom: 4), + child: DefaultTextStyle.merge( + style: kIsWeb + ? const TextStyle( + decoration: TextDecoration.none) + : null, + child: SBUTextButtonComponent( + height: 32, + padding: const EdgeInsets.all(8), + text: SBUTextComponent( + text: (widget.closeButtonText != null) + ? widget.closeButtonText! + : strings.close, + textType: SBUTextType.button, + textColorType: SBUTextColorType.primary, + ), + onButtonClicked: () async { + Navigator.pop(context); + + if (widget.onCloseButtonClicked != null) { + widget.onCloseButtonClicked!(); + } + }, + ), + ), + ), + ], + ), + const SizedBox(height: 12), + ], + ), + ), + ), + )); + } +} diff --git a/lib/src/internal/resource/sbu_text_styles.dart b/lib/src/internal/resource/sbu_text_styles.dart index 6105fd0..f495352 100644 --- a/lib/src/internal/resource/sbu_text_styles.dart +++ b/lib/src/internal/resource/sbu_text_styles.dart @@ -12,6 +12,7 @@ enum SBUTextType { body1, body2, body3, + body3Bold, button, caption1, caption2, @@ -123,6 +124,16 @@ class SBUTextStyles { decorationThickness: 0, leadingDistribution: TextLeadingDistribution.even, ); + case SBUTextType.body3Bold: + return TextStyle( + fontFamily: fontFamily, + fontWeight: FontWeight.w700, + fontSize: 14, + height: 1.428, + color: color, + decorationThickness: 0, + leadingDistribution: TextLeadingDistribution.even, + ); case SBUTextType.button: return TextStyle( fontFamily: fontFamily, diff --git a/lib/src/internal/utils/sbu_time_extensions.dart b/lib/src/internal/utils/sbu_time_extensions.dart new file mode 100644 index 0000000..c81eaa4 --- /dev/null +++ b/lib/src/internal/utils/sbu_time_extensions.dart @@ -0,0 +1,18 @@ +// Copyright (c) 2025 Sendbird, Inc. All rights reserved. + +extension SBUTimeExtensions on int { + String toTimeString() { + final int hours = this ~/ 3600; + final int minutes = (this % 3600) ~/ 60; + final int seconds = this % 60; + + if (hours == 0) { + return '${minutes.toString().padLeft(2, '0')}:' + '${seconds.toString().padLeft(2, '0')}'; + } + + return '${hours.toString().padLeft(2, '0')}:' + '${minutes.toString().padLeft(2, '0')}:' + '${seconds.toString().padLeft(2, '0')}'; + } +} diff --git a/lib/src/public/resource/sbu_icons.dart b/lib/src/public/resource/sbu_icons.dart index b266e0f..f142b9f 100644 --- a/lib/src/public/resource/sbu_icons.dart +++ b/lib/src/public/resource/sbu_icons.dart @@ -84,62 +84,64 @@ class SBUIcons { const IconData(0xe81e, fontFamily: _kFontFam, fontPackage: _kFontPkg); static IconData leave = const IconData(0xe81f, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static IconData moderations = + static IconData markAsUnread = const IconData(0xe820, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static IconData more = + static IconData members = const IconData(0xe821, fontFamily: _kFontFam, fontPackage: _kFontPkg); static IconData message = const IconData(0xe822, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static IconData members = + static IconData moderations = const IconData(0xe823, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static IconData mute = + static IconData more = const IconData(0xe824, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static IconData notificationsFilled = + static IconData mute = const IconData(0xe825, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static IconData notificationsOffFilled = + static IconData notificationsFilled = const IconData(0xe826, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static IconData notifications = + static IconData notificationsOffFilled = const IconData(0xe827, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static IconData operator = + static IconData notifications = const IconData(0xe828, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static IconData photo = + static IconData operator = const IconData(0xe829, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static IconData play = + static IconData photo = const IconData(0xe82a, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static IconData plus = + static IconData play = const IconData(0xe82b, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static IconData question = + static IconData plus = const IconData(0xe82c, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static IconData remove = + static IconData question = const IconData(0xe82d, fontFamily: _kFontFam, fontPackage: _kFontPkg); static IconData refresh = const IconData(0xe82e, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static IconData replyFilled = + static IconData remove = const IconData(0xe82f, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static IconData reply = + static IconData replyFilled = const IconData(0xe830, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static IconData send = + static IconData reply = const IconData(0xe831, fontFamily: _kFontFam, fontPackage: _kFontPkg); static IconData search = const IconData(0xe832, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static IconData settingFilled = + static IconData send = const IconData(0xe833, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static IconData spinner = + static IconData settingFilled = const IconData(0xe834, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static IconData spinner = + const IconData(0xe835, fontFamily: _kFontFam, fontPackage: _kFontPkg); static IconData streaming = const IconData(0xe836, fontFamily: _kFontFam, fontPackage: _kFontPkg); static IconData supergroup = const IconData(0xe837, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static IconData thumbnailNone = + static IconData theme = const IconData(0xe838, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static IconData user = + static IconData thumbnailNone = const IconData(0xe839, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static IconData unarchive = + static IconData time = const IconData(0xe83a, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static IconData markAsUnread = + static IconData unarchive = + const IconData(0xe83b, fontFamily: _kFontFam, fontPackage: _kFontPkg); + static IconData user = const IconData(0xe83c, fontFamily: _kFontFam, fontPackage: _kFontPkg); - static IconData theme = - const IconData(0xe83d, fontFamily: _kFontFam, fontPackage: _kFontPkg); /// Sets icons. static void setIcons({ @@ -175,10 +177,11 @@ class SBUIcons { required IconData gif, required IconData info, required IconData leave, + required IconData markAsUnread, + required IconData members, + required IconData message, required IconData moderations, required IconData more, - required IconData message, - required IconData members, required IconData mute, required IconData notificationsFilled, required IconData notificationsOffFilled, @@ -192,17 +195,17 @@ class SBUIcons { required IconData refresh, required IconData replyFilled, required IconData reply, - required IconData send, required IconData search, + required IconData send, required IconData settingFilled, required IconData spinner, required IconData streaming, required IconData supergroup, + required IconData theme, required IconData thumbnailNone, - required IconData user, + required IconData time, required IconData unarchive, - required IconData markAsUnread, - required IconData theme, + required IconData user, }) { SBUIcons.add = add; SBUIcons.archive = archive; @@ -236,10 +239,11 @@ class SBUIcons { SBUIcons.gif = gif; SBUIcons.info = info; SBUIcons.leave = leave; + SBUIcons.markAsUnread = markAsUnread; + SBUIcons.members = members; + SBUIcons.message = message; SBUIcons.moderations = moderations; SBUIcons.more = more; - SBUIcons.message = message; - SBUIcons.members = members; SBUIcons.mute = mute; SBUIcons.notificationsFilled = notificationsFilled; SBUIcons.notificationsOffFilled = notificationsOffFilled; @@ -253,16 +257,16 @@ class SBUIcons { SBUIcons.refresh = refresh; SBUIcons.replyFilled = replyFilled; SBUIcons.reply = reply; - SBUIcons.send = send; SBUIcons.search = search; + SBUIcons.send = send; SBUIcons.settingFilled = settingFilled; SBUIcons.spinner = spinner; SBUIcons.streaming = streaming; SBUIcons.supergroup = supergroup; + SBUIcons.theme = theme; SBUIcons.thumbnailNone = thumbnailNone; - SBUIcons.user = user; + SBUIcons.time = time; SBUIcons.unarchive = unarchive; - SBUIcons.markAsUnread = markAsUnread; - SBUIcons.theme = theme; + SBUIcons.user = user; } } diff --git a/lib/src/public/resource/sbu_string_provider.dart b/lib/src/public/resource/sbu_string_provider.dart index a99b406..0b6faed 100644 --- a/lib/src/public/resource/sbu_string_provider.dart +++ b/lib/src/public/resource/sbu_string_provider.dart @@ -117,6 +117,12 @@ class SBUStrings { String inviteMembers; String invite; + // Waiting for connection + String youWillBeReconnectedShortly; + String estimatedWaitingTime; + String refresh; + String close; + SBUStrings({ // GroupChannel list required this.channels, @@ -210,6 +216,12 @@ class SBUStrings { // GroupChannel invite required this.inviteMembers, required this.invite, + + // Waiting for connection + required this.youWillBeReconnectedShortly, + required this.estimatedWaitingTime, + required this.refresh, + required this.close, }); static SBUStrings defaultStrings = SBUStrings( @@ -311,5 +323,12 @@ class SBUStrings { // GroupChannel invite inviteMembers: 'Invite members', invite: 'Invite', + + // Waiting for connection + youWillBeReconnectedShortly: + 'Something went wrong.\nYou\'ll be reconnected shortly.', + estimatedWaitingTime: 'Estimated waiting time:', + refresh: 'Refresh', + close: 'Close', ); } diff --git a/lib/src/public/screen/group_channel/channel_list/sbu_group_channel_create_screen.dart b/lib/src/public/screen/group_channel/channel_list/sbu_group_channel_create_screen.dart index 18ad069..1cbffe8 100644 --- a/lib/src/public/screen/group_channel/channel_list/sbu_group_channel_create_screen.dart +++ b/lib/src/public/screen/group_channel/channel_list/sbu_group_channel_create_screen.dart @@ -118,7 +118,10 @@ class SBUGroupChannelCreateScreenState ..name = '' ..isDistinct = false, ).then((channel) { - Navigator.pop(context); + if (context.mounted) { + Navigator.pop(context); + } + if (widget.onChannelCreated != null) { widget.onChannelCreated!(channel); } diff --git a/lib/src/public/sendbird_uikit.dart b/lib/src/public/sendbird_uikit.dart index 4371ba1..6be1352 100644 --- a/lib/src/public/sendbird_uikit.dart +++ b/lib/src/public/sendbird_uikit.dart @@ -2,11 +2,12 @@ import 'dart:async'; -import 'package:flutter/widgets.dart'; +import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:provider/provider.dart'; import 'package:sendbird_chat_sdk/sendbird_chat_sdk.dart'; import 'package:sendbird_uikit/sendbird_uikit.dart'; +import 'package:sendbird_uikit/src/internal/component/dialog/sbu_delayed_connecting_dialog.dart'; import 'package:sendbird_uikit/src/internal/provider/sbu_group_channel_collection_provider.dart'; import 'package:sendbird_uikit/src/internal/provider/sbu_message_collection_provider.dart'; import 'package:sendbird_uikit/src/internal/resource/sbu_text_styles.dart'; @@ -20,7 +21,7 @@ import 'package:sendbird_uikit/src/internal/utils/sbu_reply_manager.dart'; /// SendbirdUIKit class SendbirdUIKit { /// UIKit version - static const version = '1.1.0'; + static const version = '1.2.0'; SendbirdUIKit._(); @@ -30,6 +31,11 @@ class SendbirdUIKit { bool _isInitialized = false; + // DelayedConnectingDialog + GlobalKey? _navigatorKey; + BuildContext? _currentDialogContext; + bool? _addCloseButtonInDelayedConnectingDialog; + Future Function()? _takePhoto; Future Function()? get takePhoto => _takePhoto; @@ -97,6 +103,9 @@ class SendbirdUIKit { required String appId, SendbirdChatOptions? options, SBUTheme? theme, + GlobalKey? + navigatorKey, // To show the delayed connecting dialog + bool addCloseButtonInDelayedConnectingDialog = false, Future Function()? takePhoto, Future Function()? takeVideo, Future Function()? choosePhoto, @@ -119,11 +128,18 @@ class SendbirdUIKit { options: options, ); + SendbirdChat.addConnectionHandler( + 'sendbird-uikit-flutter', _MyConnectionHandler()); + await SBUPreferences().initialize(); if (theme != null) { SBUThemeProvider().setTheme(theme); } + _uikit._navigatorKey = navigatorKey; + _uikit._addCloseButtonInDelayedConnectingDialog = + addCloseButtonInDelayedConnectingDialog; + _uikit._takePhoto = takePhoto; _uikit._takeVideo = takeVideo; _uikit._choosePhoto = choosePhoto; @@ -204,4 +220,64 @@ class SendbirdUIKit { static void setFontFamily(String fontFamily) { SBUTextStyles.fontFamily = fontFamily; } + + // Shows the delayed connecting dialog with retryAfter time. + static void _showDelayedConnectingDialog(int retryAfter) { + final context = _uikit._navigatorKey?.currentContext; + if (context != null) { + if (_uikit._currentDialogContext != null) { + Navigator.of(_uikit._currentDialogContext!, rootNavigator: true).pop(); + _uikit._currentDialogContext = null; + } + + showDialog( + context: context, + barrierDismissible: false, + builder: (dialogContext) { + _uikit._currentDialogContext = dialogContext; + return SBUDelayedConnectingDialog( + retryAfter: retryAfter, + showCloseButton: + _uikit._addCloseButtonInDelayedConnectingDialog ?? false, + ); + }, + ).then((_) { + _uikit._currentDialogContext = null; + }); + } + } + + // Dismisses the delayed connecting dialog if it is currently shown. + static void _dismissDelayedConnectingDialog() { + if (_uikit._currentDialogContext != null) { + Navigator.of(_uikit._currentDialogContext!, rootNavigator: true).pop(); + _uikit._currentDialogContext = null; + } + } +} + +class _MyConnectionHandler extends ConnectionHandler { + @override + void onConnected(String userId) {} + + @override + void onDisconnected(String userId) {} + + @override + void onReconnectFailed() {} + + @override + void onReconnectStarted() {} + + @override + void onReconnectSucceeded() { + SendbirdUIKit._dismissDelayedConnectingDialog(); + } + + @override + void onConnectionDelayed(int retryAfter) { + if (retryAfter > 0) { + SendbirdUIKit._showDelayedConnectingDialog(retryAfter); + } + } } diff --git a/pubspec.yaml b/pubspec.yaml index 70267c7..700eb82 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: sendbird_uikit description: With Sendbird UIKit for Flutter, you can easily build an in-app chat with all the essential messaging features. -version: 1.1.0 +version: 1.2.0 homepage: https://sendbird.com repository: https://github.com/sendbird/sendbird-uikit-flutter documentation: https://sendbird.com/docs/chat/uikit/v3/flutter/overview @@ -17,7 +17,7 @@ dependencies: flutter: sdk: flutter - sendbird_chat_sdk: ^4.5.1 + sendbird_chat_sdk: ^4.7.0 provider: ^6.1.2 intl: '>=0.18.1 <1.0.0' collection: ^1.18.0