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 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 08:23:24 -04:00
parent baba5c3462
commit 844f68d376
2 changed files with 56 additions and 37 deletions
+11 -5
View File
@@ -152,10 +152,14 @@ class _ShellState extends ConsumerState<_Shell> {
@override @override
void initState() { void initState() {
super.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((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
final repoUrl = ref.read(forgejoRepoUrlProvider); 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); ref.read(updateProvider.notifier).check(repoUrl);
} }
}); });
@@ -356,8 +360,10 @@ class _QuickCaptureBarState extends ConsumerState<_QuickCaptureBar> {
if (!mounted) break; if (!mounted) break;
try { try {
final result = await api.capture(text); 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); await ref.read(captureQueueProvider.notifier).dequeue(text);
if (!mounted) break;
switch (result.type) { switch (result.type) {
case 'note': case 'note':
ref.invalidate(notesProvider); ref.invalidate(notesProvider);
@@ -368,12 +374,12 @@ class _QuickCaptureBarState extends ConsumerState<_QuickCaptureBar> {
} on NetworkException { } on NetworkException {
break; break;
} catch (_) { } catch (_) {
if (mounted) { // Server error or unexpected failure — drop from queue to prevent
// ghost items that can never be cleared.
await ref.read(captureQueueProvider.notifier).dequeue(text); await ref.read(captureQueueProvider.notifier).dequeue(text);
} }
} }
} }
}
String _hintForLocation(String location) { String _hintForLocation(String location) {
if (location.startsWith(Routes.library) && if (location.startsWith(Routes.library) &&
+28 -15
View File
@@ -154,41 +154,54 @@ class _BriefingScreenState extends ConsumerState<BriefingScreen> {
return Column( return Column(
children: [ children: [
// Digest card header // Digest card + messages share one scroll view so expanding the
BriefingDigestCard( // card doesn't trap the user — the whole page just becomes taller.
Expanded(
child: CustomScrollView(
controller: _scrollController,
slivers: [
SliverToBoxAdapter(
child: BriefingDigestCard(
message: firstAssistant, message: firstAssistant,
onGenerateNow: _refresh, onGenerateNow: _refresh,
), ),
),
// Divider + label if (conv.messages.isNotEmpty)
if (conv.messages.isNotEmpty) ...[ SliverToBoxAdapter(
const SizedBox(height: 4), child: Padding(
Row(children: [ padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(children: [
const Expanded(child: Divider()), const Expanded(child: Divider()),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 10), padding:
const EdgeInsets.symmetric(horizontal: 10),
child: Text( child: Text(
'Conversation', 'Conversation',
style: Theme.of(context).textTheme.labelSmall?.copyWith( style: Theme.of(context)
color: scheme.onSurfaceVariant, .textTheme
), .labelSmall
?.copyWith(
color: scheme.onSurfaceVariant),
), ),
), ),
const Expanded(child: Divider()), const Expanded(child: Divider()),
]), ]),
], ),
),
// Message list SliverPadding(
Expanded(
child: ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 8), horizontal: 8, vertical: 8),
sliver: SliverList.builder(
itemCount: conv.messages.length, itemCount: conv.messages.length,
itemBuilder: (_, i) => itemBuilder: (_, i) =>
ChatMessageBubble(message: conv.messages[i]), ChatMessageBubble(message: conv.messages[i]),
), ),
), ),
],
),
),
// Progress bar while streaming // Progress bar while streaming
if (isStreaming) if (isStreaming)