relatica/lib/riverpod_controllers/connection_manager_services...

402 wiersze
13 KiB
Dart
Czysty Zwykły widok Historia

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 '../friendica_client/paging_data.dart';
import '../models/auth/profile.dart';
import '../models/connection.dart';
import '../models/exec_error.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<IConnectionsRepo> _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<IConnectionsRepo, bool> build(Profile profile) {
_crsLogger.info('Build $profile');
ref
.watch(_connectionsRepoProvider(profile).future)
.then((repo) => state = Result.ok(repo));
return Result.error(true);
}
}
@riverpod
List<Connection> knownUsersByName(Ref ref, Profile profile, String userName) {
return ref.watch(_connectionsRepoSyncProvider(profile)).fold(
onSuccess: (repo) => repo.getKnownUsersByName(userName),
onError: (_) => []);
}
@riverpod
Future<List<Connection>> 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<Connection, ExecError> 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<Connection, ExecError> 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<Connection, ExecError> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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 = <String, Connection>{};
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;
}
}