From bcce59b7f957310c2ec90d22a1601060798161fb Mon Sep 17 00:00:00 2001 From: 3nln Date: Tue, 5 May 2026 10:39:37 +0500 Subject: [PATCH 1/3] feat: implement UcharSnackBar for consistent notification handling --- lib/config/themes.dart | 24 +++-- lib/pages/chat/chat.dart | 38 ++++---- lib/pages/chat_list/chat_list.dart | 31 +++--- lib/utils/fluffy_share.dart | 6 +- lib/utils/show_update_snackbar.dart | 17 ++-- lib/utils/url_launcher.dart | 12 ++- lib/widgets/uchar_snack_bar.dart | 143 ++++++++++++++++++++++++++++ 7 files changed, 205 insertions(+), 66 deletions(-) create mode 100644 lib/widgets/uchar_snack_bar.dart diff --git a/lib/config/themes.dart b/lib/config/themes.dart index 7c7d213a0c..65fe5dfab1 100644 --- a/lib/config/themes.dart +++ b/lib/config/themes.dart @@ -117,13 +117,23 @@ abstract class FluffyThemes { color: colorScheme.primary, refreshBackgroundColor: colorScheme.primaryContainer, ), - snackBarTheme: isColumnMode - ? const SnackBarThemeData( - showCloseIcon: true, - behavior: SnackBarBehavior.floating, - width: FluffyThemes.columnWidth * 1.5, - ) - : const SnackBarThemeData(behavior: SnackBarBehavior.floating), + snackBarTheme: SnackBarThemeData( + behavior: SnackBarBehavior.floating, + showCloseIcon: true, + closeIconColor: colorScheme.onSurfaceVariant, + backgroundColor: colorScheme.surfaceContainerHigh, + contentTextStyle: TextStyle( + color: colorScheme.onSurface, + fontSize: 14, + fontWeight: FontWeight.w500, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + elevation: 4, + width: isColumnMode ? FluffyThemes.columnWidth * 1.5 : null, + insetPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16), + ), elevatedButtonTheme: ElevatedButtonThemeData( style: ElevatedButton.styleFrom( backgroundColor: colorScheme.secondaryContainer, diff --git a/lib/pages/chat/chat.dart b/lib/pages/chat/chat.dart index 5202964ae1..f50b4d8c1a 100644 --- a/lib/pages/chat/chat.dart +++ b/lib/pages/chat/chat.dart @@ -43,6 +43,7 @@ import 'package:fluffychat/widgets/adaptive_dialogs/show_text_input_dialog.dart' import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; import 'package:fluffychat/widgets/share_scaffold_dialog.dart'; +import 'package:fluffychat/widgets/uchar_snack_bar.dart'; import '../../utils/account_bundles.dart'; import '../../utils/localized_exception_extension.dart'; import '../../utils/element_call/call_service.dart'; @@ -342,17 +343,9 @@ class ChatController extends State final shareItems = widget.shareItems; if (shareItems == null || shareItems.isEmpty) return; if (!room.otherPartyCanReceiveMessages) { - final theme = Theme.of(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - backgroundColor: theme.colorScheme.errorContainer, - closeIconColor: theme.colorScheme.onErrorContainer, - content: Text( - L10n.of(context).otherPartyNotLoggedIn, - style: TextStyle(color: theme.colorScheme.onErrorContainer), - ), - showCloseIcon: true, - ), + ScaffoldMessenger.of(context).showUcharSnackBar( + message: L10n.of(context).otherPartyNotLoggedIn, + type: UcharNotificationType.error, ); return; } @@ -872,8 +865,9 @@ class ChatController extends State ); } catch (e) { if (!mounted) return; - scaffoldMessenger.showSnackBar( - SnackBar(content: Text((e as Object).toLocalizedString(context))), + scaffoldMessenger.showUcharSnackBar( + message: (e as Object).toLocalizedString(context), + type: UcharNotificationType.error, ); return; } @@ -977,8 +971,9 @@ class ChatController extends State showEmojiPicker = false; selectedEvents.clear(); }); - scaffoldMessenger.showSnackBar( - SnackBar(content: Text(l10n.contentHasBeenReported)), + scaffoldMessenger.showUcharSnackBar( + message: l10n.contentHasBeenReported, + type: UcharNotificationType.success, ); } @@ -1510,8 +1505,9 @@ class ChatController extends State // Check if user has permission to join/start calls if (!CallService.canJoinCall(room)) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(L10n.of(context).noCallPermission)), + ScaffoldMessenger.of(context).showUcharSnackBar( + message: L10n.of(context).noCallPermission, + type: UcharNotificationType.warning, ); } return; @@ -1523,12 +1519,10 @@ class ChatController extends State if (!cameraStatus.isGranted || !micStatus.isGranted) { if (context.mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( + ScaffoldMessenger.of(context).showUcharSnackBar( + message: 'Camera and microphone permissions are required for calls', - ), - ), + type: UcharNotificationType.warning, ); } return; diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 30cc55bb9b..1609f3cd45 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -33,6 +33,7 @@ import '../../../utils/account_bundles.dart'; import '../../config/setting_keys.dart'; import '../../utils/url_launcher.dart'; import '../../widgets/matrix.dart'; +import '../../widgets/uchar_snack_bar.dart'; enum PopupMenuAction { settings, @@ -956,27 +957,17 @@ class ChatListController extends State (device) => !device.verified && !device.blocked, ) ?? false) { + final messenger = ScaffoldMessenger.of(context); late final ScaffoldFeatureController controller; - final theme = Theme.of(context); - controller = ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - duration: const Duration(seconds: 15), - showCloseIcon: true, - backgroundColor: theme.colorScheme.errorContainer, - closeIconColor: theme.colorScheme.onErrorContainer, - content: Text( - L10n.of(context).oneOfYourDevicesIsNotVerified, - style: TextStyle(color: theme.colorScheme.onErrorContainer), - ), - action: SnackBarAction( - onPressed: () { - controller.close(); - router.go('/rooms/settings/devices'); - }, - textColor: theme.colorScheme.onErrorContainer, - label: L10n.of(context).settings, - ), - ), + controller = messenger.showUcharSnackBar( + message: L10n.of(context).oneOfYourDevicesIsNotVerified, + type: UcharNotificationType.error, + actionLabel: L10n.of(context).settings, + onAction: () { + controller.close(); + router.go('/rooms/settings/devices'); + }, + duration: const Duration(seconds: 15), ); } } diff --git a/lib/utils/fluffy_share.dart b/lib/utils/fluffy_share.dart index 4885393761..0771a7cd8f 100644 --- a/lib/utils/fluffy_share.dart +++ b/lib/utils/fluffy_share.dart @@ -1,5 +1,6 @@ import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/uchar_snack_bar.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:share_plus/share_plus.dart'; @@ -26,8 +27,9 @@ abstract class FluffyShare { } await Clipboard.setData(ClipboardData(text: text)); if (!PlatformInfos.isMobile) { - scaffoldMessenger.showSnackBar( - SnackBar(showCloseIcon: true, content: Text(l10n.copiedToClipboard)), + scaffoldMessenger.showUcharSnackBar( + message: l10n.copiedToClipboard, + type: UcharNotificationType.success, ); } return; diff --git a/lib/utils/show_update_snackbar.dart b/lib/utils/show_update_snackbar.dart index fda1719d0c..795d3a6b67 100644 --- a/lib/utils/show_update_snackbar.dart +++ b/lib/utils/show_update_snackbar.dart @@ -1,6 +1,7 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/l10n/l10n.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/uchar_snack_bar.dart'; import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:url_launcher/url_launcher_string.dart'; @@ -17,16 +18,12 @@ abstract class UpdateNotifier { if (currentVersion != storedVersion) { if (storedVersion != null) { - scaffoldMessenger.showSnackBar( - SnackBar( - duration: const Duration(seconds: 30), - showCloseIcon: true, - content: Text(l10n.updateInstalled(currentVersion)), - action: SnackBarAction( - label: l10n.changelog, - onPressed: () => launchUrlString(AppConfig.changelogUrl), - ), - ), + scaffoldMessenger.showUcharSnackBar( + message: l10n.updateInstalled(currentVersion), + type: UcharNotificationType.success, + actionLabel: l10n.changelog, + onAction: () => launchUrlString(AppConfig.changelogUrl), + duration: const Duration(seconds: 30), ); } await store.setString(versionStoreKey, currentVersion); diff --git a/lib/utils/url_launcher.dart b/lib/utils/url_launcher.dart index 34649cd154..7f9d490c4f 100644 --- a/lib/utils/url_launcher.dart +++ b/lib/utils/url_launcher.dart @@ -5,6 +5,7 @@ import 'package:fluffychat/widgets/adaptive_dialogs/show_ok_cancel_alert_dialog. import 'package:fluffychat/widgets/adaptive_dialogs/user_dialog.dart'; import 'package:fluffychat/widgets/future_loading_dialog.dart'; import 'package:fluffychat/widgets/matrix.dart'; +import 'package:fluffychat/widgets/uchar_snack_bar.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:matrix/matrix.dart'; @@ -37,9 +38,9 @@ class UrlLauncher { } final uri = Uri.tryParse(url!); if (uri == null) { - // we can't open this thing - scaffoldMessenger.showSnackBar( - SnackBar(content: Text(l10n.cantOpenUri(url!))), + scaffoldMessenger.showUcharSnackBar( + message: l10n.cantOpenUri(url!), + type: UcharNotificationType.error, ); return; } @@ -92,8 +93,9 @@ class UrlLauncher { return; } if (uri.host.isEmpty) { - scaffoldMessenger.showSnackBar( - SnackBar(content: Text(l10n.cantOpenUri(url!))), + scaffoldMessenger.showUcharSnackBar( + message: l10n.cantOpenUri(url!), + type: UcharNotificationType.error, ); return; } diff --git a/lib/widgets/uchar_snack_bar.dart b/lib/widgets/uchar_snack_bar.dart new file mode 100644 index 0000000000..f7ba850bcd --- /dev/null +++ b/lib/widgets/uchar_snack_bar.dart @@ -0,0 +1,143 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_tabler_icons/flutter_tabler_icons.dart'; + +enum UcharNotificationType { info, success, warning, error } + +class UcharSnackBarContent extends StatelessWidget { + final String message; + final UcharNotificationType type; + final String? actionLabel; + final VoidCallback? onAction; + + const UcharSnackBarContent({ + super.key, + required this.message, + this.type = UcharNotificationType.info, + this.actionLabel, + this.onAction, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final colors = _resolveColors(theme); + + return Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: colors.iconBg, + borderRadius: BorderRadius.circular(10), + ), + child: Icon(_resolveIcon(), size: 20, color: colors.iconFg), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + message, + style: theme.textTheme.bodyMedium?.copyWith( + color: colors.text, + fontWeight: FontWeight.w500, + ), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + if (actionLabel != null && onAction != null) ...[ + const SizedBox(width: 8), + TextButton( + onPressed: onAction, + style: TextButton.styleFrom( + foregroundColor: colors.action, + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + textStyle: const TextStyle( + fontWeight: FontWeight.w700, + fontSize: 13, + ), + ), + child: Text(actionLabel!), + ), + ], + ], + ); + } + + IconData _resolveIcon() => switch (type) { + UcharNotificationType.error => TablerIcons.alert_circle, + UcharNotificationType.warning => TablerIcons.alert_triangle, + UcharNotificationType.success => TablerIcons.circle_check, + UcharNotificationType.info => TablerIcons.info_circle, + }; + + _NotificationColors _resolveColors(ThemeData theme) => switch (type) { + UcharNotificationType.error => _NotificationColors( + iconBg: theme.colorScheme.errorContainer, + iconFg: theme.colorScheme.onErrorContainer, + text: theme.colorScheme.onErrorContainer, + action: theme.colorScheme.error, + ), + UcharNotificationType.warning => _NotificationColors( + iconBg: theme.colorScheme.tertiaryContainer, + iconFg: theme.colorScheme.onTertiaryContainer, + text: theme.colorScheme.onTertiaryContainer, + action: theme.colorScheme.tertiary, + ), + UcharNotificationType.success => _NotificationColors( + iconBg: theme.colorScheme.secondaryContainer, + iconFg: theme.colorScheme.onSecondaryContainer, + text: theme.colorScheme.onSecondaryContainer, + action: theme.colorScheme.secondary, + ), + UcharNotificationType.info => _NotificationColors( + iconBg: theme.colorScheme.primaryContainer, + iconFg: theme.colorScheme.onPrimaryContainer, + text: theme.colorScheme.onPrimaryContainer, + action: theme.colorScheme.primary, + ), + }; +} + +class _NotificationColors { + final Color iconBg; + final Color iconFg; + final Color text; + final Color action; + + const _NotificationColors({ + required this.iconBg, + required this.iconFg, + required this.text, + required this.action, + }); +} + +extension UcharScaffoldMessenger on ScaffoldMessengerState { + ScaffoldFeatureController showUcharSnackBar({ + required String message, + UcharNotificationType type = UcharNotificationType.info, + String? actionLabel, + VoidCallback? onAction, + Duration duration = const Duration(seconds: 6), + bool showCloseIcon = true, + }) { + late ScaffoldFeatureController controller; + controller = showSnackBar( + SnackBar( + duration: duration, + showCloseIcon: showCloseIcon, + content: UcharSnackBarContent( + message: message, + type: type, + actionLabel: actionLabel, + onAction: onAction, + ), + ), + ); + return controller; + } +} From d09db0d49ea3dc49f0ef7a65933f167a724e1821 Mon Sep 17 00:00:00 2001 From: 3nln Date: Tue, 5 May 2026 10:53:57 +0500 Subject: [PATCH 2/3] feat: update Snackbar theme for improved visibility and styling --- lib/config/themes.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lib/config/themes.dart b/lib/config/themes.dart index 65fe5dfab1..9fcfd886f2 100644 --- a/lib/config/themes.dart +++ b/lib/config/themes.dart @@ -121,7 +121,7 @@ abstract class FluffyThemes { behavior: SnackBarBehavior.floating, showCloseIcon: true, closeIconColor: colorScheme.onSurfaceVariant, - backgroundColor: colorScheme.surfaceContainerHigh, + backgroundColor: colorScheme.surfaceContainerLowest, contentTextStyle: TextStyle( color: colorScheme.onSurface, fontSize: 14, @@ -129,8 +129,12 @@ abstract class FluffyThemes { ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), + side: BorderSide( + color: colorScheme.outlineVariant.withAlpha(40), + width: 1, + ), ), - elevation: 4, + elevation: 2, width: isColumnMode ? FluffyThemes.columnWidth * 1.5 : null, insetPadding: const EdgeInsets.fromLTRB(16, 0, 16, 16), ), From 9f641e1fdcc9ad64d454087c74a6acccb154391b Mon Sep 17 00:00:00 2001 From: 3nln Date: Fri, 8 May 2026 01:02:04 +0500 Subject: [PATCH 3/3] feat: empty page text use instead of image --- lib/widgets/layouts/empty_page.dart | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/widgets/layouts/empty_page.dart b/lib/widgets/layouts/empty_page.dart index 5a0be7862f..02329a5bfc 100644 --- a/lib/widgets/layouts/empty_page.dart +++ b/lib/widgets/layouts/empty_page.dart @@ -1,30 +1,30 @@ -import 'dart:math'; - import 'package:flutter/material.dart'; class EmptyPage extends StatelessWidget { - static const double _width = 400; const EmptyPage({super.key}); @override Widget build(BuildContext context) { - final width = min(MediaQuery.sizeOf(context).width, EmptyPage._width) / 2; final theme = Theme.of(context); return Scaffold( - // Add invisible appbar to make status bar on Android tablets bright. appBar: AppBar( automaticallyImplyLeading: false, elevation: 0, backgroundColor: Colors.transparent, ), extendBodyBehindAppBar: true, - body: Container( - alignment: Alignment.center, - child: Image.asset( - 'assets/logo_transparent.png', - color: theme.colorScheme.surfaceContainerHigh, - width: width, - height: width, - filterQuality: FilterQuality.medium, + body: Center( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 1), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(24), + ), + child: Text( + 'Select a chat to start messaging', + style: theme.textTheme.titleSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), ), ), );