Files
Immich/mobile/lib/data/store/activity.dart
T
Adam GastineauandGitHub 6874c07dbe chore(mobile): static Store to unify and streamline reactivity (#31099)
* chore(mobile): static Store to unify and streamline reactivity

* Add tests of caching and new stores

* Don't instantiate PeopleDbRepository

* Slight DB rename and fix throwing store methods
2026-09-11 10:56:00 -07:00

91 lines
3.5 KiB
Dart

import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/data/server/activity.dart';
import 'package:immich_mobile/data/server/errors.dart';
import 'package:immich_mobile/data/store/util/cache.dart';
import 'package:immich_mobile/models/activities/activity.model.dart';
import 'package:logging/logging.dart';
final _log = Logger("ActivityStore");
/// The `albumId` and optional `assetId` identifying an activity list
typedef ActivityScope = (String albumId, String? assetId);
/// Activities (comments and likes) on shared albums and their assets
///
/// State is fetched over HTTP and mutated and cached in memory
extension type const ActivityStore._(Provider<ActivityMutations> _provider) implements Provider<ActivityMutations> {
/// Internal: access through `Store.activity`
static final ActivityStore instance = ActivityStore._(Provider((ref) => ActivityMutations._(ref)));
/// All activities for an album specified by [albumId], or all activities for a specific asset within that album
///
/// **NOTE:** This is currently reactive only to in memory mutation, not live HTTP changes
CacheListQuery<Activity, ActivityScope> list(String albumId, {String? assetId}) => _cache.family((albumId, assetId));
}
final _cache = _ActivityCache();
class _ActivityCache extends StoreCache<Activity, ActivityScope> {
@override
Future<List<Activity>> fetch(Ref ref, ActivityScope scope) async {
try {
return await ref.watch(activityApiRepositoryProvider).getAll(scope.$1, assetId: scope.$2);
} catch (error, stack) {
_log.severe("Failed to get all activities for album ${scope.$1}", error, stack);
return const [];
}
}
@override
Object identity(Activity item) => item.id;
@override
bool shouldContain(ActivityScope scope, Activity item) =>
scope.$1 == item.albumId && (scope.$2 == null || scope.$2 == item.assetId);
}
class ActivityMutations extends CachedStoreMutations<Activity, ActivityScope> {
ActivityMutations._(Ref ref) : super(ref, _cache);
/// Add a comment to an album or asset. Providing [assetId] will add to the corresponding asset, otherwise the comment will be added to the album
Future<Activity> addComment(String albumId, String comment, {String? assetId}) async {
try {
final activity = await read(
activityApiRepositoryProvider,
).create(albumId, ActivityType.comment, assetId: assetId, comment: comment);
cacheUpsert(activity);
return activity;
} catch (error, stack) {
_log.severe("Failed to create comment for album $albumId", error, stack);
rethrow;
}
}
/// Add a like to an album or asset. Providing [assetId] will add to the corresponding asset, otherwise the like will be added to the album
Future<Activity> addLike(String albumId, {String? assetId}) async {
try {
final activity = await read(activityApiRepositoryProvider).create(albumId, ActivityType.like, assetId: assetId);
cacheUpsert(activity);
return activity;
} catch (error, stack) {
_log.severe("Failed to create like for album $albumId", error, stack);
rethrow;
}
}
/// Remove [activity] from its album
Future<void> remove(Activity activity) async {
try {
await read(activityApiRepositoryProvider).delete(activity.id);
} on NoResponseDtoError {
// TODO(agg23): This error should not be thrown at all
} catch (error, stack) {
_log.severe("Failed to delete activity", error, stack);
rethrow;
}
// Only apply on success (including the broken NoResponseDtoError)
cacheRemove(activity.id);
}
}