From 1fc1cf97396681ed6800bd60a43b7119090759a3 Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Fri, 19 Sep 2025 23:35:46 +0530 Subject: [PATCH] feat: title on the header --- lib/features/chat/views/chat_page.dart | 127 +++++-- .../navigation/widgets/chats_drawer.dart | 329 +++--------------- .../utils/conversation_context_menu.dart | 270 ++++++++++++++ 3 files changed, 412 insertions(+), 314 deletions(-) create mode 100644 lib/shared/utils/conversation_context_menu.dart diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index 9466111..5d81034 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -40,6 +40,7 @@ import '../../../shared/widgets/conduit_components.dart'; import '../../../shared/widgets/middle_ellipsis_text.dart'; import '../../../shared/widgets/modal_safe_area.dart'; import '../../../core/services/settings_service.dart'; +import '../../../shared/utils/conversation_context_menu.dart'; // Removed unused PlatformUtils import import '../../../core/services/platform_service.dart' as ps; import 'package:flutter/gestures.dart' show DragStartBehavior; @@ -913,6 +914,7 @@ class _ChatPageState extends ConsumerState { @override Widget build(BuildContext context) { final theme = Theme.of(context); + final l10n = AppLocalizations.of(context)!; // Use select to watch only connectivity status to reduce rebuilds final isOnline = ref.watch(isOnlineProvider.select((status) => status)); @@ -924,6 +926,28 @@ class _ChatPageState extends ConsumerState { // Watch reviewer mode and auto-select model if needed final isReviewerMode = ref.watch(reviewerModeProvider); + final omitProviderInModelName = ref.watch( + appSettingsProvider.select( + (settings) => settings.omitProviderInModelName, + ), + ); + final conversationTitle = ref.watch( + activeConversationProvider.select((conv) => conv?.title), + ); + final displayConversationTitle = (() { + final trimmed = conversationTitle?.trim(); + if (trimmed != null && trimmed.isNotEmpty) { + return trimmed; + } + return l10n.newChat; + })(); + final formattedModelName = selectedModel != null + ? _formatModelDisplayName( + selectedModel.name, + omitProvider: omitProviderInModelName, + ) + : null; + // Keyboard visibility final keyboardVisible = MediaQuery.of(context).viewInsets.bottom > 0; // Whether the messages list can actually scroll (avoids showing button when not needed) @@ -1023,7 +1047,7 @@ class _ChatPageState extends ConsumerState { elevation: Elevation.none, surfaceTintColor: Colors.transparent, shadowColor: Colors.transparent, - toolbarHeight: kToolbarHeight - 8, + toolbarHeight: kToolbarHeight + 8, centerTitle: true, titleSpacing: 0.0, leading: _isSelectionMode @@ -1071,13 +1095,35 @@ class _ChatPageState extends ConsumerState { (models) => _showModelDropdown(context, ref, models), ); }, + onLongPress: () { + final conversation = ref.read(activeConversationProvider); + if (conversation == null) return; + showConversationContextMenu( + context: context, + ref: ref, + conversation: conversation, + ); + }, child: Column( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, children: [ + MiddleEllipsisText( + displayConversationTitle, + style: AppTypography.headlineSmallStyle.copyWith( + color: context.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + fontSize: 18, + height: 1.3, + ), + textAlign: TextAlign.center, + semanticsLabel: displayConversationTitle, + ), + const SizedBox(height: Spacing.xs), Transform.translate( offset: const Offset(0, 0), child: SizedBox( - height: 28, + height: 24, child: Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, @@ -1114,28 +1160,15 @@ class _ChatPageState extends ConsumerState { ), const SizedBox(width: Spacing.xs), Flexible( - child: Builder( - builder: (context) { - final omitProvider = ref - .watch(appSettingsProvider) - .omitProviderInModelName; - final label = _formatModelDisplayName( - selectedModel.name, - omitProvider: omitProvider, - ); - return MiddleEllipsisText( - label, - style: AppTypography.headlineSmallStyle - .copyWith( - color: context - .conduitTheme - .textPrimary, - fontWeight: FontWeight.w600, - ), - textAlign: TextAlign.center, - semanticsLabel: label, - ); - }, + child: MiddleEllipsisText( + formattedModelName!, + style: AppTypography.small.copyWith( + color: context.conduitTheme.textSecondary, + fontWeight: FontWeight.w600, + height: 1.2, + ), + textAlign: TextAlign.center, + semanticsLabel: formattedModelName, ), ), const SizedBox(width: Spacing.xs), @@ -1169,7 +1202,7 @@ class _ChatPageState extends ConsumerState { ), ), ), - if (ref.watch(reviewerModeProvider)) + if (isReviewerMode) Padding( padding: const EdgeInsets.only(top: 2.0), child: Container( @@ -1210,13 +1243,35 @@ class _ChatPageState extends ConsumerState { (models) => _showModelDropdown(context, ref, models), ); }, + onLongPress: () { + final conversation = ref.read(activeConversationProvider); + if (conversation == null) return; + showConversationContextMenu( + context: context, + ref: ref, + conversation: conversation, + ); + }, child: Column( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, children: [ + MiddleEllipsisText( + displayConversationTitle, + style: AppTypography.headlineSmallStyle.copyWith( + color: context.conduitTheme.textPrimary, + fontWeight: FontWeight.w600, + fontSize: 18, + height: 1.3, + ), + textAlign: TextAlign.center, + semanticsLabel: displayConversationTitle, + ), + const SizedBox(height: Spacing.xs), Transform.translate( offset: const Offset(0, 0), child: SizedBox( - height: 28, + height: 24, child: Row( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, @@ -1253,17 +1308,15 @@ class _ChatPageState extends ConsumerState { ), const SizedBox(width: Spacing.xs), Flexible( - child: Text( - 'Choose Model', - style: AppTypography.headlineSmallStyle - .copyWith( - color: - context.conduitTheme.textPrimary, - fontWeight: FontWeight.w600, - ), - maxLines: 1, - overflow: TextOverflow.ellipsis, + child: MiddleEllipsisText( + l10n.chooseModel, + style: AppTypography.small.copyWith( + color: context.conduitTheme.textSecondary, + fontWeight: FontWeight.w600, + height: 1.2, + ), textAlign: TextAlign.center, + semanticsLabel: l10n.chooseModel, ), ), const SizedBox(width: Spacing.xs), @@ -1297,7 +1350,7 @@ class _ChatPageState extends ConsumerState { ), ), ), - if (ref.watch(reviewerModeProvider)) + if (isReviewerMode) Padding( padding: const EdgeInsets.only(top: 2.0), child: Container( diff --git a/lib/features/navigation/widgets/chats_drawer.dart b/lib/features/navigation/widgets/chats_drawer.dart index 646b0b4..91c7114 100644 --- a/lib/features/navigation/widgets/chats_drawer.dart +++ b/lib/features/navigation/widgets/chats_drawer.dart @@ -8,7 +8,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../core/providers/app_providers.dart'; import '../../../shared/theme/theme_extensions.dart'; -import '../../../shared/widgets/modal_safe_area.dart'; import '../../chat/providers/chat_providers.dart' as chat; // import '../../files/views/files_page.dart'; import '../../profile/views/profile_page.dart'; @@ -17,6 +16,7 @@ import '../../../shared/widgets/themed_dialogs.dart'; import '../../../core/auth/auth_state_manager.dart'; import 'package:conduit/l10n/app_localizations.dart'; import '../../../core/utils/user_display_name.dart'; +import '../../../shared/utils/conversation_context_menu.dart'; class ChatsDrawer extends ConsumerStatefulWidget { const ChatsDrawer({super.key}); @@ -798,62 +798,31 @@ class _ChatsDrawerState extends ConsumerState { String folderId, String folderName, ) { - final theme = context.conduitTheme; - // Ensure consistent modal padding/insets across the app - // ignore: unnecessary_import + final l10n = AppLocalizations.of(context)!; - showModalBottomSheet( + showConduitContextMenu( context: context, - backgroundColor: theme.surfaceBackground, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(AppBorderRadius.lg), + actions: [ + ConduitContextMenuAction( + cupertinoIcon: CupertinoIcons.pencil, + materialIcon: Icons.edit_rounded, + label: l10n.rename, + onBeforeClose: () => HapticFeedback.selectionClick(), + onSelected: () async { + await _renameFolder(context, folderId, folderName); + }, ), - ), - builder: (sheetContext) { - return ModalSheetSafeArea( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.modalPadding, - vertical: Spacing.modalPadding, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: Icon( - Platform.isIOS ? CupertinoIcons.pencil : Icons.edit_rounded, - color: theme.iconPrimary, - ), - title: Text( - AppLocalizations.of(context)!.rename, - style: TextStyle(color: theme.textPrimary), - ), - onTap: () async { - HapticFeedback.selectionClick(); - Navigator.pop(sheetContext); - await _renameFolder(context, folderId, folderName); - }, - ), - const Divider(height: 1), - ListTile( - leading: Icon( - Platform.isIOS ? CupertinoIcons.delete : Icons.delete_rounded, - color: theme.error, - ), - title: Text( - AppLocalizations.of(context)!.delete, - style: TextStyle(color: theme.error), - ), - onTap: () async { - HapticFeedback.mediumImpact(); - Navigator.pop(sheetContext); - await _confirmAndDeleteFolder(context, folderId, folderName); - }, - ), - ], - ), - ); - }, + ConduitContextMenuAction( + cupertinoIcon: CupertinoIcons.delete, + materialIcon: Icons.delete_rounded, + label: l10n.delete, + destructive: true, + onBeforeClose: () => HapticFeedback.mediumImpact(), + onSelected: () async { + await _confirmAndDeleteFolder(context, folderId, folderName); + }, + ), + ], ); } @@ -1017,8 +986,11 @@ class _ChatsDrawerState extends ConsumerState { : () => _selectConversation(context, conv.id), onLongPress: null, onMorePressed: () { - HapticFeedback.selectionClick(); - _showConversationContextMenu(context, conv); + showConversationContextMenu( + context: context, + ref: ref, + conversation: conv, + ); }, ); @@ -1321,209 +1293,6 @@ class _ChatsDrawerState extends ConsumerState { ), ); } - - void _showConversationContextMenu(BuildContext context, dynamic conv) { - final theme = context.conduitTheme; - final bool isPinned = conv.pinned == true; - final bool isArchived = conv.archived == true; - - HapticFeedback.selectionClick(); - showModalBottomSheet( - context: context, - backgroundColor: theme.surfaceBackground, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - top: Radius.circular(AppBorderRadius.lg), - ), - ), - builder: (sheetContext) { - return ModalSheetSafeArea( - padding: const EdgeInsets.symmetric( - horizontal: Spacing.modalPadding, - vertical: Spacing.modalPadding, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - leading: Icon( - isPinned - ? (Platform.isIOS - ? CupertinoIcons.pin_slash - : Icons.push_pin_outlined) - : (Platform.isIOS - ? CupertinoIcons.pin_fill - : Icons.push_pin_rounded), - color: theme.iconPrimary, - ), - title: Text( - isPinned - ? AppLocalizations.of(context)!.unpin - : AppLocalizations.of(context)!.pin, - style: TextStyle(color: theme.textPrimary), - ), - onTap: () async { - HapticFeedback.lightImpact(); - Navigator.pop(sheetContext); - final pinErrorMessage = AppLocalizations.of( - context, - )!.failedToUpdatePin; - try { - await chat.pinConversation(ref, conv.id, !isPinned); - } catch (_) { - if (!mounted) return; - UiUtils.showMessage( - this.context, - pinErrorMessage, - isError: true, - ); - } - }, - ), - ListTile( - leading: Icon( - isArchived - ? (Platform.isIOS - ? CupertinoIcons.archivebox_fill - : Icons.unarchive_rounded) - : (Platform.isIOS - ? CupertinoIcons.archivebox - : Icons.archive_rounded), - color: theme.iconPrimary, - ), - title: Text( - isArchived - ? AppLocalizations.of(context)!.unarchive - : AppLocalizations.of(context)!.archive, - style: TextStyle(color: theme.textPrimary), - ), - onTap: () async { - HapticFeedback.lightImpact(); - Navigator.pop(sheetContext); - final archiveErrorMessage = AppLocalizations.of( - context, - )!.failedToUpdateArchive; - try { - await chat.archiveConversation(ref, conv.id, !isArchived); - } catch (_) { - if (!mounted) return; - UiUtils.showMessage( - this.context, - archiveErrorMessage, - isError: true, - ); - } - }, - ), - ListTile( - leading: Icon( - Platform.isIOS ? CupertinoIcons.pencil : Icons.edit_rounded, - color: theme.iconPrimary, - ), - title: Text( - AppLocalizations.of(context)!.rename, - style: TextStyle(color: theme.textPrimary), - ), - onTap: () async { - HapticFeedback.selectionClick(); - Navigator.pop(sheetContext); - await _renameConversation(context, conv.id, conv.title ?? ''); - }, - ), - const Divider(height: 1), - ListTile( - leading: Icon( - Platform.isIOS ? CupertinoIcons.delete : Icons.delete_rounded, - color: theme.error, - ), - title: Text( - AppLocalizations.of(context)!.delete, - style: TextStyle(color: theme.error), - ), - onTap: () async { - HapticFeedback.mediumImpact(); - Navigator.pop(sheetContext); - await _confirmAndDeleteConversation(context, conv.id); - }, - ), - ], - ), - ); - }, - ); - } - - Future _renameConversation( - BuildContext context, - 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 (!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(); - // Reflect changes - ref.invalidate(conversationsProvider); - final active = ref.read(activeConversationProvider); - if (active?.id == conversationId) { - ref.read(activeConversationProvider.notifier).state = active!.copyWith( - title: newName, - ); - } - } catch (_) { - if (!mounted) return; - UiUtils.showMessage(this.context, renameError, isError: true); - } - } - - Future _confirmAndDeleteConversation( - BuildContext context, - 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 (!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(); - // Clear if deleting active - 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 (!mounted) return; - UiUtils.showMessage(this.context, deleteError, isError: true); - } - } } class _DragConversationData { @@ -1596,7 +1365,7 @@ class _ConversationTileContent extends StatelessWidget { final theme = context.conduitTheme; final textStyle = AppTypography.standard.copyWith( color: theme.textPrimary, - fontWeight: FontWeight.w400, + fontWeight: selected ? FontWeight.w600 : FontWeight.w400, height: 1.4, ); @@ -1696,15 +1465,22 @@ class _ConversationTile extends StatelessWidget { @override Widget build(BuildContext context) { final theme = context.conduitTheme; - final borderColor = selected - ? theme.navigationSelected - : pinned - ? theme.navigationSelected.withValues(alpha: 0.30) - : theme.surfaceContainerHighest.withValues(alpha: 0.40); - final backgroundColor = theme.surfaceContainer; - final highlightColor = theme.navigationSelectedBackground.withValues( - alpha: 0.45, - ); + final brightness = Theme.of(context).brightness; + final borderRadius = BorderRadius.circular(AppBorderRadius.navigation); + final Color background = selected + ? theme.buttonPrimary.withValues( + alpha: brightness == Brightness.dark ? 0.28 : 0.16, + ) + : theme.surfaceContainer; + final Color borderColor; + if (selected) { + borderColor = theme.buttonPrimary.withValues(alpha: 0.7); + } else if (pinned) { + borderColor = theme.buttonPrimary.withValues(alpha: 0.35); + } else { + borderColor = theme.surfaceContainerHighest.withValues(alpha: 0.40); + } + final List shadow = selected ? ConduitShadows.low : const []; Color? overlayForStates(Set states) { if (states.contains(WidgetState.pressed)) { @@ -1721,13 +1497,10 @@ class _ConversationTile extends StatelessWidget { selected: selected, button: true, child: Material( - color: backgroundColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.zero, - side: BorderSide(color: borderColor, width: BorderWidth.thin), - ), + color: Colors.transparent, + shape: RoundedRectangleBorder(borderRadius: borderRadius), child: InkWell( - borderRadius: BorderRadius.zero, + borderRadius: borderRadius, onTap: isLoading ? null : onTap, onLongPress: onLongPress, overlayColor: WidgetStateProperty.resolveWith(overlayForStates), @@ -1735,8 +1508,10 @@ class _ConversationTile extends StatelessWidget { duration: const Duration(milliseconds: 160), curve: Curves.easeOut, decoration: BoxDecoration( - color: selected ? highlightColor : Colors.transparent, - borderRadius: BorderRadius.zero, + color: background, + borderRadius: borderRadius, + border: Border.all(color: borderColor, width: BorderWidth.thin), + boxShadow: shadow, ), child: ConstrainedBox( constraints: const BoxConstraints( diff --git a/lib/shared/utils/conversation_context_menu.dart b/lib/shared/utils/conversation_context_menu.dart new file mode 100644 index 0000000..e38e3b8 --- /dev/null +++ b/lib/shared/utils/conversation_context_menu.dart @@ -0,0 +1,270 @@ +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); + } +}