From d63a1bcd07f70909c13b8a2256c6754eea23dd75 Mon Sep 17 00:00:00 2001 From: idubnori Date: Thu, 18 Dec 2025 01:31:29 +0900 Subject: [PATCH] feat(mobile): add the number of activities to the activity button label --- i18n/en.json | 1 + i18n/ja.json | 1 + mobile/lib/models/activities/activity.model.dart | 5 ++++- .../open_activity_action_button.widget.dart | 10 +++++++++- mobile/lib/providers/activity.provider.dart | 14 +++++++++----- mobile/lib/providers/activity.provider.g.dart | 2 +- .../providers/activity_statistics.provider.dart | 4 ++-- .../activity_statistics.provider.g.dart | 14 +++++++------- .../repositories/activity_api.repository.dart | 2 +- mobile/lib/services/activity.service.dart | 2 +- .../modules/activity/activity_provider_test.dart | 16 +++++++++------- .../activity_statistics_provider_test.dart | 8 +++++--- 12 files changed, 50 insertions(+), 29 deletions(-) diff --git a/i18n/en.json b/i18n/en.json index 5903d7850e..b8e5aac7fa 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -10,6 +10,7 @@ "active_count": "Active: {count}", "activity": "Activity", "activity_changed": "Activity is {enabled, select, true {enabled} other {disabled}}", + "activity_count": "{count, plural, =0 {Activity} =1 {1 activity} other {{count} activities}}", "add": "Add", "add_a_description": "Add a description", "add_a_location": "Add a location", diff --git a/i18n/ja.json b/i18n/ja.json index 1fb9fbbd21..c8efe85025 100644 --- a/i18n/ja.json +++ b/i18n/ja.json @@ -9,6 +9,7 @@ "active": "アクティブ", "activity": "アクティビティ", "activity_changed": "アクティビティは{enabled, select, true {有効} other {無効}}になりました", + "activity_count": "{count, plural, =0 {コメント} other {コメント\n{count}件}}", "add": "追加", "add_a_description": "説明を追加", "add_a_location": "場所を追加", diff --git a/mobile/lib/models/activities/activity.model.dart b/mobile/lib/models/activities/activity.model.dart index 38c2bef77a..3f9adc132a 100644 --- a/mobile/lib/models/activities/activity.model.dart +++ b/mobile/lib/models/activities/activity.model.dart @@ -62,6 +62,9 @@ class Activity { class ActivityStats { final int comments; + final int likes; - const ActivityStats({required this.comments}); + const ActivityStats({required this.comments, required this.likes}); + + int get total => comments + likes; } diff --git a/mobile/lib/presentation/widgets/action_buttons/open_activity_action_button.widget.dart b/mobile/lib/presentation/widgets/action_buttons/open_activity_action_button.widget.dart index ac93b3f3a7..ae4f8cfd5f 100644 --- a/mobile/lib/presentation/widgets/action_buttons/open_activity_action_button.widget.dart +++ b/mobile/lib/presentation/widgets/action_buttons/open_activity_action_button.widget.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/events.model.dart'; import 'package:immich_mobile/domain/utils/event_stream.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; import 'package:immich_mobile/presentation/widgets/action_buttons/base_action_button.widget.dart'; +import 'package:immich_mobile/providers/activity_statistics.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/infrastructure/current_album.provider.dart'; class OpenActivityActionButton extends ConsumerWidget { const OpenActivityActionButton({super.key, this.iconOnly = false, this.menuItem = false}); @@ -13,9 +17,13 @@ class OpenActivityActionButton extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final album = ref.watch(currentRemoteAlbumProvider); + final asset = ref.watch(currentAssetNotifier) as RemoteAsset?; + final count = album != null && album.id.isNotEmpty ? ref.watch(activityStatisticsProvider(album.id, asset?.id)) : 0; + return BaseActionButton( iconData: Icons.chat_outlined, - label: "activity".t(context: context), + label: "activity_count".t(args: {"count": count}), onPressed: () => EventStream.shared.emit(const ViewerOpenBottomSheetEvent(activitiesMode: true)), iconOnly: iconOnly, menuItem: menuItem, diff --git a/mobile/lib/providers/activity.provider.dart b/mobile/lib/providers/activity.provider.dart index 5e0e71d85d..8fbf270957 100644 --- a/mobile/lib/providers/activity.provider.dart +++ b/mobile/lib/providers/activity.provider.dart @@ -26,11 +26,10 @@ class AlbumActivity extends _$AlbumActivity { ref.read(albumActivityProvider(albumId).notifier)._removeFromState(id); } - if (removedActivity.type == ActivityType.comment) { - ref.watch(activityStatisticsProvider(albumId, assetId).notifier).removeActivity(); - if (assetId != null) { - ref.watch(activityStatisticsProvider(albumId).notifier).removeActivity(); - } + // Update statistics for both comments and likes + ref.watch(activityStatisticsProvider(albumId, assetId).notifier).removeActivity(); + if (assetId != null) { + ref.watch(activityStatisticsProvider(albumId).notifier).removeActivity(); } } } @@ -42,6 +41,11 @@ class AlbumActivity extends _$AlbumActivity { if (assetId != null) { ref.read(albumActivityProvider(albumId).notifier)._addToState(activity.requireValue); } + // Update statistics when adding a like + ref.watch(activityStatisticsProvider(albumId, assetId).notifier).addActivity(); + if (assetId != null) { + ref.watch(activityStatisticsProvider(albumId).notifier).addActivity(); + } } } diff --git a/mobile/lib/providers/activity.provider.g.dart b/mobile/lib/providers/activity.provider.g.dart index 6ca99e4f72..59d036a160 100644 --- a/mobile/lib/providers/activity.provider.g.dart +++ b/mobile/lib/providers/activity.provider.g.dart @@ -6,7 +6,7 @@ part of 'activity.provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$albumActivityHash() => r'154e8ae98da3efc142369eae46d4005468fd67da'; +String _$albumActivityHash() => r'fe472b87b56ce4e3802833799bf707779ccc02e3'; /// Copied from Dart SDK class _SystemHash { diff --git a/mobile/lib/providers/activity_statistics.provider.dart b/mobile/lib/providers/activity_statistics.provider.dart index 96d2633d1b..3addaa46bf 100644 --- a/mobile/lib/providers/activity_statistics.provider.dart +++ b/mobile/lib/providers/activity_statistics.provider.dart @@ -4,12 +4,12 @@ import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'activity_statistics.provider.g.dart'; // ignore: unintended_html_in_doc_comment -/// Maintains the current number of comments by +/// Maintains the current number of activities (comments + likes) by @riverpod class ActivityStatistics extends _$ActivityStatistics { @override int build(String albumId, [String? assetId]) { - ref.watch(activityServiceProvider).getStatistics(albumId, assetId: assetId).then((stats) => state = stats.comments); + ref.watch(activityServiceProvider).getStatistics(albumId, assetId: assetId).then((stats) => state = stats.total); return 0; } diff --git a/mobile/lib/providers/activity_statistics.provider.g.dart b/mobile/lib/providers/activity_statistics.provider.g.dart index 83d887f6dc..fa47580d4c 100644 --- a/mobile/lib/providers/activity_statistics.provider.g.dart +++ b/mobile/lib/providers/activity_statistics.provider.g.dart @@ -7,7 +7,7 @@ part of 'activity_statistics.provider.dart'; // ************************************************************************** String _$activityStatisticsHash() => - r'1f43f0bcb11c754ca3cb586a13570db25023b9a8'; + r'ae0123a6ce129988e21f7327273af4d9acbae0f6'; /// Copied from Dart SDK class _SystemHash { @@ -37,22 +37,22 @@ abstract class _$ActivityStatistics extends BuildlessAutoDisposeNotifier { int build(String albumId, [String? assetId]); } -/// Maintains the current number of comments by +/// Maintains the current number of activities (comments + likes) by /// /// Copied from [ActivityStatistics]. @ProviderFor(ActivityStatistics) const activityStatisticsProvider = ActivityStatisticsFamily(); -/// Maintains the current number of comments by +/// Maintains the current number of activities (comments + likes) by /// /// Copied from [ActivityStatistics]. class ActivityStatisticsFamily extends Family { - /// Maintains the current number of comments by + /// Maintains the current number of activities (comments + likes) by /// /// Copied from [ActivityStatistics]. const ActivityStatisticsFamily(); - /// Maintains the current number of comments by + /// Maintains the current number of activities (comments + likes) by /// /// Copied from [ActivityStatistics]. ActivityStatisticsProvider call(String albumId, [String? assetId]) { @@ -81,12 +81,12 @@ class ActivityStatisticsFamily extends Family { String? get name => r'activityStatisticsProvider'; } -/// Maintains the current number of comments by +/// Maintains the current number of activities (comments + likes) by /// /// Copied from [ActivityStatistics]. class ActivityStatisticsProvider extends AutoDisposeNotifierProviderImpl { - /// Maintains the current number of comments by + /// Maintains the current number of activities (comments + likes) by /// /// Copied from [ActivityStatistics]. ActivityStatisticsProvider(String albumId, [String? assetId]) diff --git a/mobile/lib/repositories/activity_api.repository.dart b/mobile/lib/repositories/activity_api.repository.dart index e8f9abc8c8..a5285217d9 100644 --- a/mobile/lib/repositories/activity_api.repository.dart +++ b/mobile/lib/repositories/activity_api.repository.dart @@ -36,7 +36,7 @@ class ActivityApiRepository extends ApiRepository { Future getStats(String albumId, {String? assetId}) async { final response = await checkNull(_api.getActivityStatistics(albumId, assetId: assetId)); - return ActivityStats(comments: response.comments); + return ActivityStats(comments: response.comments, likes: response.likes); } static Activity _toActivity(ActivityResponseDto dto) => Activity( diff --git a/mobile/lib/services/activity.service.dart b/mobile/lib/services/activity.service.dart index 382a7fe107..9fc3824fef 100644 --- a/mobile/lib/services/activity.service.dart +++ b/mobile/lib/services/activity.service.dart @@ -32,7 +32,7 @@ class ActivityService with ErrorLoggerMixin { Future getStatistics(String albumId, {String? assetId}) async { return logError( () => _activityApiRepository.getStats(albumId, assetId: assetId), - defaultValue: const ActivityStats(comments: 0), + defaultValue: const ActivityStats(comments: 0, likes: 0), errorMessage: "Failed to statistics for album $albumId", ); } diff --git a/mobile/test/modules/activity/activity_provider_test.dart b/mobile/test/modules/activity/activity_provider_test.dart index 84eba62b70..d1a08fbb3d 100644 --- a/mobile/test/modules/activity/activity_provider_test.dart +++ b/mobile/test/modules/activity/activity_provider_test.dart @@ -108,9 +108,9 @@ void main() { expect(activities, hasLength(5)); expect(activities, contains(like)); - // Never bump activity count for new likes - verifyNever(() => activityStatisticsMock.addActivity()); - verifyNever(() => albumActivityStatisticsMock.addActivity()); + // Verify activity count is bumped for new likes + verify(() => activityStatisticsMock.addActivity()).called(1); + verify(() => albumActivityStatisticsMock.addActivity()).called(1); final albumActivities = container.read(albumProvider).requireValue; expect(albumActivities, hasLength(5)); @@ -155,8 +155,9 @@ void main() { expect(activities, hasLength(3)); expect(activities, isNot(anyElement(predicate((Activity a) => a.id == '3')))); - verifyNever(() => activityStatisticsMock.removeActivity()); - verifyNever(() => albumActivityStatisticsMock.removeActivity()); + // Verify activity count is decreased for removed likes + verify(() => activityStatisticsMock.removeActivity()).called(1); + verify(() => albumActivityStatisticsMock.removeActivity()).called(1); }); test('Remove Like failed', () async { @@ -204,8 +205,9 @@ void main() { expect(albumActivities, isNot(anyElement(predicate((Activity a) => a.id == '3')))); verify(() => activityMock.removeActivity('3')); - verifyNever(() => activityStatisticsMock.removeActivity()); - verifyNever(() => albumActivityStatisticsMock.removeActivity()); + // Verify activity count is decreased when removing from asset-scoped provider + verify(() => activityStatisticsMock.removeActivity()).called(1); + verify(() => albumActivityStatisticsMock.removeActivity()).called(1); }); }); diff --git a/mobile/test/modules/activity/activity_statistics_provider_test.dart b/mobile/test/modules/activity/activity_statistics_provider_test.dart index 7fe73868f5..9d90c86705 100644 --- a/mobile/test/modules/activity/activity_statistics_provider_test.dart +++ b/mobile/test/modules/activity/activity_statistics_provider_test.dart @@ -22,7 +22,7 @@ void main() { test('Returns the proper count family', () async { when( () => activityMock.getStatistics('test-album', assetId: 'test-asset'), - ).thenAnswer((_) async => const ActivityStats(comments: 5)); + ).thenAnswer((_) async => const ActivityStats(comments: 5, likes: 0)); // Read here to make the getStatistics call container.read(activityStatisticsProvider('test-album', 'test-asset')); @@ -38,7 +38,9 @@ void main() { }); test('Adds activity', () async { - when(() => activityMock.getStatistics('test-album')).thenAnswer((_) async => const ActivityStats(comments: 10)); + when( + () => activityMock.getStatistics('test-album'), + ).thenAnswer((_) async => const ActivityStats(comments: 10, likes: 0)); final provider = activityStatisticsProvider('test-album'); container.listen(provider, listener.call, fireImmediately: true); @@ -55,7 +57,7 @@ void main() { test('Removes activity', () async { when( () => activityMock.getStatistics('new-album', assetId: 'test-asset'), - ).thenAnswer((_) async => const ActivityStats(comments: 10)); + ).thenAnswer((_) async => const ActivityStats(comments: 10, likes: 0)); final provider = activityStatisticsProvider('new-album', 'test-asset'); container.listen(provider, listener.call, fireImmediately: true);