relatica/lib/services/connections_manager.dart

390 lines
12 KiB
Dart

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 = <String, Connection>{};
final _connectionsByName = <String, Connection>{};
final _connectionsByProfileUrl = <Uri, Connection>{};
final _groupsForConnection = <String, List<GroupData>>{};
final _myGroups = <GroupData>{};
final _myContacts = <Connection>[];
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<Connection> 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<Connection> newConnections) {
bool result = true;
for (final connection in newConnections) {
result &= addConnection(connection);
}
return result;
}
Future<void> acceptFollowRequest(Connection connection) async {
_logger.finest(
'Attempting to accept follow request ${connection.name}: ${connection.status}');
await getIt<AuthService>()
.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<void> rejectFollowRequest(Connection connection) async {
_logger.finest(
'Attempting to accept follow request ${connection.name}: ${connection.status}');
await getIt<AuthService>()
.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<void> ignoreFollowRequest(Connection connection) async {
_logger.finest(
'Attempting to accept follow request ${connection.name}: ${connection.status}');
await getIt<AuthService>()
.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<void> follow(Connection connection) async {
_logger.finest(
'Attempting to follow ${connection.name}: ${connection.status}');
await getIt<AuthService>()
.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<void> unfollow(Connection connection) async {
_logger.finest(
'Attempting to unfollow ${connection.name}: ${connection.status}');
await getIt<AuthService>()
.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<Connection> getMyContacts() {
if (!_myContactsInitialized) {
updateAllContacts();
_myContactsInitialized = true;
}
return _myContacts.toList(growable: false);
}
Future<void> updateAllContacts() async {
_logger.fine('Updating all contacts');
final clientResult = getIt<AuthService>().currentClient;
if (clientResult.isFailure) {
_logger.severe(
'Unable to update contacts due to client error: ${clientResult.error}');
return;
}
final client = clientResult.value;
final results = <String, Connection>{};
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<GroupData> getMyGroups() {
if (_myGroups.isNotEmpty) {
return _myGroups.toList(growable: false);
}
_updateMyGroups(true);
return [];
}
Result<List<GroupData>, ExecError> getGroupsForUser(String id) {
if (!_groupsForConnection.containsKey(id)) {
_refreshGroupListData(id, true);
return Result.ok([]);
}
return Result.ok(_groupsForConnection[id]!);
}
FutureResult<bool, ExecError> addUserToGroup(
GroupData group, Connection connection) async {
_logger.finest('Adding ${connection.name} to group: ${group.name}');
final result = await getIt<AuthService>().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<bool, ExecError> removeUserFromGroup(
GroupData group, Connection connection) async {
_logger.finest('Removing ${connection.name} from group: ${group.name}');
final result = await getIt<AuthService>().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<Connection, String> 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<Connection, String> 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<Connection, String> 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<void> fullRefresh(Connection connection) async {
await _updateMyGroups(false);
await _refreshGroupListData(connection.id, false);
await _refreshConnection(connection, false);
notifyListeners();
}
Future<void> _refreshGroupListData(String id, bool withNotification) async {
_logger.finest('Refreshing member list data for Connection $id');
await getIt<AuthService>()
.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<void> _refreshConnection(
Connection connection, bool withNotification) async {
_logger.finest('Refreshing connection data for ${connection.name}');
await getIt<AuthService>()
.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<void> _updateMyGroups(bool withNotification) async {
_logger.finest('Refreshing my groups list');
await getIt<AuthService>()
.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');
},
);
}
}