diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index a157b4f..1bc15db 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -969,7 +969,37 @@ class _ChatPageState extends ConsumerState { } void _copyMessage(String content) { - Clipboard.setData(ClipboardData(text: content)); + // Strip reasoning details from the copied content + String cleanedContent = content; + + // Remove
blocks + cleanedContent = cleanedContent.replaceAll( + RegExp( + r']*>[\s\S]*?<\/details>', + multiLine: true, + dotAll: true, + ), + '', + ); + + // Remove raw reasoning tags + cleanedContent = cleanedContent.replaceAll( + RegExp(r'[\s\S]*?<\/think>', multiLine: true, dotAll: true), + '', + ); + cleanedContent = cleanedContent.replaceAll( + RegExp( + r'[\s\S]*?<\/reasoning>', + multiLine: true, + dotAll: true, + ), + '', + ); + + // Clean up any extra whitespace + cleanedContent = cleanedContent.trim(); + + Clipboard.setData(ClipboardData(text: cleanedContent)); } void _regenerateMessage(dynamic message) async { @@ -1248,8 +1278,8 @@ class _ChatPageState extends ConsumerState { final maxFraction = isTablet ? 0.42 : 0.84; final edgeFraction = isTablet ? 0.36 : 0.50; // large phone edge final scrim = Platform.isIOS - ? context.colorTokens.overlayMedium - : context.colorTokens.overlayStrong; + ? context.colorTokens.scrimMedium + : context.colorTokens.scrimStrong; return ResponsiveDrawerLayout( maxFraction: maxFraction, diff --git a/lib/features/chat/widgets/assistant_message_widget.dart b/lib/features/chat/widgets/assistant_message_widget.dart index 6bd4ea9..0359fba 100644 --- a/lib/features/chat/widgets/assistant_message_widget.dart +++ b/lib/features/chat/widgets/assistant_message_widget.dart @@ -435,10 +435,6 @@ class _AssistantMessageWidgetState extends ConsumerState Widget _buildSegmentedContent() { final children = []; - // Determine if media (attachments or generated images) is rendered above. - final hasMediaAbove = - (widget.message.attachmentIds?.isNotEmpty ?? false) || - (widget.message.files?.isNotEmpty ?? false); bool firstToolSpacerAdded = false; int idx = 0; for (final seg in _segments) { @@ -450,11 +446,6 @@ class _AssistantMessageWidgetState extends ConsumerState } children.add(_buildToolCallTile(seg.toolCall!)); } else if (seg.isReasoning && seg.reasoning != null) { - // If a reasoning tile is the very first content and sits at the top, - // add a small spacer above it for breathing room. - if (children.isEmpty && !hasMediaAbove) { - children.add(const SizedBox(height: Spacing.sm)); - } children.add(_buildReasoningTile(seg.reasoning!, idx)); } else if ((seg.text ?? '').trim().isNotEmpty) { children.add(_buildEnhancedMarkdownContent(seg.text!)); diff --git a/lib/features/chat/widgets/modern_chat_input.dart b/lib/features/chat/widgets/modern_chat_input.dart index d1a1632..452c62f 100644 --- a/lib/features/chat/widgets/modern_chat_input.dart +++ b/lib/features/chat/widgets/modern_chat_input.dart @@ -210,7 +210,14 @@ class _ModernChatInputState extends ConsumerState PlatformUtils.lightHaptic(); widget.onSendMessage(text); _controller.clear(); - // Keep focus and keyboard open; do not collapse automatically + + // Dismiss keyboard after sending to recover screen space + _focusNode.unfocus(); + try { + SystemChannels.textInput.invokeMethod('TextInput.hide'); + } catch (_) { + // Silently handle if keyboard dismissal fails + } } void _insertNewline() { @@ -786,7 +793,7 @@ class _ModernChatInputState extends ConsumerState Spacing.sm, ), child: Row( - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.end, children: [ _buildOverflowButton( tooltip: AppLocalizations.of(context)!.more, @@ -796,42 +803,47 @@ class _ModernChatInputState extends ConsumerState ), const SizedBox(width: Spacing.sm), Expanded( - child: AnimatedContainer( - duration: const Duration(milliseconds: 180), - curve: Curves.easeOutCubic, - padding: const EdgeInsets.symmetric(horizontal: Spacing.md), - constraints: const BoxConstraints( - minHeight: TouchTarget.input, + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.25, ), - decoration: BoxDecoration( - color: composerSurface.withValues( - alpha: brightness == Brightness.dark ? 0.9 : 0.2, + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + curve: Curves.easeOutCubic, + padding: const EdgeInsets.symmetric(horizontal: Spacing.md), + constraints: const BoxConstraints( + minHeight: TouchTarget.input, ), - borderRadius: BorderRadius.circular(AppBorderRadius.round), - border: Border.all( - color: outlineColor.withValues( - alpha: brightness == Brightness.dark ? 0.32 : 0.2, + decoration: BoxDecoration( + color: composerSurface.withValues( + alpha: brightness == Brightness.dark ? 0.9 : 0.2, ), - width: BorderWidth.micro, - ), - ), - child: Row( - children: [ - Expanded( - child: _buildComposerTextField( - brightness: brightness, - sendOnEnter: sendOnEnter, - placeholderBase: placeholderBase, - placeholderFocused: placeholderFocused, - contentPadding: const EdgeInsets.symmetric( - vertical: Spacing.xs, - ), - isActive: isActive, + borderRadius: BorderRadius.circular(_composerRadius), + border: Border.all( + color: outlineColor.withValues( + alpha: brightness == Brightness.dark ? 0.32 : 0.2, ), + width: BorderWidth.micro, ), - if (!_hasText && voiceAvailable && !isGenerating) - _buildInlineMicIcon(voiceAvailable), - ], + ), + child: Row( + children: [ + Expanded( + child: _buildComposerTextField( + brightness: brightness, + sendOnEnter: sendOnEnter, + placeholderBase: placeholderBase, + placeholderFocused: placeholderFocused, + contentPadding: const EdgeInsets.symmetric( + vertical: Spacing.xs, + ), + isActive: isActive, + ), + ), + if (!_hasText && voiceAvailable && !isGenerating) + _buildInlineMicIcon(voiceAvailable), + ], + ), ), ), ), diff --git a/lib/features/chat/widgets/user_message_bubble.dart b/lib/features/chat/widgets/user_message_bubble.dart index 8304cde..896d35c 100644 --- a/lib/features/chat/widgets/user_message_bubble.dart +++ b/lib/features/chat/widgets/user_message_bubble.dart @@ -7,10 +7,11 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'dart:io' show Platform; import 'package:conduit/l10n/app_localizations.dart'; -import 'package:conduit/shared/widgets/chat_action_button.dart'; +import 'package:flutter/services.dart'; import '../../../core/providers/app_providers.dart'; import '../providers/chat_providers.dart'; import '../../../shared/services/tasks/task_queue.dart'; +import '../../../shared/utils/conversation_context_menu.dart'; import '../../tools/providers/tools_providers.dart'; class UserMessageBubble extends ConsumerStatefulWidget { @@ -41,28 +42,15 @@ class UserMessageBubble extends ConsumerStatefulWidget { ConsumerState createState() => _UserMessageBubbleState(); } -class _UserMessageBubbleState extends ConsumerState - with TickerProviderStateMixin { - bool _showActions = false; - late AnimationController _fadeController; - late AnimationController _slideController; - // press state handled by shared ChatActionButton - +class _UserMessageBubbleState extends ConsumerState { bool _isEditing = false; late final TextEditingController _editController; final FocusNode _editFocusNode = FocusNode(); + final GlobalKey _bubbleKey = GlobalKey(); @override void initState() { super.initState(); - _fadeController = AnimationController( - duration: AnimationDuration.microInteraction, - vsync: this, - ); - _slideController = AnimationController( - duration: AnimationDuration.messageSlide, - vsync: this, - ); _editController = TextEditingController( text: widget.message?.content ?? '', ); @@ -417,25 +405,56 @@ class _UserMessageBubbleState extends ConsumerState @override void dispose() { - _fadeController.dispose(); - _slideController.dispose(); _editController.dispose(); _editFocusNode.dispose(); super.dispose(); } - void _toggleActions() { - setState(() { - _showActions = !_showActions; - }); + Future _showMessageMenu(BuildContext context) async { + // Don't show menu while editing - use the visible Save/Cancel buttons instead + if (_isEditing) return; - if (_showActions) { - _fadeController.forward(); - _slideController.forward(); - } else { - _fadeController.reverse(); - _slideController.reverse(); + final l10n = AppLocalizations.of(context)!; + HapticFeedback.selectionClick(); + + // Get the position of the bubble to show menu below it + Offset? menuPosition; + final RenderBox? renderBox = + _bubbleKey.currentContext?.findRenderObject() as RenderBox?; + if (renderBox != null) { + final position = renderBox.localToGlobal(Offset.zero); + final size = renderBox.size; + // Position menu at bottom-right of the bubble + menuPosition = Offset( + position.dx + size.width, + position.dy + size.height, + ); } + + await showConduitContextMenu( + context: context, + position: menuPosition, + actions: [ + ConduitContextMenuAction( + cupertinoIcon: CupertinoIcons.pencil, + materialIcon: Icons.edit_outlined, + label: l10n.edit, + onBeforeClose: () => HapticFeedback.selectionClick(), + onSelected: () async => _startInlineEdit(), + ), + ConduitContextMenuAction( + cupertinoIcon: CupertinoIcons.doc_on_clipboard, + materialIcon: Icons.content_copy, + label: l10n.copy, + onBeforeClose: () => HapticFeedback.selectionClick(), + onSelected: () async { + if (widget.onCopy != null) { + widget.onCopy!(); + } + }, + ), + ], + ); } @override @@ -458,7 +477,7 @@ class _UserMessageBubbleState extends ConsumerState ); return GestureDetector( - onLongPress: () => _toggleActions(), + onLongPress: () => _showMessageMenu(context), behavior: HitTestBehavior.translucent, child: Container( width: double.infinity, @@ -490,6 +509,7 @@ class _UserMessageBubbleState extends ConsumerState maxWidth: MediaQuery.of(context).size.width * 0.82, ), child: Container( + key: _bubbleKey, padding: const EdgeInsets.symmetric( horizontal: Spacing.chatBubblePadding, vertical: Spacing.sm, @@ -584,12 +604,11 @@ class _UserMessageBubbleState extends ConsumerState ), ], ), - if (hasText) const SizedBox(height: Spacing.xs), - // Action buttons below the message - if (_showActions) ...[ + // Edit action buttons - show Save/Cancel when editing + if (_isEditing) ...[ const SizedBox(height: Spacing.sm), - _buildUserActionButtons(), + _buildEditActionButtons(), ], ], ), @@ -597,59 +616,100 @@ class _UserMessageBubbleState extends ConsumerState ); } - // Assistant-only message renderer removed. + Widget _buildEditActionButtons() { + final l10n = AppLocalizations.of(context)!; + final theme = context.conduitTheme; - // Markdown rendering and typing indicator helpers removed. - - // Removed unused assistant action buttons builder. - - Widget _buildActionButton({ - required IconData icon, - required String label, - VoidCallback? onTap, - }) { - return ChatActionButton(icon: icon, label: label, onTap: onTap); - } - - Widget _buildUserActionButtons() { - return Wrap( - spacing: Spacing.sm, - runSpacing: Spacing.sm, + return Row( + mainAxisAlignment: MainAxisAlignment.end, children: [ - if (_isEditing) ...[ - _buildActionButton( - icon: Platform.isIOS ? CupertinoIcons.check_mark : Icons.check, - label: AppLocalizations.of(context)!.save, - onTap: _saveInlineEdit, - ), - _buildActionButton( - icon: Platform.isIOS ? CupertinoIcons.xmark : Icons.close, - label: AppLocalizations.of(context)!.cancel, + // Cancel button + Material( + color: Colors.transparent, + child: InkWell( onTap: _cancelInlineEdit, + borderRadius: BorderRadius.circular(AppBorderRadius.small), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.xs, + ), + decoration: BoxDecoration( + color: theme.surfaceContainer, + borderRadius: BorderRadius.circular(AppBorderRadius.small), + border: Border.all( + color: theme.cardBorder, + width: BorderWidth.thin, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Platform.isIOS ? CupertinoIcons.xmark : Icons.close, + size: IconSize.xs, + color: theme.textSecondary, + ), + const SizedBox(width: Spacing.xs), + Text( + l10n.cancel, + style: AppTypography.standard.copyWith( + color: theme.textSecondary, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), ), - ] else ...[ - _buildActionButton( - icon: Platform.isIOS ? CupertinoIcons.pencil : Icons.edit_outlined, - label: AppLocalizations.of(context)!.edit, - onTap: widget.onEdit ?? _startInlineEdit, + ), + const SizedBox(width: Spacing.sm), + // Save button + Material( + color: Colors.transparent, + child: InkWell( + onTap: _saveInlineEdit, + borderRadius: BorderRadius.circular(AppBorderRadius.small), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: Spacing.md, + vertical: Spacing.xs, + ), + decoration: BoxDecoration( + color: theme.buttonPrimary, + borderRadius: BorderRadius.circular(AppBorderRadius.small), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Platform.isIOS ? CupertinoIcons.check_mark : Icons.check, + size: IconSize.xs, + color: theme.buttonPrimaryText, + ), + const SizedBox(width: Spacing.xs), + Text( + l10n.save, + style: AppTypography.standard.copyWith( + color: theme.buttonPrimaryText, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), ), - _buildActionButton( - icon: Platform.isIOS - ? CupertinoIcons.doc_on_clipboard - : Icons.content_copy, - label: AppLocalizations.of(context)!.copy, - onTap: widget.onCopy, - ), - ], + ), ], ); } + // Assistant-only message renderer removed. + void _startInlineEdit() { if (_isEditing) return; setState(() { _isEditing = true; - _showActions = true; // ensure actions visible for Save/Cancel _editController.text = widget.message.content ?? ''; }); // Request focus after frame to show keyboard @@ -664,7 +724,6 @@ class _UserMessageBubbleState extends ConsumerState if (!_isEditing) return; setState(() { _isEditing = false; - // keep actions panel open; user can close with long-press _editController.text = widget.message.content ?? ''; }); _editFocusNode.unfocus(); 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/theme/color_tokens.dart b/lib/shared/theme/color_tokens.dart index 2e4f3ff..ce5d660 100644 --- a/lib/shared/theme/color_tokens.dart +++ b/lib/shared/theme/color_tokens.dart @@ -39,6 +39,8 @@ class AppColorTokens extends ThemeExtension { required this.overlayWeak, required this.overlayMedium, required this.overlayStrong, + required this.scrimMedium, + required this.scrimStrong, required this.codeBackground, required this.codeBorder, required this.codeText, @@ -84,6 +86,10 @@ class AppColorTokens extends ThemeExtension { final Color overlayMedium; final Color overlayStrong; + // Scrim tokens (for drawer/modal overlays) + final Color scrimMedium; + final Color scrimStrong; + // Markdown/code tokens final Color codeBackground; final Color codeBorder; @@ -194,6 +200,14 @@ class AppColorTokens extends ThemeExtension { alpha: isLight ? 0.32 : 0.36, ); + // Scrim tokens use black to create darkening effect in both modes + final Color scrimMedium = Colors.black.withValues( + alpha: isLight ? 0.2 : 0.5, + ); + final Color scrimStrong = Colors.black.withValues( + alpha: isLight ? 0.32 : 0.6, + ); + final Color codeBackground = mix(variant.muted, neutralTone00, 0.5); final Color codeBorder = mix(variant.border, neutralTone40, 0.6); final Color codeText = _ensureContrast( @@ -232,6 +246,8 @@ class AppColorTokens extends ThemeExtension { overlayWeak: overlayWeak, overlayMedium: overlayMedium, overlayStrong: overlayStrong, + scrimMedium: scrimMedium, + scrimStrong: scrimStrong, codeBackground: codeBackground, codeBorder: codeBorder, codeText: codeText, @@ -269,6 +285,8 @@ class AppColorTokens extends ThemeExtension { Color? overlayWeak, Color? overlayMedium, Color? overlayStrong, + Color? scrimMedium, + Color? scrimStrong, Color? codeBackground, Color? codeBorder, Color? codeText, @@ -303,6 +321,8 @@ class AppColorTokens extends ThemeExtension { overlayWeak: overlayWeak ?? this.overlayWeak, overlayMedium: overlayMedium ?? this.overlayMedium, overlayStrong: overlayStrong ?? this.overlayStrong, + scrimMedium: scrimMedium ?? this.scrimMedium, + scrimStrong: scrimStrong ?? this.scrimStrong, codeBackground: codeBackground ?? this.codeBackground, codeBorder: codeBorder ?? this.codeBorder, codeText: codeText ?? this.codeText, @@ -364,6 +384,8 @@ class AppColorTokens extends ThemeExtension { overlayWeak: Color.lerp(overlayWeak, other.overlayWeak, t)!, overlayMedium: Color.lerp(overlayMedium, other.overlayMedium, t)!, overlayStrong: Color.lerp(overlayStrong, other.overlayStrong, t)!, + scrimMedium: Color.lerp(scrimMedium, other.scrimMedium, t)!, + scrimStrong: Color.lerp(scrimStrong, other.scrimStrong, t)!, codeBackground: Color.lerp(codeBackground, other.codeBackground, t)!, codeBorder: Color.lerp(codeBorder, other.codeBorder, t)!, codeText: Color.lerp(codeText, other.codeText, t)!, diff --git a/lib/shared/utils/conversation_context_menu.dart b/lib/shared/utils/conversation_context_menu.dart index 0a27832..893a1e1 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,78 @@ 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.small), + ), + color: theme.surfaceBackground, + elevation: 4, + items: actions.map((action) { + return PopupMenuItem( + value: action, padding: const EdgeInsets.symmetric( - horizontal: Spacing.screenPadding, - vertical: Spacing.screenPadding, + horizontal: Spacing.sm, + vertical: Spacing.xxs, ), - 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), - ], - ), + height: 36, + child: Row( + children: [ + Icon( + Platform.isIOS ? action.cupertinoIcon : action.materialIcon, + color: action.destructive ? Colors.red : theme.iconPrimary, + size: IconSize.xs, + ), + const SizedBox(width: Spacing.sm), + Expanded( + child: Text( + action.label, + style: AppTypography.standard.copyWith( + color: action.destructive ? Colors.red : theme.textPrimary, + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + ), + ], ), ); - }, + }).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({