From d6c7dadb2427ff9b1450665897a9390472973e71 Mon Sep 17 00:00:00 2001 From: Christian Pauly Date: Sun, 11 Sep 2022 11:07:04 +0200 Subject: [PATCH] chore: Add unread badge to navigation rail and adjust design --- lib/config/themes.dart | 5 +- lib/pages/chat/chat_view.dart | 9 +- lib/pages/chat_list/chat_list.dart | 67 ++++++-------- lib/pages/chat_list/chat_list_view.dart | 108 ++++++++++++++-------- lib/widgets/unread_badge_back_button.dart | 58 ------------ lib/widgets/unread_rooms_badge.dart | 58 ++++++++++++ pubspec.lock | 7 ++ pubspec.yaml | 1 + 8 files changed, 171 insertions(+), 142 deletions(-) delete mode 100644 lib/widgets/unread_badge_back_button.dart create mode 100644 lib/widgets/unread_rooms_badge.dart diff --git a/lib/config/themes.dart b/lib/config/themes.dart index a1f01793..a29f50c9 100644 --- a/lib/config/themes.dart +++ b/lib/config/themes.dart @@ -4,7 +4,6 @@ import 'package:flutter/services.dart'; import 'package:vrouter/vrouter.dart'; import 'package:fluffychat/utils/platform_infos.dart'; -import '../widgets/matrix.dart'; import 'app_config.dart'; abstract class FluffyThemes { @@ -16,9 +15,7 @@ abstract class FluffyThemes { isColumnModeByWidth(MediaQuery.of(context).size.width); static bool getDisplayNavigationRail(BuildContext context) => - !VRouter.of(context).path.startsWith('/settings') && - (Matrix.of(context).client.rooms.any((room) => room.isSpace) || - AppConfig.separateChatTypes); + !VRouter.of(context).path.startsWith('/settings'); static const fallbackTextStyle = TextStyle( fontFamily: 'Roboto', diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 907de5c4..e5c7bfd2 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:badges/badges.dart'; import 'package:desktop_drop/desktop_drop.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:future_loading_dialog/future_loading_dialog.dart'; @@ -19,7 +20,7 @@ import 'package:fluffychat/pages/chat/tombstone_display.dart'; import 'package:fluffychat/widgets/chat_settings_popup_menu.dart'; import 'package:fluffychat/widgets/connection_status_header.dart'; import 'package:fluffychat/widgets/matrix.dart'; -import 'package:fluffychat/widgets/unread_badge_back_button.dart'; +import 'package:fluffychat/widgets/unread_rooms_badge.dart'; import '../../utils/stream_extension.dart'; import '../../widgets/m2_popup_menu_button.dart'; import 'chat_emoji_picker.dart'; @@ -179,7 +180,11 @@ class ChatView extends StatelessWidget { tooltip: L10n.of(context)!.close, color: Theme.of(context).colorScheme.primary, ) - : UnreadBadgeBackButton(roomId: controller.roomId!), + : UnreadRoomsBadge( + filter: (r) => r.id != controller.roomId!, + badgePosition: BadgePosition.topEnd(end: 8, top: 4), + child: const Center(child: BackButton()), + ), titleSpacing: 0, title: ChatAppBarTitle(controller), actions: _appBarActions(context), diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 42682e24..9037c9cf 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -101,64 +101,55 @@ class ChatListController extends State } } - void onDestinationSelected(int? i) { + ActiveFilter getActiveFilterByDestination(int? i) { switch (i) { - case 0: - if (AppConfig.separateChatTypes) { - setState(() { - activeFilter = ActiveFilter.groups; - }); - } else { - setState(() { - activeFilter = ActiveFilter.allChats; - }); - } - break; case 1: if (AppConfig.separateChatTypes) { - setState(() { - activeFilter = ActiveFilter.messages; - }); - } else { - setState(() { - activeFilter = ActiveFilter.spaces; - }); + return ActiveFilter.messages; } - break; + return ActiveFilter.spaces; case 2: - setState(() { - activeFilter = ActiveFilter.spaces; - }); - break; + return ActiveFilter.spaces; + case 0: + default: + if (AppConfig.separateChatTypes) { + return ActiveFilter.groups; + } + return ActiveFilter.allChats; } } + void onDestinationSelected(int? i) { + setState(() { + activeFilter = getActiveFilterByDestination(i); + }); + } + ActiveFilter activeFilter = AppConfig.separateChatTypes ? ActiveFilter.messages : ActiveFilter.allChats; - List get filteredRooms { - final rooms = Matrix.of(context).client.rooms; + bool Function(Room) getRoomFilterByActiveFilter(ActiveFilter activeFilter) { switch (activeFilter) { case ActiveFilter.allChats: - return rooms - .where((room) => !room.isSpace && !room.isStoryRoom) - .toList(); + return (room) => !room.isSpace && !room.isStoryRoom; case ActiveFilter.groups: - return rooms - .where((room) => - !room.isSpace && !room.isDirectChat && !room.isStoryRoom) - .toList(); + return (room) => + !room.isSpace && !room.isDirectChat && !room.isStoryRoom; case ActiveFilter.messages: - return rooms - .where((room) => - !room.isSpace && room.isDirectChat && !room.isStoryRoom) - .toList(); + return (room) => + !room.isSpace && room.isDirectChat && !room.isStoryRoom; case ActiveFilter.spaces: - return rooms.where((room) => room.isSpace).toList(); + return (r) => r.isSpace; } } + List get filteredRooms => Matrix.of(context) + .client + .rooms + .where(getRoomFilterByActiveFilter(activeFilter)) + .toList(); + bool isSearchMode = false; Future? publicRoomsResponse; String? searchServer; diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index 019a43c5..bc9f5990 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:badges/badges.dart'; import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; import 'package:vrouter/vrouter.dart'; @@ -9,6 +10,7 @@ import 'package:fluffychat/config/app_config.dart'; import 'package:fluffychat/config/themes.dart'; import 'package:fluffychat/pages/chat_list/chat_list.dart'; import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/unread_rooms_badge.dart'; import '../../widgets/matrix.dart'; import 'chat_list_body.dart'; import 'chat_list_header.dart'; @@ -19,32 +21,62 @@ class ChatListView extends StatelessWidget { const ChatListView(this.controller, {Key? key}) : super(key: key); - List getNavigationDestinations(BuildContext context) => - [ - if (AppConfig.separateChatTypes) ...[ - NavigationDestination( - icon: const Icon(Icons.groups_outlined), - selectedIcon: const Icon(Icons.groups), - label: L10n.of(context)!.groups, + List getNavigationDestinations(BuildContext context) { + final badgePosition = BadgePosition.topEnd(top: -12, end: -8); + return [ + if (AppConfig.separateChatTypes) ...[ + NavigationDestination( + icon: UnreadRoomsBadge( + badgePosition: badgePosition, + filter: controller.getRoomFilterByActiveFilter(ActiveFilter.groups), + child: const Icon(Icons.groups_outlined), ), - NavigationDestination( - icon: const Icon(Icons.chat_outlined), - selectedIcon: const Icon(Icons.chat), - label: L10n.of(context)!.messages, + selectedIcon: UnreadRoomsBadge( + badgePosition: badgePosition, + filter: controller.getRoomFilterByActiveFilter(ActiveFilter.groups), + child: const Icon(Icons.groups), ), - ] else - NavigationDestination( - icon: const Icon(Icons.chat_outlined), - selectedIcon: const Icon(Icons.chat), - label: L10n.of(context)!.chats, + label: L10n.of(context)!.groups, + ), + NavigationDestination( + icon: UnreadRoomsBadge( + badgePosition: badgePosition, + filter: + controller.getRoomFilterByActiveFilter(ActiveFilter.messages), + child: const Icon(Icons.chat_outlined), ), - if (controller.spaces.isNotEmpty) - const NavigationDestination( - icon: Icon(Icons.workspaces_outlined), - selectedIcon: Icon(Icons.workspaces), - label: 'Spaces', + selectedIcon: UnreadRoomsBadge( + badgePosition: badgePosition, + filter: + controller.getRoomFilterByActiveFilter(ActiveFilter.messages), + child: const Icon(Icons.chat), ), - ]; + label: L10n.of(context)!.messages, + ), + ] else + NavigationDestination( + icon: UnreadRoomsBadge( + badgePosition: badgePosition, + filter: + controller.getRoomFilterByActiveFilter(ActiveFilter.allChats), + child: const Icon(Icons.chat_outlined), + ), + selectedIcon: UnreadRoomsBadge( + badgePosition: badgePosition, + filter: + controller.getRoomFilterByActiveFilter(ActiveFilter.allChats), + child: const Icon(Icons.chat), + ), + label: L10n.of(context)!.chats, + ), + if (controller.spaces.isNotEmpty) + const NavigationDestination( + icon: Icon(Icons.workspaces_outlined), + selectedIcon: Icon(Icons.workspaces), + label: 'Spaces', + ), + ]; + } @override Widget build(BuildContext context) { @@ -102,11 +134,6 @@ class ChatListView extends StatelessWidget { height: 64, width: 64, decoration: BoxDecoration( - color: isSelected - ? Theme.of(context) - .colorScheme - .secondaryContainer - : Theme.of(context).colorScheme.background, border: Border( bottom: i == (destinations.length - 1) ? BorderSide( @@ -129,15 +156,21 @@ class ChatListView extends StatelessWidget { alignment: Alignment.center, child: IconButton( color: isSelected - ? Theme.of(context).colorScheme.primary + ? Theme.of(context).colorScheme.secondary : null, icon: CircleAvatar( - backgroundColor: Theme.of(context) - .colorScheme - .secondaryContainer, - foregroundColor: Theme.of(context) - .colorScheme - .onSecondaryContainer, + backgroundColor: isSelected + ? Theme.of(context).colorScheme.secondary + : Theme.of(context) + .colorScheme + .background, + foregroundColor: isSelected + ? Theme.of(context) + .colorScheme + .onSecondary + : Theme.of(context) + .colorScheme + .onBackground, child: i == controller.selectedIndex ? destinations[i].selectedIcon ?? destinations[i].icon @@ -156,15 +189,10 @@ class ChatListView extends StatelessWidget { height: 64, width: 64, decoration: BoxDecoration( - color: isSelected - ? Theme.of(context) - .colorScheme - .secondaryContainer - : Theme.of(context).colorScheme.background, border: Border( left: BorderSide( color: isSelected - ? Theme.of(context).colorScheme.primary + ? Theme.of(context).colorScheme.secondary : Colors.transparent, width: 4, ), diff --git a/lib/widgets/unread_badge_back_button.dart b/lib/widgets/unread_badge_back_button.dart deleted file mode 100644 index 0a4b71a4..00000000 --- a/lib/widgets/unread_badge_back_button.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:matrix/matrix.dart'; - -import '../config/app_config.dart'; -import 'matrix.dart'; - -class UnreadBadgeBackButton extends StatelessWidget { - final String roomId; - - const UnreadBadgeBackButton({ - Key? key, - required this.roomId, - }) : super(key: key); - - @override - Widget build(BuildContext context) { - return Stack( - children: [ - StreamBuilder( - stream: Matrix.of(context).client.onSync.stream, - builder: (context, _) { - final unreadCount = Matrix.of(context) - .client - .rooms - .where((r) => - r.id != roomId && - (r.isUnread || r.membership == Membership.invite)) - .length; - return unreadCount > 0 - ? Align( - alignment: Alignment.bottomRight, - child: Container( - padding: const EdgeInsets.all(4), - margin: const EdgeInsets.only(bottom: 4, right: 8), - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primaryContainer, - borderRadius: - BorderRadius.circular(AppConfig.borderRadius), - ), - child: Text( - '$unreadCount', - style: TextStyle( - fontSize: 12, - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), - ), - ), - ) - : Container(); - }), - const Center(child: BackButton()), - ], - ); - } -} diff --git a/lib/widgets/unread_rooms_badge.dart b/lib/widgets/unread_rooms_badge.dart new file mode 100644 index 00000000..828b87f9 --- /dev/null +++ b/lib/widgets/unread_rooms_badge.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; + +import 'package:badges/badges.dart'; +import 'package:matrix/matrix.dart'; + +import 'matrix.dart'; + +class UnreadRoomsBadge extends StatelessWidget { + final bool Function(Room) filter; + final BadgePosition? badgePosition; + final Widget? child; + + const UnreadRoomsBadge({ + Key? key, + required this.filter, + this.badgePosition, + this.child, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: Matrix.of(context) + .client + .onSync + .stream + .where((syncUpdate) => syncUpdate.hasRoomUpdate), + builder: (context, _) { + final unreadCount = Matrix.of(context) + .client + .rooms + .where(filter) + .where((r) => (r.isUnread || r.membership == Membership.invite)) + .length; + return Badge( + alignment: Alignment.bottomRight, + badgeContent: Text( + unreadCount.toString(), + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimary, + fontSize: 12, + ), + ), + showBadge: unreadCount != 0, + animationType: BadgeAnimationType.scale, + badgeColor: Theme.of(context).colorScheme.primary, + position: badgePosition, + elevation: 4, + borderSide: BorderSide( + color: Theme.of(context).colorScheme.background, + width: 2, + strokeAlign: StrokeAlign.outside, + ), + child: child, + ); + }); + } +} diff --git a/pubspec.lock b/pubspec.lock index 39bc2090..1cffb526 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -85,6 +85,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.6+1" + badges: + dependency: "direct main" + description: + name: badges + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.3" base58check: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4bd926ff..1f039de6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,6 +11,7 @@ dependencies: adaptive_theme: ^3.0.0 animations: ^2.0.2 async: ^2.8.2 + badges: ^2.0.3 blurhash_dart: ^1.1.0 callkeep: ^0.3.2 chewie: ^1.2.2