relatica/lib/riverpod_controllers/timeline_services.dart

231 wiersze
6.7 KiB
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 '../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<TimelineIdentifiers> 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<void> 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<void> 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 = <MediaAttachment>[];
@override
Future<Result<List<MediaAttachment>, ExecError>> build(
Profile profile, String accountId) async {
await updateTimeline(reset: true, withNotification: false);
return Result.ok(media);
}
Future<Result<List<MediaAttachment>, 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 = <TimelineEntry>[];
@override
Future<Result<List<TimelineEntry>, ExecError>> build(
Profile profile, String accountId) async {
await updateTimeline(reset: true, withNotification: false);
return Result.ok(entries);
}
Future<Result<List<TimelineEntry>, 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();
}
}