diff --git a/lib/features/navigation/widgets/chats_drawer.dart b/lib/features/navigation/widgets/chats_drawer.dart index 6efd9fe..093e398 100644 --- a/lib/features/navigation/widgets/chats_drawer.dart +++ b/lib/features/navigation/widgets/chats_drawer.dart @@ -41,6 +41,74 @@ class _ChatsDrawerState extends ConsumerState { (ref) => {}, ); + Future _refreshChats() async { + try { + // Always refresh folders + ref.invalidate(foldersProvider); + + if (_query.trim().isEmpty) { + // Refresh main conversations list + ref.invalidate(conversationsProvider); + try { + await ref.read(conversationsProvider.future); + } catch (_) {} + } else { + // Refresh server-side search results + ref.invalidate(serverSearchProvider(_query)); + try { + await ref.read(serverSearchProvider(_query).future); + } catch (_) {} + } + + // Await folders as well so the list stabilizes + try { + await ref.read(foldersProvider.future); + } catch (_) {} + } catch (_) {} + } + + Widget _buildRefreshableScrollable({required List children}) { + // Common padding used in both scrollable variants + const padding = EdgeInsets.fromLTRB( + Spacing.md, + Spacing.sm, + Spacing.md, + Spacing.md, + ); + + if (Platform.isIOS) { + // Use Cupertino-style pull-to-refresh on iOS + final scroll = CustomScrollView( + controller: _listController, + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + const CupertinoSliverRefreshControl(), + SliverPadding( + padding: padding, + sliver: SliverList( + delegate: SliverChildListDelegate(children), + ), + ), + ], + ); + return CupertinoScrollbar(controller: _listController, child: scroll); + } + + // Material pull-to-refresh elsewhere + return RefreshIndicator( + onRefresh: _refreshChats, + child: Scrollbar( + controller: _listController, + child: ListView( + controller: _listController, + physics: const AlwaysScrollableScrollPhysics(), + padding: padding, + children: children, + ), + ), + ); + } + @override void dispose() { _debounce?.cancel(); @@ -226,95 +294,84 @@ class _ChatsDrawerState extends ConsumerState { final archived = list.where((c) => c.archived == true).toList(); - return Scrollbar( - controller: _listController, - child: ListView( - controller: _listController, - padding: const EdgeInsets.fromLTRB( - Spacing.md, - Spacing.sm, - Spacing.md, - Spacing.md, + final children = [ + if (pinned.isNotEmpty) ...[ + _buildSectionHeader( + AppLocalizations.of(context)!.pinned, + pinned.length, ), - children: [ - if (pinned.isNotEmpty) ...[ - _buildSectionHeader( - AppLocalizations.of(context)!.pinned, - pinned.length, - ), - const SizedBox(height: Spacing.xs), - ...pinned.map((conv) => _buildTileFor(conv)), - const SizedBox(height: Spacing.md), - ], + const SizedBox(height: Spacing.xs), + ...pinned.map((conv) => _buildTileFor(conv)), + const SizedBox(height: Spacing.md), + ], - // Folders section (shown even if empty) - _buildFoldersSectionHeader(), - const SizedBox(height: Spacing.xs), - if (_isDragging && _draggingHasFolder) ...[ - _buildUnfileDropTarget(), - const SizedBox(height: Spacing.sm), - ], - ...ref - .watch(foldersProvider) - .when( - data: (folders) { - final grouped = >{}; - for (final c in foldered) { - final id = c.folderId!; - grouped.putIfAbsent(id, () => []).add(c); - } + // Folders section (shown even if empty) + _buildFoldersSectionHeader(), + const SizedBox(height: Spacing.xs), + if (_isDragging && _draggingHasFolder) ...[ + _buildUnfileDropTarget(), + const SizedBox(height: Spacing.sm), + ], + ...ref + .watch(foldersProvider) + .when( + data: (folders) { + final grouped = >{}; + for (final c in foldered) { + final id = c.folderId!; + grouped.putIfAbsent(id, () => []).add(c); + } - // 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 []; - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildFolderHeader( - folder.id, - folder.name, - convs.length, - ), - if (isExpanded && convs.isNotEmpty) ...[ - const SizedBox(height: Spacing.xs), - ...convs.map( - (c) => _buildTileFor(c, inFolder: true), - ), - const SizedBox(height: Spacing.xs), - ], - const SizedBox(height: Spacing.xs), - ], - ); - }).toList(); - return sections.isEmpty - ? [const SizedBox.shrink()] - : sections; - }, - loading: () => [const SizedBox.shrink()], - error: (e, st) => [const SizedBox.shrink()], - ), - const SizedBox(height: Spacing.md), + // 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 []; + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildFolderHeader( + folder.id, + folder.name, + convs.length, + ), + if (isExpanded && convs.isNotEmpty) ...[ + const SizedBox(height: Spacing.xs), + ...convs.map( + (c) => _buildTileFor(c, inFolder: true), + ), + const SizedBox(height: Spacing.xs), + ], + const SizedBox(height: Spacing.xs), + ], + ); + }).toList(); + return sections.isEmpty + ? [const SizedBox.shrink()] + : sections; + }, + loading: () => [const SizedBox.shrink()], + error: (e, st) => [const SizedBox.shrink()], + ), + const SizedBox(height: Spacing.md), - if (regular.isNotEmpty) ...[ - _buildSectionHeader( - AppLocalizations.of(context)!.recent, - regular.length, - ), - const SizedBox(height: Spacing.xs), - ...regular.map(_buildTileFor), - ], + if (regular.isNotEmpty) ...[ + _buildSectionHeader( + AppLocalizations.of(context)!.recent, + regular.length, + ), + const SizedBox(height: Spacing.xs), + ...regular.map(_buildTileFor), + ], - if (archived.isNotEmpty) ...[ - const SizedBox(height: Spacing.md), - _buildArchivedSection(archived), - ], - ], - ), - ); + if (archived.isNotEmpty) ...[ + const SizedBox(height: Spacing.md), + _buildArchivedSection(archived), + ], + ]; + return _buildRefreshableScrollable(children: children); }, loading: () => const Center(child: CircularProgressIndicator(strokeWidth: 2.0)), @@ -377,90 +434,79 @@ class _ChatsDrawerState extends ConsumerState { final archived = list.where((c) => c.archived == true).toList(); - return Scrollbar( - controller: _listController, - child: ListView( - controller: _listController, - padding: const EdgeInsets.fromLTRB( - Spacing.md, - Spacing.sm, - Spacing.md, - Spacing.md, + final children = [ + _buildSectionHeader('Results', list.length), + const SizedBox(height: Spacing.xs), + if (pinned.isNotEmpty) ...[ + _buildSectionHeader( + AppLocalizations.of(context)!.pinned, + pinned.length, ), - children: [ - _buildSectionHeader('Results', list.length), - const SizedBox(height: Spacing.xs), - if (pinned.isNotEmpty) ...[ - _buildSectionHeader( - AppLocalizations.of(context)!.pinned, - pinned.length, - ), - const SizedBox(height: Spacing.xs), - ...pinned.map((conv) => _buildTileFor(conv)), - const SizedBox(height: Spacing.md), - ], - // Folders section (shown even if empty) - _buildFoldersSectionHeader(), - const SizedBox(height: Spacing.xs), - if (_isDragging && _draggingHasFolder) ...[ - _buildUnfileDropTarget(), - const SizedBox(height: Spacing.sm), - ], - ...ref - .watch(foldersProvider) - .when( - data: (folders) { - final grouped = >{}; - for (final c in foldered) { - final id = c.folderId!; - grouped.putIfAbsent(id, () => []).add(c); - } + const SizedBox(height: Spacing.xs), + ...pinned.map((conv) => _buildTileFor(conv)), + const SizedBox(height: Spacing.md), + ], + // Folders section (shown even if empty) + _buildFoldersSectionHeader(), + const SizedBox(height: Spacing.xs), + if (_isDragging && _draggingHasFolder) ...[ + _buildUnfileDropTarget(), + const SizedBox(height: Spacing.sm), + ], + ...ref + .watch(foldersProvider) + .when( + data: (folders) { + final grouped = >{}; + for (final c in foldered) { + final id = c.folderId!; + 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 []; - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - _buildFolderHeader( - folder.id, - folder.name, - convs.length, - ), - if (isExpanded && convs.isNotEmpty) ...[ - const SizedBox(height: Spacing.xs), - ...convs.map( - (c) => _buildTileFor(c, inFolder: true), - ), - const SizedBox(height: Spacing.sm), - ], - ], - ); - }).toList(); - return sections.isEmpty - ? [const SizedBox.shrink()] - : sections; - }, - loading: () => [const SizedBox.shrink()], - error: (e, st) => [const SizedBox.shrink()], - ), - const SizedBox(height: Spacing.md), - if (regular.isNotEmpty) ...[ - _buildSectionHeader( - AppLocalizations.of(context)!.recent, - regular.length, - ), - const SizedBox(height: Spacing.xs), - ...regular.map(_buildTileFor), - ], - if (archived.isNotEmpty) ...[ - const SizedBox(height: Spacing.md), - _buildArchivedSection(archived), - ], - ], - ), - ); + final sections = folders.map((folder) { + final expandedMap = ref.watch(_expandedFoldersProvider); + final isExpanded = expandedMap[folder.id] ?? false; + final convs = grouped[folder.id] ?? const []; + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildFolderHeader( + folder.id, + folder.name, + convs.length, + ), + if (isExpanded && convs.isNotEmpty) ...[ + const SizedBox(height: Spacing.xs), + ...convs.map( + (c) => _buildTileFor(c, inFolder: true), + ), + const SizedBox(height: Spacing.sm), + ], + ], + ); + }).toList(); + return sections.isEmpty + ? [const SizedBox.shrink()] + : sections; + }, + loading: () => [const SizedBox.shrink()], + error: (e, st) => [const SizedBox.shrink()], + ), + const SizedBox(height: Spacing.md), + if (regular.isNotEmpty) ...[ + _buildSectionHeader( + AppLocalizations.of(context)!.recent, + regular.length, + ), + const SizedBox(height: Spacing.xs), + ...regular.map(_buildTileFor), + ], + if (archived.isNotEmpty) ...[ + const SizedBox(height: Spacing.md), + _buildArchivedSection(archived), + ], + ]; + return _buildRefreshableScrollable(children: children); }, loading: () => const Center(child: CircularProgressIndicator(strokeWidth: 2.0)),