From 73ccb14b2096a6e84aba1c935b24dbe14ecd57df Mon Sep 17 00:00:00 2001 From: cogwheel0 <172976095+cogwheel0@users.noreply.github.com> Date: Thu, 2 Oct 2025 00:06:00 +0530 Subject: [PATCH] refactor: enhance conversation fetching and folder management - Improved the logic for fetching conversations within folders by introducing a new condition to determine when to fetch folder conversations. - Added detailed logging for successful and failed fetch attempts to aid in debugging and monitoring. - Implemented a method to resolve folder conversations, ensuring that conversations are displayed in the correct order and that placeholders are used when necessary. - Updated the ChatsDrawer to utilize the new conversation resolution logic, enhancing the user experience by ensuring all relevant conversations are displayed. - This refactor streamlines conversation management and improves the overall efficiency of the chat interface. --- lib/core/providers/app_providers.dart | 52 +++++++-- .../chat/widgets/modern_chat_input.dart | 104 +++++------------ .../navigation/widgets/chats_drawer.dart | 110 ++++++++++++++++-- 3 files changed, 173 insertions(+), 93 deletions(-) diff --git a/lib/core/providers/app_providers.dart b/lib/core/providers/app_providers.dart index 696fce8..c160ccd 100644 --- a/lib/core/providers/app_providers.dart +++ b/lib/core/providers/app_providers.dart @@ -814,20 +814,37 @@ Future> conversations(Ref ref) async { final missingIds = folder.conversationIds .where((id) => !existingIds.contains(id)) .toList(); - if (missingIds.isEmpty) continue; + + final hasKnownConversations = conversationMap.values.any( + (conversation) => conversation.folderId == folder.id, + ); + + final shouldFetchFolder = + apiSvc != null && + (missingIds.isNotEmpty || + (!hasKnownConversations && folder.conversationIds.isEmpty)); List folderConvs = const []; - try { - if (apiSvc != null) { + if (shouldFetchFolder) { + try { folderConvs = await apiSvc.getConversationsInFolder(folder.id); + DebugLogger.log( + 'folder-sync', + scope: 'conversations/map', + data: { + 'folderId': folder.id, + 'fetched': folderConvs.length, + 'missingIds': missingIds.length, + }, + ); + } catch (e) { + DebugLogger.error( + 'folder-fetch-failed', + scope: 'conversations/map', + error: e, + data: {'folderId': folder.id}, + ); } - } catch (e) { - DebugLogger.error( - 'folder-fetch-failed', - scope: 'conversations/map', - error: e, - data: {'folderId': folder.id}, - ); } // Index fetched folder conversations for quick lookup @@ -867,6 +884,21 @@ Future> conversations(Ref ref) async { ); } } + + if (folderConvs.isNotEmpty && folder.conversationIds.isEmpty) { + for (final conv in folderConvs) { + final toAdd = conv.folderId == null + ? conv.copyWith(folderId: folder.id) + : conv; + conversationMap[toAdd.id] = toAdd; + existingIds.add(toAdd.id); + DebugLogger.log( + 'add-folder-fetch', + scope: 'conversations/map', + data: {'conversationId': toAdd.id, 'folderId': folder.id}, + ); + } + } } // Convert map back to list - this ensures no duplicates by ID diff --git a/lib/features/chat/widgets/modern_chat_input.dart b/lib/features/chat/widgets/modern_chat_input.dart index 99e2b20..f6f04da 100644 --- a/lib/features/chat/widgets/modern_chat_input.dart +++ b/lib/features/chat/widgets/modern_chat_input.dart @@ -740,10 +740,9 @@ class _ModernChatInputState extends ConsumerState ], ), width: double.infinity, - child: Padding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).viewPadding.bottom, - ), + child: SafeArea( + top: false, + bottom: true, child: ConstrainedBox( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.4, @@ -1001,9 +1000,9 @@ class _ModernChatInputState extends ConsumerState ), Padding( padding: const EdgeInsets.fromLTRB( - Spacing.inputPadding - Spacing.xs, + Spacing.inputPadding, 0, - Spacing.lg + Spacing.xs, + Spacing.inputPadding, 0, ), child: Row( @@ -1113,7 +1112,7 @@ class _ModernChatInputState extends ConsumerState activeColor = null; } - const double iconSize = IconSize.large; + const double iconSize = IconSize.xl; final Color iconColor = !enabled ? context.conduitTheme.textPrimary.withValues(alpha: Alpha.disabled) @@ -1124,24 +1123,14 @@ class _ModernChatInputState extends ConsumerState message: tooltip, child: Opacity( opacity: enabled ? 1.0 : Alpha.disabled, - child: SizedBox( - width: TouchTarget.minimum, - height: TouchTarget.minimum, - child: Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.circular(AppBorderRadius.round), - onTap: enabled - ? () { - HapticFeedback.selectionClick(); - _showOverflowSheet(); - } - : null, - child: Center( - child: Icon(icon, size: iconSize, color: iconColor), - ), - ), - ), + child: IconButton( + onPressed: enabled + ? () { + HapticFeedback.selectionClick(); + _showOverflowSheet(); + } + : null, + icon: Icon(icon, size: iconSize, color: iconColor), ), ), ); @@ -1280,54 +1269,25 @@ class _ModernChatInputState extends ConsumerState message: AppLocalizations.of(context)!.voiceInput, child: Opacity( opacity: enabledMic ? Alpha.primary : Alpha.disabled, - child: IgnorePointer( - ignoring: !enabledMic, - child: Material( - color: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(radius), - side: BorderSide( - color: _isRecording - ? context.conduitTheme.buttonPrimary - : context.conduitTheme.cardBorder.withValues( - alpha: enabledMic ? Alpha.strong : Alpha.medium, - ), - width: BorderWidth.regular, - ), - ), - child: InkWell( - borderRadius: BorderRadius.circular(radius), - onTap: enabledMic - ? () { - HapticFeedback.selectionClick(); - _toggleVoice(); - } - : null, - child: Container( - width: buttonSize, - height: buttonSize, - decoration: BoxDecoration( - color: _isRecording - ? context.conduitTheme.buttonPrimary.withValues( - alpha: Alpha.buttonPressed, + child: IconButton( + onPressed: enabledMic + ? () { + HapticFeedback.selectionClick(); + _toggleVoice(); + } + : null, + icon: Icon( + Platform.isIOS ? CupertinoIcons.mic : Icons.mic, + size: IconSize.large, + color: _isRecording + ? context.conduitTheme.buttonPrimary + : (enabledMic + ? context.conduitTheme.textPrimary.withValues( + alpha: Alpha.strong, ) - : context.conduitTheme.cardBackground, - borderRadius: BorderRadius.circular(radius), - boxShadow: ConduitShadows.button, - ), - child: Icon( - Platform.isIOS ? CupertinoIcons.mic_fill : Icons.mic, - size: IconSize.medium, - color: _isRecording - ? context.conduitTheme.buttonPrimary - : (enabledMic - ? context.conduitTheme.textPrimary - : context.conduitTheme.textPrimary.withValues( - alpha: Alpha.disabled, - )), - ), - ), - ), + : 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 38c7c8d..fd57849 100644 --- a/lib/features/navigation/widgets/chats_drawer.dart +++ b/lib/features/navigation/widgets/chats_drawer.dart @@ -23,6 +23,8 @@ import '../../../shared/utils/conversation_context_menu.dart'; import '../../../shared/widgets/user_avatar.dart'; import '../../../shared/widgets/model_avatar.dart'; import '../../../core/models/model.dart'; +import '../../../core/models/conversation.dart'; +import '../../../core/models/folder.dart'; class ChatsDrawer extends ConsumerStatefulWidget { const ChatsDrawer({super.key}); @@ -322,11 +324,18 @@ class _ChatsDrawerState extends ConsumerState { grouped.putIfAbsent(id, () => []).add(c); } + final expandedMap = ref.watch(_expandedFoldersProvider); + // Show all folders (including empty) final sections = folders.map((folder) { - final expandedMap = ref.watch(_expandedFoldersProvider); - final isExpanded = expandedMap[folder.id] ?? false; - final convs = grouped[folder.id] ?? const []; + final existing = grouped[folder.id] ?? const []; + final convs = _resolveFolderConversations( + folder, + existing, + ); + final isExpanded = + expandedMap[folder.id] ?? folder.isExpanded; + final hasItems = convs.isNotEmpty; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -334,8 +343,9 @@ class _ChatsDrawerState extends ConsumerState { folder.id, folder.name, convs.length, + defaultExpanded: folder.isExpanded, ), - if (isExpanded && convs.isNotEmpty) ...[ + if (isExpanded && hasItems) ...[ const SizedBox(height: Spacing.xs), ...convs.map( (c) => _buildTileFor(c, inFolder: true), @@ -480,10 +490,14 @@ class _ChatsDrawerState extends ConsumerState { grouped.putIfAbsent(id, () => []).add(c); } + final expandedMap = ref.watch(_expandedFoldersProvider); + final sections = folders.map((folder) { - final expandedMap = ref.watch(_expandedFoldersProvider); - final isExpanded = expandedMap[folder.id] ?? false; - final convs = grouped[folder.id] ?? const []; + final existing = grouped[folder.id] ?? const []; + final convs = _resolveFolderConversations(folder, existing); + final isExpanded = + expandedMap[folder.id] ?? folder.isExpanded; + final hasItems = convs.isNotEmpty; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ @@ -491,8 +505,9 @@ class _ChatsDrawerState extends ConsumerState { folder.id, folder.name, convs.length, + defaultExpanded: folder.isExpanded, ), - if (isExpanded && convs.isNotEmpty) ...[ + if (isExpanded && hasItems) ...[ const SizedBox(height: Spacing.xs), ...convs.map((c) => _buildTileFor(c, inFolder: true)), const SizedBox(height: Spacing.sm), @@ -627,10 +642,15 @@ class _ChatsDrawerState extends ConsumerState { } } - Widget _buildFolderHeader(String folderId, String name, int count) { + Widget _buildFolderHeader( + String folderId, + String name, + int count, { + bool defaultExpanded = false, + }) { final theme = context.conduitTheme; final expandedMap = ref.watch(_expandedFoldersProvider); - final isExpanded = expandedMap[folderId] ?? false; + final isExpanded = expandedMap[folderId] ?? defaultExpanded; final isHover = _dragHoverFolderId == folderId; return DragTarget<_DragConversationData>( onWillAcceptWithDetails: (details) { @@ -696,7 +716,8 @@ class _ChatsDrawerState extends ConsumerState { borderRadius: BorderRadius.zero, onTap: () { final current = {...ref.read(_expandedFoldersProvider)}; - current[folderId] = !isExpanded; + final next = !isExpanded; + current[folderId] = next; ref.read(_expandedFoldersProvider.notifier).set(current); }, onLongPress: () { @@ -780,6 +801,73 @@ class _ChatsDrawerState extends ConsumerState { ); } + List _resolveFolderConversations( + Folder folder, + List existing, + ) { + // Preserve the current conversational ordering while ensuring items from + // the folder metadata appear even if the main list has not fetched them + // yet. This primarily happens when chats live exclusively inside folders + // and the conversations endpoint omits them. + final result = []; + + final existingMap = {}; + for (final item in existing) { + final id = _conversationId(item); + if (id != null) { + existingMap[id] = item; + } + } + + if (folder.conversationIds.isNotEmpty) { + for (final convId in folder.conversationIds) { + final existingItem = existingMap.remove(convId); + if (existingItem != null) { + result.add(existingItem); + } else { + result.add(_placeholderConversation(convId, folder.id)); + } + } + + // Append any remaining conversations that claim this folder but are + // missing from the folder metadata list (defensive for API drift). + result.addAll(existingMap.values); + } else { + result.addAll(existingMap.values); + } + + return result; + } + + Conversation _placeholderConversation( + String conversationId, + String folderId, + ) { + const fallbackTitle = 'Chat'; + final epoch = DateTime.fromMillisecondsSinceEpoch(0); + return Conversation( + id: conversationId, + title: fallbackTitle, + createdAt: epoch, + updatedAt: epoch, + folderId: folderId, + messages: const [], + ); + } + + String? _conversationId(dynamic item) { + if (item is Conversation) return item.id; + try { + final value = (item as dynamic).id; + if (value is String) { + return value; + } + } catch (_) { + return null; + } + return null; + } + void _showFolderContextMenu( BuildContext context, String folderId,