mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	feat(mobile): shared album activities (#4833)
* fix(server): global activity like duplicate search * mobile: user_circle_avatar - fallback to text icon if no profile pic available * mobile: use favourite icon in search "your activity" * feat(mobile): shared album activities * mobile: align hearts with user profile icon * styling * replace bottom sheet with dismissible * add auto focus to the input --------- Co-authored-by: shalong-tanwen <139912620+shalong-tanwen@users.noreply.github.com> Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
		
							parent
							
								
									c74ea7282a
								
							
						
					
					
						commit
						26fd9d7e5f
					
				@ -373,5 +373,8 @@
 | 
				
			|||||||
  "viewer_stack_use_as_main_asset": "Use as Main Asset",
 | 
					  "viewer_stack_use_as_main_asset": "Use as Main Asset",
 | 
				
			||||||
  "app_bar_signout_dialog_title": "Sign out",
 | 
					  "app_bar_signout_dialog_title": "Sign out",
 | 
				
			||||||
  "app_bar_signout_dialog_content": "Are you sure you wanna sign out?",
 | 
					  "app_bar_signout_dialog_content": "Are you sure you wanna sign out?",
 | 
				
			||||||
  "app_bar_signout_dialog_ok": "Yes"
 | 
					  "app_bar_signout_dialog_ok": "Yes",
 | 
				
			||||||
 | 
					  "shared_album_activities_input_hint": "Say something",
 | 
				
			||||||
 | 
					  "shared_album_activity_remove_title": "Delete Activity",
 | 
				
			||||||
 | 
					  "shared_album_activity_remove_content": "Do you want to delete this activity?"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										90
									
								
								mobile/lib/modules/activities/models/activity.model.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								mobile/lib/modules/activities/models/activity.model.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,90 @@
 | 
				
			|||||||
 | 
					import 'package:immich_mobile/shared/models/user.dart';
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum ActivityType { comment, like }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class Activity {
 | 
				
			||||||
 | 
					  final String id;
 | 
				
			||||||
 | 
					  final String? assetId;
 | 
				
			||||||
 | 
					  final String? comment;
 | 
				
			||||||
 | 
					  final DateTime createdAt;
 | 
				
			||||||
 | 
					  final ActivityType type;
 | 
				
			||||||
 | 
					  final User user;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const Activity({
 | 
				
			||||||
 | 
					    required this.id,
 | 
				
			||||||
 | 
					    this.assetId,
 | 
				
			||||||
 | 
					    this.comment,
 | 
				
			||||||
 | 
					    required this.createdAt,
 | 
				
			||||||
 | 
					    required this.type,
 | 
				
			||||||
 | 
					    required this.user,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Activity copyWith({
 | 
				
			||||||
 | 
					    String? id,
 | 
				
			||||||
 | 
					    String? assetId,
 | 
				
			||||||
 | 
					    String? comment,
 | 
				
			||||||
 | 
					    DateTime? createdAt,
 | 
				
			||||||
 | 
					    ActivityType? type,
 | 
				
			||||||
 | 
					    User? user,
 | 
				
			||||||
 | 
					  }) {
 | 
				
			||||||
 | 
					    return Activity(
 | 
				
			||||||
 | 
					      id: id ?? this.id,
 | 
				
			||||||
 | 
					      assetId: assetId ?? this.assetId,
 | 
				
			||||||
 | 
					      comment: comment ?? this.comment,
 | 
				
			||||||
 | 
					      createdAt: createdAt ?? this.createdAt,
 | 
				
			||||||
 | 
					      type: type ?? this.type,
 | 
				
			||||||
 | 
					      user: user ?? this.user,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Activity.fromDto(ActivityResponseDto dto)
 | 
				
			||||||
 | 
					      : id = dto.id,
 | 
				
			||||||
 | 
					        assetId = dto.assetId,
 | 
				
			||||||
 | 
					        comment = dto.comment,
 | 
				
			||||||
 | 
					        createdAt = dto.createdAt,
 | 
				
			||||||
 | 
					        type = dto.type == ActivityResponseDtoTypeEnum.comment
 | 
				
			||||||
 | 
					            ? ActivityType.comment
 | 
				
			||||||
 | 
					            : ActivityType.like,
 | 
				
			||||||
 | 
					        user = User(
 | 
				
			||||||
 | 
					          email: dto.user.email,
 | 
				
			||||||
 | 
					          firstName: dto.user.firstName,
 | 
				
			||||||
 | 
					          lastName: dto.user.lastName,
 | 
				
			||||||
 | 
					          profileImagePath: dto.user.profileImagePath,
 | 
				
			||||||
 | 
					          id: dto.user.id,
 | 
				
			||||||
 | 
					          // Placeholder values
 | 
				
			||||||
 | 
					          isAdmin: false,
 | 
				
			||||||
 | 
					          updatedAt: DateTime.now(),
 | 
				
			||||||
 | 
					          isPartnerSharedBy: false,
 | 
				
			||||||
 | 
					          isPartnerSharedWith: false,
 | 
				
			||||||
 | 
					          memoryEnabled: false,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() {
 | 
				
			||||||
 | 
					    return 'Activity(id: $id, assetId: $assetId, comment: $comment, createdAt: $createdAt, type: $type, user: $user)';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  bool operator ==(Object other) {
 | 
				
			||||||
 | 
					    if (identical(this, other)) return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return other is Activity &&
 | 
				
			||||||
 | 
					        other.id == id &&
 | 
				
			||||||
 | 
					        other.assetId == assetId &&
 | 
				
			||||||
 | 
					        other.comment == comment &&
 | 
				
			||||||
 | 
					        other.createdAt == createdAt &&
 | 
				
			||||||
 | 
					        other.type == type &&
 | 
				
			||||||
 | 
					        other.user == user;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  int get hashCode {
 | 
				
			||||||
 | 
					    return id.hashCode ^
 | 
				
			||||||
 | 
					        assetId.hashCode ^
 | 
				
			||||||
 | 
					        comment.hashCode ^
 | 
				
			||||||
 | 
					        createdAt.hashCode ^
 | 
				
			||||||
 | 
					        type.hashCode ^
 | 
				
			||||||
 | 
					        user.hashCode;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										130
									
								
								mobile/lib/modules/activities/providers/activity.provider.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								mobile/lib/modules/activities/providers/activity.provider.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,130 @@
 | 
				
			|||||||
 | 
					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';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ActivityNotifier extends StateNotifier<AsyncValue<List<Activity>>> {
 | 
				
			||||||
 | 
					  final Ref _ref;
 | 
				
			||||||
 | 
					  final ActivityService _activityService;
 | 
				
			||||||
 | 
					  final String albumId;
 | 
				
			||||||
 | 
					  final String? assetId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> removeActivity(String id) async {
 | 
				
			||||||
 | 
					    final activities = state.asData?.value ?? [];
 | 
				
			||||||
 | 
					    if (await _activityService.removeActivity(id)) {
 | 
				
			||||||
 | 
					      final removedActivity = activities.firstWhere((a) => a.id == id);
 | 
				
			||||||
 | 
					      activities.remove(removedActivity);
 | 
				
			||||||
 | 
					      state = AsyncData(activities);
 | 
				
			||||||
 | 
					      if (removedActivity.type == ActivityType.comment) {
 | 
				
			||||||
 | 
					        _ref
 | 
				
			||||||
 | 
					            .read(
 | 
				
			||||||
 | 
					              activityStatisticsStateProvider(
 | 
				
			||||||
 | 
					                (albumId: albumId, assetId: assetId),
 | 
				
			||||||
 | 
					              ).notifier,
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            .removeActivity();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<void> addComment(String comment) async {
 | 
				
			||||||
 | 
					    final activity = await _activityService.addActivity(
 | 
				
			||||||
 | 
					      albumId,
 | 
				
			||||||
 | 
					      ActivityType.comment,
 | 
				
			||||||
 | 
					      assetId: assetId,
 | 
				
			||||||
 | 
					      comment: comment,
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (activity != null) {
 | 
				
			||||||
 | 
					      final activities = state.asData?.value ?? [];
 | 
				
			||||||
 | 
					      state = AsyncData([...activities, activity]);
 | 
				
			||||||
 | 
					      _ref
 | 
				
			||||||
 | 
					          .read(
 | 
				
			||||||
 | 
					            activityStatisticsStateProvider(
 | 
				
			||||||
 | 
					              (albumId: albumId, assetId: assetId),
 | 
				
			||||||
 | 
					            ).notifier,
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					          .addActivity();
 | 
				
			||||||
 | 
					      if (assetId != null) {
 | 
				
			||||||
 | 
					        // Add a count to the current album's provider as well
 | 
				
			||||||
 | 
					        _ref
 | 
				
			||||||
 | 
					            .read(
 | 
				
			||||||
 | 
					              activityStatisticsStateProvider(
 | 
				
			||||||
 | 
					                (albumId: albumId, assetId: null),
 | 
				
			||||||
 | 
					              ).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 {
 | 
				
			||||||
 | 
					    state = await _activityService.getStatistics(albumId, assetId: assetId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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,
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										85
									
								
								mobile/lib/modules/activities/services/activity.service.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								mobile/lib/modules/activities/services/activity.service.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,85 @@
 | 
				
			|||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/activities/models/activity.model.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/providers/api.provider.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/services/api.service.dart';
 | 
				
			||||||
 | 
					import 'package:logging/logging.dart';
 | 
				
			||||||
 | 
					import 'package:openapi/api.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					final activityServiceProvider =
 | 
				
			||||||
 | 
					    Provider((ref) => ActivityService(ref.watch(apiServiceProvider)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ActivityService {
 | 
				
			||||||
 | 
					  final ApiService _apiService;
 | 
				
			||||||
 | 
					  final Logger _log = Logger("ActivityService");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  ActivityService(this._apiService);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<List<Activity>> getAllActivities(
 | 
				
			||||||
 | 
					    String albumId,
 | 
				
			||||||
 | 
					    String? assetId,
 | 
				
			||||||
 | 
					  ) async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final list = await _apiService.activityApi
 | 
				
			||||||
 | 
					          .getActivities(albumId, assetId: assetId);
 | 
				
			||||||
 | 
					      return list != null ? list.map(Activity.fromDto).toList() : [];
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      _log.severe(
 | 
				
			||||||
 | 
					        "failed to fetch activities for albumId - $albumId; assetId - $assetId -> $e",
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      rethrow;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<int> getStatistics(String albumId, {String? assetId}) async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final dto = await _apiService.activityApi
 | 
				
			||||||
 | 
					          .getActivityStatistics(albumId, assetId: assetId);
 | 
				
			||||||
 | 
					      return dto?.comments ?? 0;
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      _log.severe(
 | 
				
			||||||
 | 
					        "failed to fetch activity statistics for albumId - $albumId; assetId - $assetId -> $e",
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return 0;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<bool> removeActivity(String id) async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      await _apiService.activityApi.deleteActivity(id);
 | 
				
			||||||
 | 
					      return true;
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      _log.severe(
 | 
				
			||||||
 | 
					        "failed to remove activity id - $id -> $e",
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return false;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Future<Activity?> addActivity(
 | 
				
			||||||
 | 
					    String albumId,
 | 
				
			||||||
 | 
					    ActivityType type, {
 | 
				
			||||||
 | 
					    String? assetId,
 | 
				
			||||||
 | 
					    String? comment,
 | 
				
			||||||
 | 
					  }) async {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      final dto = await _apiService.activityApi.createActivity(
 | 
				
			||||||
 | 
					        ActivityCreateDto(
 | 
				
			||||||
 | 
					          albumId: albumId,
 | 
				
			||||||
 | 
					          type: type == ActivityType.comment
 | 
				
			||||||
 | 
					              ? ReactionType.comment
 | 
				
			||||||
 | 
					              : ReactionType.like,
 | 
				
			||||||
 | 
					          assetId: assetId,
 | 
				
			||||||
 | 
					          comment: comment,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      if (dto != null) {
 | 
				
			||||||
 | 
					        return Activity.fromDto(dto);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch (e) {
 | 
				
			||||||
 | 
					      _log.severe(
 | 
				
			||||||
 | 
					        "failed to add activity for albumId - $albumId; assetId - $assetId -> $e",
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return null;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										312
									
								
								mobile/lib/modules/activities/views/activities_page.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										312
									
								
								mobile/lib/modules/activities/views/activities_page.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,312 @@
 | 
				
			|||||||
 | 
					import 'package:cached_network_image/cached_network_image.dart';
 | 
				
			||||||
 | 
					import 'package:collection/collection.dart';
 | 
				
			||||||
 | 
					import 'package:easy_localization/easy_localization.dart';
 | 
				
			||||||
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
 | 
					import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
 | 
				
			||||||
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/activities/models/activity.model.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/models/store.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/utils/datetime_extensions.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/utils/image_url_builder.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ActivitiesPage extends HookConsumerWidget {
 | 
				
			||||||
 | 
					  final String albumId;
 | 
				
			||||||
 | 
					  final String? assetId;
 | 
				
			||||||
 | 
					  final bool withAssetThumbs;
 | 
				
			||||||
 | 
					  final String appBarTitle;
 | 
				
			||||||
 | 
					  final bool isOwner;
 | 
				
			||||||
 | 
					  const ActivitiesPage(
 | 
				
			||||||
 | 
					    this.albumId, {
 | 
				
			||||||
 | 
					    this.appBarTitle = "",
 | 
				
			||||||
 | 
					    this.assetId,
 | 
				
			||||||
 | 
					    this.withAssetThumbs = true,
 | 
				
			||||||
 | 
					    this.isOwner = false,
 | 
				
			||||||
 | 
					    super.key,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
 | 
					    final provider =
 | 
				
			||||||
 | 
					        activityStateProvider((albumId: albumId, assetId: assetId));
 | 
				
			||||||
 | 
					    final activities = ref.watch(provider);
 | 
				
			||||||
 | 
					    final inputController = useTextEditingController();
 | 
				
			||||||
 | 
					    final inputFocusNode = useFocusNode();
 | 
				
			||||||
 | 
					    final listViewScrollController = useScrollController();
 | 
				
			||||||
 | 
					    final currentUser = Store.tryGet(StoreKey.currentUser);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    useEffect(
 | 
				
			||||||
 | 
					      () {
 | 
				
			||||||
 | 
					        inputFocusNode.requestFocus();
 | 
				
			||||||
 | 
					        return null;
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      [],
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    buildTitleWithTimestamp(Activity activity, {bool leftAlign = true}) {
 | 
				
			||||||
 | 
					      final textColor = Theme.of(context).brightness == Brightness.dark
 | 
				
			||||||
 | 
					          ? Colors.white
 | 
				
			||||||
 | 
					          : Colors.black;
 | 
				
			||||||
 | 
					      final textStyle = Theme.of(context)
 | 
				
			||||||
 | 
					          .textTheme
 | 
				
			||||||
 | 
					          .bodyMedium
 | 
				
			||||||
 | 
					          ?.copyWith(color: textColor.withOpacity(0.6));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return Row(
 | 
				
			||||||
 | 
					        mainAxisAlignment: leftAlign
 | 
				
			||||||
 | 
					            ? MainAxisAlignment.start
 | 
				
			||||||
 | 
					            : MainAxisAlignment.spaceBetween,
 | 
				
			||||||
 | 
					        mainAxisSize: leftAlign ? MainAxisSize.min : MainAxisSize.max,
 | 
				
			||||||
 | 
					        children: [
 | 
				
			||||||
 | 
					          Text(
 | 
				
			||||||
 | 
					            "${activity.user.firstName} ${activity.user.lastName}",
 | 
				
			||||||
 | 
					            style: textStyle,
 | 
				
			||||||
 | 
					            overflow: TextOverflow.ellipsis,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          if (leftAlign)
 | 
				
			||||||
 | 
					            Text(
 | 
				
			||||||
 | 
					              " • ",
 | 
				
			||||||
 | 
					              style: textStyle,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          Expanded(
 | 
				
			||||||
 | 
					            child: Text(
 | 
				
			||||||
 | 
					              activity.createdAt.copyWith().timeAgo(),
 | 
				
			||||||
 | 
					              style: textStyle,
 | 
				
			||||||
 | 
					              overflow: TextOverflow.ellipsis,
 | 
				
			||||||
 | 
					              textAlign: leftAlign ? TextAlign.left : TextAlign.right,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        ],
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    buildAssetThumbnail(Activity activity) {
 | 
				
			||||||
 | 
					      return withAssetThumbs && activity.assetId != null
 | 
				
			||||||
 | 
					          ? Container(
 | 
				
			||||||
 | 
					              width: 40,
 | 
				
			||||||
 | 
					              height: 30,
 | 
				
			||||||
 | 
					              decoration: BoxDecoration(
 | 
				
			||||||
 | 
					                borderRadius: BorderRadius.circular(4),
 | 
				
			||||||
 | 
					                image: DecorationImage(
 | 
				
			||||||
 | 
					                  image: CachedNetworkImageProvider(
 | 
				
			||||||
 | 
					                    getThumbnailUrlForRemoteId(
 | 
				
			||||||
 | 
					                      activity.assetId!,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    cacheKey: getThumbnailCacheKeyForRemoteId(
 | 
				
			||||||
 | 
					                      activity.assetId!,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                    headers: {
 | 
				
			||||||
 | 
					                      "Authorization":
 | 
				
			||||||
 | 
					                          'Bearer ${Store.get(StoreKey.accessToken)}',
 | 
				
			||||||
 | 
					                    },
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                  fit: BoxFit.cover,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              child: const SizedBox.shrink(),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					          : null;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    buildTextField(String? likedId) {
 | 
				
			||||||
 | 
					      final liked = likedId != null;
 | 
				
			||||||
 | 
					      return Padding(
 | 
				
			||||||
 | 
					        padding: const EdgeInsets.only(bottom: 10),
 | 
				
			||||||
 | 
					        child: TextField(
 | 
				
			||||||
 | 
					          controller: inputController,
 | 
				
			||||||
 | 
					          focusNode: inputFocusNode,
 | 
				
			||||||
 | 
					          textInputAction: TextInputAction.send,
 | 
				
			||||||
 | 
					          autofocus: false,
 | 
				
			||||||
 | 
					          decoration: InputDecoration(
 | 
				
			||||||
 | 
					            border: InputBorder.none,
 | 
				
			||||||
 | 
					            focusedBorder: InputBorder.none,
 | 
				
			||||||
 | 
					            prefixIcon: currentUser != null
 | 
				
			||||||
 | 
					                ? Padding(
 | 
				
			||||||
 | 
					                    padding: const EdgeInsets.symmetric(horizontal: 15),
 | 
				
			||||||
 | 
					                    child: UserCircleAvatar(
 | 
				
			||||||
 | 
					                      user: currentUser,
 | 
				
			||||||
 | 
					                      size: 30,
 | 
				
			||||||
 | 
					                      radius: 15,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  )
 | 
				
			||||||
 | 
					                : null,
 | 
				
			||||||
 | 
					            suffixIcon: Padding(
 | 
				
			||||||
 | 
					              padding: const EdgeInsets.only(right: 10),
 | 
				
			||||||
 | 
					              child: IconButton(
 | 
				
			||||||
 | 
					                icon: Icon(
 | 
				
			||||||
 | 
					                  liked
 | 
				
			||||||
 | 
					                      ? Icons.favorite_rounded
 | 
				
			||||||
 | 
					                      : Icons.favorite_border_rounded,
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					                onPressed: () async {
 | 
				
			||||||
 | 
					                  liked
 | 
				
			||||||
 | 
					                      ? await ref
 | 
				
			||||||
 | 
					                          .read(provider.notifier)
 | 
				
			||||||
 | 
					                          .removeActivity(likedId)
 | 
				
			||||||
 | 
					                      : await ref.read(provider.notifier).addLike();
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            suffixIconColor: liked ? Colors.red[700] : null,
 | 
				
			||||||
 | 
					            hintText: 'shared_album_activities_input_hint'.tr(),
 | 
				
			||||||
 | 
					            hintStyle: TextStyle(
 | 
				
			||||||
 | 
					              fontWeight: FontWeight.normal,
 | 
				
			||||||
 | 
					              fontSize: 14,
 | 
				
			||||||
 | 
					              color: Colors.grey[600],
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          onEditingComplete: () async {
 | 
				
			||||||
 | 
					            await ref.read(provider.notifier).addComment(inputController.text);
 | 
				
			||||||
 | 
					            inputController.clear();
 | 
				
			||||||
 | 
					            inputFocusNode.unfocus();
 | 
				
			||||||
 | 
					            listViewScrollController.animateTo(
 | 
				
			||||||
 | 
					              listViewScrollController.position.maxScrollExtent,
 | 
				
			||||||
 | 
					              duration: const Duration(milliseconds: 800),
 | 
				
			||||||
 | 
					              curve: Curves.fastOutSlowIn,
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					          onTapOutside: (_) => inputFocusNode.unfocus(),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    getDismissibleWidget(
 | 
				
			||||||
 | 
					      Widget widget,
 | 
				
			||||||
 | 
					      Activity activity,
 | 
				
			||||||
 | 
					      bool canDelete,
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      return Dismissible(
 | 
				
			||||||
 | 
					        key: Key(activity.id),
 | 
				
			||||||
 | 
					        dismissThresholds: const {
 | 
				
			||||||
 | 
					          DismissDirection.horizontal: 0.7,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        direction: DismissDirection.horizontal,
 | 
				
			||||||
 | 
					        confirmDismiss: (direction) => canDelete
 | 
				
			||||||
 | 
					            ? showDialog(
 | 
				
			||||||
 | 
					                context: context,
 | 
				
			||||||
 | 
					                builder: (context) => ConfirmDialog(
 | 
				
			||||||
 | 
					                  onOk: () {},
 | 
				
			||||||
 | 
					                  title: "shared_album_activity_remove_title",
 | 
				
			||||||
 | 
					                  content: "shared_album_activity_remove_content",
 | 
				
			||||||
 | 
					                  ok: "delete_dialog_ok",
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              )
 | 
				
			||||||
 | 
					            : Future.value(false),
 | 
				
			||||||
 | 
					        onDismissed: (direction) async =>
 | 
				
			||||||
 | 
					            await ref.read(provider.notifier).removeActivity(activity.id),
 | 
				
			||||||
 | 
					        background: Container(
 | 
				
			||||||
 | 
					          color: canDelete ? Colors.red[400] : Colors.grey[600],
 | 
				
			||||||
 | 
					          alignment: AlignmentDirectional.centerStart,
 | 
				
			||||||
 | 
					          child: canDelete
 | 
				
			||||||
 | 
					              ? const Padding(
 | 
				
			||||||
 | 
					                  padding: EdgeInsets.all(15),
 | 
				
			||||||
 | 
					                  child: Icon(
 | 
				
			||||||
 | 
					                    Icons.delete_sweep_rounded,
 | 
				
			||||||
 | 
					                    color: Colors.black,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              : null,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        secondaryBackground: Container(
 | 
				
			||||||
 | 
					          color: canDelete ? Colors.red[400] : Colors.grey[600],
 | 
				
			||||||
 | 
					          alignment: AlignmentDirectional.centerEnd,
 | 
				
			||||||
 | 
					          child: canDelete
 | 
				
			||||||
 | 
					              ? const Padding(
 | 
				
			||||||
 | 
					                  padding: EdgeInsets.all(15),
 | 
				
			||||||
 | 
					                  child: Icon(
 | 
				
			||||||
 | 
					                    Icons.delete_sweep_rounded,
 | 
				
			||||||
 | 
					                    color: Colors.black,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              : null,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        child: widget,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return Scaffold(
 | 
				
			||||||
 | 
					      appBar: AppBar(title: Text(appBarTitle)),
 | 
				
			||||||
 | 
					      body: activities.maybeWhen(
 | 
				
			||||||
 | 
					        orElse: () {
 | 
				
			||||||
 | 
					          return const Center(child: ImmichLoadingIndicator());
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        data: (data) {
 | 
				
			||||||
 | 
					          final liked = data.firstWhereOrNull(
 | 
				
			||||||
 | 
					            (a) =>
 | 
				
			||||||
 | 
					                a.type == ActivityType.like &&
 | 
				
			||||||
 | 
					                a.user.id == currentUser?.id &&
 | 
				
			||||||
 | 
					                a.assetId == assetId,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          return Stack(
 | 
				
			||||||
 | 
					            children: [
 | 
				
			||||||
 | 
					              ListView.builder(
 | 
				
			||||||
 | 
					                controller: listViewScrollController,
 | 
				
			||||||
 | 
					                itemCount: data.length + 1,
 | 
				
			||||||
 | 
					                itemBuilder: (context, index) {
 | 
				
			||||||
 | 
					                  // Vertical gap after the last element
 | 
				
			||||||
 | 
					                  if (index == data.length) {
 | 
				
			||||||
 | 
					                    return const SizedBox(
 | 
				
			||||||
 | 
					                      height: 80,
 | 
				
			||||||
 | 
					                    );
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  final activity = data[index];
 | 
				
			||||||
 | 
					                  final canDelete =
 | 
				
			||||||
 | 
					                      activity.user.id == currentUser?.id || isOwner;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  return Padding(
 | 
				
			||||||
 | 
					                    padding: const EdgeInsets.all(5),
 | 
				
			||||||
 | 
					                    child: activity.type == ActivityType.comment
 | 
				
			||||||
 | 
					                        ? getDismissibleWidget(
 | 
				
			||||||
 | 
					                            ListTile(
 | 
				
			||||||
 | 
					                              minVerticalPadding: 15,
 | 
				
			||||||
 | 
					                              leading: UserCircleAvatar(user: activity.user),
 | 
				
			||||||
 | 
					                              title: buildTitleWithTimestamp(
 | 
				
			||||||
 | 
					                                activity,
 | 
				
			||||||
 | 
					                                leftAlign:
 | 
				
			||||||
 | 
					                                    withAssetThumbs && activity.assetId != null,
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                              titleAlignment: ListTileTitleAlignment.top,
 | 
				
			||||||
 | 
					                              trailing: buildAssetThumbnail(activity),
 | 
				
			||||||
 | 
					                              subtitle: Text(activity.comment!),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            activity,
 | 
				
			||||||
 | 
					                            canDelete,
 | 
				
			||||||
 | 
					                          )
 | 
				
			||||||
 | 
					                        : getDismissibleWidget(
 | 
				
			||||||
 | 
					                            ListTile(
 | 
				
			||||||
 | 
					                              minVerticalPadding: 15,
 | 
				
			||||||
 | 
					                              leading: Container(
 | 
				
			||||||
 | 
					                                width: 44,
 | 
				
			||||||
 | 
					                                alignment: Alignment.center,
 | 
				
			||||||
 | 
					                                child: Icon(
 | 
				
			||||||
 | 
					                                  Icons.favorite_rounded,
 | 
				
			||||||
 | 
					                                  color: Colors.red[700],
 | 
				
			||||||
 | 
					                                ),
 | 
				
			||||||
 | 
					                              ),
 | 
				
			||||||
 | 
					                              title: buildTitleWithTimestamp(activity),
 | 
				
			||||||
 | 
					                              trailing: buildAssetThumbnail(activity),
 | 
				
			||||||
 | 
					                            ),
 | 
				
			||||||
 | 
					                            activity,
 | 
				
			||||||
 | 
					                            canDelete,
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                  );
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					              Align(
 | 
				
			||||||
 | 
					                alignment: Alignment.bottomCenter,
 | 
				
			||||||
 | 
					                child: Container(
 | 
				
			||||||
 | 
					                  color: Theme.of(context).scaffoldBackgroundColor,
 | 
				
			||||||
 | 
					                  child: buildTextField(liked?.id),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -3,6 +3,7 @@ import 'package:easy_localization/easy_localization.dart';
 | 
				
			|||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:fluttertoast/fluttertoast.dart';
 | 
					import 'package:fluttertoast/fluttertoast.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
 | 
					import 'package:immich_mobile/modules/album/providers/album.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
 | 
					import 'package:immich_mobile/modules/album/providers/album_detail.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
 | 
					import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
 | 
				
			||||||
@ -26,6 +27,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
 | 
				
			|||||||
    required this.titleFocusNode,
 | 
					    required this.titleFocusNode,
 | 
				
			||||||
    this.onAddPhotos,
 | 
					    this.onAddPhotos,
 | 
				
			||||||
    this.onAddUsers,
 | 
					    this.onAddUsers,
 | 
				
			||||||
 | 
					    required this.onActivities,
 | 
				
			||||||
  }) : super(key: key);
 | 
					  }) : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final Album album;
 | 
					  final Album album;
 | 
				
			||||||
@ -35,11 +37,19 @@ class AlbumViewerAppbar extends HookConsumerWidget
 | 
				
			|||||||
  final FocusNode titleFocusNode;
 | 
					  final FocusNode titleFocusNode;
 | 
				
			||||||
  final Function(Album album)? onAddPhotos;
 | 
					  final Function(Album album)? onAddPhotos;
 | 
				
			||||||
  final Function(Album album)? onAddUsers;
 | 
					  final Function(Album album)? onAddUsers;
 | 
				
			||||||
 | 
					  final Function(Album album) onActivities;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
 | 
					    final newAlbumTitle = ref.watch(albumViewerProvider).editTitleText;
 | 
				
			||||||
    final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
 | 
					    final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
 | 
				
			||||||
 | 
					    final comments = album.shared
 | 
				
			||||||
 | 
					        ? ref.watch(
 | 
				
			||||||
 | 
					            activityStatisticsStateProvider(
 | 
				
			||||||
 | 
					              (albumId: album.remoteId!, assetId: null),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					        : 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    deleteAlbum() async {
 | 
					    deleteAlbum() async {
 | 
				
			||||||
      ImmichLoadingOverlayController.appLoader.show();
 | 
					      ImmichLoadingOverlayController.appLoader.show();
 | 
				
			||||||
@ -310,6 +320,33 @@ class AlbumViewerAppbar extends HookConsumerWidget
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Widget buildActivitiesButton() {
 | 
				
			||||||
 | 
					      return IconButton(
 | 
				
			||||||
 | 
					        onPressed: () {
 | 
				
			||||||
 | 
					          onActivities(album);
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        icon: Row(
 | 
				
			||||||
 | 
					          crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
 | 
					          children: [
 | 
				
			||||||
 | 
					            const Icon(
 | 
				
			||||||
 | 
					              Icons.mode_comment_outlined,
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            if (comments != 0)
 | 
				
			||||||
 | 
					              Padding(
 | 
				
			||||||
 | 
					                padding: const EdgeInsets.only(left: 5),
 | 
				
			||||||
 | 
					                child: Text(
 | 
				
			||||||
 | 
					                  comments.toString(),
 | 
				
			||||||
 | 
					                  style: TextStyle(
 | 
				
			||||||
 | 
					                    fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					                    color: Theme.of(context).primaryColor,
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    buildLeadingButton() {
 | 
					    buildLeadingButton() {
 | 
				
			||||||
      if (selected.isNotEmpty) {
 | 
					      if (selected.isNotEmpty) {
 | 
				
			||||||
        return IconButton(
 | 
					        return IconButton(
 | 
				
			||||||
@ -353,6 +390,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
 | 
				
			|||||||
      title: selected.isNotEmpty ? Text('${selected.length}') : null,
 | 
					      title: selected.isNotEmpty ? Text('${selected.length}') : null,
 | 
				
			||||||
      centerTitle: false,
 | 
					      centerTitle: false,
 | 
				
			||||||
      actions: [
 | 
					      actions: [
 | 
				
			||||||
 | 
					        if (album.shared) buildActivitiesButton(),
 | 
				
			||||||
        if (album.isRemote)
 | 
					        if (album.isRemote)
 | 
				
			||||||
          IconButton(
 | 
					          IconButton(
 | 
				
			||||||
            splashRadius: 25,
 | 
					            splashRadius: 25,
 | 
				
			||||||
 | 
				
			|||||||
@ -232,6 +232,18 @@ class AlbumViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    onActivitiesPressed(Album album) {
 | 
				
			||||||
 | 
					      if (album.remoteId != null) {
 | 
				
			||||||
 | 
					        AutoRouter.of(context).push(
 | 
				
			||||||
 | 
					          ActivitiesRoute(
 | 
				
			||||||
 | 
					            albumId: album.remoteId!,
 | 
				
			||||||
 | 
					            appBarTitle: album.name,
 | 
				
			||||||
 | 
					            isOwner: userId == album.ownerId,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return Scaffold(
 | 
					    return Scaffold(
 | 
				
			||||||
      appBar: album.when(
 | 
					      appBar: album.when(
 | 
				
			||||||
        data: (data) => AlbumViewerAppbar(
 | 
					        data: (data) => AlbumViewerAppbar(
 | 
				
			||||||
@ -242,6 +254,7 @@ class AlbumViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
          selectionDisabled: disableSelection,
 | 
					          selectionDisabled: disableSelection,
 | 
				
			||||||
          onAddPhotos: onAddPhotosPressed,
 | 
					          onAddPhotos: onAddPhotosPressed,
 | 
				
			||||||
          onAddUsers: onAddUsersPressed,
 | 
					          onAddUsers: onAddUsersPressed,
 | 
				
			||||||
 | 
					          onActivities: onActivitiesPressed,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
        error: (error, stackTrace) => AppBar(title: const Text("Error")),
 | 
					        error: (error, stackTrace) => AppBar(title: const Text("Error")),
 | 
				
			||||||
        loading: () => AppBar(),
 | 
					        loading: () => AppBar(),
 | 
				
			||||||
@ -266,6 +279,7 @@ class AlbumViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
                ],
 | 
					                ],
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              isOwner: userId == data.ownerId,
 | 
					              isOwner: userId == data.ownerId,
 | 
				
			||||||
 | 
					              sharedAlbumId: data.remoteId,
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
import 'package:auto_route/auto_route.dart';
 | 
					import 'package:auto_route/auto_route.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/models/asset.dart';
 | 
					import 'package:immich_mobile/shared/models/asset.dart';
 | 
				
			||||||
import 'package:immich_mobile/shared/providers/asset.provider.dart';
 | 
					import 'package:immich_mobile/shared/providers/asset.provider.dart';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -16,6 +17,8 @@ class TopControlAppBar extends HookConsumerWidget {
 | 
				
			|||||||
    required this.onFavorite,
 | 
					    required this.onFavorite,
 | 
				
			||||||
    required this.onUploadPressed,
 | 
					    required this.onUploadPressed,
 | 
				
			||||||
    required this.isOwner,
 | 
					    required this.isOwner,
 | 
				
			||||||
 | 
					    required this.shareAlbumId,
 | 
				
			||||||
 | 
					    required this.onActivitiesPressed,
 | 
				
			||||||
  }) : super(key: key);
 | 
					  }) : super(key: key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final Asset asset;
 | 
					  final Asset asset;
 | 
				
			||||||
@ -24,14 +27,23 @@ class TopControlAppBar extends HookConsumerWidget {
 | 
				
			|||||||
  final VoidCallback? onDownloadPressed;
 | 
					  final VoidCallback? onDownloadPressed;
 | 
				
			||||||
  final VoidCallback onToggleMotionVideo;
 | 
					  final VoidCallback onToggleMotionVideo;
 | 
				
			||||||
  final VoidCallback onAddToAlbumPressed;
 | 
					  final VoidCallback onAddToAlbumPressed;
 | 
				
			||||||
 | 
					  final VoidCallback onActivitiesPressed;
 | 
				
			||||||
  final Function(Asset) onFavorite;
 | 
					  final Function(Asset) onFavorite;
 | 
				
			||||||
  final bool isPlayingMotionVideo;
 | 
					  final bool isPlayingMotionVideo;
 | 
				
			||||||
  final bool isOwner;
 | 
					  final bool isOwner;
 | 
				
			||||||
 | 
					  final String? shareAlbumId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  Widget build(BuildContext context, WidgetRef ref) {
 | 
					  Widget build(BuildContext context, WidgetRef ref) {
 | 
				
			||||||
    const double iconSize = 22.0;
 | 
					    const double iconSize = 22.0;
 | 
				
			||||||
    final a = ref.watch(assetWatcher(asset)).value ?? asset;
 | 
					    final a = ref.watch(assetWatcher(asset)).value ?? asset;
 | 
				
			||||||
 | 
					    final comments = shareAlbumId != null
 | 
				
			||||||
 | 
					        ? ref.watch(
 | 
				
			||||||
 | 
					            activityStatisticsStateProvider(
 | 
				
			||||||
 | 
					              (albumId: shareAlbumId!, assetId: asset.remoteId),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          )
 | 
				
			||||||
 | 
					        : 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Widget buildFavoriteButton(a) {
 | 
					    Widget buildFavoriteButton(a) {
 | 
				
			||||||
      return IconButton(
 | 
					      return IconButton(
 | 
				
			||||||
@ -94,6 +106,34 @@ class TopControlAppBar extends HookConsumerWidget {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Widget buildActivitiesButton() {
 | 
				
			||||||
 | 
					      return IconButton(
 | 
				
			||||||
 | 
					        onPressed: () {
 | 
				
			||||||
 | 
					          onActivitiesPressed();
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        icon: Row(
 | 
				
			||||||
 | 
					          crossAxisAlignment: CrossAxisAlignment.center,
 | 
				
			||||||
 | 
					          children: [
 | 
				
			||||||
 | 
					            Icon(
 | 
				
			||||||
 | 
					              Icons.mode_comment_outlined,
 | 
				
			||||||
 | 
					              color: Colors.grey[200],
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            if (comments != 0)
 | 
				
			||||||
 | 
					              Padding(
 | 
				
			||||||
 | 
					                padding: const EdgeInsets.only(left: 5),
 | 
				
			||||||
 | 
					                child: Text(
 | 
				
			||||||
 | 
					                  comments.toString(),
 | 
				
			||||||
 | 
					                  style: TextStyle(
 | 
				
			||||||
 | 
					                    fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					                    color: Colors.grey[200],
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Widget buildUploadButton() {
 | 
					    Widget buildUploadButton() {
 | 
				
			||||||
      return IconButton(
 | 
					      return IconButton(
 | 
				
			||||||
        onPressed: onUploadPressed,
 | 
					        onPressed: onUploadPressed,
 | 
				
			||||||
@ -130,6 +170,7 @@ class TopControlAppBar extends HookConsumerWidget {
 | 
				
			|||||||
        if (asset.isLocal && !asset.isRemote) buildUploadButton(),
 | 
					        if (asset.isLocal && !asset.isRemote) buildUploadButton(),
 | 
				
			||||||
        if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(),
 | 
					        if (asset.isRemote && !asset.isLocal && isOwner) buildDownloadButton(),
 | 
				
			||||||
        if (asset.isRemote && isOwner) buildAddToAlbumButtom(),
 | 
					        if (asset.isRemote && isOwner) buildAddToAlbumButtom(),
 | 
				
			||||||
 | 
					        if (shareAlbumId != null) buildActivitiesButton(),
 | 
				
			||||||
        buildMoreInfoButton(),
 | 
					        buildMoreInfoButton(),
 | 
				
			||||||
      ],
 | 
					      ],
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
				
			|||||||
@ -49,6 +49,7 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
  final int heroOffset;
 | 
					  final int heroOffset;
 | 
				
			||||||
  final bool showStack;
 | 
					  final bool showStack;
 | 
				
			||||||
  final bool isOwner;
 | 
					  final bool isOwner;
 | 
				
			||||||
 | 
					  final String? sharedAlbumId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  GalleryViewerPage({
 | 
					  GalleryViewerPage({
 | 
				
			||||||
    super.key,
 | 
					    super.key,
 | 
				
			||||||
@ -58,6 +59,7 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
    this.heroOffset = 0,
 | 
					    this.heroOffset = 0,
 | 
				
			||||||
    this.showStack = false,
 | 
					    this.showStack = false,
 | 
				
			||||||
    this.isOwner = true,
 | 
					    this.isOwner = true,
 | 
				
			||||||
 | 
					    this.sharedAlbumId,
 | 
				
			||||||
  }) : controller = PageController(initialPage: initialIndex);
 | 
					  }) : controller = PageController(initialPage: initialIndex);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final PageController controller;
 | 
					  final PageController controller;
 | 
				
			||||||
@ -327,6 +329,19 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    handleActivities() {
 | 
				
			||||||
 | 
					      if (sharedAlbumId != null) {
 | 
				
			||||||
 | 
					        AutoRouter.of(context).push(
 | 
				
			||||||
 | 
					          ActivitiesRoute(
 | 
				
			||||||
 | 
					            albumId: sharedAlbumId!,
 | 
				
			||||||
 | 
					            assetId: asset().remoteId,
 | 
				
			||||||
 | 
					            withAssetThumbs: false,
 | 
				
			||||||
 | 
					            isOwner: isOwner,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    buildAppBar() {
 | 
					    buildAppBar() {
 | 
				
			||||||
      return IgnorePointer(
 | 
					      return IgnorePointer(
 | 
				
			||||||
        ignoring: !ref.watch(showControlsProvider),
 | 
					        ignoring: !ref.watch(showControlsProvider),
 | 
				
			||||||
@ -355,6 +370,8 @@ class GalleryViewerPage extends HookConsumerWidget {
 | 
				
			|||||||
                isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
 | 
					                isPlayingMotionVideo.value = !isPlayingMotionVideo.value;
 | 
				
			||||||
              }),
 | 
					              }),
 | 
				
			||||||
              onAddToAlbumPressed: () => addToAlbum(asset()),
 | 
					              onAddToAlbumPressed: () => addToAlbum(asset()),
 | 
				
			||||||
 | 
					              shareAlbumId: sharedAlbumId,
 | 
				
			||||||
 | 
					              onActivitiesPressed: handleActivities,
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
 | 
				
			|||||||
@ -34,6 +34,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
 | 
				
			|||||||
  final bool showDragScroll;
 | 
					  final bool showDragScroll;
 | 
				
			||||||
  final bool showStack;
 | 
					  final bool showStack;
 | 
				
			||||||
  final bool isOwner;
 | 
					  final bool isOwner;
 | 
				
			||||||
 | 
					  final String? sharedAlbumId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const ImmichAssetGrid({
 | 
					  const ImmichAssetGrid({
 | 
				
			||||||
    super.key,
 | 
					    super.key,
 | 
				
			||||||
@ -55,6 +56,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
 | 
				
			|||||||
    this.showDragScroll = true,
 | 
					    this.showDragScroll = true,
 | 
				
			||||||
    this.showStack = false,
 | 
					    this.showStack = false,
 | 
				
			||||||
    this.isOwner = true,
 | 
					    this.isOwner = true,
 | 
				
			||||||
 | 
					    this.sharedAlbumId,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@ -120,6 +122,7 @@ class ImmichAssetGrid extends HookConsumerWidget {
 | 
				
			|||||||
          showDragScroll: showDragScroll,
 | 
					          showDragScroll: showDragScroll,
 | 
				
			||||||
          showStack: showStack,
 | 
					          showStack: showStack,
 | 
				
			||||||
          isOwner: isOwner,
 | 
					          isOwner: isOwner,
 | 
				
			||||||
 | 
					          sharedAlbumId: sharedAlbumId,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
				
			|||||||
@ -39,6 +39,7 @@ class ImmichAssetGridView extends StatefulWidget {
 | 
				
			|||||||
  final bool showDragScroll;
 | 
					  final bool showDragScroll;
 | 
				
			||||||
  final bool showStack;
 | 
					  final bool showStack;
 | 
				
			||||||
  final bool isOwner;
 | 
					  final bool isOwner;
 | 
				
			||||||
 | 
					  final String? sharedAlbumId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const ImmichAssetGridView({
 | 
					  const ImmichAssetGridView({
 | 
				
			||||||
    super.key,
 | 
					    super.key,
 | 
				
			||||||
@ -60,6 +61,7 @@ class ImmichAssetGridView extends StatefulWidget {
 | 
				
			|||||||
    this.showDragScroll = true,
 | 
					    this.showDragScroll = true,
 | 
				
			||||||
    this.showStack = false,
 | 
					    this.showStack = false,
 | 
				
			||||||
    this.isOwner = true,
 | 
					    this.isOwner = true,
 | 
				
			||||||
 | 
					    this.sharedAlbumId,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
@ -141,6 +143,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
 | 
				
			|||||||
      heroOffset: widget.heroOffset,
 | 
					      heroOffset: widget.heroOffset,
 | 
				
			||||||
      showStack: widget.showStack,
 | 
					      showStack: widget.showStack,
 | 
				
			||||||
      isOwner: widget.isOwner,
 | 
					      isOwner: widget.isOwner,
 | 
				
			||||||
 | 
					      sharedAlbumId: widget.sharedAlbumId,
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -21,6 +21,7 @@ class ThumbnailImage extends StatelessWidget {
 | 
				
			|||||||
  final Function? onSelect;
 | 
					  final Function? onSelect;
 | 
				
			||||||
  final Function? onDeselect;
 | 
					  final Function? onDeselect;
 | 
				
			||||||
  final int heroOffset;
 | 
					  final int heroOffset;
 | 
				
			||||||
 | 
					  final String? sharedAlbumId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const ThumbnailImage({
 | 
					  const ThumbnailImage({
 | 
				
			||||||
    Key? key,
 | 
					    Key? key,
 | 
				
			||||||
@ -31,6 +32,7 @@ class ThumbnailImage extends StatelessWidget {
 | 
				
			|||||||
    this.showStorageIndicator = true,
 | 
					    this.showStorageIndicator = true,
 | 
				
			||||||
    this.showStack = false,
 | 
					    this.showStack = false,
 | 
				
			||||||
    this.isOwner = true,
 | 
					    this.isOwner = true,
 | 
				
			||||||
 | 
					    this.sharedAlbumId,
 | 
				
			||||||
    this.useGrayBoxPlaceholder = false,
 | 
					    this.useGrayBoxPlaceholder = false,
 | 
				
			||||||
    this.isSelected = false,
 | 
					    this.isSelected = false,
 | 
				
			||||||
    this.multiselectEnabled = false,
 | 
					    this.multiselectEnabled = false,
 | 
				
			||||||
@ -184,6 +186,7 @@ class ThumbnailImage extends StatelessWidget {
 | 
				
			|||||||
              heroOffset: heroOffset,
 | 
					              heroOffset: heroOffset,
 | 
				
			||||||
              showStack: showStack,
 | 
					              showStack: showStack,
 | 
				
			||||||
              isOwner: isOwner,
 | 
					              isOwner: isOwner,
 | 
				
			||||||
 | 
					              sharedAlbumId: sharedAlbumId,
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
				
			|||||||
@ -172,7 +172,7 @@ class SearchPage extends HookConsumerWidget {
 | 
				
			|||||||
                ),
 | 
					                ),
 | 
				
			||||||
                ListTile(
 | 
					                ListTile(
 | 
				
			||||||
                  leading: Icon(
 | 
					                  leading: Icon(
 | 
				
			||||||
                    Icons.star_outline,
 | 
					                    Icons.favorite_border_rounded,
 | 
				
			||||||
                    color: categoryIconColor,
 | 
					                    color: categoryIconColor,
 | 
				
			||||||
                  ),
 | 
					                  ),
 | 
				
			||||||
                  title:
 | 
					                  title:
 | 
				
			||||||
 | 
				
			|||||||
@ -1,6 +1,7 @@
 | 
				
			|||||||
import 'package:auto_route/auto_route.dart';
 | 
					import 'package:auto_route/auto_route.dart';
 | 
				
			||||||
import 'package:flutter/material.dart';
 | 
					import 'package:flutter/material.dart';
 | 
				
			||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
					import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
				
			||||||
 | 
					import 'package:immich_mobile/modules/activities/views/activities_page.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
 | 
					import 'package:immich_mobile/modules/album/models/asset_selection_page_result.model.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/album/views/album_options_part.dart';
 | 
					import 'package:immich_mobile/modules/album/views/album_options_part.dart';
 | 
				
			||||||
import 'package:immich_mobile/modules/album/views/album_viewer_page.dart';
 | 
					import 'package:immich_mobile/modules/album/views/album_viewer_page.dart';
 | 
				
			||||||
@ -160,6 +161,12 @@ part 'router.gr.dart';
 | 
				
			|||||||
    AutoRoute(page: TrashPage, guards: [AuthGuard, DuplicateGuard]),
 | 
					    AutoRoute(page: TrashPage, guards: [AuthGuard, DuplicateGuard]),
 | 
				
			||||||
    AutoRoute(page: SharedLinkPage, guards: [AuthGuard, DuplicateGuard]),
 | 
					    AutoRoute(page: SharedLinkPage, guards: [AuthGuard, DuplicateGuard]),
 | 
				
			||||||
    AutoRoute(page: SharedLinkEditPage, guards: [AuthGuard, DuplicateGuard]),
 | 
					    AutoRoute(page: SharedLinkEditPage, guards: [AuthGuard, DuplicateGuard]),
 | 
				
			||||||
 | 
					    CustomRoute(
 | 
				
			||||||
 | 
					      page: ActivitiesPage,
 | 
				
			||||||
 | 
					      guards: [AuthGuard, DuplicateGuard],
 | 
				
			||||||
 | 
					      transitionsBuilder: TransitionsBuilders.slideLeft,
 | 
				
			||||||
 | 
					      durationInMilliseconds: 200,
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
class AppRouter extends _$AppRouter {
 | 
					class AppRouter extends _$AppRouter {
 | 
				
			||||||
 | 
				
			|||||||
@ -73,6 +73,7 @@ class _$AppRouter extends RootStackRouter {
 | 
				
			|||||||
          heroOffset: args.heroOffset,
 | 
					          heroOffset: args.heroOffset,
 | 
				
			||||||
          showStack: args.showStack,
 | 
					          showStack: args.showStack,
 | 
				
			||||||
          isOwner: args.isOwner,
 | 
					          isOwner: args.isOwner,
 | 
				
			||||||
 | 
					          sharedAlbumId: args.sharedAlbumId,
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@ -337,6 +338,24 @@ class _$AppRouter extends RootStackRouter {
 | 
				
			|||||||
        ),
 | 
					        ),
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    ActivitiesRoute.name: (routeData) {
 | 
				
			||||||
 | 
					      final args = routeData.argsAs<ActivitiesRouteArgs>();
 | 
				
			||||||
 | 
					      return CustomPage<dynamic>(
 | 
				
			||||||
 | 
					        routeData: routeData,
 | 
				
			||||||
 | 
					        child: ActivitiesPage(
 | 
				
			||||||
 | 
					          args.albumId,
 | 
				
			||||||
 | 
					          appBarTitle: args.appBarTitle,
 | 
				
			||||||
 | 
					          assetId: args.assetId,
 | 
				
			||||||
 | 
					          withAssetThumbs: args.withAssetThumbs,
 | 
				
			||||||
 | 
					          isOwner: args.isOwner,
 | 
				
			||||||
 | 
					          key: args.key,
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        transitionsBuilder: TransitionsBuilders.slideLeft,
 | 
				
			||||||
 | 
					        durationInMilliseconds: 200,
 | 
				
			||||||
 | 
					        opaque: true,
 | 
				
			||||||
 | 
					        barrierDismissible: false,
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    HomeRoute.name: (routeData) {
 | 
					    HomeRoute.name: (routeData) {
 | 
				
			||||||
      return MaterialPageX<dynamic>(
 | 
					      return MaterialPageX<dynamic>(
 | 
				
			||||||
        routeData: routeData,
 | 
					        routeData: routeData,
 | 
				
			||||||
@ -674,6 +693,14 @@ class _$AppRouter extends RootStackRouter {
 | 
				
			|||||||
            duplicateGuard,
 | 
					            duplicateGuard,
 | 
				
			||||||
          ],
 | 
					          ],
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
 | 
					        RouteConfig(
 | 
				
			||||||
 | 
					          ActivitiesRoute.name,
 | 
				
			||||||
 | 
					          path: '/activities-page',
 | 
				
			||||||
 | 
					          guards: [
 | 
				
			||||||
 | 
					            authGuard,
 | 
				
			||||||
 | 
					            duplicateGuard,
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
      ];
 | 
					      ];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -749,6 +776,7 @@ class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
 | 
				
			|||||||
    int heroOffset = 0,
 | 
					    int heroOffset = 0,
 | 
				
			||||||
    bool showStack = false,
 | 
					    bool showStack = false,
 | 
				
			||||||
    bool isOwner = true,
 | 
					    bool isOwner = true,
 | 
				
			||||||
 | 
					    String? sharedAlbumId,
 | 
				
			||||||
  }) : super(
 | 
					  }) : super(
 | 
				
			||||||
          GalleryViewerRoute.name,
 | 
					          GalleryViewerRoute.name,
 | 
				
			||||||
          path: '/gallery-viewer-page',
 | 
					          path: '/gallery-viewer-page',
 | 
				
			||||||
@ -760,6 +788,7 @@ class GalleryViewerRoute extends PageRouteInfo<GalleryViewerRouteArgs> {
 | 
				
			|||||||
            heroOffset: heroOffset,
 | 
					            heroOffset: heroOffset,
 | 
				
			||||||
            showStack: showStack,
 | 
					            showStack: showStack,
 | 
				
			||||||
            isOwner: isOwner,
 | 
					            isOwner: isOwner,
 | 
				
			||||||
 | 
					            sharedAlbumId: sharedAlbumId,
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -775,6 +804,7 @@ class GalleryViewerRouteArgs {
 | 
				
			|||||||
    this.heroOffset = 0,
 | 
					    this.heroOffset = 0,
 | 
				
			||||||
    this.showStack = false,
 | 
					    this.showStack = false,
 | 
				
			||||||
    this.isOwner = true,
 | 
					    this.isOwner = true,
 | 
				
			||||||
 | 
					    this.sharedAlbumId,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final Key? key;
 | 
					  final Key? key;
 | 
				
			||||||
@ -791,9 +821,11 @@ class GalleryViewerRouteArgs {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  final bool isOwner;
 | 
					  final bool isOwner;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final String? sharedAlbumId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @override
 | 
					  @override
 | 
				
			||||||
  String toString() {
 | 
					  String toString() {
 | 
				
			||||||
    return 'GalleryViewerRouteArgs{key: $key, initialIndex: $initialIndex, loadAsset: $loadAsset, totalAssets: $totalAssets, heroOffset: $heroOffset, showStack: $showStack, isOwner: $isOwner}';
 | 
					    return 'GalleryViewerRouteArgs{key: $key, initialIndex: $initialIndex, loadAsset: $loadAsset, totalAssets: $totalAssets, heroOffset: $heroOffset, showStack: $showStack, isOwner: $isOwner, sharedAlbumId: $sharedAlbumId}';
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -1527,6 +1559,60 @@ class SharedLinkEditRouteArgs {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// generated route for
 | 
				
			||||||
 | 
					/// [ActivitiesPage]
 | 
				
			||||||
 | 
					class ActivitiesRoute extends PageRouteInfo<ActivitiesRouteArgs> {
 | 
				
			||||||
 | 
					  ActivitiesRoute({
 | 
				
			||||||
 | 
					    required String albumId,
 | 
				
			||||||
 | 
					    String appBarTitle = "",
 | 
				
			||||||
 | 
					    String? assetId,
 | 
				
			||||||
 | 
					    bool withAssetThumbs = true,
 | 
				
			||||||
 | 
					    bool isOwner = false,
 | 
				
			||||||
 | 
					    Key? key,
 | 
				
			||||||
 | 
					  }) : super(
 | 
				
			||||||
 | 
					          ActivitiesRoute.name,
 | 
				
			||||||
 | 
					          path: '/activities-page',
 | 
				
			||||||
 | 
					          args: ActivitiesRouteArgs(
 | 
				
			||||||
 | 
					            albumId: albumId,
 | 
				
			||||||
 | 
					            appBarTitle: appBarTitle,
 | 
				
			||||||
 | 
					            assetId: assetId,
 | 
				
			||||||
 | 
					            withAssetThumbs: withAssetThumbs,
 | 
				
			||||||
 | 
					            isOwner: isOwner,
 | 
				
			||||||
 | 
					            key: key,
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static const String name = 'ActivitiesRoute';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class ActivitiesRouteArgs {
 | 
				
			||||||
 | 
					  const ActivitiesRouteArgs({
 | 
				
			||||||
 | 
					    required this.albumId,
 | 
				
			||||||
 | 
					    this.appBarTitle = "",
 | 
				
			||||||
 | 
					    this.assetId,
 | 
				
			||||||
 | 
					    this.withAssetThumbs = true,
 | 
				
			||||||
 | 
					    this.isOwner = false,
 | 
				
			||||||
 | 
					    this.key,
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final String albumId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final String appBarTitle;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final String? assetId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final bool withAssetThumbs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final bool isOwner;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final Key? key;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @override
 | 
				
			||||||
 | 
					  String toString() {
 | 
				
			||||||
 | 
					    return 'ActivitiesRouteArgs{albumId: $albumId, appBarTitle: $appBarTitle, assetId: $assetId, withAssetThumbs: $withAssetThumbs, isOwner: $isOwner, key: $key}';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// generated route for
 | 
					/// generated route for
 | 
				
			||||||
/// [HomePage]
 | 
					/// [HomePage]
 | 
				
			||||||
class HomeRoute extends PageRouteInfo<void> {
 | 
					class HomeRoute extends PageRouteInfo<void> {
 | 
				
			||||||
 | 
				
			|||||||
@ -22,6 +22,7 @@ class ApiService {
 | 
				
			|||||||
  late PersonApi personApi;
 | 
					  late PersonApi personApi;
 | 
				
			||||||
  late AuditApi auditApi;
 | 
					  late AuditApi auditApi;
 | 
				
			||||||
  late SharedLinkApi sharedLinkApi;
 | 
					  late SharedLinkApi sharedLinkApi;
 | 
				
			||||||
 | 
					  late ActivityApi activityApi;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  ApiService() {
 | 
					  ApiService() {
 | 
				
			||||||
    final endpoint = Store.tryGet(StoreKey.serverEndpoint);
 | 
					    final endpoint = Store.tryGet(StoreKey.serverEndpoint);
 | 
				
			||||||
@ -47,6 +48,7 @@ class ApiService {
 | 
				
			|||||||
    personApi = PersonApi(_apiClient);
 | 
					    personApi = PersonApi(_apiClient);
 | 
				
			||||||
    auditApi = AuditApi(_apiClient);
 | 
					    auditApi = AuditApi(_apiClient);
 | 
				
			||||||
    sharedLinkApi = SharedLinkApi(_apiClient);
 | 
					    sharedLinkApi = SharedLinkApi(_apiClient);
 | 
				
			||||||
 | 
					    activityApi = ActivityApi(_apiClient);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  Future<String> resolveAndSetEndpoint(String serverUrl) async {
 | 
					  Future<String> resolveAndSetEndpoint(String serverUrl) async {
 | 
				
			||||||
 | 
				
			|||||||
@ -40,19 +40,23 @@ class UserCircleAvatar extends ConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    final profileImageUrl =
 | 
					    final profileImageUrl =
 | 
				
			||||||
        '${Store.get(StoreKey.serverEndpoint)}/user/profile-image/${user.id}?d=${Random().nextInt(1024)}';
 | 
					        '${Store.get(StoreKey.serverEndpoint)}/user/profile-image/${user.id}?d=${Random().nextInt(1024)}';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final textIcon = Text(
 | 
				
			||||||
 | 
					      user.firstName[0].toUpperCase(),
 | 
				
			||||||
 | 
					      style: TextStyle(
 | 
				
			||||||
 | 
					        fontWeight: FontWeight.bold,
 | 
				
			||||||
 | 
					        color: Theme.of(context).brightness == Brightness.dark
 | 
				
			||||||
 | 
					            ? Colors.black
 | 
				
			||||||
 | 
					            : Colors.white,
 | 
				
			||||||
 | 
					      ),
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
    return CircleAvatar(
 | 
					    return CircleAvatar(
 | 
				
			||||||
      backgroundColor: useRandomBackgroundColor
 | 
					      backgroundColor: useRandomBackgroundColor
 | 
				
			||||||
          ? randomColors[Random().nextInt(randomColors.length)]
 | 
					          ? randomColors[Random().nextInt(randomColors.length)]
 | 
				
			||||||
          : Theme.of(context).primaryColor,
 | 
					          : Theme.of(context).primaryColor,
 | 
				
			||||||
      radius: radius,
 | 
					      radius: radius,
 | 
				
			||||||
      child: user.profileImagePath == ""
 | 
					      child: user.profileImagePath == ""
 | 
				
			||||||
          ? Text(
 | 
					          ? textIcon
 | 
				
			||||||
              user.firstName[0].toUpperCase(),
 | 
					 | 
				
			||||||
              style: const TextStyle(
 | 
					 | 
				
			||||||
                fontWeight: FontWeight.bold,
 | 
					 | 
				
			||||||
                color: Colors.black,
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
          : ClipRRect(
 | 
					          : ClipRRect(
 | 
				
			||||||
              borderRadius: BorderRadius.circular(50),
 | 
					              borderRadius: BorderRadius.circular(50),
 | 
				
			||||||
              child: CachedNetworkImage(
 | 
					              child: CachedNetworkImage(
 | 
				
			||||||
@ -66,8 +70,7 @@ class UserCircleAvatar extends ConsumerWidget {
 | 
				
			|||||||
                  "Authorization": "Bearer ${Store.get(StoreKey.accessToken)}",
 | 
					                  "Authorization": "Bearer ${Store.get(StoreKey.accessToken)}",
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
                fadeInDuration: const Duration(milliseconds: 300),
 | 
					                fadeInDuration: const Duration(milliseconds: 300),
 | 
				
			||||||
                errorWidget: (context, error, stackTrace) =>
 | 
					                errorWidget: (context, error, stackTrace) => textIcon,
 | 
				
			||||||
                    Image.memory(kTransparentImage),
 | 
					 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										36
									
								
								mobile/lib/utils/datetime_extensions.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								mobile/lib/utils/datetime_extensions.dart
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,36 @@
 | 
				
			|||||||
 | 
					extension TimeAgoExtension on DateTime {
 | 
				
			||||||
 | 
					  String timeAgo({bool numericDates = true}) {
 | 
				
			||||||
 | 
					    DateTime date = toLocal();
 | 
				
			||||||
 | 
					    final date2 = DateTime.now().toLocal();
 | 
				
			||||||
 | 
					    final difference = date2.difference(date);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (difference.inSeconds < 5) {
 | 
				
			||||||
 | 
					      return 'Just now';
 | 
				
			||||||
 | 
					    } else if (difference.inSeconds < 60) {
 | 
				
			||||||
 | 
					      return '${difference.inSeconds} seconds ago';
 | 
				
			||||||
 | 
					    } else if (difference.inMinutes <= 1) {
 | 
				
			||||||
 | 
					      return (numericDates) ? '1 minute ago' : 'A minute ago';
 | 
				
			||||||
 | 
					    } else if (difference.inMinutes < 60) {
 | 
				
			||||||
 | 
					      return '${difference.inMinutes} minutes ago';
 | 
				
			||||||
 | 
					    } else if (difference.inHours <= 1) {
 | 
				
			||||||
 | 
					      return (numericDates) ? '1 hour ago' : 'An hour ago';
 | 
				
			||||||
 | 
					    } else if (difference.inHours < 60) {
 | 
				
			||||||
 | 
					      return '${difference.inHours} hours ago';
 | 
				
			||||||
 | 
					    } else if (difference.inDays <= 1) {
 | 
				
			||||||
 | 
					      return (numericDates) ? '1 day ago' : 'Yesterday';
 | 
				
			||||||
 | 
					    } else if (difference.inDays < 6) {
 | 
				
			||||||
 | 
					      return '${difference.inDays} days ago';
 | 
				
			||||||
 | 
					    } else if ((difference.inDays / 7).ceil() <= 1) {
 | 
				
			||||||
 | 
					      return (numericDates) ? '1 week ago' : 'Last week';
 | 
				
			||||||
 | 
					    } else if ((difference.inDays / 7).ceil() < 4) {
 | 
				
			||||||
 | 
					      return '${(difference.inDays / 7).ceil()} weeks ago';
 | 
				
			||||||
 | 
					    } else if ((difference.inDays / 30).ceil() <= 1) {
 | 
				
			||||||
 | 
					      return (numericDates) ? '1 month ago' : 'Last month';
 | 
				
			||||||
 | 
					    } else if ((difference.inDays / 30).ceil() < 30) {
 | 
				
			||||||
 | 
					      return '${(difference.inDays / 30).ceil()} months ago';
 | 
				
			||||||
 | 
					    } else if ((difference.inDays / 365).ceil() <= 1) {
 | 
				
			||||||
 | 
					      return (numericDates) ? '1 year ago' : 'Last year';
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return '${(difference.inDays / 365).floor()} years ago';
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -58,6 +58,7 @@ export class ActivityService {
 | 
				
			|||||||
      delete dto.comment;
 | 
					      delete dto.comment;
 | 
				
			||||||
      [activity] = await this.repository.search({
 | 
					      [activity] = await this.repository.search({
 | 
				
			||||||
        ...common,
 | 
					        ...common,
 | 
				
			||||||
 | 
					        isGlobal: !dto.assetId,
 | 
				
			||||||
        isLiked: true,
 | 
					        isLiked: true,
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
      duplicate = !!activity;
 | 
					      duplicate = !!activity;
 | 
				
			||||||
 | 
				
			|||||||
@ -1,7 +1,7 @@
 | 
				
			|||||||
import { IActivityRepository } from '@app/domain';
 | 
					import { IActivityRepository } from '@app/domain';
 | 
				
			||||||
import { Injectable } from '@nestjs/common';
 | 
					import { Injectable } from '@nestjs/common';
 | 
				
			||||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
					import { InjectRepository } from '@nestjs/typeorm';
 | 
				
			||||||
import { Repository } from 'typeorm';
 | 
					import { IsNull, Repository } from 'typeorm';
 | 
				
			||||||
import { ActivityEntity } from '../entities/activity.entity';
 | 
					import { ActivityEntity } from '../entities/activity.entity';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ActivitySearch {
 | 
					export interface ActivitySearch {
 | 
				
			||||||
@ -9,6 +9,7 @@ export interface ActivitySearch {
 | 
				
			|||||||
  assetId?: string;
 | 
					  assetId?: string;
 | 
				
			||||||
  userId?: string;
 | 
					  userId?: string;
 | 
				
			||||||
  isLiked?: boolean;
 | 
					  isLiked?: boolean;
 | 
				
			||||||
 | 
					  isGlobal?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@Injectable()
 | 
					@Injectable()
 | 
				
			||||||
@ -16,11 +17,11 @@ export class ActivityRepository implements IActivityRepository {
 | 
				
			|||||||
  constructor(@InjectRepository(ActivityEntity) private repository: Repository<ActivityEntity>) {}
 | 
					  constructor(@InjectRepository(ActivityEntity) private repository: Repository<ActivityEntity>) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  search(options: ActivitySearch): Promise<ActivityEntity[]> {
 | 
					  search(options: ActivitySearch): Promise<ActivityEntity[]> {
 | 
				
			||||||
    const { userId, assetId, albumId, isLiked } = options;
 | 
					    const { userId, assetId, albumId, isLiked, isGlobal } = options;
 | 
				
			||||||
    return this.repository.find({
 | 
					    return this.repository.find({
 | 
				
			||||||
      where: {
 | 
					      where: {
 | 
				
			||||||
        userId,
 | 
					        userId,
 | 
				
			||||||
        assetId,
 | 
					        assetId: isGlobal ? IsNull() : assetId,
 | 
				
			||||||
        albumId,
 | 
					        albumId,
 | 
				
			||||||
        isLiked,
 | 
					        isLiked,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user