feat: pass button BuildContext to conversation menu to position it

This commit is contained in:
cogwheel0
2025-10-23 23:38:32 +05:30
parent 625631c096
commit c1eae6608d
2 changed files with 86 additions and 80 deletions

View File

@@ -1254,9 +1254,9 @@ class _ChatsDrawerState extends ConsumerState<ChatsDrawer> {
? null ? null
: () => _selectConversation(context, conv.id), : () => _selectConversation(context, conv.id),
onLongPress: null, onLongPress: null,
onMorePressed: () { onMorePressed: (buttonContext) {
showConversationContextMenu( showConversationContextMenu(
context: context, context: buttonContext,
ref: ref, ref: ref,
conversation: conv, conversation: conv,
); );
@@ -1609,7 +1609,7 @@ class _ConversationTileContent extends StatelessWidget {
final bool pinned; final bool pinned;
final bool selected; final bool selected;
final bool isLoading; final bool isLoading;
final VoidCallback? onMorePressed; final void Function(BuildContext)? onMorePressed;
final Widget? leading; final Widget? leading;
const _ConversationTileContent({ const _ConversationTileContent({
@@ -1664,22 +1664,29 @@ class _ConversationTileContent extends StatelessWidget {
} else if (onMorePressed != null) { } else if (onMorePressed != null) {
trailing.addAll([ trailing.addAll([
const SizedBox(width: Spacing.sm), const SizedBox(width: Spacing.sm),
IconButton( Builder(
iconSize: IconSize.sm, builder: (buttonContext) {
visualDensity: const VisualDensity(horizontal: -2, vertical: -2), return IconButton(
padding: EdgeInsets.zero, iconSize: IconSize.sm,
constraints: const BoxConstraints( visualDensity: const VisualDensity(
minWidth: TouchTarget.listItem, horizontal: -2,
minHeight: TouchTarget.listItem, vertical: -2,
), ),
icon: Icon( padding: EdgeInsets.zero,
Platform.isIOS constraints: const BoxConstraints(
? CupertinoIcons.ellipsis minWidth: TouchTarget.listItem,
: Icons.more_vert_rounded, minHeight: TouchTarget.listItem,
color: theme.iconSecondary, ),
), icon: Icon(
onPressed: onMorePressed, Platform.isIOS
tooltip: AppLocalizations.of(context)!.more, ? 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 Widget? leading;
final VoidCallback? onTap; final VoidCallback? onTap;
final VoidCallback? onLongPress; final VoidCallback? onLongPress;
final VoidCallback? onMorePressed; final void Function(BuildContext)? onMorePressed;
const _ConversationTile({ const _ConversationTile({
required this.title, required this.title,

View File

@@ -3,9 +3,6 @@ import 'dart:io' show Platform;
import 'package:conduit/core/providers/app_providers.dart'; import 'package:conduit/core/providers/app_providers.dart';
import 'package:conduit/l10n/app_localizations.dart'; import 'package:conduit/l10n/app_localizations.dart';
import 'package:conduit/shared/theme/theme_extensions.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:conduit/shared/widgets/themed_dialogs.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -35,74 +32,76 @@ class ConduitContextMenuAction {
Future<void> showConduitContextMenu({ Future<void> showConduitContextMenu({
required BuildContext context, required BuildContext context,
required List<ConduitContextMenuAction> actions, required List<ConduitContextMenuAction> actions,
Offset? position,
}) async { }) async {
if (actions.isEmpty) return; if (actions.isEmpty) return;
final theme = context.conduitTheme; 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<ConduitContextMenuAction>(
context: context, context: context,
backgroundColor: Colors.transparent, position: RelativeRect.fromLTRB(
builder: (sheetContext) { menuPosition.dx,
Future<void> handleAction(ConduitContextMenuAction action) async { menuPosition.dy,
action.onBeforeClose?.call(); overlay.size.width - menuPosition.dx,
Navigator.of(sheetContext).pop(); overlay.size.height - menuPosition.dy,
await Future.microtask(action.onSelected); ),
} shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppBorderRadius.md),
List<Widget> buildActionTiles() { ),
return actions color: theme.surfaceBackground,
.map( elevation: 8,
(action) => ConduitListItem( items: actions.map((action) {
isCompact: true, return PopupMenuItem<ConduitContextMenuAction>(
leading: Icon( value: action,
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(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: Spacing.screenPadding, horizontal: Spacing.md,
vertical: Spacing.screenPadding, vertical: Spacing.xs,
), ),
child: Container( child: Row(
decoration: BoxDecoration( children: [
color: theme.surfaceBackground, Icon(
borderRadius: BorderRadius.circular(AppBorderRadius.lg), Platform.isIOS ? action.cupertinoIcon : action.materialIcon,
boxShadow: ConduitShadows.modal(context), color: action.destructive ? Colors.red : theme.iconPrimary,
), size: IconSize.sm,
child: Column( ),
mainAxisSize: MainAxisSize.min, const SizedBox(width: Spacing.md),
children: [ Expanded(
const SizedBox(height: Spacing.sm), child: Text(
const SheetHandle(), action.label,
const SizedBox(height: Spacing.sm), style: AppTypography.standard.copyWith(
for (var i = 0; i < actionTiles.length; i++) ...[ color: action.destructive ? Colors.red : theme.textPrimary,
if (i != 0) const ConduitDivider(isCompact: true), fontWeight: FontWeight.w500,
actionTiles[i], ),
], ),
const SizedBox(height: Spacing.sm), ),
], ],
),
), ),
); );
}, }).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<void> showConversationContextMenu({ Future<void> showConversationContextMenu({