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, ); },