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/utils/ui_utils.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'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:conduit/features/chat/providers/chat_providers.dart' as chat; class ConduitContextMenuAction { final IconData cupertinoIcon; final IconData materialIcon; final String label; final Future Function() onSelected; final VoidCallback? onBeforeClose; final bool destructive; const ConduitContextMenuAction({ required this.cupertinoIcon, required this.materialIcon, required this.label, required this.onSelected, this.onBeforeClose, this.destructive = false, }); } Future showConduitContextMenu({ required BuildContext context, required List actions, }) async { if (actions.isEmpty) return; final theme = context.conduitTheme; await showModalBottomSheet( 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 ? theme.error : theme.iconPrimary, size: IconSize.modal, ), title: Text( action.label, style: AppTypography.standard.copyWith( color: action.destructive ? theme.error : theme.textPrimary, fontWeight: FontWeight.w500, ), ), onTap: () => handleAction(action), ), ) .toList(); } final actionTiles = buildActionTiles(); return ModalSheetSafeArea( padding: const EdgeInsets.symmetric( horizontal: Spacing.screenPadding, vertical: Spacing.screenPadding, ), child: Container( decoration: BoxDecoration( color: theme.surfaceBackground, borderRadius: BorderRadius.circular(AppBorderRadius.lg), boxShadow: ConduitShadows.modal, ), 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), ], ), ), ); }, ); } Future showConversationContextMenu({ required BuildContext context, required WidgetRef ref, required dynamic conversation, }) async { if (conversation == null) return; final l10n = AppLocalizations.of(context)!; final bool isPinned = conversation.pinned == true; final bool isArchived = conversation.archived == true; Future togglePin() async { final errorMessage = l10n.failedToUpdatePin; try { await chat.pinConversation(ref, conversation.id, !isPinned); } catch (_) { if (!context.mounted) return; UiUtils.showMessage(context, errorMessage, isError: true); } } Future toggleArchive() async { final errorMessage = l10n.failedToUpdateArchive; try { await chat.archiveConversation(ref, conversation.id, !isArchived); } catch (_) { if (!context.mounted) return; UiUtils.showMessage(context, errorMessage, isError: true); } } Future rename() async { await _renameConversation( context, ref, conversation.id, conversation.title ?? '', ); } Future deleteConversation() async { await _confirmAndDeleteConversation(context, ref, conversation.id); } HapticFeedback.selectionClick(); await showConduitContextMenu( context: context, actions: [ ConduitContextMenuAction( cupertinoIcon: isPinned ? CupertinoIcons.pin_slash : CupertinoIcons.pin_fill, materialIcon: isPinned ? Icons.push_pin_outlined : Icons.push_pin_rounded, label: isPinned ? l10n.unpin : l10n.pin, onBeforeClose: () => HapticFeedback.lightImpact(), onSelected: togglePin, ), ConduitContextMenuAction( cupertinoIcon: isArchived ? CupertinoIcons.archivebox_fill : CupertinoIcons.archivebox, materialIcon: isArchived ? Icons.unarchive_rounded : Icons.archive_rounded, label: isArchived ? l10n.unarchive : l10n.archive, onBeforeClose: () => HapticFeedback.lightImpact(), onSelected: toggleArchive, ), ConduitContextMenuAction( cupertinoIcon: CupertinoIcons.pencil, materialIcon: Icons.edit_rounded, label: l10n.rename, onBeforeClose: () => HapticFeedback.selectionClick(), onSelected: rename, ), ConduitContextMenuAction( cupertinoIcon: CupertinoIcons.delete, materialIcon: Icons.delete_rounded, label: l10n.delete, destructive: true, onBeforeClose: () => HapticFeedback.mediumImpact(), onSelected: deleteConversation, ), ], ); } Future _renameConversation( BuildContext context, WidgetRef ref, String conversationId, String currentTitle, ) async { final l10n = AppLocalizations.of(context)!; final newName = await ThemedDialogs.promptTextInput( context, title: l10n.renameChat, hintText: l10n.enterChatName, initialValue: currentTitle, confirmText: l10n.save, cancelText: l10n.cancel, ); if (!context.mounted) return; if (newName == null) return; if (newName.isEmpty || newName == currentTitle) return; final renameError = l10n.failedToRenameChat; try { final api = ref.read(apiServiceProvider); if (api == null) throw Exception('No API service'); await api.updateConversation(conversationId, title: newName); HapticFeedback.selectionClick(); ref.invalidate(conversationsProvider); final active = ref.read(activeConversationProvider); if (active?.id == conversationId) { ref.read(activeConversationProvider.notifier).state = active!.copyWith( title: newName, ); } } catch (_) { if (!context.mounted) return; UiUtils.showMessage(context, renameError, isError: true); } } Future _confirmAndDeleteConversation( BuildContext context, WidgetRef ref, String conversationId, ) async { final l10n = AppLocalizations.of(context)!; final confirmed = await ThemedDialogs.confirm( context, title: l10n.deleteChatTitle, message: l10n.deleteChatMessage, confirmText: l10n.delete, isDestructive: true, ); if (!context.mounted) return; if (!confirmed) return; final deleteError = l10n.failedToDeleteChat; try { final api = ref.read(apiServiceProvider); if (api == null) throw Exception('No API service'); await api.deleteConversation(conversationId); HapticFeedback.mediumImpact(); final active = ref.read(activeConversationProvider); if (active?.id == conversationId) { ref.read(activeConversationProvider.notifier).state = null; ref.read(chat.chatMessagesProvider.notifier).clearMessages(); } ref.invalidate(conversationsProvider); } catch (_) { if (!context.mounted) return; UiUtils.showMessage(context, deleteError, isError: true); } }