From 844f68d3760fbd189e2106ed77e78ba5f26dd900 Mon Sep 17 00:00:00 2001 From: bvandeusen Date: Thu, 12 Mar 2026 08:23:24 -0400 Subject: [PATCH] fix: update loop, ghost queue item, briefing card scroll trap - Update dialog no longer re-appears on every shell re-mount; check() is skipped if status is already available/upToDate/downloading - Offline queue dequeue now happens before the mounted check, preventing ghost items when the widget disposes mid-drain - Briefing digest card and messages now share a CustomScrollView so expanding the card doesn't trap the user without scroll access Co-Authored-By: Claude Sonnet 4.6 --- lib/app.dart | 18 ++++-- lib/screens/briefing/briefing_screen.dart | 75 +++++++++++++---------- 2 files changed, 56 insertions(+), 37 deletions(-) diff --git a/lib/app.dart b/lib/app.dart index 963b26c..2992b77 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -152,10 +152,14 @@ class _ShellState extends ConsumerState<_Shell> { @override void initState() { super.initState(); - // Silent update check on first app load. + // Silent update check — only if we haven't already checked this session. + // Skipping when status is not idle/error prevents the dialog from + // re-appearing every time the shell re-mounts (e.g. after visiting settings). WidgetsBinding.instance.addPostFrameCallback((_) { final repoUrl = ref.read(forgejoRepoUrlProvider); - if (repoUrl != null && repoUrl.isNotEmpty) { + if (repoUrl == null || repoUrl.isEmpty) return; + final status = ref.read(updateProvider).status; + if (status == UpdateStatus.idle || status == UpdateStatus.error) { ref.read(updateProvider.notifier).check(repoUrl); } }); @@ -356,8 +360,10 @@ class _QuickCaptureBarState extends ConsumerState<_QuickCaptureBar> { if (!mounted) break; try { final result = await api.capture(text); - if (!mounted) break; + // Dequeue before the mounted check — SharedPreferences doesn't need + // the widget alive, and skipping this would leave a ghost item. await ref.read(captureQueueProvider.notifier).dequeue(text); + if (!mounted) break; switch (result.type) { case 'note': ref.invalidate(notesProvider); @@ -368,9 +374,9 @@ class _QuickCaptureBarState extends ConsumerState<_QuickCaptureBar> { } on NetworkException { break; } catch (_) { - if (mounted) { - await ref.read(captureQueueProvider.notifier).dequeue(text); - } + // Server error or unexpected failure — drop from queue to prevent + // ghost items that can never be cleared. + await ref.read(captureQueueProvider.notifier).dequeue(text); } } } diff --git a/lib/screens/briefing/briefing_screen.dart b/lib/screens/briefing/briefing_screen.dart index 69a71ad..b1cdfe3 100644 --- a/lib/screens/briefing/briefing_screen.dart +++ b/lib/screens/briefing/briefing_screen.dart @@ -154,39 +154,52 @@ class _BriefingScreenState extends ConsumerState { return Column( children: [ - // Digest card header - BriefingDigestCard( - message: firstAssistant, - onGenerateNow: _refresh, - ), - - // Divider + label - if (conv.messages.isNotEmpty) ...[ - const SizedBox(height: 4), - Row(children: [ - const Expanded(child: Divider()), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: Text( - 'Conversation', - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: scheme.onSurfaceVariant, - ), - ), - ), - const Expanded(child: Divider()), - ]), - ], - - // Message list + // Digest card + messages share one scroll view so expanding the + // card doesn't trap the user — the whole page just becomes taller. Expanded( - child: ListView.builder( + child: CustomScrollView( controller: _scrollController, - padding: const EdgeInsets.symmetric( - horizontal: 8, vertical: 8), - itemCount: conv.messages.length, - itemBuilder: (_, i) => - ChatMessageBubble(message: conv.messages[i]), + slivers: [ + SliverToBoxAdapter( + child: BriefingDigestCard( + message: firstAssistant, + onGenerateNow: _refresh, + ), + ), + + if (conv.messages.isNotEmpty) + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row(children: [ + const Expanded(child: Divider()), + Padding( + padding: + const EdgeInsets.symmetric(horizontal: 10), + child: Text( + 'Conversation', + style: Theme.of(context) + .textTheme + .labelSmall + ?.copyWith( + color: scheme.onSurfaceVariant), + ), + ), + const Expanded(child: Divider()), + ]), + ), + ), + + SliverPadding( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 8), + sliver: SliverList.builder( + itemCount: conv.messages.length, + itemBuilder: (_, i) => + ChatMessageBubble(message: conv.messages[i]), + ), + ), + ], ), ),