From 5014eca9ac2f5de84ae074c30101167fed64e07f Mon Sep 17 00:00:00 2001 From: bvandeusen Date: Mon, 6 Apr 2026 12:45:33 -0400 Subject: [PATCH] feat: auto-refresh data on app resume and tab switch - App resume: invalidates all main providers (conversations, calendar, news, knowledge, briefing) when the app returns to foreground. Throttled to once per 5 minutes to avoid hammering the server. - Tab switch: refreshes the incoming tab's provider when navigating between Briefing / Knowledge / Conversations shell tabs. Co-Authored-By: Claude Sonnet 4.6 --- lib/app.dart | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/lib/app.dart b/lib/app.dart index 68f8538..ddba82b 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -11,6 +11,11 @@ import 'providers/api_client_provider.dart'; import 'providers/auth_provider.dart'; import 'providers/capture_queue_provider.dart'; import 'providers/capture_work_queue_provider.dart'; +import 'providers/briefing_provider.dart'; +import 'providers/calendar_provider.dart'; +import 'providers/chat_provider.dart'; +import 'providers/knowledge_provider.dart'; +import 'providers/news_provider.dart'; import 'providers/notes_provider.dart'; import 'providers/settings_provider.dart'; import 'providers/update_provider.dart'; @@ -182,16 +187,22 @@ class _Shell extends ConsumerStatefulWidget { ConsumerState<_Shell> createState() => _ShellState(); } -class _ShellState extends ConsumerState<_Shell> { +class _ShellState extends ConsumerState<_Shell> with WidgetsBindingObserver { static const _tabs = [ Routes.briefing, Routes.knowledge, Routes.conversations, ]; + // Minimum gap between app-resume refreshes to avoid hammering the server. + static const _resumeCooldown = Duration(minutes: 5); + DateTime? _lastResumeRefresh; + int? _prevTabIndex; + @override void initState() { super.initState(); + WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addPostFrameCallback((_) { // Silent update check — only if we haven't already checked this session. final repoUrl = ref.read(forgejoRepoUrlProvider); @@ -206,6 +217,49 @@ class _ShellState extends ConsumerState<_Shell> { }); } + @override + void dispose() { + WidgetsBinding.instance.removeObserver(this); + super.dispose(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + if (state != AppLifecycleState.resumed) return; + final now = DateTime.now(); + if (_lastResumeRefresh != null && + now.difference(_lastResumeRefresh!) < _resumeCooldown) { + return; + } + _lastResumeRefresh = now; + _refreshAll(); + } + + /// Refresh every major data provider. Safe to call speculatively — + /// providers that aren't currently watched are already disposed. + void _refreshAll() { + ref.invalidate(conversationsProvider); + ref.invalidate(calendarProvider); + ref.invalidate(newsProvider); + // Notifier (not AsyncNotifier) — needs explicit refresh call. + ref.read(knowledgeProvider.notifier).refresh(); + // briefingProvider is an AsyncNotifier family; invalidating the family + // is safe even if no conversation is open. + ref.invalidate(briefingProvider); + } + + /// Refresh only the provider backing the given shell tab index. + void _refreshTab(int index) { + switch (index) { + case 0: + ref.invalidate(briefingProvider); + case 1: + ref.read(knowledgeProvider.notifier).refresh(); + case 2: + ref.invalidate(conversationsProvider); + } + } + Future _syncTimezone() async { try { final tzInfo = await FlutterTimezone.getLocalTimezone(); @@ -340,6 +394,13 @@ class _ShellState extends ConsumerState<_Shell> { }); final location = GoRouterState.of(context).matchedLocation; final index = _tabIndex(location); + + // Refresh the incoming tab's data when switching between shell tabs. + if (_prevTabIndex != null && _prevTabIndex != index && index < 3) { + WidgetsBinding.instance.addPostFrameCallback((_) => _refreshTab(index)); + } + _prevTabIndex = index; + final child = widget.child; final isWide = MediaQuery.of(context).size.width >= 600;