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 'package:stack_trace/stack_trace.dart'; import '../data/interfaces/circles_repo_intf.dart'; import '../data/memory/memory_circles_repo.dart'; import '../models/auth/profile.dart'; import '../models/connection.dart'; import '../models/exec_error.dart'; import '../models/networking/paging_data.dart'; import '../models/timeline_grouping_list_data.dart'; import 'connection_manager_services.dart'; import 'networking/friendica_timeline_grouping_client_services.dart'; part 'circles_repo_services.g.dart'; final _crLogger = Logger('CirclesRepoProvider'); @Riverpod(keepAlive: true) class _CirclesRepo extends _$CirclesRepo { @override ICirclesRepo build(Profile profile) { _crLogger.info('Creating for $profile'); return MemoryCirclesRepo(); } Future, ExecError>> refreshCircleData() async { _crLogger.info('Refreshing member circle data '); return await ref .read(timelineGroupingListDataClientProvider(profile).future) .andThen((circles) { state.clearMyCircles(); state.addAllCircles(circles); ref.notifyListeners(); return Result.ok(state.getMyCircles()); }).withError( (error) { _crLogger.severe('Error getting list data: $error', Trace.current()); }, ).execErrorCastAsync(); } } final _cpLogger = Logger('CirclesProvider'); @Riverpod(keepAlive: true) class Circles extends _$Circles { @override List build(Profile profile) { _cpLogger.info('Creating for $profile'); profile = profile; final circles = ref.watch(_circlesRepoProvider(profile)).getMyCircles(); if (circles.isEmpty) { Future.delayed(const Duration(milliseconds: 1), refresh); } return circles; } Future refresh() async { _cpLogger.info('Refreshing circles provider'); await ref .read(_circlesRepoProvider(profile).notifier) .refreshCircleData() .withResult((circles) => state = circles); } void upsertCircle(TimelineGroupingListData circle) { ref.read(_circlesRepoProvider(profile)).upsertCircle(circle); state = ref.read(_circlesRepoProvider(profile)).getMyCircles(); } FutureResult createCircle( String newName) async { final result = await ref .read(createCircleProvider(profile, newName).future) .withResult((newCircle) { ref.read(_circlesRepoProvider(profile)).upsertCircle(newCircle); state = ref.read(_circlesRepoProvider(profile)).getMyCircles(); }); return result.execErrorCast(); } FutureResult deleteCircle( TimelineGroupingListData circleData) async { final result = await ref .read(deleteCircleProvider(profile, circleData).future) .withResult((_) { ref.read(_circlesRepoProvider(profile)).deleteCircle(circleData); state = ref.read(_circlesRepoProvider(profile)).getMyCircles(); }); return result.execErrorCast(); } void clear() { ref.read(_circlesRepoProvider(profile)).clear(); state = ref.read(_circlesRepoProvider(profile)).getMyCircles(); } void addAllCircles(List circles) { ref.read(_circlesRepoProvider(profile)).addAllCircles(circles); state = ref.read(_circlesRepoProvider(profile)).getMyCircles(); } Result, ExecError> getCirclesForUser( String id) { final result = ref.read(_circlesRepoProvider(profile)).getCirclesForUser(id); if (result.isSuccess) { _cpLogger.finer("Circles for user $id: $result"); return result; } if (result.isFailure && result.error.type != ErrorType.notFound) { return result; } refreshConnectionCircleData(id, true); return Result.ok([]); } bool updateConnectionCircleData( String id, List currentCircles) => ref .read(_circlesRepoProvider(profile)) .updateConnectionCircleData(id, currentCircles); FutureResult renameCircle( String id, String newName) async { // TODO retire old members provider? final result = await ref .read(renameCircleProvider(profile, id, newName).future) .withResult((renamedCircle) { ref.read(_circlesRepoProvider(profile)).upsertCircle(renamedCircle); state = ref.read(_circlesRepoProvider(profile)).getMyCircles(); }); return result.execErrorCast(); } Future refreshConnectionCircleData( String id, bool withNotification) async { _cpLogger.finest('Refreshing member list data for Connection $id'); await ref .read(memberCirclesForConnectionProvider(profile, id).future) .match( onSuccess: (circles) { updateConnectionCircleData(id, circles); if (withNotification) { state = ref.read(_circlesRepoProvider(profile)).getMyCircles(); } }, onError: (error) { _cpLogger.severe( 'Error getting list data for $id: $error', Trace.current(), ); }, ); } } @Riverpod(keepAlive: true) class CircleMembers extends _$CircleMembers { @override List build(Profile profile, TimelineGroupingListData circle) { return ref .read(_circlesRepoProvider(profile)) .getCircleMembers(circle) .getValueOrElse(() => []); } FutureResult addConnectionToCircle( Connection connection, ) async { _cpLogger.finest('Adding ${connection.name} to circle: ${circle.name}'); return await ref .read(addConnectionToCircleProvider(profile, circle, connection).future) .withResult((_) { ref .read(_circlesRepoProvider(profile)) .addConnectionToCircle(circle, connection); _setState(ref .read(_circlesRepoProvider(profile)) .getCircleMembers(circle) .getValueOrElse(() => [])); }).mapError((error) { _cpLogger.severe( 'Error adding ${connection.name} from circle: ${circle.name}', Trace.current(), ); return error; }); } FutureResult removeConnectionFromCircle( Connection connection) async { return ref .read(removeConnectionFromCircleProvider(profile, circle, connection) .future) .withResult((_) => ref .read(_circlesRepoProvider(profile)) .removeConnectionFromCircle(circle, connection)) .withResult((_) => _setState(ref .read(_circlesRepoProvider(profile)) .getCircleMembers(circle) .getValueOrElse(() => []))) .mapError( (error) { _cpLogger.severe( 'Error removing ${connection.name} from circle: ${circle.name}', Trace.current(), ); return error; }, ); } Future refreshCircleMemberships() async { _cpLogger.info('Refreshing Circle Memberships for ${circle.name}'); var page = const PagingData(limit: 50); final allResults = {}; var moreResults = true; while (moreResults) { await ref .read(circleMembersNetworkProvider(profile, circle, page).future) .match(onSuccess: (results) { _cpLogger .info('Received next set of memberships for Circle ${circle.name}'); moreResults = results.data.isNotEmpty && results.next != null; page = results.next ?? page; allResults.addAll(results.data); }, onError: (error) { _cpLogger.severe( 'Error getting circle listing data: $error', Trace.current(), ); moreResults = false; }); } ref.read(_circlesRepoProvider(profile)).deleteCircle(circle); ref.read(_circlesRepoProvider(profile)).upsertCircle(circle); _cpLogger.info( 'Adding ${allResults.length} Updated Memberships for ${circle.name}'); for (final c in allResults) { await ref .read(connectionModifierProvider(profile, c).notifier) .upsertConnection(c); ref.read(_circlesRepoProvider(profile)).addConnectionToCircle(circle, c); } _setState(allResults.toList()); _cpLogger.info( 'Completed ${allResults.length} updating memberships for ${circle.name}'); } void _setState(List newState) { newState.sort((c1, c2) => c1.name.compareTo(c2.name)); state = newState; } } final _tglLogger = Logger('TimelineGroupingListProvider'); @riverpod Result circleData( Ref ref, Profile profile, String id) { final circles = ref.watch(circlesProvider(profile)).where((c) => c.id == id); if (circles.isEmpty) { return buildErrorResult( type: ErrorType.notFound, message: 'Circle not found: $id', ); } return Result.ok(circles.first); } @riverpod List timelineGroupingList( Ref ref, Profile profile, GroupingType type) { _tglLogger.info('Creating for $type for $profile'); final circles = ref.watch(circlesProvider(profile)); final result = circles.where((e) => e.groupingType == type).toList(); result.sort((g1, g2) => g1.name.compareTo(g2.name)); return result; }