import 'dart:collection'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:result_monad/result_monad.dart'; import '../data/interfaces/connections_repo_intf.dart'; import '../data/interfaces/groups_repo.intf.dart'; import '../friendica_client/friendica_client.dart'; import '../friendica_client/paging_data.dart'; import '../globals.dart'; import '../models/auth/profile.dart'; import '../models/connection.dart'; import '../models/exec_error.dart'; import '../models/group_data.dart'; import '../utils/active_profile_selector.dart'; import 'persistent_info_service.dart'; class ConnectionsManager extends ChangeNotifier { static final _logger = Logger('$ConnectionsManager'); late final IConnectionsRepo conRepo; late final IGroupsRepo groupsRepo; late final Profile profile; var groupsNotInitialized = true; var _lastUpdateStatus = ''; String get lastUpdateStatus => _lastUpdateStatus.isNotEmpty ? _lastUpdateStatus : getIt>() .getForProfile(profile) .transform( (info) => 'Last updated at: ${info.lastMyConnectionsUpdate}') .withResult((text) => _lastUpdateStatus = text) .getValueOrElse(() => 'Unknown'); ConnectionsManager(this.profile, this.conRepo, this.groupsRepo); void clear() { conRepo.clear(); groupsRepo.clear(); groupsNotInitialized = true; _lastUpdateStatus = ''; notifyListeners(); } List getKnownUsersByName(String name) { return conRepo.getKnownUsersByName(name); } bool upsertConnection(Connection connection) { if (connection.status != ConnectionStatus.unknown) { return conRepo.upsertConnection(connection); } return conRepo.getById(connection.id).fold( onSuccess: (original) => conRepo.upsertConnection( connection.copy(status: original.status), ), onError: (_) => conRepo.upsertConnection(connection), ); } Future upsertAllConnections(Iterable newConnections) async { var result = true; for (var c in newConnections) { result &= await Future.delayed(Duration.zero, () => upsertConnection(c)); } return result; } Future acceptFollowRequest(Connection connection) async { _logger.finest( 'Attempting to accept follow request ${connection.name}: ${connection.status}'); await RelationshipsClient(profile).acceptFollow(connection).match( onSuccess: (update) { _logger .finest('Successfully followed ${update.name}: ${update.status}'); upsertConnection(update); notifyListeners(); }, onError: (error) { _logger.severe('Error following ${connection.name}: $error'); }, ); } Future rejectFollowRequest(Connection connection) async { _logger.finest( 'Attempting to accept follow request ${connection.name}: ${connection.status}'); await RelationshipsClient(profile).rejectFollow(connection).match( onSuccess: (update) { _logger .finest('Successfully followed ${update.name}: ${update.status}'); upsertConnection(update); notifyListeners(); }, onError: (error) { _logger.severe('Error following ${connection.name}: $error'); }, ); } Future ignoreFollowRequest(Connection connection) async { _logger.finest( 'Attempting to accept follow request ${connection.name}: ${connection.status}'); await RelationshipsClient(profile).ignoreFollow(connection).match( onSuccess: (update) { _logger .finest('Successfully followed ${update.name}: ${update.status}'); upsertConnection(update); notifyListeners(); }, onError: (error) { _logger.severe('Error following ${connection.name}'); }, ); } Future follow(Connection connection) async { _logger.finest( 'Attempting to follow ${connection.name}: ${connection.status}'); await RelationshipsClient(profile).followConnection(connection).match( onSuccess: (update) { _logger .finest('Successfully followed ${update.name}: ${update.status}'); upsertConnection(update); notifyListeners(); }, onError: (error) { _logger.severe('Error following ${connection.name}'); }, ); } Future unfollow(Connection connection) async { _logger.finest( 'Attempting to unfollow ${connection.name}: ${connection.status}'); await RelationshipsClient(profile).unFollowConnection(connection).match( onSuccess: (update) { _logger .finest('Successfully unfollowed ${update.name}: ${update.status}'); upsertConnection(update); notifyListeners(); }, onError: (error) { _logger.severe('Error following ${connection.name}'); }, ); } List getMyContacts() { return conRepo.getMyContacts(); } Future updateAllContacts() async { _logger.fine('Updating all contacts'); _lastUpdateStatus = 'Updating'; final persistentInfo = getIt>() .getForProfile(profile) .value; final originalTime = persistentInfo.lastMyConnectionsUpdate; await persistentInfo.updateLastMyConnectionUpdate(DateTime.now()); notifyListeners(); try { final client = RelationshipsClient(profile); final results = {}; var moreResults = true; var maxId = -1; const limit = 50; var currentPage = PagingData(limit: limit); final originalContacts = conRepo.getMyContacts().toSet(); 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) { _logger.severe('Error getting followers data: $error'); }); await Future.delayed(Duration.zero); } moreResults = true; currentPage = PagingData(limit: limit); while (moreResults) { await client.getMyFollowing(currentPage).match(onSuccess: (following) { for (final f in following.data) { originalContacts.remove(f); if (results.containsKey(f.id)) { results[f.id] = f.copy(status: ConnectionStatus.mutual); } else { results[f.id] = f.copy(status: ConnectionStatus.youFollowThem); } int id = int.parse(f.id); maxId = max(maxId, id); } if (following.next != null) { currentPage = following.next!; } moreResults = following.next != null; }, onError: (error) { _logger.severe('Error getting followers data: $error'); }); await Future.delayed(Duration.zero); } for (final noLongerFollowed in originalContacts) { results[noLongerFollowed.id] = noLongerFollowed.copy(status: ConnectionStatus.none); } upsertAllConnections(results.values); final myContacts = conRepo.getMyContacts().toList(); myContacts.sort((c1, c2) => c1.name.compareTo(c2.name)); await persistentInfo.updateLastMyConnectionUpdate(DateTime.now()); _logger.fine('# Contacts:${myContacts.length}'); } catch (e) { await persistentInfo.updateLastMyConnectionUpdate(originalTime); } _lastUpdateStatus = 'Last updated at: ${persistentInfo.lastMyConnectionsUpdate}'; notifyListeners(); } List getMyGroups() { if (groupsNotInitialized) { groupsNotInitialized = false; _updateMyGroups(true); } return groupsRepo.getMyGroups(); } Result, ExecError> getGroupMembers(GroupData group) { return groupsRepo .getGroupMembers(group) .transform( (members) => members ..sort((c1, c2) => c1.name.toLowerCase().compareTo(c2.name.toLowerCase())), ) .execErrorCast(); } FutureResult createGroup(String newName) async { final result = await GroupsClient(profile) .createGroup(newName) .withResultAsync((newGroup) async { groupsRepo.upsertGroup(newGroup); notifyListeners(); }); return result.execErrorCast(); } FutureResult renameGroup( String id, String newName) async { final result = await GroupsClient(profile) .renameGroup(id, newName) .withResultAsync((renamedGroup) async { groupsRepo.upsertGroup(renamedGroup); notifyListeners(); }); return result.execErrorCast(); } FutureResult deleteGroup(GroupData groupData) async { final result = await GroupsClient(profile) .deleteGroup(groupData) .withResultAsync((_) async { groupsRepo.deleteGroup(groupData); notifyListeners(); }); return result.execErrorCast(); } void refreshGroups() { _updateMyGroups(true); } Future refreshGroupMemberships(GroupData group) async { var page = PagingData(limit: 50); final client = GroupsClient(profile); final allResults = {}; var moreResults = true; while (moreResults) { await client.getGroupMembers(group, page).match(onSuccess: (results) { moreResults = results.data.isNotEmpty && results.next != null; page = results.next ?? page; allResults.addAll(results.data); }, onError: (error) { _logger.severe('Error getting group listing data: $error'); moreResults = false; }); } groupsRepo.deleteGroup(group); groupsRepo.upsertGroup(group); for (final c in allResults) { upsertConnection(c); groupsRepo.addConnectionToGroup(group, c); } notifyListeners(); } Result, ExecError> getGroupsForUser(String id) { final result = groupsRepo.getGroupsForUser(id); if (result.isSuccess) { return result; } if (result.isFailure && result.error.type != ErrorType.notFound) { return result; } _refreshGroupListData(id, true); return Result.ok(UnmodifiableListView([])); } FutureResult addUserToGroup( GroupData group, Connection connection) async { _logger.finest('Adding ${connection.name} to group: ${group.name}'); return await GroupsClient(profile) .addConnectionToGroup(group, connection) .withResultAsync((_) async => refreshGroupMemberships(group)) .withResult((_) => notifyListeners()) .mapError((error) { _logger .severe('Error adding ${connection.name} from group: ${group.name}'); return error; }); } FutureResult removeUserFromGroup( GroupData group, Connection connection) async { _logger.finest('Removing ${connection.name} from group: ${group.name}'); return GroupsClient(profile) .removeConnectionFromGroup(group, connection) .withResultAsync((_) async => refreshGroupMemberships(group)) .withResult((_) => notifyListeners()) .mapError( (error) { _logger.severe( 'Error removing ${connection.name} from group: ${group.name}'); return error; }, ); } Result getById(String id, {bool forceUpdate = false}) { return conRepo.getById(id).andThenSuccess((c) { if (forceUpdate) { _refreshConnection(c, true); } return c; }).execErrorCast(); } Result getByName(String name) { return conRepo.getByName(name).andThenSuccess((c) { if (c.status == ConnectionStatus.unknown) { _refreshConnection(c, true); } return c; }).execErrorCast(); } Result getByHandle(String handle) { return conRepo.getByHandle(handle).andThenSuccess((c) { if (c.status == ConnectionStatus.unknown) { _refreshConnection(c, true); } return c; }).execErrorCast(); } Future fullRefresh(Connection connection) async { await _updateMyGroups(false); await _refreshGroupListData(connection.id, false); await _refreshConnection(connection, false); notifyListeners(); } Future _refreshGroupListData(String id, bool withNotification) async { _logger.finest('Refreshing member list data for Connection $id'); await GroupsClient(profile).getMemberGroupsForConnection(id).match( onSuccess: (groups) { groupsRepo.updateConnectionGroupData(id, groups); if (withNotification) { notifyListeners(); } }, onError: (error) { _logger.severe('Error getting list data for $id: $error'); }, ); } Future _refreshConnection( Connection connection, bool withNotification) async { _logger.finest('Refreshing connection data for ${connection.name}'); await RelationshipsClient(profile) .getConnectionWithStatus(connection) .match( onSuccess: (update) { upsertConnection(update); if (withNotification) { notifyListeners(); } }, onError: (error) { _logger.severe('Error getting updates for ${connection.name}: $error'); }, ); } Future _updateMyGroups(bool withNotification) async { _logger.finest('Refreshing my groups list'); await GroupsClient(profile).getGroups().match( onSuccess: (groups) { _logger.finest('Got updated groups:${groups.map((e) => e.name)}'); groupsRepo.clearMyGroups(); groupsRepo.addAllGroups(groups); if (withNotification) { notifyListeners(); } }, onError: (error) { _logger.severe('Error getting my groups: $error'); }, ); } }