import 'dart:math'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:result_monad/result_monad.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'); final _connectionsById = {}; final _connectionsByName = {}; final _connectionsByProfileUrl = {}; final _groupsForConnection = >{}; final _myGroups = {}; final _myContacts = []; var _myContactsInitialized = false; int get length => _connectionsById.length; void clearCaches() { _connectionsById.clear(); _connectionsByName.clear(); _connectionsByProfileUrl.clear(); _groupsForConnection.clear(); _myGroups.clear(); _myContacts.clear(); } bool addConnection(Connection connection) { if (_connectionsById.containsKey(connection.id)) { return false; } return updateConnection(connection); } List getKnownUsersByName(String name) { return _connectionsByName.values.where((it) { final normalizedHandle = it.handle.toLowerCase(); final normalizedName = it.name.toLowerCase(); final normalizedQuery = name.toLowerCase(); return normalizedHandle.contains(normalizedQuery) || normalizedName.contains(normalizedQuery); }).toList(); } bool updateConnection(Connection connection) { _connectionsById[connection.id] = connection; _connectionsByName[connection.name] = connection; _connectionsByProfileUrl[connection.profileUrl] = connection; int index = _myContacts.indexWhere((c) => c.id == connection.id); if (index >= 0) { _myContacts.removeAt(index); } switch (connection.status) { case ConnectionStatus.youFollowThem: case ConnectionStatus.theyFollowYou: case ConnectionStatus.mutual: if (index > 0) { _myContacts.insert(index, connection); } else { _myContacts.add(connection); } break; default: break; } return true; } bool addAllConnections(Iterable newConnections) { bool result = true; for (final connection in newConnections) { result &= addConnection(connection); } return result; } Future acceptFollowRequest(Connection connection) async { _logger.finest( 'Attempting to accept follow request ${connection.name}: ${connection.status}'); await getIt() .currentClient .andThenAsync((client) => client.acceptFollow(connection)) .match( onSuccess: (update) { _logger .finest('Successfully followed ${update.name}: ${update.status}'); updateConnection(update); notifyListeners(); }, onError: (error) { _logger.severe('Error following ${connection.name}'); }, ); } Future rejectFollowRequest(Connection connection) async { _logger.finest( 'Attempting to accept follow request ${connection.name}: ${connection.status}'); await getIt() .currentClient .andThenAsync((client) => client.rejectFollow(connection)) .match( onSuccess: (update) { _logger .finest('Successfully followed ${update.name}: ${update.status}'); updateConnection(update); notifyListeners(); }, onError: (error) { _logger.severe('Error following ${connection.name}'); }, ); } Future ignoreFollowRequest(Connection connection) async { _logger.finest( 'Attempting to accept follow request ${connection.name}: ${connection.status}'); await getIt() .currentClient .andThenAsync((client) => client.ignoreFollow(connection)) .match( onSuccess: (update) { _logger .finest('Successfully followed ${update.name}: ${update.status}'); updateConnection(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 getIt() .currentClient .andThenAsync((client) => client.followConnection(connection)) .match( onSuccess: (update) { _logger .finest('Successfully followed ${update.name}: ${update.status}'); updateConnection(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 getIt() .currentClient .andThenAsync((client) => client.unFollowConnection(connection)) .match( onSuccess: (update) { _logger .finest('Successfully unfollowed ${update.name}: ${update.status}'); updateConnection(update); notifyListeners(); }, onError: (error) { _logger.severe('Error following ${connection.name}'); }, ); } List getMyContacts() { if (!_myContactsInitialized) { updateAllContacts(); _myContactsInitialized = true; } return _myContacts.toList(growable: false); } Future updateAllContacts() async { _logger.fine('Updating all contacts'); final clientResult = getIt().currentClient; if (clientResult.isFailure) { _logger.severe( 'Unable to update contacts due to client error: ${clientResult.error}'); return; } final client = clientResult.value; final results = {}; var moreResults = true; var maxId = -1; const limit = 1000; while (moreResults) { await client.getMyFollowers(sinceId: maxId, limit: limit).match( onSuccess: (followers) { for (final f in followers) { results[f.id] = f.copy(status: ConnectionStatus.theyFollowYou); int id = int.parse(f.id); maxId = max(maxId, id); } moreResults = followers.length >= limit; }, onError: (error) { _logger.severe('Error getting followers data: $error'); }); } moreResults = true; maxId = -1; while (moreResults) { await client.getMyFollowing(sinceId: maxId, limit: limit).match( onSuccess: (following) { for (final f in following) { 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); } moreResults = following.length >= limit; }, onError: (error) { _logger.severe('Error getting followers data: $error'); }); } _myContacts.clear(); _myContacts.addAll(results.values); addAllConnections(results.values); _myContacts.sort((c1, c2) => c1.name.compareTo(c2.name)); _logger.finest('# Contacts:${_myContacts.length}'); notifyListeners(); } List getMyGroups() { if (_myGroups.isNotEmpty) { return _myGroups.toList(growable: false); } _updateMyGroups(true); return []; } Result, ExecError> getGroupsForUser(String id) { if (!_groupsForConnection.containsKey(id)) { _refreshGroupListData(id, true); return Result.ok([]); } return Result.ok(_groupsForConnection[id]!); } FutureResult addUserToGroup( GroupData group, Connection connection) async { _logger.finest('Adding ${connection.name} to group: ${group.name}'); final result = await getIt().currentClient.andThenAsync( (client) => client.addConnectionToGroup(group, connection)); result.match( onSuccess: (_) => _refreshGroupListData(connection.id, true), onError: (error) { _logger .severe('Error adding ${connection.name} to group: ${group.name}'); }, ); return result.execErrorCast(); } FutureResult removeUserFromGroup( GroupData group, Connection connection) async { _logger.finest('Removing ${connection.name} from group: ${group.name}'); final result = await getIt().currentClient.andThenAsync( (client) => client.removeConnectionFromGroup(group, connection)); result.match( onSuccess: (_) => _refreshGroupListData(connection.id, true), onError: (error) { _logger.severe( 'Error removing ${connection.name} from group: ${group.name}'); }, ); return result.execErrorCast(); } Result getById(String id) { final result = _connectionsById[id]; if (result == null) { return Result.error('$id not found'); } if (result.status == ConnectionStatus.unknown) { _refreshConnection(result, true); } return Result.ok(result); } Result getByName(String name) { final result = _connectionsByName[name]; if (result == null) { Result.error('$name not found'); } if (result!.status == ConnectionStatus.unknown) { _refreshConnection(result, true); } return Result.ok(result); } Result getByProfileUrl(Uri url) { final result = _connectionsByProfileUrl[url]; if (result == null) { Result.error('$url not found'); } if (result!.status == ConnectionStatus.unknown) { _refreshConnection(result, true); } return Result.ok(result); } 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 getIt() .currentClient .andThenAsync((client) => client.getMemberGroupsForConnection(id)) .match( onSuccess: (lists) { _groupsForConnection[id] = lists; 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 getIt() .currentClient .andThenAsync((client) => client.getConnectionWithStatus(connection)) .match( onSuccess: (update) { updateConnection(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 getIt() .currentClient .andThenAsync((client) => client.getGroups()) .match( onSuccess: (groups) { _logger.finest('Got updated groups:${groups.map((e) => e.name)}'); _myGroups.clear(); _myGroups.addAll(groups); if (withNotification) { notifyListeners(); } }, onError: (error) { _logger.severe('Error getting my groups: $error'); }, ); } }