From c1eae6608d0b014ebd56ed4e4c889a2a5724f33f Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Thu, 23 Oct 2025 23:38:32 +0530 Subject: [PATCH] feat: pass button BuildContext to conversation menu to position it --- .../navigation/widgets/chats_drawer.dart | 47 ++++--- .../utils/conversation_context_menu.dart | 119 +++++++++--------- 2 files changed, 86 insertions(+), 80 deletions(-) diff --git a/lib/features/navigation/widgets/chats_drawer.dart b/lib/features/navigation/widgets/chats_drawer.dart index 849b660..33d36c9 100644 --- a/lib/features/navigation/widgets/chats_drawer.dart +++ b/lib/features/navigation/widgets/chats_drawer.dart @@ -1254,9 +1254,9 @@ class _ChatsDrawerState extends ConsumerState { ? null : () => _selectConversation(context, conv.id), onLongPress: null, - onMorePressed: () { + onMorePressed: (buttonContext) { showConversationContextMenu( - context: context, + context: buttonContext, ref: ref, conversation: conv, ); @@ -1609,7 +1609,7 @@ class _ConversationTileContent extends StatelessWidget { final bool pinned; final bool selected; final bool isLoading; - final VoidCallback? onMorePressed; + final void Function(BuildContext)? onMorePressed; final Widget? leading; const _ConversationTileContent({ @@ -1664,22 +1664,29 @@ class _ConversationTileContent extends StatelessWidget { } else if (onMorePressed != null) { trailing.addAll([ const SizedBox(width: Spacing.sm), - IconButton( - iconSize: IconSize.sm, - visualDensity: const VisualDensity(horizontal: -2, vertical: -2), - padding: EdgeInsets.zero, - constraints: const BoxConstraints( - minWidth: TouchTarget.listItem, - minHeight: TouchTarget.listItem, - ), - icon: Icon( - Platform.isIOS - ? CupertinoIcons.ellipsis - : Icons.more_vert_rounded, - color: theme.iconSecondary, - ), - onPressed: onMorePressed, - tooltip: AppLocalizations.of(context)!.more, + Builder( + builder: (buttonContext) { + return IconButton( + iconSize: IconSize.sm, + visualDensity: const VisualDensity( + horizontal: -2, + vertical: -2, + ), + padding: EdgeInsets.zero, + constraints: const BoxConstraints( + minWidth: TouchTarget.listItem, + minHeight: TouchTarget.listItem, + ), + icon: Icon( + Platform.isIOS + ? CupertinoIcons.ellipsis + : Icons.more_vert_rounded, + color: theme.iconSecondary, + ), + onPressed: () => onMorePressed!(buttonContext), + tooltip: AppLocalizations.of(context)!.more, + ); + }, ), ]); } @@ -1720,7 +1727,7 @@ class _ConversationTile extends StatelessWidget { final Widget? leading; final VoidCallback? onTap; final VoidCallback? onLongPress; - final VoidCallback? onMorePressed; + final void Function(BuildContext)? onMorePressed; const _ConversationTile({ required this.title, diff --git a/lib/shared/utils/conversation_context_menu.dart b/lib/shared/utils/conversation_context_menu.dart index 0a27832..ca911da 100644 --- a/lib/shared/utils/conversation_context_menu.dart +++ b/lib/shared/utils/conversation_context_menu.dart @@ -3,9 +3,6 @@ import 'dart:io' show Platform; import 'package:conduit/core/providers/app_providers.dart'; import 'package:conduit/l10n/app_localizations.dart'; import 'package:conduit/shared/theme/theme_extensions.dart'; -import 'package:conduit/shared/widgets/conduit_components.dart'; -import 'package:conduit/shared/widgets/modal_safe_area.dart'; -import 'package:conduit/shared/widgets/sheet_handle.dart'; import 'package:conduit/shared/widgets/themed_dialogs.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -35,74 +32,76 @@ class ConduitContextMenuAction { Future showConduitContextMenu({ required BuildContext context, required List actions, + Offset? position, }) async { if (actions.isEmpty) return; final theme = context.conduitTheme; + final RenderBox? overlay = + Overlay.of(context).context.findRenderObject() as RenderBox?; - await showModalBottomSheet( + if (overlay == null) return; + + // Determine menu position + final Offset menuPosition = position ?? _getDefaultMenuPosition(context); + + final result = await showMenu( context: context, - backgroundColor: Colors.transparent, - builder: (sheetContext) { - Future handleAction(ConduitContextMenuAction action) async { - action.onBeforeClose?.call(); - Navigator.of(sheetContext).pop(); - await Future.microtask(action.onSelected); - } - - List buildActionTiles() { - return actions - .map( - (action) => ConduitListItem( - isCompact: true, - leading: Icon( - Platform.isIOS ? action.cupertinoIcon : action.materialIcon, - color: action.destructive ? Colors.red : theme.iconPrimary, - size: IconSize.modal, - ), - title: Text( - action.label, - style: AppTypography.standard.copyWith( - color: action.destructive ? Colors.red : theme.textPrimary, - fontWeight: FontWeight.w500, - ), - ), - onTap: () => handleAction(action), - ), - ) - .toList(); - } - - final actionTiles = buildActionTiles(); - - return ModalSheetSafeArea( + position: RelativeRect.fromLTRB( + menuPosition.dx, + menuPosition.dy, + overlay.size.width - menuPosition.dx, + overlay.size.height - menuPosition.dy, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(AppBorderRadius.md), + ), + color: theme.surfaceBackground, + elevation: 8, + items: actions.map((action) { + return PopupMenuItem( + value: action, padding: const EdgeInsets.symmetric( - horizontal: Spacing.screenPadding, - vertical: Spacing.screenPadding, + horizontal: Spacing.md, + vertical: Spacing.xs, ), - child: Container( - decoration: BoxDecoration( - color: theme.surfaceBackground, - borderRadius: BorderRadius.circular(AppBorderRadius.lg), - boxShadow: ConduitShadows.modal(context), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const SizedBox(height: Spacing.sm), - const SheetHandle(), - const SizedBox(height: Spacing.sm), - for (var i = 0; i < actionTiles.length; i++) ...[ - if (i != 0) const ConduitDivider(isCompact: true), - actionTiles[i], - ], - const SizedBox(height: Spacing.sm), - ], - ), + child: Row( + children: [ + Icon( + Platform.isIOS ? action.cupertinoIcon : action.materialIcon, + color: action.destructive ? Colors.red : theme.iconPrimary, + size: IconSize.sm, + ), + const SizedBox(width: Spacing.md), + Expanded( + child: Text( + action.label, + style: AppTypography.standard.copyWith( + color: action.destructive ? Colors.red : theme.textPrimary, + fontWeight: FontWeight.w500, + ), + ), + ), + ], ), ); - }, + }).toList(), ); + + if (result != null) { + result.onBeforeClose?.call(); + await Future.microtask(result.onSelected); + } +} + +Offset _getDefaultMenuPosition(BuildContext context) { + final RenderBox? renderBox = context.findRenderObject() as RenderBox?; + if (renderBox == null) { + return Offset.zero; + } + final position = renderBox.localToGlobal(Offset.zero); + final size = renderBox.size; + return Offset(position.dx + size.width, position.dy); } Future showConversationContextMenu({