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 '../models/auth/profile.dart'; import '../models/exec_error.dart'; import '../models/media_attachment.dart'; import '../models/networking/paging_data.dart'; import '../models/timeline.dart'; import '../models/timeline_entry.dart'; import '../models/timeline_identifiers.dart'; import '../riverpod_controllers/networking/friendica_timelines_client_services.dart'; import 'entry_tree_item_services.dart'; part 'timeline_services.g.dart'; enum TimelineRefreshType { refresh, loadOlder, loadNewer, } final _tcLogger = Logger('TimelineCleanerProvider'); @Riverpod(keepAlive: true) class TimelineMaintainer extends _$TimelineMaintainer { @override List build(Profile profile) { return []; } void add(TimelineIdentifiers timelineId) { _tcLogger.fine('Adding timeline: $timelineId'); state.add(timelineId); ref.notifyListeners(); } void sweep(String statusId) { for (final t in state) { final result = ref .read(timelineManagerProvider(profile, t).notifier) .removeTimelineEntry(statusId); _tcLogger.finest( 'Attempting to remove $statusId from $t but was it there? $result'); } } void refreshAll() async { const typesToRefresh = [ TimelineType.home, TimelineType.global, TimelineType.local, TimelineType.circle, ]; final nonUserTimelines = state.where((t) => typesToRefresh.contains(t.timeline)).toList(); for (final t in nonUserTimelines) { ref.invalidate(timelineManagerProvider(profile, t)); } } Future loadNewerForPersonalTimelines() async { const standardMyTimelines = [ TimelineType.home, TimelineType.global, TimelineType.local, TimelineType.self, ]; final openMyTimelines = state.where((i) => standardMyTimelines.contains(i.timeline)).toList(); if (openMyTimelines.isEmpty) { _tcLogger.finest( 'No personal timelines open that need checking for new entries'); } for (final t in openMyTimelines) { await ref .read(timelineManagerProvider(profile, t).notifier) .updateTimeline(TimelineRefreshType.loadNewer); } } } final _tmpLogger = Logger('TimelineManagerProvider'); @Riverpod(keepAlive: true) class TimelineManager extends _$TimelineManager { @override Timeline build(Profile profile, TimelineIdentifiers timelineId) { _tmpLogger.info('Building for $profile for $timelineId'); Future.microtask(() async => ref.read(timelineMaintainerProvider(profile).notifier).add(timelineId)); return Timeline(timelineId); } Future updateTimeline( TimelineRefreshType refreshType, ) async { _tmpLogger.info( 'Updating w/$refreshType for timeline $timelineId for profile $profile '); late final int lowestId; late final int highestId; late final InsertionType insertionType; switch (refreshType) { case TimelineRefreshType.refresh: lowestId = 0; highestId = 0; insertionType = InsertionType.end; break; case TimelineRefreshType.loadOlder: lowestId = state.lowestStatusId; highestId = 0; insertionType = InsertionType.end; break; case TimelineRefreshType.loadNewer: lowestId = 0; highestId = state.highestStatusId; insertionType = InsertionType.beginning; break; } (await ref .read(timelineUpdaterProvider(profile).notifier) .updateTimeline(timelineId, lowestId, highestId)) .match(onSuccess: (posts) { _tmpLogger .finest('Posts returned for adding to $timelineId: ${posts.length}'); final oldState = state; final newState = state = state.addOrUpdate( posts.map((p) => p.id).toList(), insertionType: insertionType, ); _tmpLogger.finest( 'Old post count: ${oldState.posts.length}, New Post count: ${newState.posts.length}'); _tmpLogger.finest('Old state == New state? ${oldState == newState}'); }, onError: (error) { _tmpLogger.severe( 'Error updating timeline: $timelineId}', Trace.current(), ); }); } bool removeTimelineEntry(String id) { final hadItem = state.removeTimelineEntry(id); if (hadItem) { ref.notifyListeners(); } _tmpLogger.finest('Remove entry $id? $hadItem'); return hadItem; } } @riverpod class UserMediaTimeline extends _$UserMediaTimeline { static const limit = 50; var nextPage = const PagingData(limit: limit); var media = []; @override Future, ExecError>> build( Profile profile, String accountId) async { await updateTimeline(reset: true, withNotification: false); return Result.ok(media); } Future, ExecError>> updateTimeline( {bool reset = true, bool withNotification = true}) async { if (reset) { nextPage = const PagingData(limit: limit); media.clear(); } final result = await ref.watch( userMediaTimelineClientProvider(profile, accountId, page: nextPage) .future); return result .withResult((result) { for (final entries in result.data) { media.addAll(entries.mediaAttachments); } nextPage = result.next!; if (withNotification) { ref.notifyListeners(); } }) .transform((_) => media) .execErrorCast(); } } @riverpod class UserPostsAndCommentsTimeline extends _$UserPostsAndCommentsTimeline { static const limit = 50; var nextPage = const PagingData(limit: limit); var entries = []; @override Future, ExecError>> build( Profile profile, String accountId) async { await updateTimeline(reset: true, withNotification: false); return Result.ok(entries); } Future, ExecError>> updateTimeline( {bool reset = true, bool withNotification = true}) async { if (reset) { nextPage = const PagingData(limit: limit); entries.clear(); } final result = await ref.watch(userPostsAndCommentsTimelineClientProvider( profile, accountId, page: nextPage) .future); return result .withResult((result) { entries.addAll(result.data); nextPage = result.next!; if (withNotification) { ref.notifyListeners(); } }) .transform((_) => entries) .execErrorCast(); } }