import 'dart:math'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; import 'package:result_monad/result_monad.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../data/interfaces/connections_repo_intf.dart'; import '../data/objectbox/objectbox_cache.dart'; import '../data/objectbox/objectbox_connections_repo.dart'; import '../friendica_client/friendica_client.dart'; import '../models/auth/profile.dart'; import '../models/connection.dart'; import '../models/exec_error.dart'; import '../models/networking/paging_data.dart'; import 'circles_repo_services.dart'; import 'persistent_info_services.dart'; part 'connection_manager_services.g.dart'; final _crLogger = Logger('ConnectionsRepoProvider'); @Riverpod(keepAlive: true) Future _connectionsRepo(Ref ref, Profile profile) async { _crLogger.info('Creating Connections repo for $profile'); final objectBox = await ObjectBoxCache.create( baseDir: 'profileboxcaches', subDir: profile.id, ); _crLogger.info('Object box object returned for $profile'); return ObjectBoxConnectionsRepo(objectBox); } final _crsLogger = Logger('ConnectionsRepoSyncProvider'); @Riverpod(keepAlive: true) class _ConnectionsRepoSync extends _$ConnectionsRepoSync { @override Result build(Profile profile) { _crsLogger.info('Build $profile'); ref .watch(_connectionsRepoProvider(profile).future) .then((repo) => state = Result.ok(repo)); return Result.error(true); } } @riverpod class ConnectionsRepoClearer extends _$ConnectionsRepoClearer { @override bool build(Profile profile) { return true; } bool clear() { return ref .read(_connectionsRepoSyncProvider(profile)) .withResult((repo) => repo.clear()) .isSuccess; } } @riverpod Future connectionRepoInit(Ref ref, Profile profile) async { await ref.read(_connectionsRepoProvider(profile).future); return true; } @riverpod List knownUsersByName(Ref ref, Profile profile, String userName) { return ref.watch(_connectionsRepoSyncProvider(profile)).fold( onSuccess: (repo) => repo.getKnownUsersByName(userName), onError: (_) => []); } @riverpod Future> myContacts(Ref ref, Profile profile) async { final repo = await ref.watch(_connectionsRepoProvider(profile).future); return repo.getMyContacts(); } //TODO May need to bootstrap RP with server call if by ID and doesn't know about it @riverpod Result connectionById( Ref ref, Profile profile, String id, {bool forceUpdate = false}) { final result = ref .read(_connectionsRepoSyncProvider(profile)) .andThen((repo) => repo.getById(id)) .transform((c) { if (c.status == ConnectionStatus.unknown && forceUpdate) { ref .read(connectionModifierProvider(profile, c).notifier) .refreshConnection(false) .then((_) async => ref.notifyListeners()); } return c; }).execErrorCast(); return result; } @riverpod Result connectionByName( Ref ref, Profile profile, String connectionName, ) { return ref .read(_connectionsRepoSyncProvider(profile)) .andThen((repo) => repo.getByName(connectionName)) .andThenSuccess((c) { if (c.status == ConnectionStatus.unknown) { ref .read(connectionModifierProvider(profile, c).notifier) .refreshConnection(false) .then((_) async => ref.notifyListeners()); } return c; }).execErrorCast(); } @riverpod Result connectionByHandle( Ref ref, Profile profile, String handle) { return ref .read(_connectionsRepoSyncProvider(profile)) .andThen((repo) => repo.getByHandle(handle)) .andThenSuccess((c) { if (c.status == ConnectionStatus.unknown) { ref .read(connectionModifierProvider(profile, c).notifier) .refreshConnection(false) .then((_) async => ref.notifyListeners()); } return c; }).execErrorCast(); } final _cmpLogger = Logger('ConnectionModifierProvider'); @riverpod class ConnectionModifier extends _$ConnectionModifier { @override Connection build(Profile profile, Connection connection) { return connection; } void upsertConnection(Connection update) { if (update.status != ConnectionStatus.unknown) { ref.read(_connectionsRepoProvider(profile).future).then((repo) { repo.upsertConnection(update); _sendUpdateNotifications(); }); state = update; return; } ref .read(_connectionsRepoProvider(profile).future) .then((repo) => repo.getById(connection.id).match( onSuccess: (original) { final forUpsert = update.copy(status: original.status); repo.upsertConnection( forUpsert, ); state = forUpsert; }, onError: (_) { repo.upsertConnection(update); state = update; }, )) .then((_) => _sendUpdateNotifications()); return; } Future acceptFollowRequest() async { _cmpLogger.finest( 'Attempting to accept follow request ${connection.name}: ${connection.status}'); await RelationshipsClient(profile).acceptFollow(connection).match( onSuccess: (update) { _cmpLogger .finest('Successfully followed ${update.name}: ${update.status}'); upsertConnection(update); }, onError: (error) { _cmpLogger.severe('Error following ${connection.name}: $error'); }, ); } Future rejectFollowRequest() async { _cmpLogger.finest( 'Attempting to accept follow request ${connection.name}: ${connection.status}'); await RelationshipsClient(profile).rejectFollow(connection).match( onSuccess: (update) { _cmpLogger .finest('Successfully followed ${update.name}: ${update.status}'); upsertConnection(update); }, onError: (error) { _cmpLogger.severe('Error following ${connection.name}: $error'); }, ); } Future ignoreFollowRequest() async { _cmpLogger.finest( 'Attempting to accept follow request ${connection.name}: ${connection.status}'); await RelationshipsClient(profile).ignoreFollow(connection).match( onSuccess: (update) { _cmpLogger .finest('Successfully followed ${update.name}: ${update.status}'); upsertConnection(update); }, onError: (error) { _cmpLogger.severe('Error following ${connection.name}'); }, ); } Future follow() async { _cmpLogger.finest( 'Attempting to follow ${connection.name}: ${connection.status}'); await RelationshipsClient(profile).followConnection(connection).match( onSuccess: (update) { _cmpLogger .finest('Successfully followed ${update.name}: ${update.status}'); upsertConnection(update); }, onError: (error) { _cmpLogger.severe('Error following ${connection.name}'); }, ); } Future unfollow() async { _cmpLogger.finest( 'Attempting to unfollow ${connection.name}: ${connection.status}'); await RelationshipsClient(profile).unFollowConnection(connection).match( onSuccess: (update) { _cmpLogger .finest('Successfully unfollowed ${update.name}: ${update.status}'); upsertConnection(update); }, onError: (error) { _cmpLogger.severe('Error following ${connection.name}'); }, ); } void _sendUpdateNotifications() { ref.invalidate(myContactsProvider); ref.invalidate(connectionByIdProvider(profile, connection.id)); ref.invalidate(connectionByHandleProvider(profile, connection.handle)); ref.invalidate(connectionByNameProvider(profile, connection.name)); } Future fullRefresh({ bool withNotifications = true, }) async { await ref.read(circlesProvider(profile).notifier).refresh(); await ref .read(circlesProvider(profile).notifier) .refreshConnectionCircleData(connection.id, false); await ref .read(connectionModifierProvider(profile, connection).notifier) .refreshConnection(false); if (withNotifications) { ref.notifyListeners(); } } Future refreshConnection(bool withNotification) async { _cmpLogger.finest('Refreshing connection data for ${connection.name}'); await RelationshipsClient(profile) .getConnectionWithStatus(connection) .match( onSuccess: (update) { upsertConnection(update); if (withNotification) { _sendUpdateNotifications(); } }, onError: (error) { _cmpLogger .severe('Error getting updates for ${connection.name}: $error'); }, ); } } @Riverpod(keepAlive: true) class _UpdateStatusInternal extends _$UpdateStatusInternal { @override String build(Profile profile) { return ''; } void update(String update) { state = update; } } @riverpod String updateStatus(Ref ref, Profile profile) { return ref.watch(_updateStatusInternalProvider(profile)); } final _acuLogger = Logger('AllContactsUpdaterProvider'); @Riverpod(keepAlive: true) class AllContactsUpdater extends _$AllContactsUpdater { @override bool build(Profile profile) { return false; } Future updateAllContacts(bool backgroundUpdate) async { // TODO check if profile is no longer same and stop if it is between pagings and cancel if so if (state) { _acuLogger.info( 'updateAllContacts called but believe it is already running so skipping'); return; } state = true; _acuLogger.info('Updating all contacts'); ref .read(_updateStatusInternalProvider(profile).notifier) .update('Updating'); final originalTime = ref.read(persistentInfoProvider(profile)); await ref .read(persistentInfoProvider(profile).notifier) .updateLastMyConnectionUpdate(DateTime.now()); final delay = backgroundUpdate ? const Duration(minutes: 5) : const Duration(seconds: 10); try { final client = RelationshipsClient(profile); final results = {}; var moreResults = true; var maxId = -1; const limit = 50; var currentPage = PagingData(limit: limit); final originalContacts = Set.from(ref.read(myContactsProvider(profile)).valueOrNull ?? []); while (moreResults) { await client.getMyFollowers(currentPage).match(onSuccess: (followers) { for (final f in followers.data) { originalContacts.remove(f); results[f.id] = f.copy(status: ConnectionStatus.theyFollowYou); int id = int.parse(f.id); maxId = max(maxId, id); } if (followers.next != null) { currentPage = followers.next!; } moreResults = followers.next != null; }, onError: (error) { _acuLogger.severe('Error getting followers data: $error'); moreResults = false; }); for (final c in results.values) { ref .read(connectionModifierProvider(profile, c).notifier) .upsertConnection(c); } await Future.delayed(delay); } moreResults = true; currentPage = PagingData(limit: limit); while (moreResults) { await client.getMyFollowing(currentPage).match(onSuccess: (following) { for (final f in following.data) { originalContacts.remove(f); final newStatus = results.containsKey(f.id) ? ConnectionStatus.mutual : ConnectionStatus.youFollowThem; ref .read(connectionModifierProvider(profile, f).notifier) .upsertConnection(f.copy(status: newStatus)); int id = int.parse(f.id); maxId = max(maxId, id); } if (following.next != null) { currentPage = following.next!; } moreResults = following.next != null; }, onError: (error) { _acuLogger.severe('Error getting followers data: $error'); }); await Future.delayed(delay); } for (final noLongerFollowed in originalContacts) { ref .read( connectionModifierProvider(profile, noLongerFollowed).notifier) .upsertConnection( noLongerFollowed.copy(status: ConnectionStatus.none)); } await ref .read(persistentInfoProvider(profile).notifier) .updateLastMyConnectionUpdate(DateTime.now()); final contactsLength = (ref.read(myContactsProvider(profile)).valueOrNull ?? []).length; _acuLogger.info('Done updating # Contacts:$contactsLength'); } catch (e) { _acuLogger.severe('Exception thrown trying to update contacts: $e'); } await ref .read(persistentInfoProvider(profile).notifier) .updateLastMyConnectionUpdate(originalTime); state = false; } }