From ff02af1e8925e7b8c66f519a95f583838b679e7c Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Thu, 2 Oct 2025 00:30:14 +0530 Subject: [PATCH] refactor: centralize conversation cache management - Introduced a new method `refreshConversationsCache` to streamline the invalidation of the conversations provider and optionally the folders provider. - Updated various components to utilize the new cache management method, enhancing code clarity and reducing redundancy. - This refactor improves the efficiency of conversation and folder synchronization across the application. --- lib/core/providers/app_providers.dart | 11 ++ lib/core/providers/app_startup_providers.dart | 4 +- .../chat/providers/chat_providers.dart | 22 ++-- lib/features/chat/views/chat_page.dart | 4 +- .../chat/widgets/modern_chat_input.dart | 2 +- .../navigation/widgets/chats_drawer.dart | 114 +++++++++++------- .../utils/conversation_context_menu.dart | 33 +++-- 7 files changed, 122 insertions(+), 68 deletions(-) diff --git a/lib/core/providers/app_providers.dart b/lib/core/providers/app_providers.dart index c160ccd..c8059f3 100644 --- a/lib/core/providers/app_providers.dart +++ b/lib/core/providers/app_providers.dart @@ -661,6 +661,17 @@ class _ConversationsCacheTimestamp extends _$ConversationsCacheTimestamp { void set(DateTime? timestamp) => state = timestamp; } +/// Clears the in-memory timestamp cache and invalidates the conversations +/// provider so the next read forces a refetch. Optionally invalidates the +/// folders provider when folder metadata must stay in sync with conversations. +void refreshConversationsCache(dynamic ref, {bool includeFolders = false}) { + ref.read(_conversationsCacheTimestampProvider.notifier).set(null); + ref.invalidate(conversationsProvider); + if (includeFolders) { + ref.invalidate(foldersProvider); + } +} + // Conversation providers - Now using correct OpenWebUI API with caching // keepAlive to maintain cache during authenticated session @Riverpod(keepAlive: true) diff --git a/lib/core/providers/app_startup_providers.dart b/lib/core/providers/app_startup_providers.dart index 16a15cb..a64f852 100644 --- a/lib/core/providers/app_startup_providers.dart +++ b/lib/core/providers/app_startup_providers.dart @@ -103,7 +103,7 @@ void _scheduleConversationWarmup(Ref ref, {bool force = false}) { return; } if (existing.hasError) { - ref.invalidate(conversationsProvider); + refreshConversationsCache(ref); } final conversations = await ref.read(conversationsProvider.future); statusController.set(_ConversationWarmupStatus.complete); @@ -368,7 +368,7 @@ class _ForegroundRefreshObserver extends WidgetsBindingObserver { // Schedule to avoid side-effects during build frames Future.microtask(() { try { - _ref.invalidate(conversationsProvider); + refreshConversationsCache(_ref); _ref .read(_conversationWarmupStatusProvider.notifier) .set(_ConversationWarmupStatus.idle); diff --git a/lib/features/chat/providers/chat_providers.dart b/lib/features/chat/providers/chat_providers.dart index a3fad2e..9624e49 100644 --- a/lib/features/chat/providers/chat_providers.dart +++ b/lib/features/chat/providers/chat_providers.dart @@ -647,7 +647,7 @@ class ChatMessagesNotifier extends Notifier> { // can pick up updated titles and ordering once streaming completes. // Best-effort: ignore if ref lifecycle/context prevents invalidation. try { - ref.invalidate(conversationsProvider); + refreshConversationsCache(ref); } catch (_) {} } } @@ -1373,10 +1373,10 @@ Future regenerateMessage( .read(activeConversationProvider.notifier) .set(active.copyWith(title: newTitle)); } - ref.invalidate(conversationsProvider); + refreshConversationsCache(ref); }, onChatTagsUpdated: () { - ref.invalidate(conversationsProvider); + refreshConversationsCache(ref); final active = ref.read(activeConversationProvider); final api = ref.read(apiServiceProvider); if (active != null && api != null) { @@ -1523,7 +1523,7 @@ Future _sendMessageInternal( try { // Guard against using ref after widget disposal if (ref.mounted == true) { - ref.invalidate(conversationsProvider); + refreshConversationsCache(ref); } } catch (_) { // If ref doesn't support mounted or is disposed, skip @@ -1920,10 +1920,10 @@ Future _sendMessageInternal( .read(activeConversationProvider.notifier) .set(active.copyWith(title: newTitle)); } - ref.invalidate(conversationsProvider); + refreshConversationsCache(ref); }, onChatTagsUpdated: () { - ref.invalidate(conversationsProvider); + refreshConversationsCache(ref); final active = ref.read(activeConversationProvider); final api = ref.read(apiServiceProvider); if (active != null && api != null) { @@ -2057,7 +2057,7 @@ Future _saveConversationLocally(dynamic ref) async { await storage.setString('conversations', jsonEncode(conversations)); ref.read(activeConversationProvider.notifier).set(updatedConversation); - ref.invalidate(conversationsProvider); + refreshConversationsCache(ref); } catch (e) { // Handle local storage errors silently } @@ -2095,7 +2095,7 @@ Future pinConversation( await api.pinConversation(conversationId, pinned); // Refresh conversations list to reflect the change - ref.invalidate(conversationsProvider); + refreshConversationsCache(ref); // Update active conversation if it's the one being pinned final activeConversation = ref.read(activeConversationProvider); @@ -2134,7 +2134,7 @@ Future archiveConversation( await api.archiveConversation(conversationId, archived); // Refresh conversations list to reflect the change - ref.invalidate(conversationsProvider); + refreshConversationsCache(ref); } catch (e) { DebugLogger.log( 'Error ${archived ? 'archiving' : 'unarchiving'} conversation: $e', @@ -2160,7 +2160,7 @@ Future shareConversation(WidgetRef ref, String conversationId) async { final shareId = await api.shareConversation(conversationId); // Refresh conversations list to reflect the change - ref.invalidate(conversationsProvider); + refreshConversationsCache(ref); return shareId; } catch (e) { @@ -2183,7 +2183,7 @@ Future cloneConversation(WidgetRef ref, String conversationId) async { // The ChatMessagesNotifier will automatically load messages when activeConversation changes // Refresh conversations list to show the new conversation - ref.invalidate(conversationsProvider); + refreshConversationsCache(ref); } catch (e) { DebugLogger.log('Error cloning conversation: $e', scope: 'chat/providers'); rethrow; diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index 9795a93..efb90ad 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -236,7 +236,7 @@ class _ChatPageState extends ConsumerState { // Force refresh conversations provider to ensure we get the demo conversations if (!mounted) return; - ref.invalidate(conversationsProvider); + refreshConversationsCache(ref); // Try to load demo conversation for (int i = 0; i < 10; i++) { @@ -1486,7 +1486,7 @@ class _ChatPageState extends ConsumerState { // Also refresh the conversations list to reconcile missed events // and keep timestamps/order in sync with the server. try { - ref.invalidate(conversationsProvider); + refreshConversationsCache(ref); // Best-effort await to stabilize UI; ignore errors. await ref.read(conversationsProvider.future); } catch (_) {} diff --git a/lib/features/chat/widgets/modern_chat_input.dart b/lib/features/chat/widgets/modern_chat_input.dart index f6f04da..d170b9f 100644 --- a/lib/features/chat/widgets/modern_chat_input.dart +++ b/lib/features/chat/widgets/modern_chat_input.dart @@ -1112,7 +1112,7 @@ class _ModernChatInputState extends ConsumerState activeColor = null; } - const double iconSize = IconSize.xl; + const double iconSize = IconSize.large; final Color iconColor = !enabled ? context.conduitTheme.textPrimary.withValues(alpha: Alpha.disabled) diff --git a/lib/features/navigation/widgets/chats_drawer.dart b/lib/features/navigation/widgets/chats_drawer.dart index fd57849..c297dfd 100644 --- a/lib/features/navigation/widgets/chats_drawer.dart +++ b/lib/features/navigation/widgets/chats_drawer.dart @@ -11,7 +11,7 @@ import '../../../core/providers/app_providers.dart'; import '../../../shared/theme/theme_extensions.dart'; import '../../chat/providers/chat_providers.dart' as chat; // import '../../files/views/files_page.dart'; -import '../../../shared/utils/ui_utils.dart'; +import '../../../core/utils/debug_logger.dart'; import '../../../core/services/navigation_service.dart'; import '../../../shared/widgets/themed_dialogs.dart'; import 'package:conduit/l10n/app_localizations.dart'; @@ -55,12 +55,11 @@ class _ChatsDrawerState extends ConsumerState { Future _refreshChats() async { try { - // Always refresh folders - ref.invalidate(foldersProvider); + // Always refresh folders and conversations cache + refreshConversationsCache(ref, includeFolders: true); if (_query.trim().isEmpty) { // Refresh main conversations list - ref.invalidate(conversationsProvider); try { await ref.read(conversationsProvider.future); } catch (_) {} @@ -629,15 +628,17 @@ class _ChatsDrawerState extends ConsumerState { if (api == null) throw Exception('No API service'); await api.createFolder(name: name); HapticFeedback.lightImpact(); - ref.invalidate(foldersProvider); + refreshConversationsCache(ref, includeFolders: true); + } catch (e, stackTrace) { if (!mounted) return; - UiUtils.showMessage(context, AppLocalizations.of(context)!.folderCreated); - } catch (e) { - if (!mounted) return; - UiUtils.showMessage( - context, + DebugLogger.error( + 'create-folder-failed', + scope: 'drawer', + error: e, + stackTrace: stackTrace, + ); + await _showDrawerError( AppLocalizations.of(context)!.failedToCreateFolder, - isError: true, ); } } @@ -668,22 +669,17 @@ class _ChatsDrawerState extends ConsumerState { if (api == null) throw Exception('No API service'); await api.moveConversationToFolder(details.data.id, folderId); HapticFeedback.selectionClick(); - ref.invalidate(conversationsProvider); - ref.invalidate(foldersProvider); + refreshConversationsCache(ref, includeFolders: true); + } catch (e, stackTrace) { + DebugLogger.error( + 'move-conversation-failed', + scope: 'drawer', + error: e, + stackTrace: stackTrace, + ); if (mounted) { - UiUtils.showMessage( - context, - AppLocalizations.of( - context, - )!.movedChatToFolder(details.data.title, name), - ); - } - } catch (_) { - if (mounted) { - UiUtils.showMessage( - context, + await _showDrawerError( AppLocalizations.of(context)!.failedToMoveChat, - isError: true, ); } } @@ -868,6 +864,28 @@ class _ChatsDrawerState extends ConsumerState { return null; } + Future _showDrawerError(String message) async { + if (!mounted) return; + final l10n = AppLocalizations.of(context)!; + final theme = context.conduitTheme; + await ThemedDialogs.show( + context, + title: l10n.errorMessage, + content: Text( + message, + style: AppTypography.bodyMediumStyle.copyWith( + color: theme.textSecondary, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(l10n.ok), + ), + ], + ); + } + void _showFolderContextMenu( BuildContext context, String folderId, @@ -923,14 +941,16 @@ class _ChatsDrawerState extends ConsumerState { if (api == null) throw Exception('No API service'); await api.updateFolder(folderId, name: newName); HapticFeedback.selectionClick(); - ref.invalidate(foldersProvider); - } catch (_) { + refreshConversationsCache(ref, includeFolders: true); + } catch (e, stackTrace) { if (!mounted) return; - UiUtils.showMessage( - this.context, - 'Failed to rename folder', - isError: true, + DebugLogger.error( + 'rename-folder-failed', + scope: 'drawer', + error: e, + stackTrace: stackTrace, ); + await _showDrawerError('Failed to rename folder'); } } @@ -956,11 +976,16 @@ class _ChatsDrawerState extends ConsumerState { if (api == null) throw Exception('No API service'); await api.deleteFolder(folderId); HapticFeedback.mediumImpact(); - ref.invalidate(foldersProvider); - ref.invalidate(conversationsProvider); - } catch (_) { + refreshConversationsCache(ref, includeFolders: true); + } catch (e, stackTrace) { if (!mounted) return; - UiUtils.showMessage(this.context, deleteFolderError, isError: true); + DebugLogger.error( + 'delete-folder-failed', + scope: 'drawer', + error: e, + stackTrace: stackTrace, + ); + await _showDrawerError(deleteFolderError); } } @@ -984,17 +1009,16 @@ class _ChatsDrawerState extends ConsumerState { if (api == null) throw Exception('No API service'); await api.moveConversationToFolder(details.data.id, null); HapticFeedback.selectionClick(); - ref.invalidate(conversationsProvider); - ref.invalidate(foldersProvider); + refreshConversationsCache(ref, includeFolders: true); + } catch (e, stackTrace) { + DebugLogger.error( + 'unfile-conversation-failed', + scope: 'drawer', + error: e, + stackTrace: stackTrace, + ); if (mounted) { - UiUtils.showMessage( - context, - 'Removed "${details.data.title}" from folder', - ); - } - } catch (_) { - if (mounted) { - UiUtils.showMessage(context, l10n.failedToMoveChat, isError: true); + await _showDrawerError(l10n.failedToMoveChat); } } }, diff --git a/lib/shared/utils/conversation_context_menu.dart b/lib/shared/utils/conversation_context_menu.dart index 9cdd91b..9fdb2fb 100644 --- a/lib/shared/utils/conversation_context_menu.dart +++ b/lib/shared/utils/conversation_context_menu.dart @@ -3,7 +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/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'; @@ -123,7 +122,7 @@ Future showConversationContextMenu({ await chat.pinConversation(ref, conversation.id, !isPinned); } catch (_) { if (!context.mounted) return; - UiUtils.showMessage(context, errorMessage, isError: true); + await _showConversationError(context, errorMessage); } } @@ -133,7 +132,7 @@ Future showConversationContextMenu({ await chat.archiveConversation(ref, conversation.id, !isArchived); } catch (_) { if (!context.mounted) return; - UiUtils.showMessage(context, errorMessage, isError: true); + await _showConversationError(context, errorMessage); } } @@ -221,7 +220,7 @@ Future _renameConversation( if (api == null) throw Exception('No API service'); await api.updateConversation(conversationId, title: newName); HapticFeedback.selectionClick(); - ref.invalidate(conversationsProvider); + refreshConversationsCache(ref); final active = ref.read(activeConversationProvider); if (active?.id == conversationId) { ref @@ -230,7 +229,7 @@ Future _renameConversation( } } catch (_) { if (!context.mounted) return; - UiUtils.showMessage(context, renameError, isError: true); + await _showConversationError(context, renameError); } } @@ -262,9 +261,29 @@ Future _confirmAndDeleteConversation( ref.read(activeConversationProvider.notifier).clear(); ref.read(chat.chatMessagesProvider.notifier).clearMessages(); } - ref.invalidate(conversationsProvider); + refreshConversationsCache(ref); } catch (_) { if (!context.mounted) return; - UiUtils.showMessage(context, deleteError, isError: true); + await _showConversationError(context, deleteError); } } + +Future _showConversationError( + BuildContext context, + String message, +) async { + if (!context.mounted) return; + final l10n = AppLocalizations.of(context)!; + final theme = context.conduitTheme; + await ThemedDialogs.show( + context, + title: l10n.errorMessage, + content: Text(message, style: TextStyle(color: theme.textSecondary)), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: Text(l10n.ok), + ), + ], + ); +}