From 254f21ce00251be883c7aae1fdfc1aafa868252b Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 15 Jul 2024 13:18:15 +0200 Subject: [PATCH 01/15] chore: Follow up new spaces design --- lib/pages/chat_list/chat_list.dart | 2 +- lib/pages/chat_list/chat_list_body.dart | 26 ++---- lib/pages/chat_list/chat_list_item.dart | 118 +++++++++++++++--------- lib/pages/chat_list/chat_list_view.dart | 4 + lib/widgets/avatar.dart | 7 +- 5 files changed, 90 insertions(+), 67 deletions(-) diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 9181b1dff..28e78dfe6 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -200,7 +200,7 @@ class ChatListController extends State if (result.error != null) return; } - void onChatTap(Room room, BuildContext context) async { + void onChatTap(Room room) async { if (room.isSpace) { setActiveSpace(room.id); return; diff --git a/lib/pages/chat_list/chat_list_body.dart b/lib/pages/chat_list/chat_list_body.dart index a6526989b..fd063ddba 100644 --- a/lib/pages/chat_list/chat_list_body.dart +++ b/lib/pages/chat_list/chat_list_body.dart @@ -32,7 +32,7 @@ class ChatListViewBody extends StatelessWidget { return SpaceView( spaceId: activeSpace, onBack: controller.clearActiveSpace, - onChatTab: (room) => controller.onChatTap(room, context), + onChatTab: (room) => controller.onChatTap(room), onChatContext: (room) => controller.chatContextAction(room), activeChat: controller.activeChat, toParentSpace: controller.setActiveSpace, @@ -62,17 +62,15 @@ class ChatListViewBody extends StatelessWidget { builder: (context, _) { final rooms = controller.filteredRooms; - final spaces = rooms.where((r) => r.isSpace); + final spaces = client.rooms.where((r) => r.isSpace); final spaceDelegateCandidates = {}; for (final space in spaces) { - spaceDelegateCandidates[space.id] = space; for (final spaceChild in space.spaceChildren) { final roomId = spaceChild.roomId; if (roomId == null) continue; spaceDelegateCandidates[roomId] = space; } } - final spaceDelegates = {}; return SafeArea( child: CustomScrollView( @@ -298,26 +296,14 @@ class ChatListViewBody extends StatelessWidget { SliverList.builder( itemCount: rooms.length, itemBuilder: (BuildContext context, int i) { - var room = rooms[i]; - if (controller.activeFilter != ActiveFilter.groups) { - final parent = room.isSpace - ? room - : spaceDelegateCandidates[room.id]; - if (parent != null) { - if (spaceDelegates.contains(parent.id)) { - return const SizedBox.shrink(); - } - spaceDelegates.add(parent.id); - room = parent; - } - } - + final room = rooms[i]; + final space = spaceDelegateCandidates[room.id]; return ChatListItem( room, - lastEventRoom: rooms[i], + space: space, key: Key('chat_list_item_${room.id}'), filter: filter, - onTap: () => controller.onChatTap(room, context), + onTap: () => controller.onChatTap(room), onLongPress: () => controller.chatContextAction(room), activeChat: controller.activeChat == room.id, ); diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 7bf5f6b52..2fcce1839 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -17,7 +17,7 @@ enum ArchivedRoomAction { delete, rejoin } class ChatListItem extends StatelessWidget { final Room room; - final Room? lastEventRoom; + final Room? space; final bool activeChat; final void Function()? onLongPress; final void Function()? onForget; @@ -31,7 +31,7 @@ class ChatListItem extends StatelessWidget { this.onLongPress, this.onForget, this.filter, - this.lastEventRoom, + this.space, super.key, }); @@ -64,21 +64,19 @@ class ChatListItem extends StatelessWidget { @override Widget build(BuildContext context) { final isMuted = room.pushRuleState != PushRuleState.notify; - final lastEventRoom = this.lastEventRoom ?? room; - final typingText = lastEventRoom.getLocalizedTypingText(context); - final lastEvent = lastEventRoom.lastEvent; + final typingText = room.getLocalizedTypingText(context); + final lastEvent = room.lastEvent; final ownMessage = lastEvent?.senderId == room.client.userID; - final unread = - lastEventRoom.isUnread || lastEventRoom.membership == Membership.invite; + final unread = room.isUnread || room.membership == Membership.invite; final theme = Theme.of(context); final directChatMatrixId = room.directChatMatrixID; final isDirectChat = directChatMatrixId != null; - final unreadBubbleSize = unread || lastEventRoom.hasNewMessages - ? lastEventRoom.notificationCount > 0 + final unreadBubbleSize = unread || room.hasNewMessages + ? room.notificationCount > 0 ? 20.0 : 14.0 : 0.0; - final hasNotifications = lastEventRoom.notificationCount > 0; + final hasNotifications = room.notificationCount > 0; final backgroundColor = activeChat ? theme.colorScheme.secondaryContainer : null; final displayname = room.getLocalizedDisplayname( @@ -92,6 +90,7 @@ class ChatListItem extends StatelessWidget { final needLastEventSender = lastEvent == null ? false : room.getState(EventTypes.RoomMember, lastEvent.senderId) == null; + final space = this.space; return Padding( padding: const EdgeInsets.symmetric( @@ -117,15 +116,67 @@ class ChatListItem extends StatelessWidget { duration: FluffyThemes.animationDuration, curve: FluffyThemes.animationCurve, scale: hovered ? 1.1 : 1.0, - child: Avatar( - borderRadius: room.isSpace - ? BorderRadius.circular(AppConfig.borderRadius / 3) - : null, - mxContent: room.avatar, - name: displayname, - presenceUserId: directChatMatrixId, - presenceBackgroundColor: backgroundColor, - onTap: onLongPress, + child: SizedBox( + width: Avatar.defaultSize, + height: Avatar.defaultSize, + child: Stack( + children: [ + if (space != null) + Positioned( + top: 0, + left: 0, + child: Avatar( + border: BorderSide( + width: 2, + color: backgroundColor ?? + Theme.of(context).colorScheme.surface, + ), + borderRadius: BorderRadius.circular( + AppConfig.borderRadius / 4, + ), + mxContent: space.avatar, + size: Avatar.defaultSize * 0.75, + name: space.getLocalizedDisplayname(), + onTap: onLongPress, + ), + ), + Positioned( + bottom: 0, + right: 0, + child: Avatar( + border: space == null + ? room.isSpace + ? BorderSide( + width: 0, + color: Theme.of(context) + .colorScheme + .outline, + ) + : null + : BorderSide( + width: 2, + color: backgroundColor ?? + Theme.of(context) + .colorScheme + .surface, + ), + borderRadius: room.isSpace + ? BorderRadius.circular( + AppConfig.borderRadius / 4, + ) + : null, + mxContent: room.avatar, + size: space != null + ? Avatar.defaultSize * 0.75 + : Avatar.defaultSize, + name: displayname, + presenceUserId: directChatMatrixId, + presenceBackgroundColor: backgroundColor, + onTap: onLongPress, + ), + ), + ], + ), ), ), ), @@ -205,20 +256,6 @@ class ChatListItem extends StatelessWidget { subtitle: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (room.isSpace) ...[ - room.id != lastEventRoom.id && - lastEventRoom.isUnreadOrInvited - ? Avatar( - mxContent: lastEventRoom.avatar, - name: lastEventRoom.name, - size: 18, - ) - : const Icon( - Icons.workspaces_outlined, - size: 18, - ), - const SizedBox(width: 4), - ], if (typingText.isEmpty && ownMessage && room.lastEvent!.status.isSending) ...[ @@ -243,7 +280,7 @@ class ChatListItem extends StatelessWidget { ), ), Expanded( - child: room.isSpace && !lastEventRoom.isUnreadOrInvited + child: room.isSpace && room.membership == Membership.join ? Text( L10n.of(context)!.countChatsAndCountParticipants( room.spaceChildren.length.toString(), @@ -297,10 +334,9 @@ class ChatListItem extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( - fontWeight: - unread || lastEventRoom.hasNewMessages - ? FontWeight.bold - : null, + fontWeight: unread || room.hasNewMessages + ? FontWeight.bold + : null, color: theme.colorScheme.onSurfaceVariant, decoration: room.lastEvent?.redacted == true ? TextDecoration.lineThrough @@ -318,9 +354,7 @@ class ChatListItem extends StatelessWidget { width: !hasNotifications && !unread && !room.hasNewMessages ? 0 : (unreadBubbleSize - 9) * - lastEventRoom.notificationCount - .toString() - .length + + room.notificationCount.toString().length + 9, decoration: BoxDecoration( color: room.highlightCount > 0 || @@ -335,7 +369,7 @@ class ChatListItem extends StatelessWidget { child: Center( child: hasNotifications ? Text( - lastEventRoom.notificationCount.toString(), + room.notificationCount.toString(), style: TextStyle( color: room.highlightCount > 0 ? Colors.white diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index 798464693..81f765125 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -26,6 +26,10 @@ class ChatListView extends StatelessWidget { controller.activeFilter == ActiveFilter.allChats, onPopInvoked: (pop) async { if (pop) return; + if (controller.activeSpaceId != null) { + controller.clearActiveSpace(); + return; + } final selMode = controller.selectMode; if (controller.isSearchMode) { controller.cancelSearch(); diff --git a/lib/widgets/avatar.dart b/lib/widgets/avatar.dart index 180f3437a..0c280a88b 100644 --- a/lib/widgets/avatar.dart +++ b/lib/widgets/avatar.dart @@ -17,6 +17,7 @@ class Avatar extends StatelessWidget { final Color? presenceBackgroundColor; final BorderRadius? borderRadius; final IconData? icon; + final BorderSide? border; const Avatar({ this.mxContent, @@ -27,6 +28,7 @@ class Avatar extends StatelessWidget { this.presenceUserId, this.presenceBackgroundColor, this.borderRadius, + this.border, this.icon, super.key, }); @@ -67,10 +69,7 @@ class Avatar extends StatelessWidget { color: color, shape: RoundedRectangleBorder( borderRadius: borderRadius, - side: BorderSide( - width: 0, - color: Theme.of(context).dividerColor, - ), + side: border ?? BorderSide.none, ), clipBehavior: Clip.hardEdge, child: noPic From 64c56f889b870e7ef27f0d51204a004da7d72804 Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 15 Jul 2024 13:21:35 +0200 Subject: [PATCH 02/15] chore: Follow up join space invites --- lib/pages/chat_list/chat_list.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 28e78dfe6..d057cf181 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -201,10 +201,6 @@ class ChatListController extends State } void onChatTap(Room room) async { - if (room.isSpace) { - setActiveSpace(room.id); - return; - } if (room.membership == Membership.invite) { final inviterId = room.getState(EventTypes.RoomMember, room.client.userID!)?.senderId; @@ -275,6 +271,10 @@ class ChatListController extends State return; } + if (room.isSpace) { + setActiveSpace(room.id); + return; + } // Share content into this room final shareContent = Matrix.of(context).shareContent; if (shareContent != null) { From 7b0e0404c06faa5f78dfae6e65a36c1dd3deed39 Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 15 Jul 2024 13:39:12 +0200 Subject: [PATCH 03/15] chore: Follow up select chats --- assets/l10n/intl_en.arb | 7 ++++++- lib/pages/chat_list/chat_list.dart | 19 +++++++++++++------ lib/pages/chat_list/chat_list_body.dart | 8 +++++--- 3 files changed, 24 insertions(+), 10 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index ab0ba910d..904b06c9c 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2705,5 +2705,10 @@ "restricted": "Restricted", "@restricted": {}, "knockRestricted": "Knock restricted", - "@knockRestricted": {} + "@knockRestricted": {}, + "goToSpace": "Go to space: {space}", + "@goToSpace": { + "type": "text", + "space": {} + } } diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index d057cf181..7084a082d 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -612,19 +612,22 @@ class ChatListController extends State super.dispose(); } - void chatContextAction(Room room) async { + void chatContextAction(Room room, [Room? space]) async { final action = await showModalActionSheet( context: context, - title: room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)), actions: [ + if (space != null) + SheetAction( + key: ChatContextAction.goToSpace, + icon: Icons.workspaces_outlined, + label: L10n.of(context)!.goToSpace(space.getLocalizedDisplayname()), + ), SheetAction( key: ChatContextAction.markUnread, icon: room.markedUnread ? Icons.mark_as_unread : Icons.mark_as_unread_outlined, - label: room.markedUnread - ? L10n.of(context)!.markAsRead - : L10n.of(context)!.unread, + label: L10n.of(context)!.toggleUnread, ), SheetAction( key: ChatContextAction.favorite, @@ -656,8 +659,11 @@ class ChatListController extends State await showFutureLoadingDialog( context: context, - future: () { + future: () async { switch (action) { + case ChatContextAction.goToSpace: + setActiveSpace(space!.id); + return; case ChatContextAction.favorite: return room.setFavourite(!room.isFavourite); case ChatContextAction.markUnread: @@ -872,6 +878,7 @@ enum InviteActions { enum AddRoomType { chat, subspace } enum ChatContextAction { + goToSpace, favorite, markUnread, mute, diff --git a/lib/pages/chat_list/chat_list_body.dart b/lib/pages/chat_list/chat_list_body.dart index fd063ddba..0b9c00889 100644 --- a/lib/pages/chat_list/chat_list_body.dart +++ b/lib/pages/chat_list/chat_list_body.dart @@ -27,13 +27,15 @@ class ChatListViewBody extends StatelessWidget { @override Widget build(BuildContext context) { + final client = Matrix.of(context).client; final activeSpace = controller.activeSpaceId; if (activeSpace != null) { return SpaceView( spaceId: activeSpace, onBack: controller.clearActiveSpace, onChatTab: (room) => controller.onChatTap(room), - onChatContext: (room) => controller.chatContextAction(room), + onChatContext: (room) => + controller.chatContextAction(room, client.getRoomById(activeSpace)), activeChat: controller.activeChat, toParentSpace: controller.setActiveSpace, ); @@ -45,7 +47,6 @@ class ChatListViewBody extends StatelessWidget { .where((room) => room.roomType == 'm.space') .toList(); final userSearchResult = controller.userSearchResult; - final client = Matrix.of(context).client; const dummyChatCount = 4; final titleColor = Theme.of(context).textTheme.bodyLarge!.color!.withAlpha(100); @@ -304,7 +305,8 @@ class ChatListViewBody extends StatelessWidget { key: Key('chat_list_item_${room.id}'), filter: filter, onTap: () => controller.onChatTap(room), - onLongPress: () => controller.chatContextAction(room), + onLongPress: () => + controller.chatContextAction(room, space), activeChat: controller.activeChat == room.id, ); }, From 8a5cd9bf160299eb28df346e2e137e4ad5ca3fdc Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 15 Jul 2024 13:45:02 +0200 Subject: [PATCH 04/15] chore: Follow up spaces ui --- lib/pages/chat_list/chat_list_body.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/pages/chat_list/chat_list_body.dart b/lib/pages/chat_list/chat_list_body.dart index 0b9c00889..1d1705ca1 100644 --- a/lib/pages/chat_list/chat_list_body.dart +++ b/lib/pages/chat_list/chat_list_body.dart @@ -163,7 +163,13 @@ class ChatListViewBody extends StatelessWidget { ), shrinkWrap: true, scrollDirection: Axis.horizontal, - children: ActiveFilter.values + children: [ + ActiveFilter.allChats, + ActiveFilter.unread, + ActiveFilter.groups, + if (spaceDelegateCandidates.isNotEmpty) + ActiveFilter.spaces, + ] .map( (filter) => Padding( padding: From 942970e04970fac768e351722f97e514603ef870 Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 15 Jul 2024 13:47:01 +0200 Subject: [PATCH 05/15] chore: Follow up spaces design --- lib/pages/chat_list/chat_list_item.dart | 140 ++++++++++-------------- 1 file changed, 58 insertions(+), 82 deletions(-) diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 2fcce1839..3df2e01c4 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -108,96 +108,72 @@ class ChatListItem extends StatelessWidget { visualDensity: const VisualDensity(vertical: -0.5), contentPadding: const EdgeInsets.symmetric(horizontal: 8), onLongPress: onLongPress, - leading: Stack( - clipBehavior: Clip.none, - children: [ - HoverBuilder( - builder: (context, hovered) => AnimatedScale( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - scale: hovered ? 1.1 : 1.0, - child: SizedBox( - width: Avatar.defaultSize, - height: Avatar.defaultSize, - child: Stack( - children: [ - if (space != null) - Positioned( - top: 0, - left: 0, - child: Avatar( - border: BorderSide( + leading: HoverBuilder( + builder: (context, hovered) => AnimatedScale( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + scale: hovered ? 1.1 : 1.0, + child: SizedBox( + width: Avatar.defaultSize, + height: Avatar.defaultSize, + child: Stack( + children: [ + if (space != null) + Positioned( + top: 0, + left: 0, + child: Avatar( + border: BorderSide( + width: 2, + color: backgroundColor ?? + Theme.of(context).colorScheme.surface, + ), + borderRadius: BorderRadius.circular( + AppConfig.borderRadius / 4, + ), + mxContent: space.avatar, + size: Avatar.defaultSize * 0.75, + name: space.getLocalizedDisplayname(), + onTap: onLongPress, + ), + ), + Positioned( + bottom: 0, + right: 0, + child: Avatar( + border: space == null + ? room.isSpace + ? BorderSide( + width: 0, + color: Theme.of(context) + .colorScheme + .outline, + ) + : null + : BorderSide( width: 2, color: backgroundColor ?? Theme.of(context).colorScheme.surface, ), - borderRadius: BorderRadius.circular( + borderRadius: room.isSpace + ? BorderRadius.circular( AppConfig.borderRadius / 4, - ), - mxContent: space.avatar, - size: Avatar.defaultSize * 0.75, - name: space.getLocalizedDisplayname(), - onTap: onLongPress, - ), - ), - Positioned( - bottom: 0, - right: 0, - child: Avatar( - border: space == null - ? room.isSpace - ? BorderSide( - width: 0, - color: Theme.of(context) - .colorScheme - .outline, - ) - : null - : BorderSide( - width: 2, - color: backgroundColor ?? - Theme.of(context) - .colorScheme - .surface, - ), - borderRadius: room.isSpace - ? BorderRadius.circular( - AppConfig.borderRadius / 4, - ) - : null, - mxContent: room.avatar, - size: space != null - ? Avatar.defaultSize * 0.75 - : Avatar.defaultSize, - name: displayname, - presenceUserId: directChatMatrixId, - presenceBackgroundColor: backgroundColor, - onTap: onLongPress, - ), - ), - ], + ) + : null, + mxContent: room.avatar, + size: space != null + ? Avatar.defaultSize * 0.75 + : Avatar.defaultSize, + name: displayname, + presenceUserId: directChatMatrixId, + presenceBackgroundColor: backgroundColor, + onTap: onLongPress, + ), ), - ), + ], ), ), - Positioned( - bottom: -2, - right: -2, - child: AnimatedScale( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - scale: (hovered) ? 1.0 : 0.0, - child: Material( - color: backgroundColor, - borderRadius: BorderRadius.circular(16), - child: const Icon( - Icons.check_circle_outlined, - size: 18, - ), - ), - ), - ), - ], + ), ), title: Row( children: [ From 3bd7257249ec035d5b8f12746ebba329b3848014 Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 15 Jul 2024 13:57:29 +0200 Subject: [PATCH 06/15] chore: Follow up spaces ui --- lib/pages/chat_list/chat_list_body.dart | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/pages/chat_list/chat_list_body.dart b/lib/pages/chat_list/chat_list_body.dart index 1d1705ca1..d2f90fc30 100644 --- a/lib/pages/chat_list/chat_list_body.dart +++ b/lib/pages/chat_list/chat_list_body.dart @@ -40,6 +40,16 @@ class ChatListViewBody extends StatelessWidget { toParentSpace: controller.setActiveSpace, ); } + final spaces = client.rooms.where((r) => r.isSpace); + final spaceDelegateCandidates = {}; + for (final space in spaces) { + for (final spaceChild in space.spaceChildren) { + final roomId = spaceChild.roomId; + if (roomId == null) continue; + spaceDelegateCandidates[roomId] = space; + } + } + final publicRooms = controller.roomSearchResult?.chunk .where((room) => room.roomType != 'm.space') .toList(); @@ -63,16 +73,6 @@ class ChatListViewBody extends StatelessWidget { builder: (context, _) { final rooms = controller.filteredRooms; - final spaces = client.rooms.where((r) => r.isSpace); - final spaceDelegateCandidates = {}; - for (final space in spaces) { - for (final spaceChild in space.spaceChildren) { - final roomId = spaceChild.roomId; - if (roomId == null) continue; - spaceDelegateCandidates[roomId] = space; - } - } - return SafeArea( child: CustomScrollView( controller: controller.scrollController, From 650f87b1d29fb7e85592aba08291390faf6a47cd Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 15 Jul 2024 15:19:50 +0200 Subject: [PATCH 07/15] chore: Follow up spaces ui --- assets/l10n/intl_en.arb | 3 +- lib/pages/chat_list/chat_list.dart | 23 +- lib/pages/chat_list/chat_list_view.dart | 9 +- lib/pages/chat_list/space_view.dart | 568 ++++++++++++------------ 4 files changed, 305 insertions(+), 298 deletions(-) diff --git a/assets/l10n/intl_en.arb b/assets/l10n/intl_en.arb index 904b06c9c..6c6da498a 100644 --- a/assets/l10n/intl_en.arb +++ b/assets/l10n/intl_en.arb @@ -2710,5 +2710,6 @@ "@goToSpace": { "type": "text", "space": {} - } + }, + "markAsUnread": "Mark as unread" } diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 7084a082d..6c8ebc755 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -619,7 +619,7 @@ class ChatListController extends State if (space != null) SheetAction( key: ChatContextAction.goToSpace, - icon: Icons.workspaces_outlined, + icon: Icons.chevron_right_outlined, label: L10n.of(context)!.goToSpace(space.getLocalizedDisplayname()), ), SheetAction( @@ -627,11 +627,13 @@ class ChatListController extends State icon: room.markedUnread ? Icons.mark_as_unread : Icons.mark_as_unread_outlined, - label: L10n.of(context)!.toggleUnread, + label: room.markedUnread + ? L10n.of(context)!.markAsRead + : L10n.of(context)!.markAsUnread, ), SheetAction( key: ChatContextAction.favorite, - icon: room.isFavourite ? Icons.pin : Icons.pin_outlined, + icon: room.isFavourite ? Icons.push_pin : Icons.push_pin_outlined, label: room.isFavourite ? L10n.of(context)!.unpin : L10n.of(context)!.pin, @@ -640,7 +642,7 @@ class ChatListController extends State key: ChatContextAction.mute, icon: room.pushRuleState == PushRuleState.notify ? Icons.notifications_off_outlined - : Icons.notifications, + : Icons.notifications_outlined, label: room.pushRuleState == PushRuleState.notify ? L10n.of(context)!.muteChat : L10n.of(context)!.unmuteChat, @@ -657,6 +659,19 @@ class ChatListController extends State if (action == null) return; if (!mounted) return; + if (action == ChatContextAction.leave) { + final confirmed = await showOkCancelAlertDialog( + useRootNavigator: false, + context: context, + title: L10n.of(context)!.areYouSure, + okLabel: L10n.of(context)!.yes, + cancelLabel: L10n.of(context)!.no, + message: L10n.of(context)!.archiveRoomDescription, + ); + if (confirmed == OkCancelResult.cancel) return; + } + if (!mounted) return; + await showFutureLoadingDialog( context: context, future: () async { diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index 81f765125..93fa8575b 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -22,14 +22,9 @@ class ChatListView extends StatelessWidget { final selectMode = controller.selectMode; return PopScope( canPop: controller.selectMode == SelectMode.normal && - !controller.isSearchMode && - controller.activeFilter == ActiveFilter.allChats, - onPopInvoked: (pop) async { + !controller.isSearchMode, + onPopInvoked: (pop) { if (pop) return; - if (controller.activeSpaceId != null) { - controller.clearActiveSpace(); - return; - } final selMode = controller.selectMode; if (controller.isSearchMode) { controller.cancelSearch(); diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index 6459de1e8..8fe443bf1 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -165,316 +165,312 @@ class _SpaceViewState extends State { final room = Matrix.of(context).client.getRoomById(widget.spaceId); final displayname = room?.getLocalizedDisplayname() ?? L10n.of(context)!.nothingFound; - return Scaffold( - appBar: AppBar( - leading: Center( - child: CloseButton( - onPressed: widget.onBack, + return PopScope( + canPop: false, + onPopInvoked: (_) => widget.onBack(), + child: Scaffold( + appBar: AppBar( + leading: Center( + child: CloseButton( + onPressed: widget.onBack, + ), ), - ), - titleSpacing: 0, - title: ListTile( - contentPadding: EdgeInsets.zero, - leading: Avatar( - mxContent: room?.avatar, - name: displayname, - borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), - ), - title: Text( - displayname, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - subtitle: room == null - ? null - : Text( - L10n.of(context)!.countChatsAndCountParticipants( - room.spaceChildren.length, - room.summary.mJoinedMemberCount ?? 1, + titleSpacing: 0, + title: ListTile( + contentPadding: EdgeInsets.zero, + leading: Avatar( + mxContent: room?.avatar, + name: displayname, + borderRadius: BorderRadius.circular(AppConfig.borderRadius / 2), + ), + title: Text( + displayname, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: room == null + ? null + : Text( + L10n.of(context)!.countChatsAndCountParticipants( + room.spaceChildren.length, + room.summary.mJoinedMemberCount ?? 1, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - actions: [ - PopupMenuButton( - onSelected: _onSpaceAction, - itemBuilder: (context) => [ - PopupMenuItem( - value: SpaceActions.settings, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.settings_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.settings), - ], - ), - ), - PopupMenuItem( - value: SpaceActions.invite, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.person_add_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.invite), - ], - ), - ), - PopupMenuItem( - value: SpaceActions.leave, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - const Icon(Icons.delete_outlined), - const SizedBox(width: 12), - Text(L10n.of(context)!.leave), - ], - ), - ), - ], ), - ], - ), - body: room == null - ? const Center( - child: Icon( - Icons.search_outlined, - size: 80, - ), - ) - : StreamBuilder( - stream: room.client.onSync.stream - .where((s) => s.hasRoomUpdate) - .rateLimit(const Duration(seconds: 1)), - builder: (context, snapshot) { - final joinedRooms = room.spaceChildren - .map((child) { - final roomId = child.roomId; - if (roomId == null) return null; - return room.client.getRoomById(roomId); - }) - .whereType() - .where((room) => room.membership != Membership.leave) - .toList(); - - // Sort rooms by last activity - joinedRooms.sort( - (b, a) => (a.lastEvent?.originServerTs ?? - DateTime.fromMillisecondsSinceEpoch(0)) - .compareTo( - b.lastEvent?.originServerTs ?? - DateTime.fromMillisecondsSinceEpoch(0), + actions: [ + PopupMenuButton( + onSelected: _onSpaceAction, + itemBuilder: (context) => [ + PopupMenuItem( + value: SpaceActions.settings, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.settings_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.settings), + ], ), - ); + ), + PopupMenuItem( + value: SpaceActions.invite, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.person_add_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.invite), + ], + ), + ), + PopupMenuItem( + value: SpaceActions.leave, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.delete_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.leave), + ], + ), + ), + ], + ), + ], + ), + body: room == null + ? const Center( + child: Icon( + Icons.search_outlined, + size: 80, + ), + ) + : StreamBuilder( + stream: room.client.onSync.stream + .where((s) => s.hasRoomUpdate) + .rateLimit(const Duration(seconds: 1)), + builder: (context, snapshot) { + final childrenIds = room.spaceChildren + .map((c) => c.roomId) + .whereType() + .toSet(); - final joinedParents = room.spaceParents - .map((parent) { - final roomId = parent.roomId; - if (roomId == null) return null; - return room.client.getRoomById(roomId); - }) - .whereType() - .toList(); - final filter = _filterController.text.trim().toLowerCase(); - return CustomScrollView( - slivers: [ - SliverAppBar( - floating: true, - toolbarHeight: 72, - scrolledUnderElevation: 0, - backgroundColor: Colors.transparent, - automaticallyImplyLeading: false, - title: TextField( - controller: _filterController, - onChanged: (_) => setState(() {}), - textInputAction: TextInputAction.search, - decoration: InputDecoration( - fillColor: - Theme.of(context).colorScheme.secondaryContainer, - border: OutlineInputBorder( - borderSide: BorderSide.none, - borderRadius: BorderRadius.circular(99), - ), - contentPadding: EdgeInsets.zero, - hintText: L10n.of(context)!.search, - hintStyle: TextStyle( - color: Theme.of(context) + final joinedRooms = room.client.rooms + .where((room) => childrenIds.remove(room.id)) + .toList(); + + final joinedParents = room.spaceParents + .map((parent) { + final roomId = parent.roomId; + if (roomId == null) return null; + return room.client.getRoomById(roomId); + }) + .whereType() + .toList(); + final filter = _filterController.text.trim().toLowerCase(); + return CustomScrollView( + slivers: [ + SliverAppBar( + floating: true, + toolbarHeight: 72, + scrolledUnderElevation: 0, + backgroundColor: Colors.transparent, + automaticallyImplyLeading: false, + title: TextField( + controller: _filterController, + onChanged: (_) => setState(() {}), + textInputAction: TextInputAction.search, + decoration: InputDecoration( + fillColor: Theme.of(context) .colorScheme - .onPrimaryContainer, - fontWeight: FontWeight.normal, - ), - floatingLabelBehavior: FloatingLabelBehavior.never, - prefixIcon: IconButton( - onPressed: () {}, - icon: Icon( - Icons.search_outlined, + .secondaryContainer, + border: OutlineInputBorder( + borderSide: BorderSide.none, + borderRadius: BorderRadius.circular(99), + ), + contentPadding: EdgeInsets.zero, + hintText: L10n.of(context)!.search, + hintStyle: TextStyle( color: Theme.of(context) .colorScheme .onPrimaryContainer, + fontWeight: FontWeight.normal, + ), + floatingLabelBehavior: FloatingLabelBehavior.never, + prefixIcon: IconButton( + onPressed: () {}, + icon: Icon( + Icons.search_outlined, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), ), ), ), ), - ), - SliverList.builder( - itemCount: joinedParents.length, - itemBuilder: (context, i) { - final displayname = - joinedParents[i].getLocalizedDisplayname(); - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 1, - ), - child: Material( - borderRadius: - BorderRadius.circular(AppConfig.borderRadius), - clipBehavior: Clip.hardEdge, - child: ListTile( - minVerticalPadding: 0, - leading: Icon( - Icons.adaptive.arrow_back_outlined, - size: 16, - ), - title: Row( - children: [ - Avatar( - mxContent: joinedParents[i].avatar, - name: displayname, - size: Avatar.defaultSize / 2, - borderRadius: BorderRadius.circular( - AppConfig.borderRadius / 4, - ), - ), - const SizedBox(width: 8), - Expanded(child: Text(displayname)), - ], - ), - onTap: () => - widget.toParentSpace(joinedParents[i].id), + SliverList.builder( + itemCount: joinedParents.length, + itemBuilder: (context, i) { + final displayname = + joinedParents[i].getLocalizedDisplayname(); + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 1, ), - ), - ); - }, - ), - SliverList.builder( - itemCount: joinedRooms.length + 1, - itemBuilder: (context, i) { - if (i == 0) { - return SearchTitle( - title: L10n.of(context)!.joinedChats, - icon: const Icon(Icons.chat_outlined), - ); - } - i--; - final room = joinedRooms[i]; - return ChatListItem( - room, - filter: filter, - onTap: () => widget.onChatTab(room), - onLongPress: () => widget.onChatContext(room), - activeChat: widget.activeChat == room.id, - ); - }, - ), - SliverList.builder( - itemCount: _discoveredChildren.length + 2, - itemBuilder: (context, i) { - if (i == 0) { - return SearchTitle( - title: L10n.of(context)!.discover, - icon: const Icon(Icons.explore_outlined), - ); - } - i--; - if (i == _discoveredChildren.length) { - if (_noMoreRooms) { - return Padding( - padding: const EdgeInsets.all(12.0), - child: Center( - child: Text( - L10n.of(context)!.noMoreChatsFound, - style: const TextStyle(fontSize: 13), + child: Material( + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + clipBehavior: Clip.hardEdge, + child: ListTile( + minVerticalPadding: 0, + leading: Icon( + Icons.adaptive.arrow_back_outlined, + size: 16, ), + title: Row( + children: [ + Avatar( + mxContent: joinedParents[i].avatar, + name: displayname, + size: Avatar.defaultSize / 2, + borderRadius: BorderRadius.circular( + AppConfig.borderRadius / 4, + ), + ), + const SizedBox(width: 8), + Expanded(child: Text(displayname)), + ], + ), + onTap: () => + widget.toParentSpace(joinedParents[i].id), + ), + ), + ); + }, + ), + SliverList.builder( + itemCount: joinedRooms.length + 1, + itemBuilder: (context, i) { + if (i == 0) { + return SearchTitle( + title: L10n.of(context)!.joinedChats, + icon: const Icon(Icons.chat_outlined), + ); + } + i--; + final room = joinedRooms[i]; + return ChatListItem( + room, + filter: filter, + onTap: () => widget.onChatTab(room), + onLongPress: () => widget.onChatContext(room), + activeChat: widget.activeChat == room.id, + ); + }, + ), + SliverList.builder( + itemCount: _discoveredChildren.length + 2, + itemBuilder: (context, i) { + if (i == 0) { + return SearchTitle( + title: L10n.of(context)!.discover, + icon: const Icon(Icons.explore_outlined), + ); + } + i--; + if (i == _discoveredChildren.length) { + if (_noMoreRooms) { + return Padding( + padding: const EdgeInsets.all(12.0), + child: Center( + child: Text( + L10n.of(context)!.noMoreChatsFound, + style: const TextStyle(fontSize: 13), + ), + ), + ); + } + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12.0, + vertical: 2.0, + ), + child: TextButton( + onPressed: _isLoading ? null : _loadHierarchy, + child: _isLoading + ? LinearProgressIndicator( + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + ) + : Text(L10n.of(context)!.loadMore), ), ); } + final item = _discoveredChildren[i]; + final displayname = item.name ?? + item.canonicalAlias ?? + L10n.of(context)!.emptyChat; + if (!displayname.toLowerCase().contains(filter)) { + return const SizedBox.shrink(); + } return Padding( padding: const EdgeInsets.symmetric( - horizontal: 12.0, - vertical: 2.0, + horizontal: 8, + vertical: 1, ), - child: TextButton( - onPressed: _isLoading ? null : _loadHierarchy, - child: _isLoading - ? LinearProgressIndicator( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, + child: Material( + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + clipBehavior: Clip.hardEdge, + child: ListTile( + onTap: () => _joinChildRoom(item), + leading: Avatar( + mxContent: item.avatarUrl, + name: displayname, + borderRadius: item.roomType == 'm.space' + ? BorderRadius.circular( + AppConfig.borderRadius / 2, + ) + : null, + ), + title: Row( + children: [ + Expanded( + child: Text( + displayname, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - ) - : Text(L10n.of(context)!.loadMore), + ), + const SizedBox(width: 8), + const Icon( + Icons.add_circle_outline_outlined, + ), + ], + ), + subtitle: Text( + item.topic ?? + L10n.of(context)!.countParticipants( + item.numJoinedMembers, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), ), ); - } - final item = _discoveredChildren[i]; - final displayname = item.name ?? - item.canonicalAlias ?? - L10n.of(context)!.emptyChat; - if (!displayname.toLowerCase().contains(filter)) { - return const SizedBox.shrink(); - } - return Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 1, - ), - child: Material( - borderRadius: - BorderRadius.circular(AppConfig.borderRadius), - clipBehavior: Clip.hardEdge, - child: ListTile( - onTap: () => _joinChildRoom(item), - leading: Avatar( - mxContent: item.avatarUrl, - name: displayname, - borderRadius: item.roomType == 'm.space' - ? BorderRadius.circular( - AppConfig.borderRadius / 2, - ) - : null, - ), - title: Row( - children: [ - Expanded( - child: Text( - displayname, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: 8), - const Icon(Icons.add_circle_outline_outlined), - ], - ), - subtitle: Text( - item.topic ?? - L10n.of(context)!.countParticipants( - item.numJoinedMembers, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ), - ), - ); - }, - ), - ], - ); - }, - ), + }, + ), + ], + ); + }, + ), + ), ); } } From cfdb86b7e97ef7a52d51c561305a81c99a726f2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?jos=C3=A9=20m?= Date: Fri, 12 Jul 2024 15:55:04 +0000 Subject: [PATCH 08/15] Translated using Weblate (Galician) Currently translated at 100.0% (634 of 634 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/gl/ --- assets/l10n/intl_gl.arb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_gl.arb b/assets/l10n/intl_gl.arb index 2fe2464f5..8a7ef7922 100644 --- a/assets/l10n/intl_gl.arb +++ b/assets/l10n/intl_gl.arb @@ -2708,5 +2708,9 @@ "restricted": "Non accesible", "@restricted": {}, "swipeRightToLeftToReply": "Despraza hacia a esquerda para responder", - "@swipeRightToLeftToReply": {} + "@swipeRightToLeftToReply": {}, + "alwaysUse24HourFormat": "falso", + "@alwaysUse24HourFormat": { + "description": "Set to true to always display time of day in 24 hour format." + } } From d436aa7ff54d9a3efd07ad7e8f5ea11063c5aaf9 Mon Sep 17 00:00:00 2001 From: Milo Ivir Date: Sun, 14 Jul 2024 21:12:44 +0000 Subject: [PATCH 09/15] Translated using Weblate (Croatian) Currently translated at 100.0% (634 of 634 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/hr/ --- assets/l10n/intl_hr.arb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/assets/l10n/intl_hr.arb b/assets/l10n/intl_hr.arb index a3f981b2d..3d6c84730 100644 --- a/assets/l10n/intl_hr.arb +++ b/assets/l10n/intl_hr.arb @@ -2708,5 +2708,9 @@ "count": {} }, "swipeRightToLeftToReply": "Za odgovaranje povuci prstom zdesna ulijevo", - "@swipeRightToLeftToReply": {} + "@swipeRightToLeftToReply": {}, + "alwaysUse24HourFormat": "true", + "@alwaysUse24HourFormat": { + "description": "Set to true to always display time of day in 24 hour format." + } } From eb0ae28adc767460a1584791d8b05b50714a6951 Mon Sep 17 00:00:00 2001 From: Christian Date: Mon, 15 Jul 2024 13:26:06 +0000 Subject: [PATCH 10/15] Translated using Weblate (German) Currently translated at 99.6% (640 of 642 strings) Translation: FluffyChat/Translations Translate-URL: https://hosted.weblate.org/projects/fluffychat/translations/de/ --- assets/l10n/intl_de.arb | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/assets/l10n/intl_de.arb b/assets/l10n/intl_de.arb index 07dea5ae8..cb0c713d9 100644 --- a/assets/l10n/intl_de.arb +++ b/assets/l10n/intl_de.arb @@ -2,8 +2,8 @@ "@@locale": "de", "@@last_modified": "2021-08-14 12:41:10.119255", "alwaysUse24HourFormat": "true", - "@alwaysUse24HourFormat": { - "description": "Set to true to always display time of day in 24 hour format." + "@alwaysUse24HourFormat": { + "description": "Set to true to always display time of day in 24 hour format." }, "about": "Über", "@about": { @@ -2710,5 +2710,22 @@ } }, "searchMore": "Weiter suchen ...", - "@searchMore": {} + "@searchMore": {}, + "unread": "Ungelesen", + "@unread": {}, + "noMoreChatsFound": "Keine weiteren Chats gefunden ...", + "@noMoreChatsFound": {}, + "joinedChats": "Beigetretene Chats", + "@joinedChats": {}, + "space": "Space", + "@space": {}, + "spaces": "Spaces", + "@spaces": {}, + "goToSpace": "Geh zum Space: {space}", + "@goToSpace": { + "type": "text", + "space": {} + }, + "markAsUnread": "Als ungelesen markieren", + "@markAsUnread": {} } From 47d1165b45801ab9af612fb3a73bbcfe7e3a81ed Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 15 Jul 2024 16:34:48 +0200 Subject: [PATCH 11/15] chore: Follow up chat list context menu --- lib/pages/chat_list/chat_list.dart | 166 ++++++-- lib/pages/chat_list/chat_list_body.dart | 74 ++-- lib/pages/chat_list/chat_list_item.dart | 495 ++++++++++++------------ lib/pages/chat_list/space_view.dart | 7 +- 4 files changed, 420 insertions(+), 322 deletions(-) diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 6c8ebc755..06cb532d4 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -21,6 +21,7 @@ import 'package:fluffychat/pages/chat_list/chat_list_view.dart'; import 'package:fluffychat/utils/localized_exception_extension.dart'; import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; import 'package:fluffychat/utils/platform_infos.dart'; +import 'package:fluffychat/widgets/avatar.dart'; import '../../../utils/account_bundles.dart'; import '../../config/setting_keys.dart'; import '../../utils/matrix_sdk_extensions/matrix_file_extension.dart'; @@ -612,46 +613,130 @@ class ChatListController extends State super.dispose(); } - void chatContextAction(Room room, [Room? space]) async { - final action = await showModalActionSheet( - context: context, - actions: [ - if (space != null) - SheetAction( - key: ChatContextAction.goToSpace, - icon: Icons.chevron_right_outlined, - label: L10n.of(context)!.goToSpace(space.getLocalizedDisplayname()), + void chatContextAction( + Room room, + BuildContext posContext, [ + Room? space, + ]) async { + final overlay = + Overlay.of(posContext).context.findRenderObject() as RenderBox; + + final button = posContext.findRenderObject() as RenderBox; + + final position = RelativeRect.fromRect( + Rect.fromPoints( + button.localToGlobal(const Offset(0, -65), ancestor: overlay), + button.localToGlobal( + button.size.bottomRight(Offset.zero) + const Offset(-50, 0), + ancestor: overlay, + ), + ), + Offset.zero & overlay.size, + ); + + final displayname = + room.getLocalizedDisplayname(MatrixLocals(L10n.of(context)!)); + + final action = await showMenu( + context: posContext, + position: position, + items: [ + PopupMenuItem( + enabled: false, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Avatar( + mxContent: room.avatar, + size: Avatar.defaultSize / 2, + name: displayname, + ), + const SizedBox(width: 12), + Text(displayname), + ], ), - SheetAction( - key: ChatContextAction.markUnread, - icon: room.markedUnread - ? Icons.mark_as_unread - : Icons.mark_as_unread_outlined, - label: room.markedUnread - ? L10n.of(context)!.markAsRead - : L10n.of(context)!.markAsUnread, ), - SheetAction( - key: ChatContextAction.favorite, - icon: room.isFavourite ? Icons.push_pin : Icons.push_pin_outlined, - label: room.isFavourite - ? L10n.of(context)!.unpin - : L10n.of(context)!.pin, + const PopupMenuDivider(), + if (space != null) + PopupMenuItem( + value: ChatContextAction.goToSpace, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.navigate_next_outlined), + const SizedBox(width: 12), + Expanded( + child: Text( + L10n.of(context)! + .goToSpace(space.getLocalizedDisplayname()), + ), + ), + ], + ), + ), + PopupMenuItem( + value: ChatContextAction.mute, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + room.pushRuleState == PushRuleState.notify + ? Icons.notifications_off_outlined + : Icons.notifications_off, + ), + const SizedBox(width: 12), + Text( + room.pushRuleState == PushRuleState.notify + ? L10n.of(context)!.muteChat + : L10n.of(context)!.unmuteChat, + ), + ], + ), ), - SheetAction( - key: ChatContextAction.mute, - icon: room.pushRuleState == PushRuleState.notify - ? Icons.notifications_off_outlined - : Icons.notifications_outlined, - label: room.pushRuleState == PushRuleState.notify - ? L10n.of(context)!.muteChat - : L10n.of(context)!.unmuteChat, + PopupMenuItem( + value: ChatContextAction.markUnread, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + room.markedUnread + ? Icons.mark_as_unread + : Icons.mark_as_unread_outlined, + ), + const SizedBox(width: 12), + Text( + room.markedUnread + ? L10n.of(context)!.markAsRead + : L10n.of(context)!.markAsUnread, + ), + ], + ), ), - SheetAction( - isDestructiveAction: true, - key: ChatContextAction.leave, - icon: Icons.delete_outlined, - label: L10n.of(context)!.leave, + PopupMenuItem( + value: ChatContextAction.favorite, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(room.isFavourite ? Icons.push_pin : Icons.push_pin_outlined), + const SizedBox(width: 12), + Text( + room.isFavourite + ? L10n.of(context)!.unpin + : L10n.of(context)!.pin, + ), + ], + ), + ), + PopupMenuItem( + value: ChatContextAction.leave, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.delete_outlined), + const SizedBox(width: 12), + Text(L10n.of(context)!.leave), + ], + ), ), ], ); @@ -659,14 +744,20 @@ class ChatListController extends State if (action == null) return; if (!mounted) return; + if (action == ChatContextAction.goToSpace) { + setActiveSpace(space!.id); + return; + } + if (action == ChatContextAction.leave) { final confirmed = await showOkCancelAlertDialog( useRootNavigator: false, context: context, title: L10n.of(context)!.areYouSure, - okLabel: L10n.of(context)!.yes, + okLabel: L10n.of(context)!.leave, cancelLabel: L10n.of(context)!.no, message: L10n.of(context)!.archiveRoomDescription, + isDestructiveAction: true, ); if (confirmed == OkCancelResult.cancel) return; } @@ -677,7 +768,6 @@ class ChatListController extends State future: () async { switch (action) { case ChatContextAction.goToSpace: - setActiveSpace(space!.id); return; case ChatContextAction.favorite: return room.setFavourite(!room.isFavourite); diff --git a/lib/pages/chat_list/chat_list_body.dart b/lib/pages/chat_list/chat_list_body.dart index d2f90fc30..4b17ad6b5 100644 --- a/lib/pages/chat_list/chat_list_body.dart +++ b/lib/pages/chat_list/chat_list_body.dart @@ -14,6 +14,7 @@ import 'package:fluffychat/pages/user_bottom_sheet/user_bottom_sheet.dart'; import 'package:fluffychat/utils/adaptive_bottom_sheet.dart'; import 'package:fluffychat/utils/stream_extension.dart'; import 'package:fluffychat/widgets/avatar.dart'; +import 'package:fluffychat/widgets/hover_builder.dart'; import 'package:fluffychat/widgets/public_room_bottom_sheet.dart'; import '../../config/themes.dart'; import '../../widgets/connection_status_header.dart'; @@ -34,8 +35,8 @@ class ChatListViewBody extends StatelessWidget { spaceId: activeSpace, onBack: controller.clearActiveSpace, onChatTab: (room) => controller.onChatTap(room), - onChatContext: (room) => - controller.chatContextAction(room, client.getRoomById(activeSpace)), + onChatContext: (room, context) => + controller.chatContextAction(room, context), activeChat: controller.activeChat, toParentSpace: controller.setActiveSpace, ); @@ -174,45 +175,54 @@ class ChatListViewBody extends StatelessWidget { (filter) => Padding( padding: const EdgeInsets.symmetric(horizontal: 4), - child: InkWell( - borderRadius: BorderRadius.circular( - AppConfig.borderRadius, - ), - onTap: () => - controller.setActiveFilter(filter), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 6, - ), - decoration: BoxDecoration( - color: filter == controller.activeFilter - ? Theme.of(context) - .colorScheme - .primary - : Theme.of(context) - .colorScheme - .secondaryContainer, + child: HoverBuilder( + builder: (context, hovered) => + AnimatedScale( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + scale: hovered ? 1.1 : 1.0, + child: InkWell( borderRadius: BorderRadius.circular( AppConfig.borderRadius, ), - ), - alignment: Alignment.center, - child: Text( - filter.toLocalizedString(context), - style: TextStyle( - fontWeight: - filter == controller.activeFilter + onTap: () => + controller.setActiveFilter(filter), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: filter == + controller.activeFilter + ? Theme.of(context) + .colorScheme + .primary + : Theme.of(context) + .colorScheme + .secondaryContainer, + borderRadius: BorderRadius.circular( + AppConfig.borderRadius, + ), + ), + alignment: Alignment.center, + child: Text( + filter.toLocalizedString(context), + style: TextStyle( + fontWeight: filter == + controller.activeFilter ? FontWeight.bold : FontWeight.normal, - color: - filter == controller.activeFilter + color: filter == + controller.activeFilter ? Theme.of(context) .colorScheme .onPrimary : Theme.of(context) .colorScheme .onSecondaryContainer, + ), + ), ), ), ), @@ -311,8 +321,8 @@ class ChatListViewBody extends StatelessWidget { key: Key('chat_list_item_${room.id}'), filter: filter, onTap: () => controller.onChatTap(room), - onLongPress: () => - controller.chatContextAction(room, space), + onLongPress: (context) => + controller.chatContextAction(room, context, space), activeChat: controller.activeChat == room.id, ); }, diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 3df2e01c4..ed46b03f7 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -19,7 +19,7 @@ class ChatListItem extends StatelessWidget { final Room room; final Room? space; final bool activeChat; - final void Function()? onLongPress; + final void Function(BuildContext context)? onLongPress; final void Function()? onForget; final void Function() onTap; final String? filter; @@ -103,271 +103,266 @@ class ChatListItem extends StatelessWidget { color: backgroundColor, child: FutureBuilder( future: room.loadHeroUsers(), - builder: (context, snapshot) => HoverBuilder( - builder: (context, hovered) => ListTile( - visualDensity: const VisualDensity(vertical: -0.5), - contentPadding: const EdgeInsets.symmetric(horizontal: 8), - onLongPress: onLongPress, - leading: HoverBuilder( - builder: (context, hovered) => AnimatedScale( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - scale: hovered ? 1.1 : 1.0, - child: SizedBox( - width: Avatar.defaultSize, - height: Avatar.defaultSize, - child: Stack( - children: [ - if (space != null) - Positioned( - top: 0, - left: 0, - child: Avatar( - border: BorderSide( - width: 2, - color: backgroundColor ?? - Theme.of(context).colorScheme.surface, - ), - borderRadius: BorderRadius.circular( - AppConfig.borderRadius / 4, - ), - mxContent: space.avatar, - size: Avatar.defaultSize * 0.75, - name: space.getLocalizedDisplayname(), - onTap: onLongPress, - ), - ), + builder: (context, snapshot) => ListTile( + visualDensity: const VisualDensity(vertical: -0.5), + contentPadding: const EdgeInsets.symmetric(horizontal: 8), + onLongPress: () => onLongPress?.call(context), + leading: HoverBuilder( + builder: (context, hovered) => AnimatedScale( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + scale: hovered ? 1.1 : 1.0, + child: SizedBox( + width: Avatar.defaultSize, + height: Avatar.defaultSize, + child: Stack( + children: [ + if (space != null) Positioned( - bottom: 0, - right: 0, + top: 0, + left: 0, child: Avatar( - border: space == null - ? room.isSpace - ? BorderSide( - width: 0, - color: Theme.of(context) - .colorScheme - .outline, - ) - : null - : BorderSide( - width: 2, - color: backgroundColor ?? - Theme.of(context).colorScheme.surface, - ), - borderRadius: room.isSpace - ? BorderRadius.circular( - AppConfig.borderRadius / 4, - ) - : null, - mxContent: room.avatar, - size: space != null - ? Avatar.defaultSize * 0.75 - : Avatar.defaultSize, - name: displayname, - presenceUserId: directChatMatrixId, - presenceBackgroundColor: backgroundColor, - onTap: onLongPress, + border: BorderSide( + width: 2, + color: backgroundColor ?? + Theme.of(context).colorScheme.surface, + ), + borderRadius: BorderRadius.circular( + AppConfig.borderRadius / 4, + ), + mxContent: space.avatar, + size: Avatar.defaultSize * 0.75, + name: space.getLocalizedDisplayname(), + onTap: () => onLongPress?.call(context), ), ), - ], - ), + Positioned( + bottom: 0, + right: 0, + child: Avatar( + border: space == null + ? room.isSpace + ? BorderSide( + width: 0, + color: + Theme.of(context).colorScheme.outline, + ) + : null + : BorderSide( + width: 2, + color: backgroundColor ?? + Theme.of(context).colorScheme.surface, + ), + borderRadius: room.isSpace + ? BorderRadius.circular( + AppConfig.borderRadius / 4, + ) + : null, + mxContent: room.avatar, + size: space != null + ? Avatar.defaultSize * 0.75 + : Avatar.defaultSize, + name: displayname, + presenceUserId: directChatMatrixId, + presenceBackgroundColor: backgroundColor, + onTap: () => onLongPress?.call(context), + ), + ), + ], ), ), ), - title: Row( - children: [ - Expanded( - child: Text( - displayname, - maxLines: 1, - overflow: TextOverflow.ellipsis, - softWrap: false, - style: unread || room.hasNewMessages - ? const TextStyle(fontWeight: FontWeight.bold) - : null, - ), + ), + title: Row( + children: [ + Expanded( + child: Text( + displayname, + maxLines: 1, + overflow: TextOverflow.ellipsis, + softWrap: false, + style: unread || room.hasNewMessages + ? const TextStyle(fontWeight: FontWeight.bold) + : null, ), - if (isMuted) - const Padding( - padding: EdgeInsets.only(left: 4.0), - child: Icon( - Icons.notifications_off_outlined, - size: 16, - ), - ), - if (room.isFavourite || room.membership == Membership.invite) - Padding( - padding: EdgeInsets.only( - right: hasNotifications ? 4.0 : 0.0, - ), - child: Icon( - Icons.push_pin, - size: 16, - color: theme.colorScheme.primary, - ), - ), - if (!room.isSpace && - lastEvent != null && - room.membership != Membership.invite) - Padding( - padding: const EdgeInsets.only(left: 4.0), - child: Text( - lastEvent.originServerTs.localizedTimeShort(context), - style: TextStyle( - fontSize: 13, - color: unread - ? theme.colorScheme.secondary - : theme.textTheme.bodyMedium!.color, - ), - ), - ), - if (room.isSpace) - const Icon( - Icons.arrow_circle_right_outlined, - size: 18, - ), - ], - ), - subtitle: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (typingText.isEmpty && - ownMessage && - room.lastEvent!.status.isSending) ...[ - const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator.adaptive(strokeWidth: 2), - ), - const SizedBox(width: 4), - ], - AnimatedContainer( - width: typingText.isEmpty ? 0 : 18, - clipBehavior: Clip.hardEdge, - decoration: const BoxDecoration(), - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - padding: const EdgeInsets.only(right: 4), + ), + if (isMuted) + const Padding( + padding: EdgeInsets.only(left: 4.0), child: Icon( - Icons.edit_outlined, - color: theme.colorScheme.secondary, - size: 14, + Icons.notifications_off_outlined, + size: 16, ), ), - Expanded( - child: room.isSpace && room.membership == Membership.join + if (room.isFavourite || room.membership == Membership.invite) + Padding( + padding: EdgeInsets.only( + right: hasNotifications ? 4.0 : 0.0, + ), + child: Icon( + Icons.push_pin, + size: 16, + color: theme.colorScheme.primary, + ), + ), + if (!room.isSpace && + lastEvent != null && + room.membership != Membership.invite) + Padding( + padding: const EdgeInsets.only(left: 4.0), + child: Text( + lastEvent.originServerTs.localizedTimeShort(context), + style: TextStyle( + fontSize: 13, + color: unread + ? theme.colorScheme.secondary + : theme.textTheme.bodyMedium!.color, + ), + ), + ), + if (room.isSpace) + const Icon( + Icons.arrow_circle_right_outlined, + size: 18, + ), + ], + ), + subtitle: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (typingText.isEmpty && + ownMessage && + room.lastEvent!.status.isSending) ...[ + const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator.adaptive(strokeWidth: 2), + ), + const SizedBox(width: 4), + ], + AnimatedContainer( + width: typingText.isEmpty ? 0 : 18, + clipBehavior: Clip.hardEdge, + decoration: const BoxDecoration(), + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + padding: const EdgeInsets.only(right: 4), + child: Icon( + Icons.edit_outlined, + color: theme.colorScheme.secondary, + size: 14, + ), + ), + Expanded( + child: room.isSpace && room.membership == Membership.join + ? Text( + L10n.of(context)!.countChatsAndCountParticipants( + room.spaceChildren.length.toString(), + (room.summary.mJoinedMemberCount ?? 1).toString(), + ), + ) + : typingText.isNotEmpty + ? Text( + typingText, + style: TextStyle( + color: theme.colorScheme.primary, + ), + maxLines: 1, + softWrap: false, + ) + : FutureBuilder( + key: ValueKey( + '${lastEvent?.eventId}_${lastEvent?.type}', + ), + future: needLastEventSender + ? lastEvent.calcLocalizedBody( + MatrixLocals(L10n.of(context)!), + hideReply: true, + hideEdit: true, + plaintextBody: true, + removeMarkdown: true, + withSenderNamePrefix: (!isDirectChat || + directChatMatrixId != + room.lastEvent?.senderId), + ) + : null, + initialData: lastEvent?.calcLocalizedBodyFallback( + MatrixLocals(L10n.of(context)!), + hideReply: true, + hideEdit: true, + plaintextBody: true, + removeMarkdown: true, + withSenderNamePrefix: (!isDirectChat || + directChatMatrixId != + room.lastEvent?.senderId), + ), + builder: (context, snapshot) => Text( + room.membership == Membership.invite + ? isDirectChat + ? L10n.of(context)!.invitePrivateChat + : L10n.of(context)!.inviteGroupChat + : snapshot.data ?? + L10n.of(context)!.emptyChat, + softWrap: false, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: unread || room.hasNewMessages + ? FontWeight.bold + : null, + color: theme.colorScheme.onSurfaceVariant, + decoration: room.lastEvent?.redacted == true + ? TextDecoration.lineThrough + : null, + ), + ), + ), + ), + const SizedBox(width: 8), + AnimatedContainer( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + padding: const EdgeInsets.symmetric(horizontal: 7), + height: unreadBubbleSize, + width: !hasNotifications && !unread && !room.hasNewMessages + ? 0 + : (unreadBubbleSize - 9) * + room.notificationCount.toString().length + + 9, + decoration: BoxDecoration( + color: room.highlightCount > 0 || + room.membership == Membership.invite + ? Colors.red + : hasNotifications || room.markedUnread + ? theme.colorScheme.primary + : theme.colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(AppConfig.borderRadius), + ), + child: Center( + child: hasNotifications ? Text( - L10n.of(context)!.countChatsAndCountParticipants( - room.spaceChildren.length.toString(), - (room.summary.mJoinedMemberCount ?? 1).toString(), + room.notificationCount.toString(), + style: TextStyle( + color: room.highlightCount > 0 + ? Colors.white + : hasNotifications + ? theme.colorScheme.onPrimary + : theme.colorScheme.onPrimaryContainer, + fontSize: 13, ), ) - : typingText.isNotEmpty - ? Text( - typingText, - style: TextStyle( - color: theme.colorScheme.primary, - ), - maxLines: 1, - softWrap: false, - ) - : FutureBuilder( - key: ValueKey( - '${lastEvent?.eventId}_${lastEvent?.type}', - ), - future: needLastEventSender - ? lastEvent.calcLocalizedBody( - MatrixLocals(L10n.of(context)!), - hideReply: true, - hideEdit: true, - plaintextBody: true, - removeMarkdown: true, - withSenderNamePrefix: (!isDirectChat || - directChatMatrixId != - room.lastEvent?.senderId), - ) - : null, - initialData: - lastEvent?.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)!), - hideReply: true, - hideEdit: true, - plaintextBody: true, - removeMarkdown: true, - withSenderNamePrefix: (!isDirectChat || - directChatMatrixId != - room.lastEvent?.senderId), - ), - builder: (context, snapshot) => Text( - room.membership == Membership.invite - ? isDirectChat - ? L10n.of(context)!.invitePrivateChat - : L10n.of(context)!.inviteGroupChat - : snapshot.data ?? - L10n.of(context)!.emptyChat, - softWrap: false, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontWeight: unread || room.hasNewMessages - ? FontWeight.bold - : null, - color: theme.colorScheme.onSurfaceVariant, - decoration: room.lastEvent?.redacted == true - ? TextDecoration.lineThrough - : null, - ), - ), - ), + : const SizedBox.shrink(), ), - const SizedBox(width: 8), - AnimatedContainer( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - padding: const EdgeInsets.symmetric(horizontal: 7), - height: unreadBubbleSize, - width: !hasNotifications && !unread && !room.hasNewMessages - ? 0 - : (unreadBubbleSize - 9) * - room.notificationCount.toString().length + - 9, - decoration: BoxDecoration( - color: room.highlightCount > 0 || - room.membership == Membership.invite - ? Colors.red - : hasNotifications || room.markedUnread - ? theme.colorScheme.primary - : theme.colorScheme.primaryContainer, - borderRadius: - BorderRadius.circular(AppConfig.borderRadius), - ), - child: Center( - child: hasNotifications - ? Text( - room.notificationCount.toString(), - style: TextStyle( - color: room.highlightCount > 0 - ? Colors.white - : hasNotifications - ? theme.colorScheme.onPrimary - : theme.colorScheme.onPrimaryContainer, - fontSize: 13, - ), - ) - : const SizedBox.shrink(), - ), - ), - ], - ), - onTap: onTap, - trailing: onForget == null - ? null - : IconButton( - icon: const Icon(Icons.delete_outlined), - onPressed: onForget, - ), + ), + ], ), + onTap: onTap, + trailing: onForget == null + ? null + : IconButton( + icon: const Icon(Icons.delete_outlined), + onPressed: onForget, + ), ), ), ), diff --git a/lib/pages/chat_list/space_view.dart b/lib/pages/chat_list/space_view.dart index 8fe443bf1..3a7fdcad0 100644 --- a/lib/pages/chat_list/space_view.dart +++ b/lib/pages/chat_list/space_view.dart @@ -20,7 +20,7 @@ class SpaceView extends StatefulWidget { final void Function() onBack; final void Function(String spaceId) toParentSpace; final void Function(Room room) onChatTab; - final void Function(Room room) onChatContext; + final void Function(Room room, BuildContext context) onChatContext; final String? activeChat; const SpaceView({ @@ -367,7 +367,10 @@ class _SpaceViewState extends State { room, filter: filter, onTap: () => widget.onChatTab(room), - onLongPress: () => widget.onChatContext(room), + onLongPress: (context) => widget.onChatContext( + room, + context, + ), activeChat: widget.activeChat == room.id, ); }, From 1b95694a5848c8b1503f963550dced896cc358a6 Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 15 Jul 2024 16:53:09 +0200 Subject: [PATCH 12/15] chore: Follow up spaces design --- lib/pages/chat_list/chat_list.dart | 6 +++++- lib/pages/chat_list/chat_list_item.dart | 13 +++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 06cb532d4..8ee1870b5 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -652,7 +652,11 @@ class ChatListController extends State name: displayname, ), const SizedBox(width: 12), - Text(displayname), + Text( + displayname, + style: + TextStyle(color: Theme.of(context).colorScheme.onSurface), + ), ], ), ), diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index ed46b03f7..0217e0c97 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -168,6 +168,19 @@ class ChatListItem extends StatelessWidget { onTap: () => onLongPress?.call(context), ), ), + if (hovered) + Positioned( + top: -2, + right: -2, + child: Material( + color: backgroundColor, + borderRadius: BorderRadius.circular(16), + child: const Icon( + Icons.arrow_drop_down_circle_outlined, + size: 18, + ), + ), + ), ], ), ), From 54ba4544af791e3ab4dbb603277e3dfffe4978c1 Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 15 Jul 2024 16:57:19 +0200 Subject: [PATCH 13/15] chore: Follow up listtilehover --- lib/pages/chat_list/chat_list_item.dart | 511 ++++++++++++------------ 1 file changed, 258 insertions(+), 253 deletions(-) diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 0217e0c97..8036aee62 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -103,279 +103,284 @@ class ChatListItem extends StatelessWidget { color: backgroundColor, child: FutureBuilder( future: room.loadHeroUsers(), - builder: (context, snapshot) => ListTile( - visualDensity: const VisualDensity(vertical: -0.5), - contentPadding: const EdgeInsets.symmetric(horizontal: 8), - onLongPress: () => onLongPress?.call(context), - leading: HoverBuilder( - builder: (context, hovered) => AnimatedScale( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - scale: hovered ? 1.1 : 1.0, - child: SizedBox( - width: Avatar.defaultSize, - height: Avatar.defaultSize, - child: Stack( - children: [ - if (space != null) + builder: (context, snapshot) => HoverBuilder( + builder: (context, listTileHovered) => ListTile( + visualDensity: const VisualDensity(vertical: -0.5), + contentPadding: const EdgeInsets.symmetric(horizontal: 8), + onLongPress: () => onLongPress?.call(context), + leading: HoverBuilder( + builder: (context, hovered) => AnimatedScale( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + scale: hovered ? 1.1 : 1.0, + child: SizedBox( + width: Avatar.defaultSize, + height: Avatar.defaultSize, + child: Stack( + children: [ + if (space != null) + Positioned( + top: 0, + left: 0, + child: Avatar( + border: BorderSide( + width: 2, + color: backgroundColor ?? + Theme.of(context).colorScheme.surface, + ), + borderRadius: BorderRadius.circular( + AppConfig.borderRadius / 4, + ), + mxContent: space.avatar, + size: Avatar.defaultSize * 0.75, + name: space.getLocalizedDisplayname(), + onTap: () => onLongPress?.call(context), + ), + ), Positioned( - top: 0, - left: 0, + bottom: 0, + right: 0, child: Avatar( - border: BorderSide( - width: 2, - color: backgroundColor ?? - Theme.of(context).colorScheme.surface, - ), - borderRadius: BorderRadius.circular( - AppConfig.borderRadius / 4, - ), - mxContent: space.avatar, - size: Avatar.defaultSize * 0.75, - name: space.getLocalizedDisplayname(), + border: space == null + ? room.isSpace + ? BorderSide( + width: 0, + color: Theme.of(context) + .colorScheme + .outline, + ) + : null + : BorderSide( + width: 2, + color: backgroundColor ?? + Theme.of(context).colorScheme.surface, + ), + borderRadius: room.isSpace + ? BorderRadius.circular( + AppConfig.borderRadius / 4, + ) + : null, + mxContent: room.avatar, + size: space != null + ? Avatar.defaultSize * 0.75 + : Avatar.defaultSize, + name: displayname, + presenceUserId: directChatMatrixId, + presenceBackgroundColor: backgroundColor, onTap: () => onLongPress?.call(context), ), ), - Positioned( - bottom: 0, - right: 0, - child: Avatar( - border: space == null - ? room.isSpace - ? BorderSide( - width: 0, - color: - Theme.of(context).colorScheme.outline, - ) - : null - : BorderSide( - width: 2, - color: backgroundColor ?? - Theme.of(context).colorScheme.surface, - ), - borderRadius: room.isSpace - ? BorderRadius.circular( - AppConfig.borderRadius / 4, - ) - : null, - mxContent: room.avatar, - size: space != null - ? Avatar.defaultSize * 0.75 - : Avatar.defaultSize, - name: displayname, - presenceUserId: directChatMatrixId, - presenceBackgroundColor: backgroundColor, - onTap: () => onLongPress?.call(context), - ), - ), - if (hovered) - Positioned( - top: -2, - right: -2, - child: Material( - color: backgroundColor, - borderRadius: BorderRadius.circular(16), - child: const Icon( - Icons.arrow_drop_down_circle_outlined, - size: 18, + if (listTileHovered) + Positioned( + top: -2, + right: -2, + child: Material( + color: backgroundColor, + borderRadius: BorderRadius.circular(16), + child: const Icon( + Icons.arrow_drop_down_circle_outlined, + size: 18, + ), ), ), - ), - ], + ], + ), ), ), ), - ), - title: Row( - children: [ - Expanded( - child: Text( - displayname, - maxLines: 1, - overflow: TextOverflow.ellipsis, - softWrap: false, - style: unread || room.hasNewMessages - ? const TextStyle(fontWeight: FontWeight.bold) - : null, - ), - ), - if (isMuted) - const Padding( - padding: EdgeInsets.only(left: 4.0), - child: Icon( - Icons.notifications_off_outlined, - size: 16, - ), - ), - if (room.isFavourite || room.membership == Membership.invite) - Padding( - padding: EdgeInsets.only( - right: hasNotifications ? 4.0 : 0.0, - ), - child: Icon( - Icons.push_pin, - size: 16, - color: theme.colorScheme.primary, - ), - ), - if (!room.isSpace && - lastEvent != null && - room.membership != Membership.invite) - Padding( - padding: const EdgeInsets.only(left: 4.0), + title: Row( + children: [ + Expanded( child: Text( - lastEvent.originServerTs.localizedTimeShort(context), - style: TextStyle( - fontSize: 13, - color: unread - ? theme.colorScheme.secondary - : theme.textTheme.bodyMedium!.color, + displayname, + maxLines: 1, + overflow: TextOverflow.ellipsis, + softWrap: false, + style: unread || room.hasNewMessages + ? const TextStyle(fontWeight: FontWeight.bold) + : null, + ), + ), + if (isMuted) + const Padding( + padding: EdgeInsets.only(left: 4.0), + child: Icon( + Icons.notifications_off_outlined, + size: 16, ), ), - ), - if (room.isSpace) - const Icon( - Icons.arrow_circle_right_outlined, - size: 18, - ), - ], - ), - subtitle: Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (typingText.isEmpty && - ownMessage && - room.lastEvent!.status.isSending) ...[ - const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator.adaptive(strokeWidth: 2), - ), - const SizedBox(width: 4), + if (room.isFavourite || room.membership == Membership.invite) + Padding( + padding: EdgeInsets.only( + right: hasNotifications ? 4.0 : 0.0, + ), + child: Icon( + Icons.push_pin, + size: 16, + color: theme.colorScheme.primary, + ), + ), + if (!room.isSpace && + lastEvent != null && + room.membership != Membership.invite) + Padding( + padding: const EdgeInsets.only(left: 4.0), + child: Text( + lastEvent.originServerTs.localizedTimeShort(context), + style: TextStyle( + fontSize: 13, + color: unread + ? theme.colorScheme.secondary + : theme.textTheme.bodyMedium!.color, + ), + ), + ), + if (room.isSpace) + const Icon( + Icons.arrow_circle_right_outlined, + size: 18, + ), ], - AnimatedContainer( - width: typingText.isEmpty ? 0 : 18, - clipBehavior: Clip.hardEdge, - decoration: const BoxDecoration(), - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - padding: const EdgeInsets.only(right: 4), - child: Icon( - Icons.edit_outlined, - color: theme.colorScheme.secondary, - size: 14, + ), + subtitle: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (typingText.isEmpty && + ownMessage && + room.lastEvent!.status.isSending) ...[ + const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator.adaptive(strokeWidth: 2), + ), + const SizedBox(width: 4), + ], + AnimatedContainer( + width: typingText.isEmpty ? 0 : 18, + clipBehavior: Clip.hardEdge, + decoration: const BoxDecoration(), + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + padding: const EdgeInsets.only(right: 4), + child: Icon( + Icons.edit_outlined, + color: theme.colorScheme.secondary, + size: 14, + ), ), - ), - Expanded( - child: room.isSpace && room.membership == Membership.join - ? Text( - L10n.of(context)!.countChatsAndCountParticipants( - room.spaceChildren.length.toString(), - (room.summary.mJoinedMemberCount ?? 1).toString(), - ), - ) - : typingText.isNotEmpty - ? Text( - typingText, - style: TextStyle( - color: theme.colorScheme.primary, - ), - maxLines: 1, - softWrap: false, - ) - : FutureBuilder( - key: ValueKey( - '${lastEvent?.eventId}_${lastEvent?.type}', - ), - future: needLastEventSender - ? lastEvent.calcLocalizedBody( - MatrixLocals(L10n.of(context)!), - hideReply: true, - hideEdit: true, - plaintextBody: true, - removeMarkdown: true, - withSenderNamePrefix: (!isDirectChat || - directChatMatrixId != - room.lastEvent?.senderId), - ) - : null, - initialData: lastEvent?.calcLocalizedBodyFallback( - MatrixLocals(L10n.of(context)!), - hideReply: true, - hideEdit: true, - plaintextBody: true, - removeMarkdown: true, - withSenderNamePrefix: (!isDirectChat || - directChatMatrixId != - room.lastEvent?.senderId), - ), - builder: (context, snapshot) => Text( - room.membership == Membership.invite - ? isDirectChat - ? L10n.of(context)!.invitePrivateChat - : L10n.of(context)!.inviteGroupChat - : snapshot.data ?? - L10n.of(context)!.emptyChat, - softWrap: false, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontWeight: unread || room.hasNewMessages - ? FontWeight.bold - : null, - color: theme.colorScheme.onSurfaceVariant, - decoration: room.lastEvent?.redacted == true - ? TextDecoration.lineThrough - : null, - ), - ), - ), - ), - const SizedBox(width: 8), - AnimatedContainer( - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - padding: const EdgeInsets.symmetric(horizontal: 7), - height: unreadBubbleSize, - width: !hasNotifications && !unread && !room.hasNewMessages - ? 0 - : (unreadBubbleSize - 9) * - room.notificationCount.toString().length + - 9, - decoration: BoxDecoration( - color: room.highlightCount > 0 || - room.membership == Membership.invite - ? Colors.red - : hasNotifications || room.markedUnread - ? theme.colorScheme.primary - : theme.colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(AppConfig.borderRadius), - ), - child: Center( - child: hasNotifications + Expanded( + child: room.isSpace && room.membership == Membership.join ? Text( - room.notificationCount.toString(), - style: TextStyle( - color: room.highlightCount > 0 - ? Colors.white - : hasNotifications - ? theme.colorScheme.onPrimary - : theme.colorScheme.onPrimaryContainer, - fontSize: 13, + L10n.of(context)!.countChatsAndCountParticipants( + room.spaceChildren.length.toString(), + (room.summary.mJoinedMemberCount ?? 1).toString(), ), ) - : const SizedBox.shrink(), + : typingText.isNotEmpty + ? Text( + typingText, + style: TextStyle( + color: theme.colorScheme.primary, + ), + maxLines: 1, + softWrap: false, + ) + : FutureBuilder( + key: ValueKey( + '${lastEvent?.eventId}_${lastEvent?.type}', + ), + future: needLastEventSender + ? lastEvent.calcLocalizedBody( + MatrixLocals(L10n.of(context)!), + hideReply: true, + hideEdit: true, + plaintextBody: true, + removeMarkdown: true, + withSenderNamePrefix: (!isDirectChat || + directChatMatrixId != + room.lastEvent?.senderId), + ) + : null, + initialData: + lastEvent?.calcLocalizedBodyFallback( + MatrixLocals(L10n.of(context)!), + hideReply: true, + hideEdit: true, + plaintextBody: true, + removeMarkdown: true, + withSenderNamePrefix: (!isDirectChat || + directChatMatrixId != + room.lastEvent?.senderId), + ), + builder: (context, snapshot) => Text( + room.membership == Membership.invite + ? isDirectChat + ? L10n.of(context)!.invitePrivateChat + : L10n.of(context)!.inviteGroupChat + : snapshot.data ?? + L10n.of(context)!.emptyChat, + softWrap: false, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontWeight: unread || room.hasNewMessages + ? FontWeight.bold + : null, + color: theme.colorScheme.onSurfaceVariant, + decoration: room.lastEvent?.redacted == true + ? TextDecoration.lineThrough + : null, + ), + ), + ), ), - ), - ], + const SizedBox(width: 8), + AnimatedContainer( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + padding: const EdgeInsets.symmetric(horizontal: 7), + height: unreadBubbleSize, + width: !hasNotifications && !unread && !room.hasNewMessages + ? 0 + : (unreadBubbleSize - 9) * + room.notificationCount.toString().length + + 9, + decoration: BoxDecoration( + color: room.highlightCount > 0 || + room.membership == Membership.invite + ? Colors.red + : hasNotifications || room.markedUnread + ? theme.colorScheme.primary + : theme.colorScheme.primaryContainer, + borderRadius: + BorderRadius.circular(AppConfig.borderRadius), + ), + child: Center( + child: hasNotifications + ? Text( + room.notificationCount.toString(), + style: TextStyle( + color: room.highlightCount > 0 + ? Colors.white + : hasNotifications + ? theme.colorScheme.onPrimary + : theme.colorScheme.onPrimaryContainer, + fontSize: 13, + ), + ) + : const SizedBox.shrink(), + ), + ), + ], + ), + onTap: onTap, + trailing: onForget == null + ? null + : IconButton( + icon: const Icon(Icons.delete_outlined), + onPressed: onForget, + ), ), - onTap: onTap, - trailing: onForget == null - ? null - : IconButton( - icon: const Icon(Icons.delete_outlined), - onPressed: onForget, - ), ), ), ), From 282188f574e419fe6837a3fa6d96999b76534522 Mon Sep 17 00:00:00 2001 From: Krille Date: Mon, 15 Jul 2024 16:57:56 +0200 Subject: [PATCH 14/15] chore: Follow up listtilehovered --- lib/pages/chat_list/chat_list_item.dart | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index 8036aee62..a146a076f 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -170,10 +170,13 @@ class ChatListItem extends StatelessWidget { onTap: () => onLongPress?.call(context), ), ), - if (listTileHovered) - Positioned( - top: -2, - right: -2, + Positioned( + top: -2, + right: -2, + child: AnimatedScale( + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + scale: listTileHovered ? 1.1 : 1.0, child: Material( color: backgroundColor, borderRadius: BorderRadius.circular(16), @@ -183,6 +186,7 @@ class ChatListItem extends StatelessWidget { ), ), ), + ), ], ), ), From b05eb891a66816fb71277b94304d594389997a2d Mon Sep 17 00:00:00 2001 From: krille-chan Date: Mon, 15 Jul 2024 21:14:49 +0200 Subject: [PATCH 15/15] chore: Bring back navrail --- lib/config/routes.dart | 5 + lib/pages/chat/chat_view.dart | 15 ++- lib/pages/chat_list/chat_list.dart | 8 +- lib/pages/chat_list/chat_list_body.dart | 3 +- lib/pages/chat_list/chat_list_item.dart | 6 +- lib/pages/chat_list/chat_list_view.dart | 134 +++++++++++++++++---- lib/pages/chat_list/nav_rail_item.dart | 95 +++++++++++++++ lib/pages/chat_list/navi_rail_item.dart | 134 +++++++++++---------- lib/widgets/layouts/two_column_layout.dart | 4 +- lib/widgets/unread_rooms_badge.dart | 61 ++++------ linux/my_application.cc | 2 +- 11 files changed, 332 insertions(+), 135 deletions(-) create mode 100644 lib/pages/chat_list/nav_rail_item.dart diff --git a/lib/config/routes.dart b/lib/config/routes.dart index 4aaf63a0b..d11cd56db 100644 --- a/lib/config/routes.dart +++ b/lib/config/routes.dart @@ -92,8 +92,12 @@ abstract class AppRoutes { FluffyThemes.isColumnMode(context) && state.fullPath?.startsWith('/rooms/settings') == false ? TwoColumnLayout( + displayNavigationRail: + state.path?.startsWith('/rooms/settings') != true, mainView: ChatList( activeChat: state.pathParameters['roomid'], + displayNavigationRail: + state.path?.startsWith('/rooms/settings') != true, ), sideView: child, ) @@ -171,6 +175,7 @@ abstract class AppRoutes { ? TwoColumnLayout( mainView: const Settings(), sideView: child, + displayNavigationRail: false, ) : child, ), diff --git a/lib/pages/chat/chat_view.dart b/lib/pages/chat/chat_view.dart index 9974a9e70..80a84fa41 100644 --- a/lib/pages/chat/chat_view.dart +++ b/lib/pages/chat/chat_view.dart @@ -182,10 +182,17 @@ class ChatView extends StatelessWidget { tooltip: L10n.of(context)!.close, color: Theme.of(context).colorScheme.primary, ) - : UnreadRoomsBadge( - filter: (r) => r.id != controller.roomId, - badgePosition: BadgePosition.topEnd(end: 8, top: 4), - child: const Center(child: BackButton()), + : StreamBuilder( + stream: Matrix.of(context) + .client + .onSync + .stream + .where((syncUpdate) => syncUpdate.hasRoomUpdate), + builder: (context, _) => UnreadRoomsBadge( + filter: (r) => r.id != controller.roomId, + badgePosition: BadgePosition.topEnd(end: 8, top: 4), + child: const Center(child: BackButton()), + ), ), titleSpacing: 0, title: ChatAppBarTitle(controller), diff --git a/lib/pages/chat_list/chat_list.dart b/lib/pages/chat_list/chat_list.dart index 8ee1870b5..d4f6067ed 100644 --- a/lib/pages/chat_list/chat_list.dart +++ b/lib/pages/chat_list/chat_list.dart @@ -73,10 +73,12 @@ extension LocalizedActiveFilter on ActiveFilter { class ChatList extends StatefulWidget { static BuildContext? contextForVoip; final String? activeChat; + final bool displayNavigationRail; const ChatList({ super.key, required this.activeChat, + this.displayNavigationRail = false, }); @override @@ -667,7 +669,11 @@ class ChatListController extends State child: Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.navigate_next_outlined), + Avatar( + mxContent: space.avatar, + size: Avatar.defaultSize / 2, + name: space.getLocalizedDisplayname(), + ), const SizedBox(width: 12), Expanded( child: Text( diff --git a/lib/pages/chat_list/chat_list_body.dart b/lib/pages/chat_list/chat_list_body.dart index 4b17ad6b5..e596440b3 100644 --- a/lib/pages/chat_list/chat_list_body.dart +++ b/lib/pages/chat_list/chat_list_body.dart @@ -168,7 +168,8 @@ class ChatListViewBody extends StatelessWidget { ActiveFilter.allChats, ActiveFilter.unread, ActiveFilter.groups, - if (spaceDelegateCandidates.isNotEmpty) + if (spaceDelegateCandidates.isNotEmpty && + !controller.widget.displayNavigationRail) ActiveFilter.spaces, ] .map( diff --git a/lib/pages/chat_list/chat_list_item.dart b/lib/pages/chat_list/chat_list_item.dart index a146a076f..f704c41bf 100644 --- a/lib/pages/chat_list/chat_list_item.dart +++ b/lib/pages/chat_list/chat_list_item.dart @@ -171,12 +171,12 @@ class ChatListItem extends StatelessWidget { ), ), Positioned( - top: -2, - right: -2, + top: 0, + right: 0, child: AnimatedScale( duration: FluffyThemes.animationDuration, curve: FluffyThemes.animationCurve, - scale: listTileHovered ? 1.1 : 1.0, + scale: listTileHovered ? 1.0 : 0.0, child: Material( color: backgroundColor, borderRadius: BorderRadius.circular(16), diff --git a/lib/pages/chat_list/chat_list_view.dart b/lib/pages/chat_list/chat_list_view.dart index 93fa8575b..5bb6687a0 100644 --- a/lib/pages/chat_list/chat_list_view.dart +++ b/lib/pages/chat_list/chat_list_view.dart @@ -5,7 +5,12 @@ import 'package:flutter_gen/gen_l10n/l10n.dart'; import 'package:go_router/go_router.dart'; import 'package:keyboard_shortcuts/keyboard_shortcuts.dart'; +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/pages/chat_list/navi_rail_item.dart'; +import 'package:fluffychat/utils/matrix_sdk_extensions/matrix_locals.dart'; +import 'package:fluffychat/widgets/avatar.dart'; import '../../widgets/matrix.dart'; import 'chat_list_body.dart'; @@ -35,32 +40,113 @@ class ChatListView extends StatelessWidget { return; } }, - child: GestureDetector( - onTap: FocusManager.instance.primaryFocus?.unfocus, - excludeFromSemantics: true, - behavior: HitTestBehavior.translucent, - child: Scaffold( - body: ChatListViewBody(controller), - floatingActionButton: KeyBoardShortcuts( - keysToPress: { - LogicalKeyboardKey.controlLeft, - LogicalKeyboardKey.keyN, - }, - onKeysPressed: () => context.go('/rooms/newprivatechat'), - helpLabel: L10n.of(context)!.newChat, - child: - selectMode == SelectMode.normal && !controller.isSearchMode - ? FloatingActionButton.extended( - onPressed: controller.addChatAction, - icon: const Icon(Icons.add_outlined), - label: Text( - L10n.of(context)!.chat, - overflow: TextOverflow.fade, + child: Row( + children: [ + if (FluffyThemes.isColumnMode(context) && + controller.widget.displayNavigationRail) ...[ + Builder( + builder: (context) { + final allSpaces = Matrix.of(context) + .client + .rooms + .where((room) => room.isSpace); + final rootSpaces = allSpaces + .where( + (space) => !allSpaces.any( + (parentSpace) => parentSpace.spaceChildren + .any((child) => child.roomId == space.id), + ), + ) + .toList(); + + return SizedBox( + width: FluffyThemes.navRailWidth, + child: ListView.builder( + scrollDirection: Axis.vertical, + itemCount: rootSpaces.length + 2, + itemBuilder: (context, i) { + if (i == 0) { + return NaviRailItem( + isSelected: controller.activeSpaceId == null, + onTap: controller.clearActiveSpace, + icon: const Icon(Icons.forum_outlined), + selectedIcon: const Icon(Icons.forum), + toolTip: L10n.of(context)!.chats, + unreadBadgeFilter: (room) => true, + ); + } + i--; + if (i == rootSpaces.length) { + return NaviRailItem( + isSelected: false, + onTap: () => context.go('/rooms/newspace'), + icon: const Icon(Icons.add), + toolTip: L10n.of(context)!.createNewSpace, + ); + } + final space = rootSpaces[i]; + final displayname = + rootSpaces[i].getLocalizedDisplayname( + MatrixLocals(L10n.of(context)!), + ); + final spaceChildrenIds = + space.spaceChildren.map((c) => c.roomId).toSet(); + return NaviRailItem( + toolTip: displayname, + isSelected: controller.activeSpaceId == space.id, + onTap: () => + controller.setActiveSpace(rootSpaces[i].id), + unreadBadgeFilter: (room) => + spaceChildrenIds.contains(room.id), + icon: Avatar( + mxContent: rootSpaces[i].avatar, + name: displayname, + size: 32, + borderRadius: BorderRadius.circular( + AppConfig.borderRadius / 4, + ), ), - ) - : const SizedBox.shrink(), + ); + }, + ), + ); + }, + ), + Container( + color: Theme.of(context).dividerColor, + width: 1, + ), + ], + Expanded( + child: GestureDetector( + onTap: FocusManager.instance.primaryFocus?.unfocus, + excludeFromSemantics: true, + behavior: HitTestBehavior.translucent, + child: Scaffold( + body: ChatListViewBody(controller), + floatingActionButton: KeyBoardShortcuts( + keysToPress: { + LogicalKeyboardKey.controlLeft, + LogicalKeyboardKey.keyN, + }, + onKeysPressed: () => context.go('/rooms/newprivatechat'), + helpLabel: L10n.of(context)!.newChat, + child: selectMode == SelectMode.normal && + !controller.isSearchMode + ? FloatingActionButton.extended( + onPressed: controller.addChatAction, + icon: const Icon(Icons.add_outlined), + label: Text( + L10n.of(context)!.chat, + overflow: TextOverflow.fade, + ), + ) + : const SizedBox.shrink(), + ), + ), + ), ), - ), + ], ), ); }, diff --git a/lib/pages/chat_list/nav_rail_item.dart b/lib/pages/chat_list/nav_rail_item.dart new file mode 100644 index 000000000..d09659f88 --- /dev/null +++ b/lib/pages/chat_list/nav_rail_item.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; + +import 'package:fluffychat/config/app_config.dart'; +import '../../config/themes.dart'; + +class NaviRailItem extends StatefulWidget { + final String toolTip; + final bool isSelected; + final void Function() onTap; + final Widget icon; + final Widget? selectedIcon; + + const NaviRailItem({ + required this.toolTip, + required this.isSelected, + required this.onTap, + required this.icon, + this.selectedIcon, + super.key, + }); + + @override + State createState() => _NaviRailItemState(); +} + +class _NaviRailItemState extends State { + bool _hovered = false; + + void _onHover(bool hover) { + if (hover == _hovered) return; + setState(() { + _hovered = hover; + }); + } + + @override + Widget build(BuildContext context) { + final borderRadius = BorderRadius.circular(AppConfig.borderRadius); + return SizedBox( + height: 64, + width: 64, + child: Stack( + children: [ + Positioned( + top: 16, + bottom: 16, + left: 0, + child: AnimatedContainer( + width: widget.isSelected ? 4 : 0, + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: const BorderRadius.only( + topRight: Radius.circular(90), + bottomRight: Radius.circular(90), + ), + ), + ), + ), + Center( + child: AnimatedScale( + scale: _hovered ? 1.2 : 1.0, + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + child: Material( + borderRadius: borderRadius, + color: widget.isSelected + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of(context).colorScheme.surface, + child: Tooltip( + message: widget.toolTip, + child: InkWell( + borderRadius: borderRadius, + onTap: widget.onTap, + onHover: _onHover, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 8.0, + ), + child: widget.isSelected + ? widget.selectedIcon ?? widget.icon + : widget.icon, + ), + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/chat_list/navi_rail_item.dart b/lib/pages/chat_list/navi_rail_item.dart index d09659f88..66ad7c041 100644 --- a/lib/pages/chat_list/navi_rail_item.dart +++ b/lib/pages/chat_list/navi_rail_item.dart @@ -1,14 +1,20 @@ import 'package:flutter/material.dart'; +import 'package:badges/badges.dart'; +import 'package:matrix/matrix.dart'; + import 'package:fluffychat/config/app_config.dart'; +import 'package:fluffychat/widgets/hover_builder.dart'; +import 'package:fluffychat/widgets/unread_rooms_badge.dart'; import '../../config/themes.dart'; -class NaviRailItem extends StatefulWidget { +class NaviRailItem extends StatelessWidget { final String toolTip; final bool isSelected; final void Function() onTap; final Widget icon; final Widget? selectedIcon; + final bool Function(Room)? unreadBadgeFilter; const NaviRailItem({ required this.toolTip, @@ -16,80 +22,78 @@ class NaviRailItem extends StatefulWidget { required this.onTap, required this.icon, this.selectedIcon, + this.unreadBadgeFilter, super.key, }); - - @override - State createState() => _NaviRailItemState(); -} - -class _NaviRailItemState extends State { - bool _hovered = false; - - void _onHover(bool hover) { - if (hover == _hovered) return; - setState(() { - _hovered = hover; - }); - } - @override Widget build(BuildContext context) { final borderRadius = BorderRadius.circular(AppConfig.borderRadius); - return SizedBox( - height: 64, - width: 64, - child: Stack( - children: [ - Positioned( - top: 16, - bottom: 16, - left: 0, - child: AnimatedContainer( - width: widget.isSelected ? 4 : 0, - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: const BorderRadius.only( - topRight: Radius.circular(90), - bottomRight: Radius.circular(90), - ), - ), - ), - ), - Center( - child: AnimatedScale( - scale: _hovered ? 1.2 : 1.0, - duration: FluffyThemes.animationDuration, - curve: FluffyThemes.animationCurve, - child: Material( - borderRadius: borderRadius, - color: widget.isSelected - ? Theme.of(context).colorScheme.primaryContainer - : Theme.of(context).colorScheme.surface, - child: Tooltip( - message: widget.toolTip, - child: InkWell( - borderRadius: borderRadius, - onTap: widget.onTap, - onHover: _onHover, - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 8.0, - ), - child: widget.isSelected - ? widget.selectedIcon ?? widget.icon - : widget.icon, + final icon = isSelected ? selectedIcon ?? this.icon : this.icon; + final unreadBadgeFilter = this.unreadBadgeFilter; + return HoverBuilder( + builder: (context, hovered) { + return SizedBox( + height: 64, + width: 64, + child: Stack( + children: [ + Positioned( + top: 16, + bottom: 16, + left: 0, + child: AnimatedContainer( + width: isSelected ? 4 : 0, + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + borderRadius: const BorderRadius.only( + topRight: Radius.circular(90), + bottomRight: Radius.circular(90), ), ), ), ), - ), + Center( + child: AnimatedScale( + scale: hovered ? 1.2 : 1.0, + duration: FluffyThemes.animationDuration, + curve: FluffyThemes.animationCurve, + child: Material( + borderRadius: borderRadius, + color: isSelected + ? Theme.of(context).colorScheme.primaryContainer + : Theme.of(context).colorScheme.surface, + child: Tooltip( + message: toolTip, + child: InkWell( + borderRadius: borderRadius, + onTap: onTap, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 8.0, + ), + child: unreadBadgeFilter == null + ? icon + : UnreadRoomsBadge( + filter: unreadBadgeFilter, + badgePosition: BadgePosition.topEnd( + top: -12, + end: -8, + ), + child: icon, + ), + ), + ), + ), + ), + ), + ), + ], ), - ], - ), + ); + }, ); } } diff --git a/lib/widgets/layouts/two_column_layout.dart b/lib/widgets/layouts/two_column_layout.dart index c270f120a..a6f4c8bdf 100644 --- a/lib/widgets/layouts/two_column_layout.dart +++ b/lib/widgets/layouts/two_column_layout.dart @@ -3,11 +3,13 @@ import 'package:flutter/material.dart'; class TwoColumnLayout extends StatelessWidget { final Widget mainView; final Widget sideView; + final bool displayNavigationRail; const TwoColumnLayout({ super.key, required this.mainView, required this.sideView, + required this.displayNavigationRail, }); @override Widget build(BuildContext context) { @@ -18,7 +20,7 @@ class TwoColumnLayout extends StatelessWidget { Container( clipBehavior: Clip.antiAlias, decoration: const BoxDecoration(), - width: 384.0, + width: 360.0 + (displayNavigationRail ? 64 : 0), child: mainView, ), Container( diff --git a/lib/widgets/unread_rooms_badge.dart b/lib/widgets/unread_rooms_badge.dart index 006691769..5270c0db3 100644 --- a/lib/widgets/unread_rooms_badge.dart +++ b/lib/widgets/unread_rooms_badge.dart @@ -19,41 +19,32 @@ class UnreadRoomsBadge extends StatelessWidget { @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 b.Badge( - badgeStyle: b.BadgeStyle( - badgeColor: Theme.of(context).colorScheme.primary, - elevation: 4, - borderSide: BorderSide( - color: Theme.of(context).colorScheme.surface, - width: 2, - ), - ), - badgeContent: Text( - unreadCount.toString(), - style: TextStyle( - color: Theme.of(context).colorScheme.onPrimary, - fontSize: 12, - ), - ), - showBadge: unreadCount != 0, - badgeAnimation: const b.BadgeAnimation.scale(), - position: badgePosition ?? b.BadgePosition.bottomEnd(), - child: child, - ); - }, + final unreadCount = Matrix.of(context) + .client + .rooms + .where(filter) + .where((r) => (r.isUnread || r.membership == Membership.invite)) + .length; + return b.Badge( + badgeStyle: b.BadgeStyle( + badgeColor: Theme.of(context).colorScheme.primary, + elevation: 4, + borderSide: BorderSide( + color: Theme.of(context).colorScheme.surface, + width: 2, + ), + ), + badgeContent: Text( + unreadCount.toString(), + style: TextStyle( + color: Theme.of(context).colorScheme.onPrimary, + fontSize: 12, + ), + ), + showBadge: unreadCount != 0, + badgeAnimation: const b.BadgeAnimation.scale(), + position: badgePosition ?? b.BadgePosition.bottomEnd(), + child: child, ); } } diff --git a/linux/my_application.cc b/linux/my_application.cc index 0abe77c60..c185bcd78 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -60,7 +60,7 @@ static void my_application_activate(GApplication* application) { gtk_window_set_title(window, "FluffyChat"); } - gtk_window_set_default_size(window, 800, 600); + gtk_window_set_default_size(window, 864, 680); gtk_widget_show(GTK_WIDGET(window)); g_autoptr(FlDartProject) project = fl_dart_project_new();