mobile bluesky app made with flutter
lazurite.stormlightlabs.org/
mobile
bluesky
flutter
1import 'dart:async';
2
3import 'package:flutter/material.dart';
4import 'package:lazurite/src/core/providers/app_lifecycle_provider.dart';
5import 'package:lazurite/src/core/utils/logger_provider.dart';
6import 'package:lazurite/src/features/auth/application/auth_providers.dart';
7import 'package:lazurite/src/features/auth/domain/auth_state.dart';
8import 'package:lazurite/src/features/feeds/application/feed_providers.dart';
9import 'package:riverpod_annotation/riverpod_annotation.dart';
10
11part 'feed_sync_controller.g.dart';
12
13/// Controller that manages automatic background synchronization of feeds.
14///
15/// Listens to app lifecycle changes and triggers [FeedRepository.syncOnResume] when the
16/// app is resumed.
17@Riverpod(keepAlive: true)
18void feedSyncController(Ref ref) {
19 final logger = ref.watch(loggerProvider('FeedSync'));
20 var hasInitialized = false;
21
22 Future<void> seedDefaults() async {
23 try {
24 final authState = ref.read(authProvider);
25 final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : 'anonymous';
26
27 await ref.read(feedRepositoryProvider).seedDefaultFeeds(ownerDid);
28 } catch (e, stack) {
29 logger.warning('Failed to seed default feeds', e, stack);
30 }
31 }
32
33 Future<void> runSync() async {
34 logger.debug('runSync() called');
35 final authState = ref.read(authProvider);
36 final ownerDid = (authState is AuthStateAuthenticated) ? authState.session.did : 'anonymous';
37
38 await seedDefaults();
39 try {
40 logger.debug('Calling repository.syncOnResume()');
41 await ref.read(feedRepositoryProvider).syncOnResume(ownerDid);
42 logger.info('syncOnResume() completed successfully');
43 } catch (e, stack) {
44 logger.error('Failed to sync feeds on resume', e, stack);
45 }
46 }
47
48 void setActiveFeed(AuthState state) {
49 final notifier = ref.read(activeFeedProvider.notifier);
50 notifier.resetToDefault(isAuthenticated: state is AuthStateAuthenticated);
51 }
52
53 ref.listen(appLifecycleProvider, (previous, next) {
54 if (next == AppLifecycleState.resumed) {
55 unawaited(runSync());
56 }
57 });
58
59 ref.listen(authProvider, (previous, next) {
60 logger.debug('Auth state changed: ${previous.runtimeType} → ${next.runtimeType}');
61 if (hasInitialized) {
62 final wasAuthed = previous is AuthStateAuthenticated;
63 final isAuthed = next is AuthStateAuthenticated;
64 logger.debug('wasAuthed=$wasAuthed, isAuthed=$isAuthed');
65
66 if (wasAuthed != isAuthed) {
67 logger.info('Auth status changed, switching active feed');
68 setActiveFeed(next);
69 if (isAuthed) {
70 logger.info('User logged in - triggering full sync');
71 unawaited(runSync());
72 }
73 } else if (isAuthed && wasAuthed) {
74 final prevSession = previous.session;
75 final nextSession = next.session;
76 if (prevSession.accessJwt != nextSession.accessJwt) {
77 logger.debug('Session refreshed - triggering sync to fetch feeds');
78 unawaited(runSync());
79 }
80 }
81 } else {
82 logger.debug('Not initialized yet, skipping auth change handling');
83 }
84 });
85
86 Future.microtask(() async {
87 logger.debug('Controller initializing...');
88 await runSync();
89 final authState = ref.read(authProvider);
90 logger.debug('Initial auth state: ${authState.runtimeType}');
91 setActiveFeed(authState);
92 hasInitialized = true;
93 logger.info('Controller initialized');
94 });
95}