import 'package:flutter/material.dart' hide Visibility; import 'package:logging/logging.dart'; import 'package:result_monad/result_monad.dart'; import '../data/interfaces/circles_repo_intf.dart'; import '../friendica_client/friendica_client.dart'; import '../models/TimelineIdentifiers.dart'; import '../models/auth/profile.dart'; import '../models/circle_data.dart'; import '../models/entry_tree_item.dart'; import '../models/exec_error.dart'; import '../models/image_entry.dart'; import '../models/media_attachment_uploads/new_entry_media_items.dart'; import '../models/timeline.dart'; import '../models/timeline_entry.dart'; import '../models/visibility.dart'; import 'entry_manager_service.dart'; enum TimelineRefreshType { refresh, loadOlder, loadNewer, } class TimelineManager extends ChangeNotifier { static final _logger = Logger('$TimelineManager'); final ICirclesRepo circlesRepo; final EntryManagerService entryManagerService; var circlesNotInitialized = true; final Profile profile; final cachedTimelines = {}; TimelineManager(this.profile, this.circlesRepo, this.entryManagerService); void clear() { circlesNotInitialized = true; cachedTimelines.clear(); entryManagerService.clear(); circlesRepo.clear(); notifyListeners(); } Result, ExecError> getCircles() { if (circlesNotInitialized) { _refreshCircleData(); circlesNotInitialized = false; return Result.ok([]); } return Result.ok(circlesRepo.getMyCircles()); } Future _refreshCircleData() async { _logger.finer('Refreshing member circle data '); await CirclesClient(profile).getCircles().match( onSuccess: (circles) { circlesRepo.addAllCircles(circles); notifyListeners(); }, onError: (error) { _logger.severe('Error getting list data: $error'); }, ); } FutureResult createNewStatus( String text, { String spoilerText = '', String inReplyToId = '', required NewEntryMediaItems newMediaItems, required List existingMediaItems, required Visibility visibility, }) async { final result = await entryManagerService.createNewStatus( text, spoilerText: spoilerText, inReplyToId: inReplyToId, mediaItems: newMediaItems, existingMediaItems: existingMediaItems, visibility: visibility, ); if (result.isSuccess) { _logger.finest('Notifying listeners of new status created'); notifyListeners(); } return result; } FutureResult editStatus( String id, String text, { String spoilerText = '', String inReplyToId = '', required NewEntryMediaItems newMediaItems, required List existingMediaItems, required Visibility newMediaItemVisibility, }) async { final result = await entryManagerService.editStatus(id, text, spoilerText: spoilerText, mediaItems: newMediaItems, existingMediaItems: existingMediaItems, newMediaItemVisibility: newMediaItemVisibility); if (result.isSuccess) { _logger.finest('Notifying listeners of updated status'); notifyListeners(); } return result; } Result getEntryById(String id) { _logger.finest('Getting entry for $id'); return entryManagerService.getEntryById(id); } FutureResult deleteEntryById(String id) async { _logger.finest('Delete entry for $id'); final result = await entryManagerService.deleteEntryById(id); if (result.isSuccess) { for (final t in cachedTimelines.values) { t.removeTimelineEntry(id); } } notifyListeners(); return result; } Result getPostTreeEntryBy(String id) { _logger.finest('Getting post for $id'); return entryManagerService.getPostTreeEntryBy(id); } // refresh timeline gets statuses newer than the newest in that timeline List getTimeline(TimelineIdentifiers type) { _logger.finest('Getting timeline $type'); return cachedTimelines.putIfAbsent(type, () => Timeline(type)).posts; } /// /// id is the id of a post or comment in the chain, including the original post. FutureResult refreshStatusChain(String id) async { _logger.finest('Refreshing post $id'); final result = await entryManagerService.refreshStatusChain(id); if (result.isSuccess) { for (final t in cachedTimelines.values) { t.addOrUpdate([result.value]); } notifyListeners(); } return result; } FutureResult resharePost(String id) async { final result = await entryManagerService.resharePost(id); final empty = EntryTreeItem.empty(); if (result.isSuccess) { for (final t in cachedTimelines.values) { if (t.posts.firstWhere((p) => p.id == id, orElse: () => empty) != empty) { t.addOrUpdate([result.value]); } } notifyListeners(); } return result; } FutureResult unResharePost(String id) async { final result = await entryManagerService.unResharePost(id); final empty = EntryTreeItem.empty(); if (result.isSuccess) { for (final t in cachedTimelines.values) { if (t.posts.firstWhere((p) => p.id == id, orElse: () => empty) != empty) { t.addOrUpdate([result.value]); } } notifyListeners(); } return result; } Future updateTimeline( TimelineIdentifiers type, TimelineRefreshType refreshType, ) async { _logger.finest('Updating w/$refreshType for timeline $type '); final timeline = cachedTimelines.putIfAbsent(type, () => Timeline(type)); late final int lowestId; late final int highestId; switch (refreshType) { case TimelineRefreshType.refresh: timeline.clear(); lowestId = 0; highestId = 0; break; case TimelineRefreshType.loadOlder: lowestId = timeline.lowestStatusId; highestId = 0; break; case TimelineRefreshType.loadNewer: lowestId = 0; highestId = timeline.highestStatusId; break; } (await entryManagerService.updateTimeline(type, lowestId, highestId)).match( onSuccess: (posts) { _logger.finest('Posts returned for adding to $type: ${posts.length}'); timeline.addOrUpdate(posts); notifyListeners(); }, onError: (error) { _logger.severe('Error getting timeline: $type}'); }); } FutureResult toggleFavorited( String id, bool newStatus) async { _logger.finer('Attempting toggling favorite $id to $newStatus'); final result = await entryManagerService.toggleFavorited(id, newStatus); if (result.isFailure) { _logger.info('Error toggling favorite $id: ${result.error}'); return result; } final update = result.value; for (final timeline in cachedTimelines.values) { update.entry.parentId.isEmpty ? timeline.addOrUpdate([update]) : timeline.tryUpdateComment(update); } notifyListeners(); return result; } // Should put backing store on timelines and entity manager so can recover from restart faster // Have a purge caches button to start that over from scratch // Should have a contacts manager with backing store as well // If our own has delete }