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.
This commit is contained in:
cogwheel0
2025-10-02 00:06:00 +05:30
parent 2d35bf4e07
commit 73ccb14b20
3 changed files with 173 additions and 93 deletions

View File

@@ -814,13 +814,29 @@ Future<List<Conversation>> 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<Conversation> folderConvs = const [];
if (shouldFetchFolder) {
try {
if (apiSvc != null) {
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',
@@ -829,6 +845,7 @@ Future<List<Conversation>> conversations(Ref ref) async {
data: {'folderId': folder.id},
);
}
}
// Index fetched folder conversations for quick lookup
final fetchedMap = {for (final c in folderConvs) c.id: c};
@@ -867,6 +884,21 @@ Future<List<Conversation>> 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

View File

@@ -740,10 +740,9 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
],
),
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<ModernChatInput>
),
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<ModernChatInput>
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<ModernChatInput>
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
child: IconButton(
onPressed: enabled
? () {
HapticFeedback.selectionClick();
_showOverflowSheet();
}
: null,
child: Center(
child: Icon(icon, size: iconSize, color: iconColor),
),
),
),
icon: Icon(icon, size: iconSize, color: iconColor),
),
),
);
@@ -1280,57 +1269,28 @@ class _ModernChatInputState extends ConsumerState<ModernChatInput>
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
child: IconButton(
onPressed: enabledMic
? () {
HapticFeedback.selectionClick();
_toggleVoice();
}
: null,
child: Container(
width: buttonSize,
height: buttonSize,
decoration: BoxDecoration(
color: _isRecording
? context.conduitTheme.buttonPrimary.withValues(
alpha: Alpha.buttonPressed,
)
: context.conduitTheme.cardBackground,
borderRadius: BorderRadius.circular(radius),
boxShadow: ConduitShadows.button,
),
child: Icon(
Platform.isIOS ? CupertinoIcons.mic_fill : Icons.mic,
size: IconSize.medium,
icon: Icon(
Platform.isIOS ? CupertinoIcons.mic : Icons.mic,
size: IconSize.large,
color: _isRecording
? context.conduitTheme.buttonPrimary
: (enabledMic
? context.conduitTheme.textPrimary
? context.conduitTheme.textPrimary.withValues(
alpha: Alpha.strong,
)
: context.conduitTheme.textPrimary.withValues(
alpha: Alpha.disabled,
)),
),
),
),
),
),
),
);
}

View File

@@ -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<ChatsDrawer> {
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 <dynamic>[];
final existing = grouped[folder.id] ?? const <dynamic>[];
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<ChatsDrawer> {
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<ChatsDrawer> {
grouped.putIfAbsent(id, () => []).add(c);
}
final sections = folders.map((folder) {
final expandedMap = ref.watch(_expandedFoldersProvider);
final isExpanded = expandedMap[folder.id] ?? false;
final convs = grouped[folder.id] ?? const <dynamic>[];
final sections = folders.map((folder) {
final existing = grouped[folder.id] ?? const <dynamic>[];
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<ChatsDrawer> {
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<ChatsDrawer> {
}
}
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<ChatsDrawer> {
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<ChatsDrawer> {
);
}
List<dynamic> _resolveFolderConversations(
Folder folder,
List<dynamic> 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 = <dynamic>[];
final existingMap = <String, dynamic>{};
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,