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/connection.dart'; import '../models/exec_error.dart'; import '../models/group_data.dart'; import 'auth_service.dart'; class ConnectionsManager extends ChangeNotifier { static final _logger = Logger('$ConnectionsManager'); late final IConnectionsRepo conRepo; late final IGroupsRepo groupsRepo; var groupsNotInitialized = true; ConnectionsManager(this.conRepo, this.groupsRepo); 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), ); } bool upsertAllConnections(Iterable newConnections) { newConnections.forEach(upsertConnection); return true; } Future acceptFollowRequest(Connection connection) async { _logger.finest( 'Attempting to accept follow request ${connection.name}: ${connection.status}'); await RelationshipsClient(getIt().currentProfile) .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(getIt().currentProfile) .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(getIt().currentProfile) .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(getIt().currentProfile) .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(getIt().currentProfile) .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'); final client = RelationshipsClient(getIt().currentProfile); 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'); }); } 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'); }); } 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)); _logger.fine('# Contacts:${myContacts.length}'); notifyListeners(); } List getMyGroups() { final myGroups = groupsRepo.getMyGroups(); if (groupsNotInitialized) { groupsNotInitialized = true; _updateMyGroups(true); } return myGroups; } 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(getIt().currentProfile) .createGroup(newName) .withResultAsync((newGroup) async { groupsRepo.upsertGroup(newGroup); notifyListeners(); }); return result.execErrorCast(); } FutureResult renameGroup( String id, String newName) async { final result = await GroupsClient(getIt().currentProfile) .renameGroup(id, newName) .withResultAsync((renamedGroup) async { groupsRepo.upsertGroup(renamedGroup); notifyListeners(); }); return result.execErrorCast(); } FutureResult deleteGroup(GroupData groupData) async { final result = await GroupsClient(getIt().currentProfile) .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(getIt().currentProfile); 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(getIt().currentProfile) .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(getIt().currentProfile) .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) { return conRepo.getById(id).andThenSuccess((c) { if (c.status == ConnectionStatus.unknown) { _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(getIt().currentProfile) .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(getIt().currentProfile) .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(getIt().currentProfile) .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'); }, ); } }