refactor(mobile): Activities (#5990)

* refactor: autoroutex pushroute

* refactor: autoroutex popRoute

* refactor: autoroutex navigate and replace

* chore: add doc comments for extension methods

* refactor: Add LoggerMixin and refactor Album activities to use mixin

* refactor: Activity page

* chore: activity user from user constructor

* fix: update current asset after build method

* refactor: tests with similar structure as lib

* chore: remove avoid-declaring-call-method rule from dcm analysis

* test: fix proper expect order

* test: activity_statistics_provider_test

* test: activity_provider_test

* test: use proper matchers

* test: activity_text_field_test & dismissible_activity_test added

* test: add http mock to return transparent image

* test: download isar core libs during test

* test: add widget tags to widget test cases

* test: activity_tile_test

* build: currentAlbumProvider to generator

* movie add / remove like to activity input tile

* test: activities_page_test.dart

* chore: better error logs

* chore: dismissibleactivity as statelesswidget

---------

Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
This commit is contained in:
shenlong
2024-01-05 05:20:55 +00:00
committed by GitHub
parent d1e16025cf
commit af32183728
108 changed files with 2847 additions and 826 deletions
@@ -1,134 +1,67 @@
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/modules/activities/models/activity.model.dart';
import 'package:immich_mobile/modules/activities/services/activity.service.dart';
import 'package:immich_mobile/modules/activities/providers/activity_service.provider.dart';
import 'package:immich_mobile/modules/activities/providers/activity_statistics.provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
class ActivityNotifier extends StateNotifier<AsyncValue<List<Activity>>> {
final Ref _ref;
final ActivityService _activityService;
final String albumId;
final String? assetId;
part 'activity.provider.g.dart';
ActivityNotifier(
this._ref,
this._activityService,
this.albumId,
this.assetId,
) : super(
const AsyncData([]),
) {
fetchActivity();
}
Future<void> fetchActivity() async {
state = const AsyncLoading();
state = await AsyncValue.guard(
() => _activityService.getAllActivities(albumId, assetId),
);
/// Maintains the current list of all activities for <share-album-id, asset>
@riverpod
class AlbumActivity extends _$AlbumActivity {
@override
Future<List<Activity>> build(String albumId, [String? assetId]) async {
return ref
.watch(activityServiceProvider)
.getAllActivities(albumId, assetId: assetId);
}
Future<void> removeActivity(String id) async {
final activities = state.asData?.value ?? [];
if (await _activityService.removeActivity(id)) {
if (await ref.watch(activityServiceProvider).removeActivity(id)) {
final activities = state.valueOrNull ?? [];
final removedActivity = activities.firstWhere((a) => a.id == id);
activities.remove(removedActivity);
state = AsyncData(activities);
// Decrement activity count only for comments
if (removedActivity.type == ActivityType.comment) {
_ref
.read(
activityStatisticsStateProvider(
(albumId: albumId, assetId: assetId),
).notifier,
)
ref
.watch(activityStatisticsProvider(albumId, assetId).notifier)
.removeActivity();
}
}
}
Future<void> addComment(String comment) async {
final activity = await _activityService.addActivity(
albumId,
ActivityType.comment,
assetId: assetId,
comment: comment,
);
if (activity != null) {
Future<void> addLike() async {
final activity = await ref
.watch(activityServiceProvider)
.addActivity(albumId, ActivityType.like, assetId: assetId);
if (activity.hasValue) {
final activities = state.asData?.value ?? [];
state = AsyncData([...activities, activity]);
_ref
.read(
activityStatisticsStateProvider(
(albumId: albumId, assetId: assetId),
).notifier,
)
state = AsyncData([...activities, activity.requireValue]);
}
}
Future<void> addComment(String comment) async {
final activity = await ref.watch(activityServiceProvider).addActivity(
albumId,
ActivityType.comment,
assetId: assetId,
comment: comment,
);
if (activity.hasValue) {
final activities = state.valueOrNull ?? [];
state = AsyncData([...activities, activity.requireValue]);
ref
.watch(activityStatisticsProvider(albumId, assetId).notifier)
.addActivity();
// The previous addActivity call would increase the count of an asset if assetId != null
// To also increase the activity count of the album, calling it once again with assetId set to null
if (assetId != null) {
// Add a count to the current album's provider as well
_ref
.read(
activityStatisticsStateProvider(
(albumId: albumId, assetId: null),
).notifier,
)
.addActivity();
ref.watch(activityStatisticsProvider(albumId).notifier).addActivity();
}
}
}
Future<void> addLike() async {
final activity = await _activityService
.addActivity(albumId, ActivityType.like, assetId: assetId);
if (activity != null) {
final activities = state.asData?.value ?? [];
state = AsyncData([...activities, activity]);
}
}
}
class ActivityStatisticsNotifier extends StateNotifier<int> {
final String albumId;
final String? assetId;
final ActivityService _activityService;
ActivityStatisticsNotifier(this._activityService, this.albumId, this.assetId)
: super(0) {
fetchStatistics();
}
Future<void> fetchStatistics() async {
final count =
await _activityService.getStatistics(albumId, assetId: assetId);
if (mounted) {
state = count;
}
}
Future<void> addActivity() async {
state = state + 1;
}
Future<void> removeActivity() async {
state = state - 1;
}
}
typedef ActivityParams = ({String albumId, String? assetId});
final activityStateProvider = StateNotifierProvider.autoDispose
.family<ActivityNotifier, AsyncValue<List<Activity>>, ActivityParams>(
(ref, args) {
return ActivityNotifier(
ref,
ref.watch(activityServiceProvider),
args.albumId,
args.assetId,
);
});
final activityStatisticsStateProvider = StateNotifierProvider.autoDispose
.family<ActivityStatisticsNotifier, int, ActivityParams>((ref, args) {
return ActivityStatisticsNotifier(
ref.watch(activityServiceProvider),
args.albumId,
args.assetId,
);
});
/// Mock class for testing
abstract class AlbumActivityInternal extends _$AlbumActivity {}
@@ -0,0 +1,209 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'activity.provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$albumActivityHash() => r'3b0d7acee4d41c84b3f220784c3b904c83f836e6';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
abstract class _$AlbumActivity
extends BuildlessAutoDisposeAsyncNotifier<List<Activity>> {
late final String albumId;
late final String? assetId;
Future<List<Activity>> build(
String albumId, [
String? assetId,
]);
}
/// Maintains the current list of all activities for <share-album-id, asset>
///
/// Copied from [AlbumActivity].
@ProviderFor(AlbumActivity)
const albumActivityProvider = AlbumActivityFamily();
/// Maintains the current list of all activities for <share-album-id, asset>
///
/// Copied from [AlbumActivity].
class AlbumActivityFamily extends Family<AsyncValue<List<Activity>>> {
/// Maintains the current list of all activities for <share-album-id, asset>
///
/// Copied from [AlbumActivity].
const AlbumActivityFamily();
/// Maintains the current list of all activities for <share-album-id, asset>
///
/// Copied from [AlbumActivity].
AlbumActivityProvider call(
String albumId, [
String? assetId,
]) {
return AlbumActivityProvider(
albumId,
assetId,
);
}
@override
AlbumActivityProvider getProviderOverride(
covariant AlbumActivityProvider provider,
) {
return call(
provider.albumId,
provider.assetId,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'albumActivityProvider';
}
/// Maintains the current list of all activities for <share-album-id, asset>
///
/// Copied from [AlbumActivity].
class AlbumActivityProvider extends AutoDisposeAsyncNotifierProviderImpl<
AlbumActivity, List<Activity>> {
/// Maintains the current list of all activities for <share-album-id, asset>
///
/// Copied from [AlbumActivity].
AlbumActivityProvider(
String albumId, [
String? assetId,
]) : this._internal(
() => AlbumActivity()
..albumId = albumId
..assetId = assetId,
from: albumActivityProvider,
name: r'albumActivityProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$albumActivityHash,
dependencies: AlbumActivityFamily._dependencies,
allTransitiveDependencies:
AlbumActivityFamily._allTransitiveDependencies,
albumId: albumId,
assetId: assetId,
);
AlbumActivityProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.albumId,
required this.assetId,
}) : super.internal();
final String albumId;
final String? assetId;
@override
Future<List<Activity>> runNotifierBuild(
covariant AlbumActivity notifier,
) {
return notifier.build(
albumId,
assetId,
);
}
@override
Override overrideWith(AlbumActivity Function() create) {
return ProviderOverride(
origin: this,
override: AlbumActivityProvider._internal(
() => create()
..albumId = albumId
..assetId = assetId,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
albumId: albumId,
assetId: assetId,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<AlbumActivity, List<Activity>>
createElement() {
return _AlbumActivityProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is AlbumActivityProvider &&
other.albumId == albumId &&
other.assetId == assetId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, albumId.hashCode);
hash = _SystemHash.combine(hash, assetId.hashCode);
return _SystemHash.finish(hash);
}
}
mixin AlbumActivityRef on AutoDisposeAsyncNotifierProviderRef<List<Activity>> {
/// The parameter `albumId` of this provider.
String get albumId;
/// The parameter `assetId` of this provider.
String? get assetId;
}
class _AlbumActivityProviderElement
extends AutoDisposeAsyncNotifierProviderElement<AlbumActivity,
List<Activity>> with AlbumActivityRef {
_AlbumActivityProviderElement(super.provider);
@override
String get albumId => (origin as AlbumActivityProvider).albumId;
@override
String? get assetId => (origin as AlbumActivityProvider).assetId;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -0,0 +1,9 @@
import 'package:immich_mobile/modules/activities/services/activity.service.dart';
import 'package:immich_mobile/shared/providers/api.provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'activity_service.provider.g.dart';
@riverpod
ActivityService activityService(ActivityServiceRef ref) =>
ActivityService(ref.watch(apiServiceProvider));
@@ -0,0 +1,25 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'activity_service.provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$activityServiceHash() => r'5dd4955d14f5bf01c00d7f8750d07e7ace7cc4b0';
/// See also [activityService].
@ProviderFor(activityService)
final activityServiceProvider = AutoDisposeProvider<ActivityService>.internal(
activityService,
name: r'activityServiceProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$activityServiceHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef ActivityServiceRef = AutoDisposeProviderRef<ActivityService>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
@@ -0,0 +1,24 @@
import 'package:immich_mobile/modules/activities/providers/activity_service.provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'activity_statistics.provider.g.dart';
/// Maintains the current number of comments by <shared-album, asset>
@riverpod
class ActivityStatistics extends _$ActivityStatistics {
@override
int build(String albumId, [String? assetId]) {
ref
.watch(activityServiceProvider)
.getStatistics(albumId, assetId: assetId)
.then((comments) => state = comments);
return 0;
}
void addActivity() => state = state + 1;
void removeActivity() => state = state - 1;
}
/// Mock class for testing
abstract class ActivityStatisticsInternal extends _$ActivityStatistics {}
@@ -0,0 +1,208 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'activity_statistics.provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$activityStatisticsHash() =>
r'a5f7bbee1891c33b72919a34e632ca9ef9cd8dbf';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
abstract class _$ActivityStatistics extends BuildlessAutoDisposeNotifier<int> {
late final String albumId;
late final String? assetId;
int build(
String albumId, [
String? assetId,
]);
}
/// Maintains the current number of comments by <shared-album, asset>
///
/// Copied from [ActivityStatistics].
@ProviderFor(ActivityStatistics)
const activityStatisticsProvider = ActivityStatisticsFamily();
/// Maintains the current number of comments by <shared-album, asset>
///
/// Copied from [ActivityStatistics].
class ActivityStatisticsFamily extends Family<int> {
/// Maintains the current number of comments by <shared-album, asset>
///
/// Copied from [ActivityStatistics].
const ActivityStatisticsFamily();
/// Maintains the current number of comments by <shared-album, asset>
///
/// Copied from [ActivityStatistics].
ActivityStatisticsProvider call(
String albumId, [
String? assetId,
]) {
return ActivityStatisticsProvider(
albumId,
assetId,
);
}
@override
ActivityStatisticsProvider getProviderOverride(
covariant ActivityStatisticsProvider provider,
) {
return call(
provider.albumId,
provider.assetId,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'activityStatisticsProvider';
}
/// Maintains the current number of comments by <shared-album, asset>
///
/// Copied from [ActivityStatistics].
class ActivityStatisticsProvider
extends AutoDisposeNotifierProviderImpl<ActivityStatistics, int> {
/// Maintains the current number of comments by <shared-album, asset>
///
/// Copied from [ActivityStatistics].
ActivityStatisticsProvider(
String albumId, [
String? assetId,
]) : this._internal(
() => ActivityStatistics()
..albumId = albumId
..assetId = assetId,
from: activityStatisticsProvider,
name: r'activityStatisticsProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$activityStatisticsHash,
dependencies: ActivityStatisticsFamily._dependencies,
allTransitiveDependencies:
ActivityStatisticsFamily._allTransitiveDependencies,
albumId: albumId,
assetId: assetId,
);
ActivityStatisticsProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.albumId,
required this.assetId,
}) : super.internal();
final String albumId;
final String? assetId;
@override
int runNotifierBuild(
covariant ActivityStatistics notifier,
) {
return notifier.build(
albumId,
assetId,
);
}
@override
Override overrideWith(ActivityStatistics Function() create) {
return ProviderOverride(
origin: this,
override: ActivityStatisticsProvider._internal(
() => create()
..albumId = albumId
..assetId = assetId,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
albumId: albumId,
assetId: assetId,
),
);
}
@override
AutoDisposeNotifierProviderElement<ActivityStatistics, int> createElement() {
return _ActivityStatisticsProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is ActivityStatisticsProvider &&
other.albumId == albumId &&
other.assetId == assetId;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, albumId.hashCode);
hash = _SystemHash.combine(hash, assetId.hashCode);
return _SystemHash.finish(hash);
}
}
mixin ActivityStatisticsRef on AutoDisposeNotifierProviderRef<int> {
/// The parameter `albumId` of this provider.
String get albumId;
/// The parameter `assetId` of this provider.
String? get assetId;
}
class _ActivityStatisticsProviderElement
extends AutoDisposeNotifierProviderElement<ActivityStatistics, int>
with ActivityStatisticsRef {
_ActivityStatisticsProviderElement(super.provider);
@override
String get albumId => (origin as ActivityStatisticsProvider).albumId;
@override
String? get assetId => (origin as ActivityStatisticsProvider).assetId;
}
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member