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:
+12
-6
@@ -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,9 +374,9 @@ 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
|
||||||
await ref.read(captureQueueProvider.notifier).dequeue(text);
|
// ghost items that can never be cleared.
|
||||||
}
|
await ref.read(captureQueueProvider.notifier).dequeue(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,39 +154,52 @@ 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.
|
||||||
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
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView.builder(
|
child: CustomScrollView(
|
||||||
controller: _scrollController,
|
controller: _scrollController,
|
||||||
padding: const EdgeInsets.symmetric(
|
slivers: [
|
||||||
horizontal: 8, vertical: 8),
|
SliverToBoxAdapter(
|
||||||
itemCount: conv.messages.length,
|
child: BriefingDigestCard(
|
||||||
itemBuilder: (_, i) =>
|
message: firstAssistant,
|
||||||
ChatMessageBubble(message: conv.messages[i]),
|
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]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user