diff --git a/mobile/analysis_options.yaml b/mobile/analysis_options.yaml
index 230e2a0d77..05ce70ffe3 100644
--- a/mobile/analysis_options.yaml
+++ b/mobile/analysis_options.yaml
@@ -52,7 +52,6 @@ dart_code_metrics:
     - avoid-cascade-after-if-null
     - avoid-collapsible-if
     - avoid-collection-methods-with-unrelated-types
-    - avoid-declaring-call-method
     - avoid-double-slash-imports
     - avoid-duplicate-cascades
     - avoid-duplicate-patterns
diff --git a/mobile/dart_test.yaml b/mobile/dart_test.yaml
new file mode 100644
index 0000000000..fa54954090
--- /dev/null
+++ b/mobile/dart_test.yaml
@@ -0,0 +1,3 @@
+# Used to filter out tags from test runs
+tags:
+  widget:
diff --git a/mobile/lib/constants/errors.dart b/mobile/lib/constants/errors.dart
new file mode 100644
index 0000000000..3d1f775033
--- /dev/null
+++ b/mobile/lib/constants/errors.dart
@@ -0,0 +1,9 @@
+/// Base class which is used to check if an Exception is a custom exception
+sealed class ImmichErrors {
+  const ImmichErrors();
+}
+
+class NoResponseDtoError extends ImmichErrors implements Exception {
+  @override
+  String toString() => "Response Dto is null";
+}
diff --git a/mobile/lib/extensions/asyncvalue_extensions.dart b/mobile/lib/extensions/asyncvalue_extensions.dart
index 036881f3c2..c616835a81 100644
--- a/mobile/lib/extensions/asyncvalue_extensions.dart
+++ b/mobile/lib/extensions/asyncvalue_extensions.dart
@@ -7,6 +7,8 @@ import 'package:logging/logging.dart';
 extension LogOnError<T> on AsyncValue<T> {
   static final Logger _asyncErrorLogger = Logger("AsyncValue");
 
+  /// Used to return the [ImmichLoadingIndicator] and [ScaffoldErrorBody] widgets by default on loading
+  /// and error cases respectively
   Widget widgetWhen({
     bool skipLoadingOnRefresh = true,
     Widget Function()? onLoading,
@@ -28,8 +30,9 @@ extension LogOnError<T> on AsyncValue<T> {
     }
 
     if (hasError && !hasValue) {
-      _asyncErrorLogger.severe("Error occured", error, stackTrace);
-      return onError?.call(error, stackTrace) ?? const ScaffoldErrorBody();
+      _asyncErrorLogger.severe("$error", error, stackTrace);
+      return onError?.call(error, stackTrace) ??
+          ScaffoldErrorBody(errorMsg: error?.toString());
     }
 
     return onData(requireValue);
diff --git a/mobile/lib/extensions/build_context_extensions.dart b/mobile/lib/extensions/build_context_extensions.dart
index 6151bd1a5c..3b99718d79 100644
--- a/mobile/lib/extensions/build_context_extensions.dart
+++ b/mobile/lib/extensions/build_context_extensions.dart
@@ -1,4 +1,3 @@
-import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 
 extension ContextHelper on BuildContext {
@@ -34,21 +33,4 @@ extension ContextHelper on BuildContext {
 
   // Pop-out from the current context with optional result
   void pop<T>([T? result]) => Navigator.of(this).pop(result);
-
-  // Auto-Push new route from the current context
-  Future<T?> autoPush<T extends Object?>(PageRouteInfo<dynamic> route) =>
-      AutoRouter.of(this).push(route);
-
-  // Auto-Push navigate route from the current context
-  Future<dynamic> autoNavigate<T extends Object?>(
-    PageRouteInfo<dynamic> route,
-  ) =>
-      AutoRouter.of(this).navigate(route);
-
-  // Auto-Push replace route from the current context
-  Future<T?> autoReplace<T extends Object?>(PageRouteInfo<dynamic> route) =>
-      AutoRouter.of(this).replace(route);
-
-  // Auto-Pop from the current context
-  Future<bool> autoPop<T>([T? result]) => AutoRouter.of(this).pop(result);
 }
diff --git a/mobile/lib/extensions/datetime_extensions.dart b/mobile/lib/extensions/datetime_extensions.dart
index d918377711..14d89e2755 100644
--- a/mobile/lib/extensions/datetime_extensions.dart
+++ b/mobile/lib/extensions/datetime_extensions.dart
@@ -1,8 +1,9 @@
 extension TimeAgoExtension on DateTime {
+  /// Displays the time difference of this [DateTime] object to the current time as a [String]
   String timeAgo({bool numericDates = true}) {
     DateTime date = toLocal();
-    final date2 = DateTime.now().toLocal();
-    final difference = date2.difference(date);
+    final now = DateTime.now().toLocal();
+    final difference = now.difference(date);
 
     if (difference.inSeconds < 5) {
       return 'Just now';
diff --git a/mobile/lib/extensions/duration_extensions.dart b/mobile/lib/extensions/duration_extensions.dart
index 68fb1b0689..ca5ba8310c 100644
--- a/mobile/lib/extensions/duration_extensions.dart
+++ b/mobile/lib/extensions/duration_extensions.dart
@@ -1,4 +1,5 @@
 extension TZOffsetExtension on Duration {
+  /// Formats the duration in the format of ±HH:MM
   String formatAsOffset() =>
       "${isNegative ? '-' : '+'}${inHours.abs().toString().padLeft(2, '0')}:${inMinutes.abs().remainder(60).toString().padLeft(2, '0')}";
 }
diff --git a/mobile/lib/extensions/string_extensions.dart b/mobile/lib/extensions/string_extensions.dart
index a25ab4f508..67411013ee 100644
--- a/mobile/lib/extensions/string_extensions.dart
+++ b/mobile/lib/extensions/string_extensions.dart
@@ -9,6 +9,7 @@ extension StringExtension on String {
 }
 
 extension DurationExtension on String {
+  /// Parses and returns the string of format HH:MM:SS as a duration object else null
   Duration? toDuration() {
     try {
       final parts = split(':')
diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart
index a12c43b6ca..b46dee8f41 100644
--- a/mobile/lib/main.dart
+++ b/mobile/lib/main.dart
@@ -73,14 +73,14 @@ Future<void> initApp() async {
   FlutterError.onError = (details) {
     FlutterError.presentError(details);
     log.severe(
-      'Catch all error: ${details.toString()} - ${details.exception} - ${details.library} - ${details.context} - ${details.stack}',
+      'FlutterError - Catch all error: ${details.toString()} - ${details.exception} - ${details.library} - ${details.context} - ${details.stack}',
       details,
       details.stack,
     );
   };
 
   PlatformDispatcher.instance.onError = (error, stack) {
-    log.severe('Catch all error: ${error.toString()} - $error', error, stack);
+    log.severe('PlatformDispatcher - Catch all error: $error', error, stack);
     return true;
   };
 
diff --git a/mobile/lib/mixins/error_logger.mixin.dart b/mobile/lib/mixins/error_logger.mixin.dart
new file mode 100644
index 0000000000..38837a716f
--- /dev/null
+++ b/mobile/lib/mixins/error_logger.mixin.dart
@@ -0,0 +1,38 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:logging/logging.dart';
+
+typedef AsyncFuture<T> = Future<AsyncValue<T>>;
+
+mixin ErrorLoggerMixin {
+  abstract final Logger logger;
+
+  /// Returns an AsyncValue<T> if the future is successfully executed
+  /// Else, logs the error to the overrided logger and returns an AsyncError<>
+  AsyncFuture<T> guardError<T>(
+    Future<T> Function() fn, {
+    Level logLevel = Level.SEVERE,
+  }) async {
+    try {
+      final result = await fn();
+      return AsyncData(result);
+    } catch (error, stackTrace) {
+      logger.log(logLevel, "$error", error, stackTrace);
+      return AsyncError(error, stackTrace);
+    }
+  }
+
+  /// Returns the result of the future if success
+  /// Else, logs the error and returns the default value
+  Future<T> logError<T>(
+    Future<T> Function() fn, {
+    required T defaultValue,
+    Level logLevel = Level.SEVERE,
+  }) async {
+    try {
+      return await fn();
+    } catch (error, stackTrace) {
+      logger.log(logLevel, "$error", error, stackTrace);
+    }
+    return defaultValue;
+  }
+}
diff --git a/mobile/lib/module_template/ui/store_ui_here.txt b/mobile/lib/module_template/widgets/store_ui_here.txt
similarity index 100%
rename from mobile/lib/module_template/ui/store_ui_here.txt
rename to mobile/lib/module_template/widgets/store_ui_here.txt
diff --git a/mobile/lib/modules/activities/models/activity.model.dart b/mobile/lib/modules/activities/models/activity.model.dart
index 2db626f54f..8ac23975af 100644
--- a/mobile/lib/modules/activities/models/activity.model.dart
+++ b/mobile/lib/modules/activities/models/activity.model.dart
@@ -46,18 +46,7 @@ class Activity {
         type = dto.type == ActivityResponseDtoTypeEnum.comment
             ? ActivityType.comment
             : ActivityType.like,
-        user = User(
-          email: dto.user.email,
-          name: dto.user.name,
-          profileImagePath: dto.user.profileImagePath,
-          id: dto.user.id,
-          // Placeholder values
-          isAdmin: false,
-          updatedAt: DateTime.now(),
-          isPartnerSharedBy: false,
-          isPartnerSharedWith: false,
-          memoryEnabled: false,
-        );
+        user = User.fromSimpleUserDto(dto.user);
 
   @override
   String toString() {
@@ -65,11 +54,10 @@ class Activity {
   }
 
   @override
-  bool operator ==(Object other) {
+  bool operator ==(covariant Activity other) {
     if (identical(this, other)) return true;
 
-    return other is Activity &&
-        other.id == id &&
+    return other.id == id &&
         other.assetId == assetId &&
         other.comment == comment &&
         other.createdAt == createdAt &&
diff --git a/mobile/lib/modules/activities/providers/activity.provider.dart b/mobile/lib/modules/activities/providers/activity.provider.dart
index 9d8a3429b1..0eb174969a 100644
--- a/mobile/lib/modules/activities/providers/activity.provider.dart
+++ b/mobile/lib/modules/activities/providers/activity.provider.dart
@@ -1,134 +1,67 @@
-import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/modules/activities/models/activity.model.dart';
-import 'package:immich_mobile/modules/activities/services/activity.service.dart';
+import 'package:immich_mobile/modules/activities/providers/activity_service.provider.dart';
+import 'package:immich_mobile/modules/activities/providers/activity_statistics.provider.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
 
-class ActivityNotifier extends StateNotifier<AsyncValue<List<Activity>>> {
-  final Ref _ref;
-  final ActivityService _activityService;
-  final String albumId;
-  final String? assetId;
+part 'activity.provider.g.dart';
 
-  ActivityNotifier(
-    this._ref,
-    this._activityService,
-    this.albumId,
-    this.assetId,
-  ) : super(
-          const AsyncData([]),
-        ) {
-    fetchActivity();
-  }
-
-  Future<void> fetchActivity() async {
-    state = const AsyncLoading();
-    state = await AsyncValue.guard(
-      () => _activityService.getAllActivities(albumId, assetId),
-    );
+/// Maintains the current list of all activities for <share-album-id, asset>
+@riverpod
+class AlbumActivity extends _$AlbumActivity {
+  @override
+  Future<List<Activity>> build(String albumId, [String? assetId]) async {
+    return ref
+        .watch(activityServiceProvider)
+        .getAllActivities(albumId, assetId: assetId);
   }
 
   Future<void> removeActivity(String id) async {
-    final activities = state.asData?.value ?? [];
-    if (await _activityService.removeActivity(id)) {
+    if (await ref.watch(activityServiceProvider).removeActivity(id)) {
+      final activities = state.valueOrNull ?? [];
       final removedActivity = activities.firstWhere((a) => a.id == id);
       activities.remove(removedActivity);
       state = AsyncData(activities);
+      // Decrement activity count only for comments
       if (removedActivity.type == ActivityType.comment) {
-        _ref
-            .read(
-              activityStatisticsStateProvider(
-                (albumId: albumId, assetId: assetId),
-              ).notifier,
-            )
+        ref
+            .watch(activityStatisticsProvider(albumId, assetId).notifier)
             .removeActivity();
       }
     }
   }
 
-  Future<void> addComment(String comment) async {
-    final activity = await _activityService.addActivity(
-      albumId,
-      ActivityType.comment,
-      assetId: assetId,
-      comment: comment,
-    );
-
-    if (activity != null) {
+  Future<void> addLike() async {
+    final activity = await ref
+        .watch(activityServiceProvider)
+        .addActivity(albumId, ActivityType.like, assetId: assetId);
+    if (activity.hasValue) {
       final activities = state.asData?.value ?? [];
-      state = AsyncData([...activities, activity]);
-      _ref
-          .read(
-            activityStatisticsStateProvider(
-              (albumId: albumId, assetId: assetId),
-            ).notifier,
-          )
+      state = AsyncData([...activities, activity.requireValue]);
+    }
+  }
+
+  Future<void> addComment(String comment) async {
+    final activity = await ref.watch(activityServiceProvider).addActivity(
+          albumId,
+          ActivityType.comment,
+          assetId: assetId,
+          comment: comment,
+        );
+
+    if (activity.hasValue) {
+      final activities = state.valueOrNull ?? [];
+      state = AsyncData([...activities, activity.requireValue]);
+      ref
+          .watch(activityStatisticsProvider(albumId, assetId).notifier)
           .addActivity();
+      // The previous addActivity call would increase the count of an asset if assetId != null
+      // To also increase the activity count of the album, calling it once again with assetId set to null
       if (assetId != null) {
-        // Add a count to the current album's provider as well
-        _ref
-            .read(
-              activityStatisticsStateProvider(
-                (albumId: albumId, assetId: null),
-              ).notifier,
-            )
-            .addActivity();
+        ref.watch(activityStatisticsProvider(albumId).notifier).addActivity();
       }
     }
   }
-
-  Future<void> addLike() async {
-    final activity = await _activityService
-        .addActivity(albumId, ActivityType.like, assetId: assetId);
-    if (activity != null) {
-      final activities = state.asData?.value ?? [];
-      state = AsyncData([...activities, activity]);
-    }
-  }
 }
 
-class ActivityStatisticsNotifier extends StateNotifier<int> {
-  final String albumId;
-  final String? assetId;
-  final ActivityService _activityService;
-  ActivityStatisticsNotifier(this._activityService, this.albumId, this.assetId)
-      : super(0) {
-    fetchStatistics();
-  }
-
-  Future<void> fetchStatistics() async {
-    final count =
-        await _activityService.getStatistics(albumId, assetId: assetId);
-    if (mounted) {
-      state = count;
-    }
-  }
-
-  Future<void> addActivity() async {
-    state = state + 1;
-  }
-
-  Future<void> removeActivity() async {
-    state = state - 1;
-  }
-}
-
-typedef ActivityParams = ({String albumId, String? assetId});
-
-final activityStateProvider = StateNotifierProvider.autoDispose
-    .family<ActivityNotifier, AsyncValue<List<Activity>>, ActivityParams>(
-        (ref, args) {
-  return ActivityNotifier(
-    ref,
-    ref.watch(activityServiceProvider),
-    args.albumId,
-    args.assetId,
-  );
-});
-
-final activityStatisticsStateProvider = StateNotifierProvider.autoDispose
-    .family<ActivityStatisticsNotifier, int, ActivityParams>((ref, args) {
-  return ActivityStatisticsNotifier(
-    ref.watch(activityServiceProvider),
-    args.albumId,
-    args.assetId,
-  );
-});
+/// Mock class for testing
+abstract class AlbumActivityInternal extends _$AlbumActivity {}
diff --git a/mobile/lib/modules/activities/providers/activity.provider.g.dart b/mobile/lib/modules/activities/providers/activity.provider.g.dart
new file mode 100644
index 0000000000..e25c0c46bf
--- /dev/null
+++ b/mobile/lib/modules/activities/providers/activity.provider.g.dart
@@ -0,0 +1,209 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'activity.provider.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+String _$albumActivityHash() => r'3b0d7acee4d41c84b3f220784c3b904c83f836e6';
+
+/// Copied from Dart SDK
+class _SystemHash {
+  _SystemHash._();
+
+  static int combine(int hash, int value) {
+    // ignore: parameter_assignments
+    hash = 0x1fffffff & (hash + value);
+    // ignore: parameter_assignments
+    hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
+    return hash ^ (hash >> 6);
+  }
+
+  static int finish(int hash) {
+    // ignore: parameter_assignments
+    hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
+    // ignore: parameter_assignments
+    hash = hash ^ (hash >> 11);
+    return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
+  }
+}
+
+abstract class _$AlbumActivity
+    extends BuildlessAutoDisposeAsyncNotifier<List<Activity>> {
+  late final String albumId;
+  late final String? assetId;
+
+  Future<List<Activity>> build(
+    String albumId, [
+    String? assetId,
+  ]);
+}
+
+/// Maintains the current list of all activities for <share-album-id, asset>
+///
+/// Copied from [AlbumActivity].
+@ProviderFor(AlbumActivity)
+const albumActivityProvider = AlbumActivityFamily();
+
+/// Maintains the current list of all activities for <share-album-id, asset>
+///
+/// Copied from [AlbumActivity].
+class AlbumActivityFamily extends Family<AsyncValue<List<Activity>>> {
+  /// Maintains the current list of all activities for <share-album-id, asset>
+  ///
+  /// Copied from [AlbumActivity].
+  const AlbumActivityFamily();
+
+  /// Maintains the current list of all activities for <share-album-id, asset>
+  ///
+  /// Copied from [AlbumActivity].
+  AlbumActivityProvider call(
+    String albumId, [
+    String? assetId,
+  ]) {
+    return AlbumActivityProvider(
+      albumId,
+      assetId,
+    );
+  }
+
+  @override
+  AlbumActivityProvider getProviderOverride(
+    covariant AlbumActivityProvider provider,
+  ) {
+    return call(
+      provider.albumId,
+      provider.assetId,
+    );
+  }
+
+  static const Iterable<ProviderOrFamily>? _dependencies = null;
+
+  @override
+  Iterable<ProviderOrFamily>? get dependencies => _dependencies;
+
+  static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
+
+  @override
+  Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
+      _allTransitiveDependencies;
+
+  @override
+  String? get name => r'albumActivityProvider';
+}
+
+/// Maintains the current list of all activities for <share-album-id, asset>
+///
+/// Copied from [AlbumActivity].
+class AlbumActivityProvider extends AutoDisposeAsyncNotifierProviderImpl<
+    AlbumActivity, List<Activity>> {
+  /// Maintains the current list of all activities for <share-album-id, asset>
+  ///
+  /// Copied from [AlbumActivity].
+  AlbumActivityProvider(
+    String albumId, [
+    String? assetId,
+  ]) : this._internal(
+          () => AlbumActivity()
+            ..albumId = albumId
+            ..assetId = assetId,
+          from: albumActivityProvider,
+          name: r'albumActivityProvider',
+          debugGetCreateSourceHash:
+              const bool.fromEnvironment('dart.vm.product')
+                  ? null
+                  : _$albumActivityHash,
+          dependencies: AlbumActivityFamily._dependencies,
+          allTransitiveDependencies:
+              AlbumActivityFamily._allTransitiveDependencies,
+          albumId: albumId,
+          assetId: assetId,
+        );
+
+  AlbumActivityProvider._internal(
+    super._createNotifier, {
+    required super.name,
+    required super.dependencies,
+    required super.allTransitiveDependencies,
+    required super.debugGetCreateSourceHash,
+    required super.from,
+    required this.albumId,
+    required this.assetId,
+  }) : super.internal();
+
+  final String albumId;
+  final String? assetId;
+
+  @override
+  Future<List<Activity>> runNotifierBuild(
+    covariant AlbumActivity notifier,
+  ) {
+    return notifier.build(
+      albumId,
+      assetId,
+    );
+  }
+
+  @override
+  Override overrideWith(AlbumActivity Function() create) {
+    return ProviderOverride(
+      origin: this,
+      override: AlbumActivityProvider._internal(
+        () => create()
+          ..albumId = albumId
+          ..assetId = assetId,
+        from: from,
+        name: null,
+        dependencies: null,
+        allTransitiveDependencies: null,
+        debugGetCreateSourceHash: null,
+        albumId: albumId,
+        assetId: assetId,
+      ),
+    );
+  }
+
+  @override
+  AutoDisposeAsyncNotifierProviderElement<AlbumActivity, List<Activity>>
+      createElement() {
+    return _AlbumActivityProviderElement(this);
+  }
+
+  @override
+  bool operator ==(Object other) {
+    return other is AlbumActivityProvider &&
+        other.albumId == albumId &&
+        other.assetId == assetId;
+  }
+
+  @override
+  int get hashCode {
+    var hash = _SystemHash.combine(0, runtimeType.hashCode);
+    hash = _SystemHash.combine(hash, albumId.hashCode);
+    hash = _SystemHash.combine(hash, assetId.hashCode);
+
+    return _SystemHash.finish(hash);
+  }
+}
+
+mixin AlbumActivityRef on AutoDisposeAsyncNotifierProviderRef<List<Activity>> {
+  /// The parameter `albumId` of this provider.
+  String get albumId;
+
+  /// The parameter `assetId` of this provider.
+  String? get assetId;
+}
+
+class _AlbumActivityProviderElement
+    extends AutoDisposeAsyncNotifierProviderElement<AlbumActivity,
+        List<Activity>> with AlbumActivityRef {
+  _AlbumActivityProviderElement(super.provider);
+
+  @override
+  String get albumId => (origin as AlbumActivityProvider).albumId;
+  @override
+  String? get assetId => (origin as AlbumActivityProvider).assetId;
+}
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
diff --git a/mobile/lib/modules/activities/providers/activity_service.provider.dart b/mobile/lib/modules/activities/providers/activity_service.provider.dart
new file mode 100644
index 0000000000..53f83cbf36
--- /dev/null
+++ b/mobile/lib/modules/activities/providers/activity_service.provider.dart
@@ -0,0 +1,9 @@
+import 'package:immich_mobile/modules/activities/services/activity.service.dart';
+import 'package:immich_mobile/shared/providers/api.provider.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+part 'activity_service.provider.g.dart';
+
+@riverpod
+ActivityService activityService(ActivityServiceRef ref) =>
+    ActivityService(ref.watch(apiServiceProvider));
diff --git a/mobile/lib/modules/activities/providers/activity_service.provider.g.dart b/mobile/lib/modules/activities/providers/activity_service.provider.g.dart
new file mode 100644
index 0000000000..8e5ef43260
--- /dev/null
+++ b/mobile/lib/modules/activities/providers/activity_service.provider.g.dart
@@ -0,0 +1,25 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'activity_service.provider.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+String _$activityServiceHash() => r'5dd4955d14f5bf01c00d7f8750d07e7ace7cc4b0';
+
+/// See also [activityService].
+@ProviderFor(activityService)
+final activityServiceProvider = AutoDisposeProvider<ActivityService>.internal(
+  activityService,
+  name: r'activityServiceProvider',
+  debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
+      ? null
+      : _$activityServiceHash,
+  dependencies: null,
+  allTransitiveDependencies: null,
+);
+
+typedef ActivityServiceRef = AutoDisposeProviderRef<ActivityService>;
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
diff --git a/mobile/lib/modules/activities/providers/activity_statistics.provider.dart b/mobile/lib/modules/activities/providers/activity_statistics.provider.dart
new file mode 100644
index 0000000000..fc2a291a1d
--- /dev/null
+++ b/mobile/lib/modules/activities/providers/activity_statistics.provider.dart
@@ -0,0 +1,24 @@
+import 'package:immich_mobile/modules/activities/providers/activity_service.provider.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+part 'activity_statistics.provider.g.dart';
+
+/// Maintains the current number of comments by <shared-album, asset>
+@riverpod
+class ActivityStatistics extends _$ActivityStatistics {
+  @override
+  int build(String albumId, [String? assetId]) {
+    ref
+        .watch(activityServiceProvider)
+        .getStatistics(albumId, assetId: assetId)
+        .then((comments) => state = comments);
+    return 0;
+  }
+
+  void addActivity() => state = state + 1;
+
+  void removeActivity() => state = state - 1;
+}
+
+/// Mock class for testing
+abstract class ActivityStatisticsInternal extends _$ActivityStatistics {}
diff --git a/mobile/lib/modules/activities/providers/activity_statistics.provider.g.dart b/mobile/lib/modules/activities/providers/activity_statistics.provider.g.dart
new file mode 100644
index 0000000000..79856c525b
--- /dev/null
+++ b/mobile/lib/modules/activities/providers/activity_statistics.provider.g.dart
@@ -0,0 +1,208 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'activity_statistics.provider.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+String _$activityStatisticsHash() =>
+    r'a5f7bbee1891c33b72919a34e632ca9ef9cd8dbf';
+
+/// Copied from Dart SDK
+class _SystemHash {
+  _SystemHash._();
+
+  static int combine(int hash, int value) {
+    // ignore: parameter_assignments
+    hash = 0x1fffffff & (hash + value);
+    // ignore: parameter_assignments
+    hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
+    return hash ^ (hash >> 6);
+  }
+
+  static int finish(int hash) {
+    // ignore: parameter_assignments
+    hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
+    // ignore: parameter_assignments
+    hash = hash ^ (hash >> 11);
+    return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
+  }
+}
+
+abstract class _$ActivityStatistics extends BuildlessAutoDisposeNotifier<int> {
+  late final String albumId;
+  late final String? assetId;
+
+  int build(
+    String albumId, [
+    String? assetId,
+  ]);
+}
+
+/// Maintains the current number of comments by <shared-album, asset>
+///
+/// Copied from [ActivityStatistics].
+@ProviderFor(ActivityStatistics)
+const activityStatisticsProvider = ActivityStatisticsFamily();
+
+/// Maintains the current number of comments by <shared-album, asset>
+///
+/// Copied from [ActivityStatistics].
+class ActivityStatisticsFamily extends Family<int> {
+  /// Maintains the current number of comments by <shared-album, asset>
+  ///
+  /// Copied from [ActivityStatistics].
+  const ActivityStatisticsFamily();
+
+  /// Maintains the current number of comments by <shared-album, asset>
+  ///
+  /// Copied from [ActivityStatistics].
+  ActivityStatisticsProvider call(
+    String albumId, [
+    String? assetId,
+  ]) {
+    return ActivityStatisticsProvider(
+      albumId,
+      assetId,
+    );
+  }
+
+  @override
+  ActivityStatisticsProvider getProviderOverride(
+    covariant ActivityStatisticsProvider provider,
+  ) {
+    return call(
+      provider.albumId,
+      provider.assetId,
+    );
+  }
+
+  static const Iterable<ProviderOrFamily>? _dependencies = null;
+
+  @override
+  Iterable<ProviderOrFamily>? get dependencies => _dependencies;
+
+  static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
+
+  @override
+  Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
+      _allTransitiveDependencies;
+
+  @override
+  String? get name => r'activityStatisticsProvider';
+}
+
+/// Maintains the current number of comments by <shared-album, asset>
+///
+/// Copied from [ActivityStatistics].
+class ActivityStatisticsProvider
+    extends AutoDisposeNotifierProviderImpl<ActivityStatistics, int> {
+  /// Maintains the current number of comments by <shared-album, asset>
+  ///
+  /// Copied from [ActivityStatistics].
+  ActivityStatisticsProvider(
+    String albumId, [
+    String? assetId,
+  ]) : this._internal(
+          () => ActivityStatistics()
+            ..albumId = albumId
+            ..assetId = assetId,
+          from: activityStatisticsProvider,
+          name: r'activityStatisticsProvider',
+          debugGetCreateSourceHash:
+              const bool.fromEnvironment('dart.vm.product')
+                  ? null
+                  : _$activityStatisticsHash,
+          dependencies: ActivityStatisticsFamily._dependencies,
+          allTransitiveDependencies:
+              ActivityStatisticsFamily._allTransitiveDependencies,
+          albumId: albumId,
+          assetId: assetId,
+        );
+
+  ActivityStatisticsProvider._internal(
+    super._createNotifier, {
+    required super.name,
+    required super.dependencies,
+    required super.allTransitiveDependencies,
+    required super.debugGetCreateSourceHash,
+    required super.from,
+    required this.albumId,
+    required this.assetId,
+  }) : super.internal();
+
+  final String albumId;
+  final String? assetId;
+
+  @override
+  int runNotifierBuild(
+    covariant ActivityStatistics notifier,
+  ) {
+    return notifier.build(
+      albumId,
+      assetId,
+    );
+  }
+
+  @override
+  Override overrideWith(ActivityStatistics Function() create) {
+    return ProviderOverride(
+      origin: this,
+      override: ActivityStatisticsProvider._internal(
+        () => create()
+          ..albumId = albumId
+          ..assetId = assetId,
+        from: from,
+        name: null,
+        dependencies: null,
+        allTransitiveDependencies: null,
+        debugGetCreateSourceHash: null,
+        albumId: albumId,
+        assetId: assetId,
+      ),
+    );
+  }
+
+  @override
+  AutoDisposeNotifierProviderElement<ActivityStatistics, int> createElement() {
+    return _ActivityStatisticsProviderElement(this);
+  }
+
+  @override
+  bool operator ==(Object other) {
+    return other is ActivityStatisticsProvider &&
+        other.albumId == albumId &&
+        other.assetId == assetId;
+  }
+
+  @override
+  int get hashCode {
+    var hash = _SystemHash.combine(0, runtimeType.hashCode);
+    hash = _SystemHash.combine(hash, albumId.hashCode);
+    hash = _SystemHash.combine(hash, assetId.hashCode);
+
+    return _SystemHash.finish(hash);
+  }
+}
+
+mixin ActivityStatisticsRef on AutoDisposeNotifierProviderRef<int> {
+  /// The parameter `albumId` of this provider.
+  String get albumId;
+
+  /// The parameter `assetId` of this provider.
+  String? get assetId;
+}
+
+class _ActivityStatisticsProviderElement
+    extends AutoDisposeNotifierProviderElement<ActivityStatistics, int>
+    with ActivityStatisticsRef {
+  _ActivityStatisticsProviderElement(super.provider);
+
+  @override
+  String get albumId => (origin as ActivityStatisticsProvider).albumId;
+  @override
+  String? get assetId => (origin as ActivityStatisticsProvider).assetId;
+}
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
diff --git a/mobile/lib/modules/activities/services/activity.service.dart b/mobile/lib/modules/activities/services/activity.service.dart
index fce77a1963..db35c17aee 100644
--- a/mobile/lib/modules/activities/services/activity.service.dart
+++ b/mobile/lib/modules/activities/services/activity.service.dart
@@ -1,67 +1,60 @@
-import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/constants/errors.dart';
+import 'package:immich_mobile/mixins/error_logger.mixin.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 {
+class ActivityService with ErrorLoggerMixin {
   final ApiService _apiService;
-  final Logger _log = Logger("ActivityService");
+
+  @override
+  final Logger logger = Logger("ActivityService");
 
   ActivityService(this._apiService);
 
   Future<List<Activity>> getAllActivities(
-    String albumId,
+    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;
-    }
+  }) async {
+    return logError(
+      () async {
+        final list = await _apiService.activityApi
+            .getActivities(albumId, assetId: assetId);
+        return list != null ? list.map(Activity.fromDto).toList() : [];
+      },
+      defaultValue: [],
+    );
   }
 
   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;
+    return logError(
+      () async {
+        final dto = await _apiService.activityApi
+            .getActivityStatistics(albumId, assetId: assetId);
+        return dto?.comments ?? 0;
+      },
+      defaultValue: 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;
+    return logError(
+      () async {
+        await _apiService.activityApi.deleteActivity(id);
+        return true;
+      },
+      defaultValue: false,
+    );
   }
 
-  Future<Activity?> addActivity(
+  AsyncFuture<Activity> addActivity(
     String albumId,
     ActivityType type, {
     String? assetId,
     String? comment,
   }) async {
-    try {
+    return guardError(() async {
       final dto = await _apiService.activityApi.createActivity(
         ActivityCreateDto(
           albumId: albumId,
@@ -75,11 +68,7 @@ class ActivityService {
       if (dto != null) {
         return Activity.fromDto(dto);
       }
-    } catch (e) {
-      _log.severe(
-        "failed to add activity for albumId - $albumId; assetId - $assetId -> $e",
-      );
-    }
-    return null;
+      throw NoResponseDtoError();
+    });
   }
 }
diff --git a/mobile/lib/modules/activities/views/activities_page.dart b/mobile/lib/modules/activities/views/activities_page.dart
index f0c68a3491..d908d83c12 100644
--- a/mobile/lib/modules/activities/views/activities_page.dart
+++ b/mobile/lib/modules/activities/views/activities_page.dart
@@ -1,6 +1,4 @@
-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';
@@ -8,236 +6,51 @@ import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.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/user_circle_avatar.dart';
-import 'package:immich_mobile/extensions/datetime_extensions.dart';
-import 'package:immich_mobile/utils/image_url_builder.dart';
+import 'package:immich_mobile/modules/activities/widgets/activity_text_field.dart';
+import 'package:immich_mobile/modules/activities/widgets/activity_tile.dart';
+import 'package:immich_mobile/modules/activities/widgets/dismissible_activity.dart';
+import 'package:immich_mobile/modules/album/providers/current_album.provider.dart';
+import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart';
+import 'package:immich_mobile/shared/providers/user.provider.dart';
 
 class ActivitiesPage extends HookConsumerWidget {
-  final String albumId;
-  final String? assetId;
-  final bool withAssetThumbs;
-  final String appBarTitle;
-  final bool isOwner;
-  final bool isReadOnly;
-  const ActivitiesPage(
-    this.albumId, {
-    this.appBarTitle = "",
-    this.assetId,
-    this.withAssetThumbs = true,
-    this.isOwner = false,
-    this.isReadOnly = false,
+  const ActivitiesPage({
     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();
+    // Album has to be set in the provider before reaching this page
+    final album = ref.watch(currentAlbumProvider)!;
+    final asset = ref.watch(currentAssetProvider);
+    final user = ref.watch(currentUserProvider);
+
+    final activityNotifier = ref
+        .read(albumActivityProvider(album.remoteId!, asset?.remoteId).notifier);
+    final activities =
+        ref.watch(albumActivityProvider(album.remoteId!, asset?.remoteId));
+
     final listViewScrollController = useScrollController();
-    final currentUser = Store.tryGet(StoreKey.currentUser);
 
-    useEffect(
-      () {
-        inputFocusNode.requestFocus();
-        return null;
-      },
-      [],
-    );
-
-    buildTitleWithTimestamp(Activity activity, {bool leftAlign = true}) {
-      final textColor = context.isDarkTheme ? Colors.white : Colors.black;
-      final textStyle = 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.name,
-            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: const BorderRadius.all(Radius.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,
-          enabled: !isReadOnly,
-          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: isReadOnly
-                ? 'shared_album_activities_input_disable'.tr()
-                : '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,
+    Future<void> onAddComment(String comment) async {
+      await activityNotifier.addComment(comment);
+      // Scroll to the end of the list to show the newly added activity
+      listViewScrollController.animateTo(
+        listViewScrollController.position.maxScrollExtent + 200,
+        duration: const Duration(milliseconds: 600),
+        curve: Curves.fastOutSlowIn,
       );
     }
 
     return Scaffold(
-      appBar: AppBar(title: Text(appBarTitle)),
+      appBar: AppBar(title: asset == null ? Text(album.name) : null),
       body: activities.widgetWhen(
         onData: (data) {
           final liked = data.firstWhereOrNull(
             (a) =>
                 a.type == ActivityType.like &&
-                a.user.id == currentUser?.id &&
-                a.assetId == assetId,
+                a.user.id == user?.id &&
+                a.assetId == asset?.remoteId,
           );
 
           return SafeArea(
@@ -245,9 +58,10 @@ class ActivitiesPage extends HookConsumerWidget {
               children: [
                 ListView.builder(
                   controller: listViewScrollController,
+                  // +1 to display an additional over-scroll space after the last element
                   itemCount: data.length + 1,
                   itemBuilder: (context, index) {
-                    // Vertical gap after the last element
+                    // Additional vertical gap after the last element
                     if (index == data.length) {
                       return const SizedBox(
                         height: 80,
@@ -255,45 +69,19 @@ class ActivitiesPage extends HookConsumerWidget {
                     }
 
                     final activity = data[index];
-                    final canDelete =
-                        activity.user.id == currentUser?.id || isOwner;
+                    final canDelete = activity.user.id == user?.id ||
+                        album.ownerId == user?.id;
 
                     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,
-                            ),
+                      child: DismissibleActivity(
+                        activity.id,
+                        ActivityTile(activity),
+                        onDismiss: canDelete
+                            ? (activityId) async => await activityNotifier
+                                .removeActivity(activity.id)
+                            : null,
+                      ),
                     );
                   },
                 ),
@@ -301,7 +89,11 @@ class ActivitiesPage extends HookConsumerWidget {
                   alignment: Alignment.bottomCenter,
                   child: Container(
                     color: context.scaffoldBackgroundColor,
-                    child: buildTextField(liked?.id),
+                    child: ActivityTextField(
+                      isEnabled: album.activityEnabled,
+                      likeId: liked?.id,
+                      onSubmit: onAddComment,
+                    ),
                   ),
                 ),
               ],
diff --git a/mobile/lib/modules/activities/widgets/activity_text_field.dart b/mobile/lib/modules/activities/widgets/activity_text_field.dart
new file mode 100644
index 0000000000..1d50fafaa5
--- /dev/null
+++ b/mobile/lib/modules/activities/widgets/activity_text_field.dart
@@ -0,0 +1,105 @@
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.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/current_album.provider.dart';
+import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart';
+import 'package:immich_mobile/shared/providers/user.provider.dart';
+import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
+
+class ActivityTextField extends HookConsumerWidget {
+  final bool isEnabled;
+  final String? likeId;
+  final Function(String) onSubmit;
+
+  const ActivityTextField({
+    required this.onSubmit,
+    this.isEnabled = true,
+    this.likeId,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final album = ref.watch(currentAlbumProvider)!;
+    final asset = ref.watch(currentAssetProvider);
+    final activityNotifier = ref
+        .read(albumActivityProvider(album.remoteId!, asset?.remoteId).notifier);
+    final user = ref.watch(currentUserProvider);
+    final inputController = useTextEditingController();
+    final inputFocusNode = useFocusNode();
+    final liked = likeId != null;
+
+    // Show keyboard immediately on activities open
+    useEffect(
+      () {
+        inputFocusNode.requestFocus();
+        return null;
+      },
+      [],
+    );
+
+    // Pass text to callback and reset controller
+    void onEditingComplete() {
+      onSubmit(inputController.text);
+      inputController.clear();
+      inputFocusNode.unfocus();
+    }
+
+    Future<void> addLike() async {
+      await activityNotifier.addLike();
+    }
+
+    Future<void> removeLike() async {
+      if (liked) {
+        await activityNotifier.removeActivity(likeId!);
+      }
+    }
+
+    return Padding(
+      padding: const EdgeInsets.only(bottom: 10),
+      child: TextField(
+        controller: inputController,
+        enabled: isEnabled,
+        focusNode: inputFocusNode,
+        textInputAction: TextInputAction.send,
+        autofocus: false,
+        decoration: InputDecoration(
+          border: InputBorder.none,
+          focusedBorder: InputBorder.none,
+          prefixIcon: user != null
+              ? Padding(
+                  padding: const EdgeInsets.symmetric(horizontal: 15),
+                  child: UserCircleAvatar(
+                    user: user,
+                    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: liked ? removeLike : addLike,
+            ),
+          ),
+          suffixIconColor: liked ? Colors.red[700] : null,
+          hintText: !isEnabled
+              ? 'shared_album_activities_input_disable'.tr()
+              : 'shared_album_activities_input_hint'.tr(),
+          hintStyle: TextStyle(
+            fontWeight: FontWeight.normal,
+            fontSize: 14,
+            color: Colors.grey[600],
+          ),
+        ),
+        onEditingComplete: onEditingComplete,
+        onTapOutside: (_) => inputFocusNode.unfocus(),
+      ),
+    );
+  }
+}
diff --git a/mobile/lib/modules/activities/widgets/activity_tile.dart b/mobile/lib/modules/activities/widgets/activity_tile.dart
new file mode 100644
index 0000000000..da5dacd58a
--- /dev/null
+++ b/mobile/lib/modules/activities/widgets/activity_tile.dart
@@ -0,0 +1,116 @@
+import 'package:flutter/material.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/extensions/datetime_extensions.dart';
+import 'package:immich_mobile/modules/activities/models/activity.model.dart';
+import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart';
+import 'package:immich_mobile/shared/ui/immich_image.dart';
+import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
+
+class ActivityTile extends HookConsumerWidget {
+  final Activity activity;
+
+  const ActivityTile(this.activity, {super.key});
+
+  @override
+  Widget build(BuildContext context, WidgetRef ref) {
+    final asset = ref.watch(currentAssetProvider);
+    final isLike = activity.type == ActivityType.like;
+    // Asset thumbnail is displayed when we are accessing activities from the album page
+    // currentAssetProvider will not be set until we open the gallery viewer
+    final showAssetThumbnail = asset == null && activity.assetId != null;
+
+    return ListTile(
+      minVerticalPadding: 15,
+      leading: isLike
+          ? Container(
+              width: 44,
+              alignment: Alignment.center,
+              child: Icon(
+                Icons.favorite_rounded,
+                color: Colors.red[700],
+              ),
+            )
+          : UserCircleAvatar(user: activity.user),
+      title: _ActivityTitle(
+        userName: activity.user.name,
+        createdAt: activity.createdAt.timeAgo(),
+        leftAlign: isLike || showAssetThumbnail,
+      ),
+      // No subtitle for like, so center title
+      titleAlignment:
+          !isLike ? ListTileTitleAlignment.top : ListTileTitleAlignment.center,
+      trailing: showAssetThumbnail
+          ? _ActivityAssetThumbnail(activity.assetId!)
+          : null,
+      subtitle: !isLike ? Text(activity.comment!) : null,
+    );
+  }
+}
+
+class _ActivityTitle extends StatelessWidget {
+  final String userName;
+  final String createdAt;
+  final bool leftAlign;
+
+  const _ActivityTitle({
+    required this.userName,
+    required this.createdAt,
+    required this.leftAlign,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    final textColor = context.isDarkTheme ? Colors.white : Colors.black;
+    final textStyle = 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(
+          userName,
+          style: textStyle,
+          overflow: TextOverflow.ellipsis,
+        ),
+        if (leftAlign)
+          Text(
+            " • ",
+            style: textStyle,
+          ),
+        Expanded(
+          child: Text(
+            createdAt,
+            style: textStyle,
+            overflow: TextOverflow.ellipsis,
+            textAlign: leftAlign ? TextAlign.left : TextAlign.right,
+          ),
+        ),
+      ],
+    );
+  }
+}
+
+class _ActivityAssetThumbnail extends StatelessWidget {
+  final String assetId;
+
+  const _ActivityAssetThumbnail(this.assetId);
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      width: 40,
+      height: 30,
+      decoration: BoxDecoration(
+        borderRadius: const BorderRadius.all(Radius.circular(4)),
+        image: DecorationImage(
+          image: ImmichImage.remoteThumbnailProviderForId(assetId),
+          fit: BoxFit.cover,
+        ),
+      ),
+      child: const SizedBox.shrink(),
+    );
+  }
+}
diff --git a/mobile/lib/modules/activities/widgets/dismissible_activity.dart b/mobile/lib/modules/activities/widgets/dismissible_activity.dart
new file mode 100644
index 0000000000..15e85f7144
--- /dev/null
+++ b/mobile/lib/modules/activities/widgets/dismissible_activity.dart
@@ -0,0 +1,75 @@
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/modules/activities/widgets/activity_tile.dart';
+import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
+
+/// Wraps an [ActivityTile] and makes it dismissible
+class DismissibleActivity extends StatelessWidget {
+  final String activityId;
+  final ActivityTile body;
+  final Function(String)? onDismiss;
+
+  const DismissibleActivity(
+    this.activityId,
+    this.body, {
+    this.onDismiss,
+    super.key,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return Dismissible(
+      key: Key(activityId),
+      dismissThresholds: const {
+        DismissDirection.horizontal: 0.7,
+      },
+      direction: DismissDirection.horizontal,
+      confirmDismiss: (direction) => onDismiss != null
+          ? 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: (_) async => onDismiss?.call(activityId),
+      // LTR
+      background: _DismissBackground(withDeleteIcon: onDismiss != null),
+      // RTL
+      secondaryBackground: _DismissBackground(
+        withDeleteIcon: onDismiss != null,
+        alignment: AlignmentDirectional.centerEnd,
+      ),
+      child: body,
+    );
+  }
+}
+
+class _DismissBackground extends StatelessWidget {
+  final AlignmentDirectional alignment;
+  final bool withDeleteIcon;
+
+  const _DismissBackground({
+    required this.withDeleteIcon,
+    this.alignment = AlignmentDirectional.centerStart,
+  });
+
+  @override
+  Widget build(BuildContext context) {
+    return Container(
+      alignment: alignment,
+      color: withDeleteIcon ? Colors.red[400] : Colors.grey[600],
+      child: withDeleteIcon
+          ? const Padding(
+              padding: EdgeInsets.all(15),
+              child: Icon(
+                Icons.delete_sweep_rounded,
+                color: Colors.black,
+              ),
+            )
+          : null,
+    );
+  }
+}
diff --git a/mobile/lib/modules/album/providers/album_sort_by_options.provider.g.dart b/mobile/lib/modules/album/providers/album_sort_by_options.provider.g.dart
index d97057ebdd..9a05bb6c7d 100644
--- a/mobile/lib/modules/album/providers/album_sort_by_options.provider.g.dart
+++ b/mobile/lib/modules/album/providers/album_sort_by_options.provider.g.dart
@@ -7,7 +7,7 @@ part of 'album_sort_by_options.provider.dart';
 // **************************************************************************
 
 String _$albumSortByOptionsHash() =>
-    r'8d22fa8b7cbca2d3d7ed20a83bf00211dc948004';
+    r'dd8da5e730af555de1b86c3b157b6c93183523ac';
 
 /// See also [AlbumSortByOptions].
 @ProviderFor(AlbumSortByOptions)
diff --git a/mobile/lib/modules/album/providers/current_album.provider.dart b/mobile/lib/modules/album/providers/current_album.provider.dart
index 9c72b5e3d2..30e75cda5c 100644
--- a/mobile/lib/modules/album/providers/current_album.provider.dart
+++ b/mobile/lib/modules/album/providers/current_album.provider.dart
@@ -1,6 +1,15 @@
-import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/shared/models/album.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
 
-final currentAlbumProvider = StateProvider<Album?>((ref) {
-  return null;
-});
+part 'current_album.provider.g.dart';
+
+@riverpod
+class CurrentAlbum extends _$CurrentAlbum {
+  @override
+  Album? build() => null;
+
+  void set(Album? a) => state = a;
+}
+
+/// Mock class for testing
+abstract class CurrentAlbumInternal extends _$CurrentAlbum {}
diff --git a/mobile/lib/modules/album/providers/current_album.provider.g.dart b/mobile/lib/modules/album/providers/current_album.provider.g.dart
new file mode 100644
index 0000000000..50e8854637
--- /dev/null
+++ b/mobile/lib/modules/album/providers/current_album.provider.g.dart
@@ -0,0 +1,25 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'current_album.provider.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+String _$currentAlbumHash() => r'61f00273d6b69da45add1532cc3d3a076ee55110';
+
+/// See also [CurrentAlbum].
+@ProviderFor(CurrentAlbum)
+final currentAlbumProvider =
+    AutoDisposeNotifierProvider<CurrentAlbum, Album?>.internal(
+  CurrentAlbum.new,
+  name: r'currentAlbumProvider',
+  debugGetCreateSourceHash:
+      const bool.fromEnvironment('dart.vm.product') ? null : _$currentAlbumHash,
+  dependencies: null,
+  allTransitiveDependencies: null,
+);
+
+typedef _$CurrentAlbum = AutoDisposeNotifier<Album?>;
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
diff --git a/mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart b/mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart
index 2f1e6b1aae..01eef96261 100644
--- a/mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart
+++ b/mobile/lib/modules/album/ui/add_to_album_bottom_sheet.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
@@ -104,7 +105,7 @@ class AddToAlbumBottomSheet extends HookConsumerWidget {
                           style: TextStyle(color: context.primaryColor),
                         ),
                         onPressed: () {
-                          context.autoPush(
+                          context.pushRoute(
                             CreateAlbumRoute(
                               isSharedAlbum: false,
                               initialAssets: assets,
diff --git a/mobile/lib/modules/album/ui/album_thumbnail_listtile.dart b/mobile/lib/modules/album/ui/album_thumbnail_listtile.dart
index adf8633605..f2219604b0 100644
--- a/mobile/lib/modules/album/ui/album_thumbnail_listtile.dart
+++ b/mobile/lib/modules/album/ui/album_thumbnail_listtile.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:cached_network_image/cached_network_image.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
@@ -60,7 +61,7 @@ class AlbumThumbnailListTile extends StatelessWidget {
       behavior: HitTestBehavior.opaque,
       onTap: onTap ??
           () {
-            context.autoPush(AlbumViewerRoute(albumId: album.id));
+            context.pushRoute(AlbumViewerRoute(albumId: album.id));
           },
       child: Padding(
         padding: const EdgeInsets.only(bottom: 12.0),
diff --git a/mobile/lib/modules/album/ui/album_viewer_appbar.dart b/mobile/lib/modules/album/ui/album_viewer_appbar.dart
index 4025e4a210..1753d3f193 100644
--- a/mobile/lib/modules/album/ui/album_viewer_appbar.dart
+++ b/mobile/lib/modules/album/ui/album_viewer_appbar.dart
@@ -1,9 +1,10 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
-import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
+import 'package:immich_mobile/modules/activities/providers/activity_statistics.provider.dart';
 import 'package:immich_mobile/modules/album/providers/album.provider.dart';
 import 'package:immich_mobile/modules/album/providers/album_viewer.provider.dart';
 import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
@@ -37,11 +38,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
     final isEditAlbum = ref.watch(albumViewerProvider).isEditAlbum;
     final isProcessing = useProcessingOverlay();
     final comments = album.shared
-        ? ref.watch(
-            activityStatisticsStateProvider(
-              (albumId: album.remoteId!, assetId: null),
-            ),
-          )
+        ? ref.watch(activityStatisticsProvider(album.remoteId!))
         : 0;
 
     deleteAlbum() async {
@@ -52,11 +49,11 @@ class AlbumViewerAppbar extends HookConsumerWidget
         success =
             await ref.watch(sharedAlbumProvider.notifier).deleteAlbum(album);
         context
-            .autoNavigate(const TabControllerRoute(children: [SharingRoute()]));
+            .navigateTo(const TabControllerRoute(children: [SharingRoute()]));
       } else {
         success = await ref.watch(albumProvider.notifier).deleteAlbum(album);
         context
-            .autoNavigate(const TabControllerRoute(children: [LibraryRoute()]));
+            .navigateTo(const TabControllerRoute(children: [LibraryRoute()]));
       }
       if (!success) {
         ImmichToast.show(
@@ -122,7 +119,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
 
       if (isSuccess) {
         context
-            .autoNavigate(const TabControllerRoute(children: [SharingRoute()]));
+            .navigateTo(const TabControllerRoute(children: [SharingRoute()]));
       } else {
         context.pop();
         ImmichToast.show(
@@ -175,7 +172,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
         ListTile(
           leading: const Icon(Icons.share_rounded),
           onTap: () {
-            context.autoPush(SharedLinkEditRoute(albumId: album.remoteId));
+            context.pushRoute(SharedLinkEditRoute(albumId: album.remoteId));
             context.pop();
           },
           title: const Text(
@@ -185,7 +182,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
         ),
         ListTile(
           leading: const Icon(Icons.settings_rounded),
-          onTap: () => context.autoNavigate(AlbumOptionsRoute(album: album)),
+          onTap: () => context.navigateTo(AlbumOptionsRoute(album: album)),
           title: const Text(
             "translated_text_options",
             style: TextStyle(fontWeight: FontWeight.w500),
@@ -280,7 +277,7 @@ class AlbumViewerAppbar extends HookConsumerWidget
         );
       } else {
         return IconButton(
-          onPressed: () async => await context.autoPop(),
+          onPressed: () async => await context.popRoute(),
           icon: const Icon(Icons.arrow_back_ios_rounded),
           splashRadius: 25,
         );
diff --git a/mobile/lib/modules/album/views/album_options_part.dart b/mobile/lib/modules/album/views/album_options_part.dart
index 6ef7733392..4e07d3c0e5 100644
--- a/mobile/lib/modules/album/views/album_options_part.dart
+++ b/mobile/lib/modules/album/views/album_options_part.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
@@ -45,7 +46,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
             await ref.read(sharedAlbumProvider.notifier).leaveAlbum(album);
 
         if (isSuccess) {
-          context.autoNavigate(
+          context.navigateTo(
             const TabControllerRoute(children: [SharingRoute()]),
           );
         } else {
@@ -181,7 +182,7 @@ class AlbumOptionsPage extends HookConsumerWidget {
       appBar: AppBar(
         leading: IconButton(
           icon: const Icon(Icons.arrow_back_ios_new_rounded),
-          onPressed: () => context.autoPop(null),
+          onPressed: () => context.popRoute(null),
         ),
         centerTitle: true,
         title: Text("translated_text_options".tr()),
diff --git a/mobile/lib/modules/album/views/album_viewer_page.dart b/mobile/lib/modules/album/views/album_viewer_page.dart
index e09649bcc3..af6077cebc 100644
--- a/mobile/lib/modules/album/views/album_viewer_page.dart
+++ b/mobile/lib/modules/album/views/album_viewer_page.dart
@@ -1,3 +1,6 @@
+import 'dart:async';
+
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
@@ -33,9 +36,12 @@ class AlbumViewerPage extends HookConsumerWidget {
   Widget build(BuildContext context, WidgetRef ref) {
     FocusNode titleFocusNode = useFocusNode();
     final album = ref.watch(albumWatcher(albumId));
+    // Listen provider to prevent autoDispose when navigating to other routes from within the viewer page
+    ref.listen(currentAlbumProvider, (_, __) {});
     album.whenData(
-      (value) =>
-          Future((() => ref.read(currentAlbumProvider.notifier).state = value)),
+      (value) => Future.microtask(
+        () => ref.read(currentAlbumProvider.notifier).set(value),
+      ),
     );
     final userId = ref.watch(authenticationProvider).userId;
     final isProcessing = useProcessingOverlay();
@@ -62,7 +68,7 @@ class AlbumViewerPage extends HookConsumerWidget {
     /// If they exist, add to selected asset state to show they are already selected.
     void onAddPhotosPressed(Album albumInfo) async {
       AssetSelectionPageResult? returnPayload =
-          await context.autoPush<AssetSelectionPageResult?>(
+          await context.pushRoute<AssetSelectionPageResult?>(
         AssetSelectionRoute(
           existingAssets: albumInfo.assets,
           canDeselect: false,
@@ -84,7 +90,7 @@ class AlbumViewerPage extends HookConsumerWidget {
     }
 
     void onAddUsersPressed(Album album) async {
-      List<String>? sharedUserIds = await context.autoPush<List<String>?>(
+      List<String>? sharedUserIds = await context.pushRoute<List<String>?>(
         SelectAdditionalUserForSharingRoute(album: album),
       );
 
@@ -178,7 +184,7 @@ class AlbumViewerPage extends HookConsumerWidget {
 
     Widget buildSharedUserIconsRow(Album album) {
       return GestureDetector(
-        onTap: () => context.autoPush(AlbumOptionsRoute(album: album)),
+        onTap: () => context.pushRoute(AlbumOptionsRoute(album: album)),
         child: SizedBox(
           height: 50,
           child: ListView.builder(
@@ -214,13 +220,8 @@ class AlbumViewerPage extends HookConsumerWidget {
 
     onActivitiesPressed(Album album) {
       if (album.remoteId != null) {
-        context.autoPush(
-          ActivitiesRoute(
-            albumId: album.remoteId!,
-            appBarTitle: album.name,
-            isOwner: userId == album.ownerId,
-            isReadOnly: !album.activityEnabled,
-          ),
+        context.pushRoute(
+          const ActivitiesRoute(),
         );
       }
     }
diff --git a/mobile/lib/modules/album/views/create_album_page.dart b/mobile/lib/modules/album/views/create_album_page.dart
index 7e5fb81682..d339421dce 100644
--- a/mobile/lib/modules/album/views/create_album_page.dart
+++ b/mobile/lib/modules/album/views/create_album_page.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
@@ -36,7 +37,7 @@ class CreateAlbumPage extends HookConsumerWidget {
     );
 
     showSelectUserPage() async {
-      final bool? ok = await context.autoPush<bool?>(
+      final bool? ok = await context.pushRoute<bool?>(
         SelectUserForSharingRoute(assets: selectedAssets.value),
       );
       if (ok == true) {
@@ -58,7 +59,7 @@ class CreateAlbumPage extends HookConsumerWidget {
 
     onSelectPhotosButtonPressed() async {
       AssetSelectionPageResult? selectedAsset =
-          await context.autoPush<AssetSelectionPageResult?>(
+          await context.pushRoute<AssetSelectionPageResult?>(
         AssetSelectionRoute(
           existingAssets: selectedAssets.value,
           canDeselect: true,
@@ -202,7 +203,7 @@ class CreateAlbumPage extends HookConsumerWidget {
         selectedAssets.value = {};
         ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
 
-        context.autoReplace(AlbumViewerRoute(albumId: newAlbum.id));
+        context.replaceRoute(AlbumViewerRoute(albumId: newAlbum.id));
       }
     }
 
@@ -214,7 +215,7 @@ class CreateAlbumPage extends HookConsumerWidget {
         leading: IconButton(
           onPressed: () {
             selectedAssets.value = {};
-            context.autoPop();
+            context.popRoute();
           },
           icon: const Icon(Icons.close_rounded),
         ),
diff --git a/mobile/lib/modules/album/views/library_page.dart b/mobile/lib/modules/album/views/library_page.dart
index 605ffdc8ef..68b3414218 100644
--- a/mobile/lib/modules/album/views/library_page.dart
+++ b/mobile/lib/modules/album/views/library_page.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
@@ -102,7 +103,7 @@ class LibraryPage extends HookConsumerWidget {
 
           return GestureDetector(
             onTap: () =>
-                context.autoPush(CreateAlbumRoute(isSharedAlbum: false)),
+                context.pushRoute(CreateAlbumRoute(isSharedAlbum: false)),
             child: Padding(
               padding:
                   const EdgeInsets.only(bottom: 32), // Adjust padding to suit
@@ -190,7 +191,7 @@ class LibraryPage extends HookConsumerWidget {
     Widget? shareTrashButton() {
       return trashEnabled
           ? InkWell(
-              onTap: () => context.autoPush(const TrashRoute()),
+              onTap: () => context.pushRoute(const TrashRoute()),
               borderRadius: const BorderRadius.all(Radius.circular(12)),
               child: const Icon(
                 Icons.delete_rounded,
@@ -219,12 +220,12 @@ class LibraryPage extends HookConsumerWidget {
                 children: [
                   buildLibraryNavButton(
                       "library_page_favorites".tr(), Icons.favorite_border, () {
-                    context.autoNavigate(const FavoritesRoute());
+                    context.navigateTo(const FavoritesRoute());
                   }),
                   const SizedBox(width: 12.0),
                   buildLibraryNavButton(
                       "library_page_archive".tr(), Icons.archive_outlined, () {
-                    context.autoNavigate(const ArchiveRoute());
+                    context.navigateTo(const ArchiveRoute());
                   }),
                 ],
               ),
@@ -270,7 +271,7 @@ class LibraryPage extends HookConsumerWidget {
 
                   return AlbumThumbnailCard(
                     album: sorted[index - 1],
-                    onTap: () => context.autoPush(
+                    onTap: () => context.pushRoute(
                       AlbumViewerRoute(
                         albumId: sorted[index - 1].id,
                       ),
@@ -314,7 +315,7 @@ class LibraryPage extends HookConsumerWidget {
                 childCount: local.length,
                 (context, index) => AlbumThumbnailCard(
                   album: local[index],
-                  onTap: () => context.autoPush(
+                  onTap: () => context.pushRoute(
                     AlbumViewerRoute(
                       albumId: local[index].id,
                     ),
diff --git a/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart b/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart
index 2aad67ef56..7f7b1cb0ec 100644
--- a/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart
+++ b/mobile/lib/modules/album/views/select_additional_user_for_sharing_page.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
@@ -22,7 +23,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
     final sharedUsersList = useState<Set<User>>({});
 
     addNewUsersHandler() {
-      context.autoPop(sharedUsersList.value.map((e) => e.id).toList());
+      context.popRoute(sharedUsersList.value.map((e) => e.id).toList());
     }
 
     buildTileIcon(User user) {
@@ -123,7 +124,7 @@ class SelectAdditionalUserForSharingPage extends HookConsumerWidget {
         leading: IconButton(
           icon: const Icon(Icons.close_rounded),
           onPressed: () {
-            context.autoPop(null);
+            context.popRoute(null);
           },
         ),
         actions: [
diff --git a/mobile/lib/modules/album/views/select_user_for_sharing_page.dart b/mobile/lib/modules/album/views/select_user_for_sharing_page.dart
index 3d6dcf6787..1089e910c0 100644
--- a/mobile/lib/modules/album/views/select_user_for_sharing_page.dart
+++ b/mobile/lib/modules/album/views/select_user_for_sharing_page.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
@@ -35,9 +36,9 @@ class SelectUserForSharingPage extends HookConsumerWidget {
         await ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
         // ref.watch(assetSelectionProvider.notifier).removeAll();
         ref.watch(albumTitleProvider.notifier).clearAlbumTitle();
-        context.autoPop(true);
+        context.popRoute(true);
         context
-            .autoNavigate(const TabControllerRoute(children: [SharingRoute()]));
+            .navigateTo(const TabControllerRoute(children: [SharingRoute()]));
       }
 
       ScaffoldMessenger(
@@ -151,7 +152,7 @@ class SelectUserForSharingPage extends HookConsumerWidget {
         leading: IconButton(
           icon: const Icon(Icons.close_rounded),
           onPressed: () async {
-            context.autoPop();
+            context.popRoute();
           },
         ),
         actions: [
diff --git a/mobile/lib/modules/album/views/sharing_page.dart b/mobile/lib/modules/album/views/sharing_page.dart
index 9defb19bd5..119caf58f2 100644
--- a/mobile/lib/modules/album/views/sharing_page.dart
+++ b/mobile/lib/modules/album/views/sharing_page.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
@@ -48,11 +49,9 @@ class SharingPage extends HookConsumerWidget {
               return AlbumThumbnailCard(
                 album: sharedAlbums[index],
                 showOwner: true,
-                onTap: () {
-                  context.autoPush(
-                    AlbumViewerRoute(albumId: sharedAlbums[index].id),
-                  );
-                },
+                onTap: () => context.pushRoute(
+                  AlbumViewerRoute(albumId: sharedAlbums[index].id),
+                ),
               );
             },
             childCount: sharedAlbums.length,
@@ -99,11 +98,8 @@ class SharingPage extends HookConsumerWidget {
                           style: context.textTheme.bodyMedium,
                         )
                       : null,
-              onTap: () {
-                context.autoPush(
-                  AlbumViewerRoute(albumId: sharedAlbums[index].id),
-                );
-              },
+              onTap: () => context
+                  .pushRoute(AlbumViewerRoute(albumId: sharedAlbums[index].id)),
             );
           },
           childCount: sharedAlbums.length,
@@ -124,9 +120,8 @@ class SharingPage extends HookConsumerWidget {
           children: [
             Expanded(
               child: ElevatedButton.icon(
-                onPressed: () {
-                  context.autoPush(CreateAlbumRoute(isSharedAlbum: true));
-                },
+                onPressed: () =>
+                    context.pushRoute(CreateAlbumRoute(isSharedAlbum: true)),
                 icon: const Icon(
                   Icons.photo_album_outlined,
                   size: 20,
@@ -144,7 +139,7 @@ class SharingPage extends HookConsumerWidget {
             const SizedBox(width: 12.0),
             Expanded(
               child: ElevatedButton.icon(
-                onPressed: () => context.autoPush(const SharedLinkRoute()),
+                onPressed: () => context.pushRoute(const SharedLinkRoute()),
                 icon: const Icon(
                   Icons.link,
                   size: 20,
@@ -214,7 +209,7 @@ class SharingPage extends HookConsumerWidget {
 
     Widget sharePartnerButton() {
       return InkWell(
-        onTap: () => context.autoPush(const PartnerRoute()),
+        onTap: () => context.pushRoute(const PartnerRoute()),
         borderRadius: const BorderRadius.all(Radius.circular(12)),
         child: const Icon(
           Icons.swap_horizontal_circle_rounded,
diff --git a/mobile/lib/modules/archive/views/archive_page.dart b/mobile/lib/modules/archive/views/archive_page.dart
index 481edcfe12..e3dc77cf9b 100644
--- a/mobile/lib/modules/archive/views/archive_page.dart
+++ b/mobile/lib/modules/archive/views/archive_page.dart
@@ -1,7 +1,7 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/archive/providers/archive_asset_provider.dart';
 import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
 import 'package:immich_mobile/shared/ui/asset_grid/multiselect_grid.dart';
@@ -16,7 +16,7 @@ class ArchivePage extends HookConsumerWidget {
       final count = archivedAssets.value?.totalAssets.toString() ?? "?";
       return AppBar(
         leading: IconButton(
-          onPressed: () => context.autoPop(),
+          onPressed: () => context.popRoute(),
           icon: const Icon(Icons.arrow_back_ios_rounded),
         ),
         centerTitle: true,
diff --git a/mobile/lib/modules/asset_viewer/providers/current_asset.provider.dart b/mobile/lib/modules/asset_viewer/providers/current_asset.provider.dart
new file mode 100644
index 0000000000..1f6166826c
--- /dev/null
+++ b/mobile/lib/modules/asset_viewer/providers/current_asset.provider.dart
@@ -0,0 +1,15 @@
+import 'package:immich_mobile/shared/models/asset.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+part 'current_asset.provider.g.dart';
+
+@riverpod
+class CurrentAsset extends _$CurrentAsset {
+  @override
+  Asset? build() => null;
+
+  void set(Asset? a) => state = a;
+}
+
+/// Mock class for testing
+abstract class CurrentAssetInternal extends _$CurrentAsset {}
diff --git a/mobile/lib/modules/asset_viewer/providers/current_asset.provider.g.dart b/mobile/lib/modules/asset_viewer/providers/current_asset.provider.g.dart
new file mode 100644
index 0000000000..53daa74a12
--- /dev/null
+++ b/mobile/lib/modules/asset_viewer/providers/current_asset.provider.g.dart
@@ -0,0 +1,25 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'current_asset.provider.dart';
+
+// **************************************************************************
+// RiverpodGenerator
+// **************************************************************************
+
+String _$currentAssetHash() => r'018d9f936991c48f06c11bf7e72130bba25806e2';
+
+/// See also [CurrentAsset].
+@ProviderFor(CurrentAsset)
+final currentAssetProvider =
+    AutoDisposeNotifierProvider<CurrentAsset, Asset?>.internal(
+  CurrentAsset.new,
+  name: r'currentAssetProvider',
+  debugGetCreateSourceHash:
+      const bool.fromEnvironment('dart.vm.product') ? null : _$currentAssetHash,
+  dependencies: null,
+  allTransitiveDependencies: null,
+);
+
+typedef _$CurrentAsset = AutoDisposeNotifier<Asset?>;
+// ignore_for_file: type=lint
+// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
diff --git a/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart b/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart
index 52c15e03db..14f8645787 100644
--- a/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart
+++ b/mobile/lib/modules/asset_viewer/ui/top_control_app_bar.dart
@@ -1,7 +1,7 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
-import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
+import 'package:immich_mobile/modules/activities/providers/activity_statistics.provider.dart';
 import 'package:immich_mobile/modules/album/providers/current_album.provider.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:immich_mobile/shared/providers/asset.provider.dart';
@@ -39,12 +39,8 @@ class TopControlAppBar extends HookConsumerWidget {
     const double iconSize = 22.0;
     final a = ref.watch(assetWatcher(asset)).value ?? asset;
     final album = ref.watch(currentAlbumProvider);
-    final comments = album != null && album.remoteId != null
-        ? ref.watch(
-            activityStatisticsStateProvider(
-              (albumId: album.remoteId!, assetId: asset.remoteId),
-            ),
-          )
+    final comments = album != null
+        ? ref.watch(activityStatisticsProvider(album.remoteId!, asset.remoteId))
         : 0;
 
     Widget buildFavoriteButton(a) {
@@ -149,7 +145,7 @@ class TopControlAppBar extends HookConsumerWidget {
     Widget buildBackButton() {
       return IconButton(
         onPressed: () {
-          context.autoPop();
+          context.popRoute();
         },
         icon: Icon(
           Icons.arrow_back_ios_new_rounded,
diff --git a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart
index 792f3623c9..b0bd2d2ac2 100644
--- a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart
+++ b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart
@@ -11,6 +11,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/album/providers/current_album.provider.dart';
 import 'package:immich_mobile/modules/asset_viewer/providers/asset_stack.provider.dart';
+import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart';
 import 'package:immich_mobile/modules/asset_viewer/providers/show_controls.provider.dart';
 import 'package:immich_mobile/modules/asset_viewer/providers/video_player_controls_provider.dart';
 import 'package:immich_mobile/modules/album/ui/add_to_album_bottom_sheet.dart';
@@ -106,6 +107,19 @@ class GalleryViewerPage extends HookConsumerWidget {
 
     bool isParent = stackIndex.value == -1 || stackIndex.value == 0;
 
+    // Listen provider to prevent autoDispose when navigating to other routes from within the gallery page
+    ref.listen(currentAssetProvider, (_, __) {});
+    useEffect(
+      () {
+        // Delay state update to after the execution of build method
+        Future.microtask(
+          () => ref.read(currentAssetProvider.notifier).set(asset()),
+        );
+        return null;
+      },
+      [asset()],
+    );
+
     useEffect(
       () {
         isLoadPreview.value =
@@ -214,7 +228,7 @@ class GalleryViewerPage extends HookConsumerWidget {
         if (isDeleted && isParent) {
           if (totalAssets == 1) {
             // Handle only one asset
-            context.autoPop();
+            context.popRoute();
           } else {
             // Go to next page otherwise
             controller.nextPage(
@@ -298,7 +312,7 @@ class GalleryViewerPage extends HookConsumerWidget {
 
       final ratio = d.dy / max(d.dx.abs(), 1);
       if (d.dy > sensitivity && ratio > ratioThreshold) {
-        context.autoPop();
+        context.popRoute();
       } else if (d.dy < -sensitivity && ratio < -ratioThreshold) {
         showInfo();
       }
@@ -311,7 +325,7 @@ class GalleryViewerPage extends HookConsumerWidget {
     handleArchive(Asset asset) {
       ref.watch(assetProvider.notifier).toggleArchive([asset]);
       if (isParent) {
-        context.autoPop();
+        context.popRoute();
         return;
       }
       removeAssetFromStack();
@@ -334,14 +348,7 @@ class GalleryViewerPage extends HookConsumerWidget {
 
     handleActivities() {
       if (album != null && album.shared && album.remoteId != null) {
-        context.autoPush(
-          ActivitiesRoute(
-            albumId: album.remoteId!,
-            assetId: asset().remoteId,
-            withAssetThumbs: false,
-            isOwner: isOwner,
-          ),
-        );
+        context.pushRoute(const ActivitiesRoute());
       }
     }
 
@@ -517,7 +524,7 @@ class GalleryViewerPage extends HookConsumerWidget {
                               stackElements.elementAt(stackIndex.value),
                             );
                         ctx.pop();
-                        context.autoPop();
+                        context.popRoute();
                       },
                       title: const Text(
                         "viewer_stack_use_as_main_asset",
@@ -544,7 +551,7 @@ class GalleryViewerPage extends HookConsumerWidget {
                           childrenToRemove: [currentAsset],
                         );
                         ctx.pop();
-                        context.autoPop();
+                        context.popRoute();
                       } else {
                         await ref.read(assetStackServiceProvider).updateStack(
                           currentAsset,
@@ -572,7 +579,7 @@ class GalleryViewerPage extends HookConsumerWidget {
                             childrenToRemove: stack,
                           );
                       ctx.pop();
-                      context.autoPop();
+                      context.popRoute();
                     },
                     title: const Text(
                       "viewer_unstack",
diff --git a/mobile/lib/modules/backup/ui/album_info_card.dart b/mobile/lib/modules/backup/ui/album_info_card.dart
index 3e579a84c7..91c9f7515c 100644
--- a/mobile/lib/modules/backup/ui/album_info_card.dart
+++ b/mobile/lib/modules/backup/ui/album_info_card.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -201,7 +202,7 @@ class AlbumInfoCard extends HookConsumerWidget {
                   ),
                   IconButton(
                     onPressed: () {
-                      context.autoPush(
+                      context.pushRoute(
                         AlbumPreviewRoute(album: albumInfo.albumEntity),
                       );
                     },
diff --git a/mobile/lib/modules/backup/ui/album_info_list_tile.dart b/mobile/lib/modules/backup/ui/album_info_list_tile.dart
index 0c27ca1bad..64b742be70 100644
--- a/mobile/lib/modules/backup/ui/album_info_list_tile.dart
+++ b/mobile/lib/modules/backup/ui/album_info_list_tile.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
@@ -134,7 +135,7 @@ class AlbumInfoListTile extends HookConsumerWidget {
         subtitle: Text(assetCount.value.toString()),
         trailing: IconButton(
           onPressed: () {
-            context.autoPush(
+            context.pushRoute(
               AlbumPreviewRoute(album: albumInfo.albumEntity),
             );
           },
diff --git a/mobile/lib/modules/backup/ui/current_backup_asset_info_box.dart b/mobile/lib/modules/backup/ui/current_backup_asset_info_box.dart
index f4d9e531d4..417fd3be59 100644
--- a/mobile/lib/modules/backup/ui/current_backup_asset_info_box.dart
+++ b/mobile/lib/modules/backup/ui/current_backup_asset_info_box.dart
@@ -1,5 +1,6 @@
 import 'dart:io';
 
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
@@ -56,9 +57,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget {
           args: [ref.watch(errorBackupListProvider).length.toString()],
         ),
         backgroundColor: Colors.white,
-        onPressed: () {
-          context.autoPush(const FailedBackupStatusRoute());
-        },
+        onPressed: () => context.pushRoute(const FailedBackupStatusRoute()),
       );
     }
 
diff --git a/mobile/lib/modules/backup/views/album_preview_page.dart b/mobile/lib/modules/backup/views/album_preview_page.dart
index cdb0204ecd..3e308e9651 100644
--- a/mobile/lib/modules/backup/views/album_preview_page.dart
+++ b/mobile/lib/modules/backup/views/album_preview_page.dart
@@ -1,9 +1,9 @@
 import 'dart:typed_data';
 
+import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart';
 import 'package:photo_manager/photo_manager.dart';
 
@@ -53,7 +53,7 @@ class AlbumPreviewPage extends HookConsumerWidget {
           ],
         ),
         leading: IconButton(
-          onPressed: () => context.autoPop(),
+          onPressed: () => context.popRoute(),
           icon: const Icon(Icons.arrow_back_ios_new_rounded),
         ),
       ),
diff --git a/mobile/lib/modules/backup/views/backup_album_selection_page.dart b/mobile/lib/modules/backup/views/backup_album_selection_page.dart
index 4c2708ab09..5673055f0b 100644
--- a/mobile/lib/modules/backup/views/backup_album_selection_page.dart
+++ b/mobile/lib/modules/backup/views/backup_album_selection_page.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
@@ -193,7 +194,7 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
     return Scaffold(
       appBar: AppBar(
         leading: IconButton(
-          onPressed: () => context.autoPop(),
+          onPressed: () => context.popRoute(),
           icon: const Icon(Icons.arrow_back_ios_rounded),
         ),
         title: const Text(
diff --git a/mobile/lib/modules/backup/views/backup_controller_page.dart b/mobile/lib/modules/backup/views/backup_controller_page.dart
index 86a35af2a7..8dd973f40c 100644
--- a/mobile/lib/modules/backup/views/backup_controller_page.dart
+++ b/mobile/lib/modules/backup/views/backup_controller_page.dart
@@ -1,6 +1,7 @@
 import 'dart:io';
 import 'dart:math';
 
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
@@ -151,7 +152,7 @@ class BackupControllerPage extends HookConsumerWidget {
             ),
             trailing: ElevatedButton(
               onPressed: () async {
-                await context.autoPush(const BackupAlbumSelectionRoute());
+                await context.pushRoute(const BackupAlbumSelectionRoute());
                 // waited until returning from selection
                 await ref
                     .read(backupProvider.notifier)
@@ -242,7 +243,7 @@ class BackupControllerPage extends HookConsumerWidget {
         leading: IconButton(
           onPressed: () {
             ref.watch(websocketProvider.notifier).listenUploadEvent();
-            context.autoPop(true);
+            context.popRoute(true);
           },
           splashRadius: 24,
           icon: const Icon(
@@ -253,7 +254,7 @@ class BackupControllerPage extends HookConsumerWidget {
           Padding(
             padding: const EdgeInsets.only(right: 8.0),
             child: IconButton(
-              onPressed: () => context.autoPush(const BackupOptionsRoute()),
+              onPressed: () => context.pushRoute(const BackupOptionsRoute()),
               splashRadius: 24,
               icon: const Icon(
                 Icons.settings_outlined,
diff --git a/mobile/lib/modules/backup/views/backup_options_page.dart b/mobile/lib/modules/backup/views/backup_options_page.dart
index e43e246cc1..d8aab96764 100644
--- a/mobile/lib/modules/backup/views/backup_options_page.dart
+++ b/mobile/lib/modules/backup/views/backup_options_page.dart
@@ -1,5 +1,6 @@
 import 'dart:io';
 
+import 'package:auto_route/auto_route.dart';
 import 'package:connectivity_plus/connectivity_plus.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
@@ -487,9 +488,7 @@ class BackupOptionsPage extends HookConsumerWidget {
           "Backup options",
         ),
         leading: IconButton(
-          onPressed: () {
-            context.autoPop(true);
-          },
+          onPressed: () => context.popRoute(true),
           splashRadius: 24,
           icon: const Icon(
             Icons.arrow_back_ios_rounded,
diff --git a/mobile/lib/modules/backup/views/failed_backup_status_page.dart b/mobile/lib/modules/backup/views/failed_backup_status_page.dart
index 433ed34204..8266e01f43 100644
--- a/mobile/lib/modules/backup/views/failed_backup_status_page.dart
+++ b/mobile/lib/modules/backup/views/failed_backup_status_page.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
@@ -20,7 +21,7 @@ class FailedBackupStatusPage extends HookConsumerWidget {
         ),
         leading: IconButton(
           onPressed: () {
-            context.autoPop(true);
+            context.popRoute(true);
           },
           splashRadius: 24,
           icon: const Icon(
diff --git a/mobile/lib/modules/favorite/views/favorites_page.dart b/mobile/lib/modules/favorite/views/favorites_page.dart
index 8a7ccfc58a..e7c73d8fe5 100644
--- a/mobile/lib/modules/favorite/views/favorites_page.dart
+++ b/mobile/lib/modules/favorite/views/favorites_page.dart
@@ -1,7 +1,7 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/favorite/providers/favorite_provider.dart';
 import 'package:immich_mobile/modules/home/providers/multiselect.provider.dart';
 import 'package:immich_mobile/shared/ui/asset_grid/multiselect_grid.dart';
@@ -14,7 +14,7 @@ class FavoritesPage extends HookConsumerWidget {
     AppBar buildAppBar() {
       return AppBar(
         leading: IconButton(
-          onPressed: () => context.autoPop(),
+          onPressed: () => context.popRoute(),
           icon: const Icon(Icons.arrow_back_ios_rounded),
         ),
         centerTitle: true,
diff --git a/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart
index ca9a3b9322..6bf19a0d27 100644
--- a/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart
+++ b/mobile/lib/modules/home/ui/asset_grid/thumbnail_image.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
@@ -174,7 +175,7 @@ class ThumbnailImage extends StatelessWidget {
             onSelect?.call();
           }
         } else {
-          context.autoPush(
+          context.pushRoute(
             GalleryViewerRoute(
               initialIndex: index,
               loadAsset: loadAsset,
diff --git a/mobile/lib/modules/login/ui/login_form.dart b/mobile/lib/modules/login/ui/login_form.dart
index 1c5a6db0fb..921eeb281f 100644
--- a/mobile/lib/modules/login/ui/login_form.dart
+++ b/mobile/lib/modules/login/ui/login_form.dart
@@ -1,4 +1,5 @@
 import 'dart:io';
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
@@ -157,7 +158,7 @@ class LoginForm extends HookConsumerWidget {
           // Resume backup (if enable) then navigate
           if (ref.read(authenticationProvider).shouldChangePassword &&
               !ref.read(authenticationProvider).isAdmin) {
-            context.autoPush(const ChangePasswordRoute());
+            context.pushRoute(const ChangePasswordRoute());
           } else {
             final hasPermission = await ref
                 .read(galleryPermissionNotifier.notifier)
@@ -166,7 +167,7 @@ class LoginForm extends HookConsumerWidget {
               // Don't resume the backup until we have gallery permission
               ref.read(backupProvider.notifier).resumeBackup();
             }
-            context.autoReplace(const TabControllerRoute());
+            context.replaceRoute(const TabControllerRoute());
           }
         } else {
           ImmichToast.show(
@@ -218,7 +219,7 @@ class LoginForm extends HookConsumerWidget {
             if (permission.isGranted || permission.isLimited) {
               ref.watch(backupProvider.notifier).resumeBackup();
             }
-            context.autoReplace(const TabControllerRoute());
+            context.replaceRoute(const TabControllerRoute());
           } else {
             ImmichToast.show(
               context: context,
@@ -264,7 +265,7 @@ class LoginForm extends HookConsumerWidget {
                       ),
                     ),
                   ),
-                  onPressed: () => context.autoPush(const SettingsRoute()),
+                  onPressed: () => context.pushRoute(const SettingsRoute()),
                   icon: const Icon(Icons.settings_rounded),
                   label: const SizedBox.shrink(),
                 ),
diff --git a/mobile/lib/modules/login/views/login_page.dart b/mobile/lib/modules/login/views/login_page.dart
index 4e1b9a6dff..04e4f1fe39 100644
--- a/mobile/lib/modules/login/views/login_page.dart
+++ b/mobile/lib/modules/login/views/login_page.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -53,7 +54,7 @@ class LoginPage extends HookConsumerWidget {
                   ),
                 ),
                 onTap: () {
-                  context.autoPush(const AppLogRoute());
+                  context.pushRoute(const AppLogRoute());
                 },
               ),
             ],
diff --git a/mobile/lib/modules/map/ui/map_location_picker.dart b/mobile/lib/modules/map/ui/map_location_picker.dart
index c3a2043aec..24873c6372 100644
--- a/mobile/lib/modules/map/ui/map_location_picker.dart
+++ b/mobile/lib/modules/map/ui/map_location_picker.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
@@ -90,12 +91,12 @@ class MapLocationPickerPage extends HookConsumerWidget {
                   mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                   children: [
                     ElevatedButton(
-                      onPressed: () => context.autoPop(selectedLatLng.value),
+                      onPressed: () => context.popRoute(selectedLatLng.value),
                       child: const Text("map_location_picker_page_use_location")
                           .tr(),
                     ),
                     ElevatedButton(
-                      onPressed: () => context.autoPop(),
+                      onPressed: () => context.popRoute(),
                       style: ElevatedButton.styleFrom(
                         backgroundColor: context.colorScheme.error,
                       ),
diff --git a/mobile/lib/modules/map/ui/map_page_app_bar.dart b/mobile/lib/modules/map/ui/map_page_app_bar.dart
index ce426cf037..bfb29ba3d0 100644
--- a/mobile/lib/modules/map/ui/map_page_app_bar.dart
+++ b/mobile/lib/modules/map/ui/map_page_app_bar.dart
@@ -1,8 +1,8 @@
 import 'dart:io';
 
+import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/disable_multi_select_button.dart';
 import 'package:immich_mobile/modules/map/ui/map_settings_dialog.dart';
 
@@ -30,7 +30,7 @@ class MapAppBar extends HookWidget implements PreferredSizeWidget {
       Padding(
         padding: const EdgeInsets.only(left: 15, top: 15),
         child: ElevatedButton(
-          onPressed: () => context.autoPop(),
+          onPressed: () => context.popRoute(),
           style: ElevatedButton.styleFrom(
             shape: const CircleBorder(),
             padding: const EdgeInsets.all(12),
diff --git a/mobile/lib/modules/map/views/map_page.dart b/mobile/lib/modules/map/views/map_page.dart
index 697ea41e06..e61bb236e0 100644
--- a/mobile/lib/modules/map/views/map_page.dart
+++ b/mobile/lib/modules/map/views/map_page.dart
@@ -1,6 +1,7 @@
 import 'dart:async';
 import 'dart:math' as math;
 
+import 'package:auto_route/auto_route.dart';
 import 'package:collection/collection.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
@@ -102,7 +103,7 @@ class MapPageState extends ConsumerState<MapPage> {
   }
 
   void openAssetInViewer(Asset asset) {
-    context.autoPush(
+    context.pushRoute(
       GalleryViewerRoute(
         initialIndex: 0,
         loadAsset: (index) => asset,
diff --git a/mobile/lib/modules/memories/ui/memory_lane.dart b/mobile/lib/modules/memories/ui/memory_lane.dart
index 0c709919b2..4e6d4f81a6 100644
--- a/mobile/lib/modules/memories/ui/memory_lane.dart
+++ b/mobile/lib/modules/memories/ui/memory_lane.dart
@@ -1,7 +1,7 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/memories/providers/memory.provider.dart';
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/shared/ui/immich_image.dart';
@@ -31,7 +31,7 @@ class MemoryLane extends HookConsumerWidget {
                         child: GestureDetector(
                           onTap: () {
                             HapticFeedback.heavyImpact();
-                            context.autoPush(
+                            context.pushRoute(
                               MemoryRoute(
                                 memories: memories,
                                 memoryIndex: index,
diff --git a/mobile/lib/modules/memories/views/memory_page.dart b/mobile/lib/modules/memories/views/memory_page.dart
index 9c135961e4..dc33151c4d 100644
--- a/mobile/lib/modules/memories/views/memory_page.dart
+++ b/mobile/lib/modules/memories/views/memory_page.dart
@@ -1,8 +1,8 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/memories/models/memory.dart';
 import 'package:immich_mobile/modules/memories/ui/memory_card.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
@@ -182,14 +182,14 @@ class MemoryPage extends HookConsumerWidget {
                   currentMemory.value.assets.length;
               if (isLastAsset &&
                   (offset > notification.metrics.maxScrollExtent + 150)) {
-                context.autoPop();
+                context.popRoute();
                 return true;
               }
             }
             // Horizontal scroll handling
             if (notification.depth == 1 &&
                 (offset > notification.metrics.maxScrollExtent + 100)) {
-              context.autoPop();
+              context.popRoute();
               return true;
             }
           }
@@ -244,7 +244,7 @@ class MemoryPage extends HookConsumerWidget {
                           child: MemoryCard(
                             asset: asset,
                             onTap: () => toNextAsset(index),
-                            onClose: () => context.autoPop(),
+                            onClose: () => context.popRoute(),
                             rightCornerText: assetProgress.value,
                             title: memories[mIndex].title,
                             showTitle: index == 0,
diff --git a/mobile/lib/modules/onboarding/views/permission_onboarding_page.dart b/mobile/lib/modules/onboarding/views/permission_onboarding_page.dart
index 771deefa32..e801e5415e 100644
--- a/mobile/lib/modules/onboarding/views/permission_onboarding_page.dart
+++ b/mobile/lib/modules/onboarding/views/permission_onboarding_page.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -16,7 +17,7 @@ class PermissionOnboardingPage extends HookConsumerWidget {
     final PermissionStatus permission = ref.watch(galleryPermissionNotifier);
 
     // Navigate to the main Tab Controller when permission is granted
-    void goToBackup() => context.autoReplace(const BackupControllerRoute());
+    void goToBackup() => context.replaceRoute(const BackupControllerRoute());
 
     // When the permission is denied, we show a request permission page
     buildRequestPermission() {
@@ -174,7 +175,7 @@ class PermissionOnboardingPage extends HookConsumerWidget {
                 ),
                 TextButton(
                   child: const Text('permission_onboarding_back').tr(),
-                  onPressed: () => context.autoPop(),
+                  onPressed: () => context.popRoute(),
                 ),
               ],
             ),
diff --git a/mobile/lib/modules/partner/ui/partner_list.dart b/mobile/lib/modules/partner/ui/partner_list.dart
index 6cf330509c..9e733b1263 100644
--- a/mobile/lib/modules/partner/ui/partner_list.dart
+++ b/mobile/lib/modules/partner/ui/partner_list.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
@@ -36,7 +37,7 @@ class PartnerList extends HookConsumerWidget {
           color: context.primaryColor,
         ),
       ),
-      onTap: () => context.autoPush((PartnerDetailRoute(partner: p))),
+      onTap: () => context.pushRoute((PartnerDetailRoute(partner: p))),
     );
   }
 }
diff --git a/mobile/lib/modules/search/ui/curated_places_row.dart b/mobile/lib/modules/search/ui/curated_places_row.dart
index 133c0e1c89..5840819f95 100644
--- a/mobile/lib/modules/search/ui/curated_places_row.dart
+++ b/mobile/lib/modules/search/ui/curated_places_row.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
@@ -26,7 +27,7 @@ class CuratedPlacesRow extends CuratedRow {
     final int actualContentIndex = isMapEnabled ? 1 : 0;
     Widget buildMapThumbnail() {
       return GestureDetector(
-        onTap: () => context.autoPush(
+        onTap: () => context.pushRoute(
           const MapRoute(),
         ),
         child: SizedBox.square(
diff --git a/mobile/lib/modules/search/ui/explore_grid.dart b/mobile/lib/modules/search/ui/explore_grid.dart
index 984f65a401..fd49fff7c2 100644
--- a/mobile/lib/modules/search/ui/explore_grid.dart
+++ b/mobile/lib/modules/search/ui/explore_grid.dart
@@ -1,5 +1,5 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/search/models/curated_content.dart';
 import 'package:immich_mobile/modules/search/ui/thumbnail_with_info.dart';
 import 'package:immich_mobile/routing/router.dart';
@@ -50,13 +50,13 @@ class ExploreGrid extends StatelessWidget {
           borderRadius: 0,
           onTap: () {
             isPeople
-                ? context.autoPush(
+                ? context.pushRoute(
                     PersonResultRoute(
                       personId: content.id,
                       personName: content.label,
                     ),
                   )
-                : context.autoPush(
+                : context.pushRoute(
                     SearchResultRoute(searchTerm: 'm:${content.label}'),
                   );
           },
diff --git a/mobile/lib/modules/search/views/all_motion_videos_page.dart b/mobile/lib/modules/search/views/all_motion_videos_page.dart
index 8290f0dd6e..1fcadb36fc 100644
--- a/mobile/lib/modules/search/views/all_motion_videos_page.dart
+++ b/mobile/lib/modules/search/views/all_motion_videos_page.dart
@@ -1,8 +1,8 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 import 'package:immich_mobile/modules/search/providers/all_motion_photos.provider.dart';
 
@@ -17,7 +17,7 @@ class AllMotionPhotosPage extends HookConsumerWidget {
       appBar: AppBar(
         title: const Text('motion_photos_page_title').tr(),
         leading: IconButton(
-          onPressed: () => context.autoPop(),
+          onPressed: () => context.popRoute(),
           icon: const Icon(Icons.arrow_back_ios_rounded),
         ),
       ),
diff --git a/mobile/lib/modules/search/views/all_people_page.dart b/mobile/lib/modules/search/views/all_people_page.dart
index 7a81831482..9cd6639757 100644
--- a/mobile/lib/modules/search/views/all_people_page.dart
+++ b/mobile/lib/modules/search/views/all_people_page.dart
@@ -1,8 +1,8 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/search/providers/people.provider.dart';
 import 'package:immich_mobile/modules/search/ui/explore_grid.dart';
 
@@ -19,7 +19,7 @@ class AllPeoplePage extends HookConsumerWidget {
           'all_people_page_title',
         ).tr(),
         leading: IconButton(
-          onPressed: () => context.autoPop(),
+          onPressed: () => context.popRoute(),
           icon: const Icon(Icons.arrow_back_ios_rounded),
         ),
       ),
diff --git a/mobile/lib/modules/search/views/all_videos_page.dart b/mobile/lib/modules/search/views/all_videos_page.dart
index 6835398801..9db3358777 100644
--- a/mobile/lib/modules/search/views/all_videos_page.dart
+++ b/mobile/lib/modules/search/views/all_videos_page.dart
@@ -1,8 +1,8 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 import 'package:immich_mobile/modules/search/providers/all_video_assets.provider.dart';
 
@@ -17,7 +17,7 @@ class AllVideosPage extends HookConsumerWidget {
       appBar: AppBar(
         title: const Text('all_videos_page_title').tr(),
         leading: IconButton(
-          onPressed: () => context.autoPop(),
+          onPressed: () => context.popRoute(),
           icon: const Icon(Icons.arrow_back_ios_rounded),
         ),
       ),
diff --git a/mobile/lib/modules/search/views/curated_location_page.dart b/mobile/lib/modules/search/views/curated_location_page.dart
index 6675e0826f..1f144f657d 100644
--- a/mobile/lib/modules/search/views/curated_location_page.dart
+++ b/mobile/lib/modules/search/views/curated_location_page.dart
@@ -1,8 +1,8 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/search/models/curated_content.dart';
 import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
 import 'package:immich_mobile/modules/search/ui/explore_grid.dart';
@@ -22,7 +22,7 @@ class CuratedLocationPage extends HookConsumerWidget {
           'curated_location_page_title',
         ).tr(),
         leading: IconButton(
-          onPressed: () => context.autoPop(),
+          onPressed: () => context.popRoute(),
           icon: const Icon(Icons.arrow_back_ios_rounded),
         ),
       ),
diff --git a/mobile/lib/modules/search/views/person_result_page.dart b/mobile/lib/modules/search/views/person_result_page.dart
index 40a2d1b14b..a1f62ae01a 100644
--- a/mobile/lib/modules/search/views/person_result_page.dart
+++ b/mobile/lib/modules/search/views/person_result_page.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
@@ -101,7 +102,7 @@ class PersonResultPage extends HookConsumerWidget {
       appBar: AppBar(
         title: Text(name.value),
         leading: IconButton(
-          onPressed: () => context.autoPop(),
+          onPressed: () => context.popRoute(),
           icon: const Icon(Icons.arrow_back_ios_rounded),
         ),
         actions: [
diff --git a/mobile/lib/modules/search/views/recently_added_page.dart b/mobile/lib/modules/search/views/recently_added_page.dart
index 538dea3d71..a2959babfd 100644
--- a/mobile/lib/modules/search/views/recently_added_page.dart
+++ b/mobile/lib/modules/search/views/recently_added_page.dart
@@ -1,8 +1,8 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/asyncvalue_extensions.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart';
 import 'package:immich_mobile/modules/search/providers/recently_added.provider.dart';
 
@@ -17,7 +17,7 @@ class RecentlyAddedPage extends HookConsumerWidget {
       appBar: AppBar(
         title: const Text('recently_added_page_title').tr(),
         leading: IconButton(
-          onPressed: () => context.autoPop(),
+          onPressed: () => context.popRoute(),
           icon: const Icon(Icons.arrow_back_ios_rounded),
         ),
       ),
diff --git a/mobile/lib/modules/search/views/search_page.dart b/mobile/lib/modules/search/views/search_page.dart
index fb4bd49794..c367674225 100644
--- a/mobile/lib/modules/search/views/search_page.dart
+++ b/mobile/lib/modules/search/views/search_page.dart
@@ -1,4 +1,5 @@
 import 'dart:math' as math;
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart' hide Store;
@@ -52,7 +53,7 @@ class SearchPage extends HookConsumerWidget {
       searchFocusNode.unfocus();
       ref.watch(searchPageStateProvider.notifier).disableSearch();
 
-      context.autoPush(
+      context.pushRoute(
         SearchResultRoute(
           searchTerm: searchTerm,
         ),
@@ -79,7 +80,7 @@ class SearchPage extends HookConsumerWidget {
           onData: (people) => CuratedPeopleRow(
             content: people.take(12).toList(),
             onTap: (content, index) {
-              context.autoPush(
+              context.pushRoute(
                 PersonResultRoute(
                   personId: content.id,
                   personName: content.label,
@@ -111,7 +112,7 @@ class SearchPage extends HookConsumerWidget {
                 .toList(),
             imageSize: imageSize,
             onTap: (content, index) {
-              context.autoPush(
+              context.pushRoute(
                 SearchResultRoute(
                   searchTerm: 'm:${content.label}',
                 ),
@@ -139,13 +140,13 @@ class SearchPage extends HookConsumerWidget {
                 SearchRowTitle(
                   title: "search_page_people".tr(),
                   onViewAllPressed: () =>
-                      context.autoPush(const AllPeopleRoute()),
+                      context.pushRoute(const AllPeopleRoute()),
                 ),
                 buildPeople(),
                 SearchRowTitle(
                   title: "search_page_places".tr(),
                   onViewAllPressed: () =>
-                      context.autoPush(const CuratedLocationRoute()),
+                      context.pushRoute(const CuratedLocationRoute()),
                   top: 0,
                 ),
                 const SizedBox(height: 10.0),
@@ -168,7 +169,7 @@ class SearchPage extends HookConsumerWidget {
                   title:
                       Text('search_page_favorites', style: categoryTitleStyle)
                           .tr(),
-                  onTap: () => context.autoPush(const FavoritesRoute()),
+                  onTap: () => context.pushRoute(const FavoritesRoute()),
                 ),
                 const CategoryDivider(),
                 ListTile(
@@ -180,7 +181,7 @@ class SearchPage extends HookConsumerWidget {
                     'search_page_recently_added',
                     style: categoryTitleStyle,
                   ).tr(),
-                  onTap: () => context.autoPush(const RecentlyAddedRoute()),
+                  onTap: () => context.pushRoute(const RecentlyAddedRoute()),
                 ),
                 const SizedBox(height: 24.0),
                 Padding(
@@ -200,7 +201,7 @@ class SearchPage extends HookConsumerWidget {
                     Icons.screenshot,
                     color: categoryIconColor,
                   ),
-                  onTap: () => context.autoPush(
+                  onTap: () => context.pushRoute(
                     SearchResultRoute(
                       searchTerm: 'screenshots',
                     ),
@@ -214,7 +215,7 @@ class SearchPage extends HookConsumerWidget {
                     Icons.photo_camera_front_outlined,
                     color: categoryIconColor,
                   ),
-                  onTap: () => context.autoPush(
+                  onTap: () => context.pushRoute(
                     SearchResultRoute(
                       searchTerm: 'selfies',
                     ),
@@ -228,7 +229,7 @@ class SearchPage extends HookConsumerWidget {
                     Icons.play_circle_outline,
                     color: categoryIconColor,
                   ),
-                  onTap: () => context.autoPush(const AllVideosRoute()),
+                  onTap: () => context.pushRoute(const AllVideosRoute()),
                 ),
                 const CategoryDivider(),
                 ListTile(
@@ -240,7 +241,7 @@ class SearchPage extends HookConsumerWidget {
                     Icons.motion_photos_on_outlined,
                     color: categoryIconColor,
                   ),
-                  onTap: () => context.autoPush(const AllMotionPhotosRoute()),
+                  onTap: () => context.pushRoute(const AllMotionPhotosRoute()),
                 ),
               ],
             ),
diff --git a/mobile/lib/modules/search/views/search_result_page.dart b/mobile/lib/modules/search/views/search_result_page.dart
index fd16c2c06b..585b55d713 100644
--- a/mobile/lib/modules/search/views/search_result_page.dart
+++ b/mobile/lib/modules/search/views/search_result_page.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
@@ -185,7 +186,7 @@ class SearchResultPage extends HookConsumerWidget {
             if (isNewSearch.value) {
               isNewSearch.value = false;
             } else {
-              context.autoPop(true);
+              context.popRoute(true);
             }
           },
           icon: const Icon(Icons.arrow_back_ios_rounded),
diff --git a/mobile/lib/modules/shared_link/ui/shared_link_item.dart b/mobile/lib/modules/shared_link/ui/shared_link_item.dart
index b147ea3c54..e42a69b6f0 100644
--- a/mobile/lib/modules/shared_link/ui/shared_link_item.dart
+++ b/mobile/lib/modules/shared_link/ui/shared_link_item.dart
@@ -1,4 +1,5 @@
 import 'dart:math' as math;
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -210,8 +211,8 @@ class SharedLinkItem extends ConsumerWidget {
               tapTargetSize:
                   MaterialTapTargetSize.shrinkWrap, // the '2023' part
             ),
-            onPressed: () =>
-                context.autoPush(SharedLinkEditRoute(existingLink: sharedLink)),
+            onPressed: () => context
+                .pushRoute(SharedLinkEditRoute(existingLink: sharedLink)),
           ),
           IconButton(
             splashRadius: 25,
diff --git a/mobile/lib/modules/shared_link/views/shared_link_edit_page.dart b/mobile/lib/modules/shared_link/views/shared_link_edit_page.dart
index e96fff56ab..7602a13233 100644
--- a/mobile/lib/modules/shared_link/views/shared_link_edit_page.dart
+++ b/mobile/lib/modules/shared_link/views/shared_link_edit_page.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -317,7 +318,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
               alignment: Alignment.bottomRight,
               child: ElevatedButton(
                 onPressed: () {
-                  context.autoPop();
+                  context.popRoute();
                 },
                 child: const Text(
                   "share_done",
@@ -417,7 +418,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
             changeExpiry: changeExpiry,
           );
       ref.invalidate(sharedLinksStateProvider);
-      context.autoPop();
+      context.popRoute();
     }
 
     return Scaffold(
diff --git a/mobile/lib/modules/trash/views/trash_page.dart b/mobile/lib/modules/trash/views/trash_page.dart
index 88fd32d013..852b6e075c 100644
--- a/mobile/lib/modules/trash/views/trash_page.dart
+++ b/mobile/lib/modules/trash/views/trash_page.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
@@ -138,7 +139,7 @@ class TrashPage extends HookConsumerWidget {
       return AppBar(
         leading: IconButton(
           onPressed: !selectionEnabledHook.value
-              ? () => context.autoPop()
+              ? () => context.popRoute()
               : () {
                   selectionEnabledHook.value = false;
                   selection.value = {};
diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart
index 79684fd02e..3fa3f18a26 100644
--- a/mobile/lib/routing/router.gr.dart
+++ b/mobile/lib/routing/router.gr.dart
@@ -340,18 +340,9 @@ 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,
-          isReadOnly: args.isReadOnly,
-          key: args.key,
-        ),
+        child: const ActivitiesPage(),
         transitionsBuilder: TransitionsBuilders.slideLeft,
         durationInMilliseconds: 200,
         opaque: true,
@@ -1587,63 +1578,16 @@ 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,
-    bool isReadOnly = false,
-    Key? key,
-  }) : super(
+class ActivitiesRoute extends PageRouteInfo<void> {
+  const ActivitiesRoute()
+      : super(
           ActivitiesRoute.name,
           path: '/activities-page',
-          args: ActivitiesRouteArgs(
-            albumId: albumId,
-            appBarTitle: appBarTitle,
-            assetId: assetId,
-            withAssetThumbs: withAssetThumbs,
-            isOwner: isOwner,
-            isReadOnly: isReadOnly,
-            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.isReadOnly = false,
-    this.key,
-  });
-
-  final String albumId;
-
-  final String appBarTitle;
-
-  final String? assetId;
-
-  final bool withAssetThumbs;
-
-  final bool isOwner;
-
-  final bool isReadOnly;
-
-  final Key? key;
-
-  @override
-  String toString() {
-    return 'ActivitiesRouteArgs{albumId: $albumId, appBarTitle: $appBarTitle, assetId: $assetId, withAssetThumbs: $withAssetThumbs, isOwner: $isOwner, isReadOnly: $isReadOnly, key: $key}';
-  }
-}
-
 /// generated route for
 /// [MapLocationPickerPage]
 class MapLocationPickerRoute extends PageRouteInfo<MapLocationPickerRouteArgs> {
diff --git a/mobile/lib/shared/models/user.dart b/mobile/lib/shared/models/user.dart
index 094509c897..aec63e7bea 100644
--- a/mobile/lib/shared/models/user.dart
+++ b/mobile/lib/shared/models/user.dart
@@ -51,6 +51,21 @@ class User {
         avatarColor = dto.avatarColor.toAvatarColor(),
         inTimeline = dto.inTimeline ?? false;
 
+  /// Base user dto used where the complete user object is not required
+  User.fromSimpleUserDto(UserDto dto)
+      : id = dto.id,
+        email = dto.email,
+        name = dto.name,
+        profileImagePath = dto.profileImagePath,
+        avatarColor = dto.avatarColor.toAvatarColor(),
+        // Fill the remaining fields with placeholders
+        isAdmin = false,
+        inTimeline = false,
+        memoryEnabled = false,
+        isPartnerSharedBy = false,
+        isPartnerSharedWith = false,
+        updatedAt = DateTime.now();
+
   @Index(unique: true, replace: false, type: IndexType.hash)
   String id;
   DateTime updatedAt;
diff --git a/mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart
index 053770a92c..856d74f168 100644
--- a/mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart
+++ b/mobile/lib/shared/ui/app_bar_dialog/app_bar_dialog.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
@@ -90,7 +91,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
       return buildActionButton(
         Icons.settings_rounded,
         "profile_drawer_settings",
-        () => context.autoPush(const SettingsRoute()),
+        () => context.pushRoute(const SettingsRoute()),
       );
     }
 
@@ -98,7 +99,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
       return buildActionButton(
         Icons.assignment_outlined,
         "profile_drawer_app_logs",
-        () => context.autoPush(const AppLogRoute()),
+        () => context.pushRoute(const AppLogRoute()),
       );
     }
 
@@ -121,7 +122,7 @@ class ImmichAppBarDialog extends HookConsumerWidget {
                   ref.watch(backupProvider.notifier).cancelBackup();
                   ref.watch(assetProvider.notifier).clearAllAsset();
                   ref.watch(websocketProvider.notifier).disconnect();
-                  context.autoReplace(const LoginRoute());
+                  context.replaceRoute(const LoginRoute());
                 },
               );
             },
diff --git a/mobile/lib/shared/ui/asset_grid/multiselect_grid.dart b/mobile/lib/shared/ui/asset_grid/multiselect_grid.dart
index 2a8923657c..e3db2ab166 100644
--- a/mobile/lib/shared/ui/asset_grid/multiselect_grid.dart
+++ b/mobile/lib/shared/ui/asset_grid/multiselect_grid.dart
@@ -1,12 +1,12 @@
 import 'dart:async';
 
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:fluttertoast/fluttertoast.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/album/providers/album.provider.dart';
 import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart';
 import 'package:immich_mobile/modules/album/services/album.service.dart';
@@ -158,7 +158,7 @@ class MultiselectGrid extends HookConsumerWidget {
         final ids =
             remoteSelection(errorMessage: "home_page_share_err_local".tr())
                 .map((e) => e.remoteId!);
-        context.autoPush(SharedLinkEditRoute(assetsList: ids.toList()));
+        context.pushRoute(SharedLinkEditRoute(assetsList: ids.toList()));
       }
       processing.value = false;
       selectionEnabledHook.value = false;
@@ -301,7 +301,7 @@ class MultiselectGrid extends HookConsumerWidget {
           ref.watch(sharedAlbumProvider.notifier).getAllSharedAlbums();
           selectionEnabledHook.value = false;
 
-          context.autoPush(AlbumViewerRoute(albumId: result.id));
+          context.pushRoute(AlbumViewerRoute(albumId: result.id));
         }
       } finally {
         processing.value = false;
diff --git a/mobile/lib/shared/ui/immich_app_bar.dart b/mobile/lib/shared/ui/immich_app_bar.dart
index 49f5453a7b..0c70e52353 100644
--- a/mobile/lib/shared/ui/immich_app_bar.dart
+++ b/mobile/lib/shared/ui/immich_app_bar.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/extensions/build_context_extensions.dart';
@@ -106,7 +107,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget {
       final badgeBackground = isDarkTheme ? Colors.blueGrey[800] : Colors.white;
 
       return InkWell(
-        onTap: () => context.autoPush(const BackupControllerRoute()),
+        onTap: () => context.pushRoute(const BackupControllerRoute()),
         borderRadius: BorderRadius.circular(12),
         child: Badge(
           label: Container(
diff --git a/mobile/lib/shared/ui/immich_image.dart b/mobile/lib/shared/ui/immich_image.dart
index 985219d6eb..427e5d11e4 100644
--- a/mobile/lib/shared/ui/immich_image.dart
+++ b/mobile/lib/shared/ui/immich_image.dart
@@ -162,6 +162,19 @@ class ImmichImage extends StatelessWidget {
         headers: authHeader,
       );
 
+  /// TODO: refactor image providers to separate class
+  static CachedNetworkImageProvider remoteThumbnailProviderForId(
+    String assetId, {
+    api.ThumbnailFormat type = api.ThumbnailFormat.WEBP,
+  }) =>
+      CachedNetworkImageProvider(
+        getThumbnailUrlForRemoteId(assetId, type: type),
+        cacheKey: getThumbnailCacheKeyForRemoteId(assetId, type: type),
+        headers: {
+          "Authorization": 'Bearer ${Store.get(StoreKey.accessToken)}',
+        },
+      );
+
   /// Precaches this asset for instant load the next time it is shown
   static Future<void> precacheAsset(
     Asset asset,
diff --git a/mobile/lib/shared/ui/location_picker.dart b/mobile/lib/shared/ui/location_picker.dart
index 9649c36adf..9ce5d96a38 100644
--- a/mobile/lib/shared/ui/location_picker.dart
+++ b/mobile/lib/shared/ui/location_picker.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/services.dart';
@@ -97,7 +98,7 @@ class _LocationPicker extends HookWidget {
           zoom: 6,
           showAttribution: false,
           onTap: (p0, p1) async {
-            final newLatLng = await context.autoPush<LatLng?>(
+            final newLatLng = await context.pushRoute<LatLng?>(
               MapLocationPickerRoute(initialLatLng: latlng),
             );
             if (newLatLng != null) {
diff --git a/mobile/lib/shared/ui/scaffold_error_body.dart b/mobile/lib/shared/ui/scaffold_error_body.dart
index ef0d9d5990..bca2934c23 100644
--- a/mobile/lib/shared/ui/scaffold_error_body.dart
+++ b/mobile/lib/shared/ui/scaffold_error_body.dart
@@ -5,8 +5,9 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
 // Error widget to be used in Scaffold when an AsyncError is received
 class ScaffoldErrorBody extends StatelessWidget {
   final bool withIcon;
+  final String? errorMsg;
 
-  const ScaffoldErrorBody({super.key, this.withIcon = true});
+  const ScaffoldErrorBody({super.key, this.withIcon = true, this.errorMsg});
 
   @override
   Widget build(BuildContext context) {
@@ -30,6 +31,15 @@ class ScaffoldErrorBody extends StatelessWidget {
               ),
             ),
           ),
+        if (withIcon && errorMsg != null)
+          Padding(
+            padding: const EdgeInsets.all(20),
+            child: Text(
+              errorMsg!,
+              style: context.textTheme.displaySmall,
+              textAlign: TextAlign.center,
+            ),
+          ),
       ],
     );
   }
diff --git a/mobile/lib/shared/views/app_log_page.dart b/mobile/lib/shared/views/app_log_page.dart
index 9d1dca19fa..a5b63d0d28 100644
--- a/mobile/lib/shared/views/app_log_page.dart
+++ b/mobile/lib/shared/views/app_log_page.dart
@@ -1,3 +1,4 @@
+import 'package:auto_route/auto_route.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter_hooks/flutter_hooks.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
@@ -103,7 +104,7 @@ class AppLogPage extends HookConsumerWidget {
         ],
         leading: IconButton(
           onPressed: () {
-            context.autoPop();
+            context.popRoute();
           },
           icon: const Icon(
             Icons.arrow_back_ios_new_rounded,
@@ -123,7 +124,7 @@ class AppLogPage extends HookConsumerWidget {
         itemBuilder: (context, index) {
           var logMessage = logMessages.value[index];
           return ListTile(
-            onTap: () => context.autoPush(
+            onTap: () => context.pushRoute(
               AppLogDetailRoute(
                 logMessage: logMessage,
               ),
diff --git a/mobile/lib/shared/views/splash_screen.dart b/mobile/lib/shared/views/splash_screen.dart
index 1298e0efe7..e51debdbac 100644
--- a/mobile/lib/shared/views/splash_screen.dart
+++ b/mobile/lib/shared/views/splash_screen.dart
@@ -1,7 +1,7 @@
+import 'package:auto_route/auto_route.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/extensions/build_context_extensions.dart';
 import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
 import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
 import 'package:immich_mobile/modules/onboarding/providers/gallery_permission.provider.dart';
@@ -57,14 +57,14 @@ class SplashScreenPage extends HookConsumerWidget {
             stackTrace,
           );
 
-          context.autoPush(const LoginRoute());
+          context.pushRoute(const LoginRoute());
         }
       }
 
       // If the device is offline and there is a currentUser stored locallly
       // Proceed into the app
       if (deviceIsOffline && Store.tryGet(StoreKey.currentUser) != null) {
-        context.autoReplace(const TabControllerRoute());
+        context.replaceRoute(const TabControllerRoute());
       } else if (isSuccess) {
         // If device was able to login through the internet successfully
         final hasPermission =
@@ -73,10 +73,10 @@ class SplashScreenPage extends HookConsumerWidget {
           // Resume backup (if enable) then navigate
           ref.watch(backupProvider.notifier).resumeBackup();
         }
-        context.autoReplace(const TabControllerRoute());
+        context.replaceRoute(const TabControllerRoute());
       } else {
         // User was unable to login through either offline or online methods
-        context.autoReplace(const LoginRoute());
+        context.replaceRoute(const LoginRoute());
       }
     }
 
@@ -85,7 +85,7 @@ class SplashScreenPage extends HookConsumerWidget {
         if (serverUrl != null && accessToken != null) {
           performLoggingIn();
         } else {
-          context.autoReplace(const LoginRoute());
+          context.replaceRoute(const LoginRoute());
         }
         return null;
       },
diff --git a/mobile/test/fixtures/album.stub.dart b/mobile/test/fixtures/album.stub.dart
index a7c6302630..663faca39d 100644
--- a/mobile/test/fixtures/album.stub.dart
+++ b/mobile/test/fixtures/album.stub.dart
@@ -50,5 +50,8 @@ final class AlbumStub {
     activityEnabled: false,
     startDate: DateTime(2019),
     endDate: DateTime(2020),
-  )..assets.addAll([AssetStub.image1, AssetStub.image2]);
+  )
+    ..assets.addAll([AssetStub.image1, AssetStub.image2])
+    ..activityEnabled = true
+    ..owner.value = UserStub.admin;
 }
diff --git a/mobile/test/fixtures/asset.stub.dart b/mobile/test/fixtures/asset.stub.dart
index 3dba434b2f..2c5106bb4c 100644
--- a/mobile/test/fixtures/asset.stub.dart
+++ b/mobile/test/fixtures/asset.stub.dart
@@ -6,6 +6,7 @@ final class AssetStub {
   static final image1 = Asset(
     checksum: "image1-checksum",
     localId: "image1",
+    remoteId: 'image1-remote',
     ownerId: 1,
     fileCreatedAt: DateTime.now(),
     fileModifiedAt: DateTime.now(),
@@ -22,6 +23,7 @@ final class AssetStub {
   static final image2 = Asset(
     checksum: "image2-checksum",
     localId: "image2",
+    remoteId: 'image2-remote',
     ownerId: 1,
     fileCreatedAt: DateTime(2000),
     fileModifiedAt: DateTime(2010),
diff --git a/mobile/test/fixtures/user.stub.dart b/mobile/test/fixtures/user.stub.dart
index b0dcab094d..4e92bffa72 100644
--- a/mobile/test/fixtures/user.stub.dart
+++ b/mobile/test/fixtures/user.stub.dart
@@ -8,6 +8,8 @@ final class UserStub {
     updatedAt: DateTime(2021),
     email: "admin@test.com",
     name: "admin",
+    avatarColor: AvatarColorEnum.green,
+    profileImagePath: '',
     isAdmin: true,
   );
 
@@ -16,6 +18,18 @@ final class UserStub {
     updatedAt: DateTime(2022),
     email: "user1@test.com",
     name: "user1",
+    avatarColor: AvatarColorEnum.red,
+    profileImagePath: '',
+    isAdmin: false,
+  );
+
+  static final user2 = User(
+    id: "user2",
+    updatedAt: DateTime(2023),
+    email: "user2@test.com",
+    name: "user2",
+    avatarColor: AvatarColorEnum.primary,
+    profileImagePath: '',
     isAdmin: false,
   );
 }
diff --git a/mobile/test/mock_http_override.dart b/mobile/test/mock_http_override.dart
new file mode 100644
index 0000000000..f247e377a2
--- /dev/null
+++ b/mobile/test/mock_http_override.dart
@@ -0,0 +1,67 @@
+import 'dart:io';
+
+import 'package:immich_mobile/shared/ui/transparent_image.dart';
+import 'package:mocktail/mocktail.dart';
+
+/// Mocks the http client to always return a transparent image for all the requests. Only useful in widget
+/// tests to return network images
+class MockHttpOverrides extends HttpOverrides {
+  @override
+  HttpClient createHttpClient(SecurityContext? context) {
+    final client = _MockHttpClient();
+    final request = _MockHttpClientRequest();
+    final response = _MockHttpClientResponse();
+    final headers = _MockHttpHeaders();
+
+    // Client mocks
+    when(() => client.autoUncompress).thenReturn(true);
+
+    // Request mocks
+    when(() => request.headers).thenAnswer((_) => headers);
+    when(() => request.close())
+        .thenAnswer((_) => Future<HttpClientResponse>.value(response));
+
+    // Response mocks
+    when(() => response.statusCode).thenReturn(HttpStatus.ok);
+    when(() => response.compressionState)
+        .thenReturn(HttpClientResponseCompressionState.decompressed);
+    when(() => response.contentLength)
+        .thenAnswer((_) => kTransparentImage.length);
+    when(
+      () => response.listen(
+        captureAny(),
+        cancelOnError: captureAny(named: 'cancelOnError'),
+        onDone: captureAny(named: 'onDone'),
+        onError: captureAny(named: 'onError'),
+      ),
+    ).thenAnswer((invocation) {
+      final onData =
+          invocation.positionalArguments[0] as void Function(List<int>);
+
+      final onDone = invocation.namedArguments[#onDone] as void Function();
+
+      final onError = invocation.namedArguments[#onError] as void
+          Function(Object, [StackTrace]);
+
+      final cancelOnError = invocation.namedArguments[#cancelOnError] as bool;
+
+      return Stream<List<int>>.fromIterable([kTransparentImage.toList()])
+          .listen(
+        onData,
+        onDone: onDone,
+        onError: onError,
+        cancelOnError: cancelOnError,
+      );
+    });
+
+    return client;
+  }
+}
+
+class _MockHttpClient extends Mock implements HttpClient {}
+
+class _MockHttpClientRequest extends Mock implements HttpClientRequest {}
+
+class _MockHttpClientResponse extends Mock implements HttpClientResponse {}
+
+class _MockHttpHeaders extends Mock implements HttpHeaders {}
diff --git a/mobile/test/mocks/app_settings_provider.mock.dart b/mobile/test/mocks/app_settings_provider.mock.dart
deleted file mode 100644
index fbdf67a411..0000000000
--- a/mobile/test/mocks/app_settings_provider.mock.dart
+++ /dev/null
@@ -1,9 +0,0 @@
-import 'package:hooks_riverpod/hooks_riverpod.dart';
-import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
-import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
-import 'package:mocktail/mocktail.dart';
-
-class AppSettingsServiceMock with Mock implements AppSettingsService {}
-
-Override getAppSettingsServiceMock(AppSettingsService service) =>
-    appSettingsServiceProvider.overrideWith((ref) => service);
diff --git a/mobile/test/modules/activity/activities_page_test.dart b/mobile/test/modules/activity/activities_page_test.dart
new file mode 100644
index 0000000000..14ca430139
--- /dev/null
+++ b/mobile/test/modules/activity/activities_page_test.dart
@@ -0,0 +1,250 @@
+@Tags(['widget'])
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.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/modules/activities/views/activities_page.dart';
+import 'package:immich_mobile/modules/activities/widgets/activity_text_field.dart';
+import 'package:immich_mobile/modules/activities/widgets/dismissible_activity.dart';
+import 'package:immich_mobile/modules/album/providers/current_album.provider.dart';
+import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart';
+import 'package:immich_mobile/shared/models/album.dart';
+import 'package:immich_mobile/shared/models/asset.dart';
+import 'package:immich_mobile/shared/models/store.dart';
+import 'package:immich_mobile/shared/models/user.dart';
+import 'package:immich_mobile/shared/providers/user.provider.dart';
+import 'package:isar/isar.dart';
+import 'package:mocktail/mocktail.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+import '../../fixtures/album.stub.dart';
+import '../../fixtures/asset.stub.dart';
+import '../../fixtures/user.stub.dart';
+import '../../test_utils.dart';
+import '../../widget_tester_extensions.dart';
+import '../asset_viewer/asset_viewer_mocks.dart';
+import '../album/album_mocks.dart';
+import '../shared/shared_mocks.dart';
+import 'activity_mocks.dart';
+
+final _activities = [
+  Activity(
+    id: '1',
+    createdAt: DateTime(100),
+    type: ActivityType.comment,
+    comment: 'First Activity',
+    assetId: 'asset-2',
+    user: UserStub.admin,
+  ),
+  Activity(
+    id: '2',
+    createdAt: DateTime(200),
+    type: ActivityType.comment,
+    comment: 'Second Activity',
+    user: UserStub.user1,
+  ),
+  Activity(
+    id: '3',
+    createdAt: DateTime(300),
+    type: ActivityType.like,
+    assetId: 'asset-1',
+    user: UserStub.user2,
+  ),
+  Activity(
+    id: '4',
+    createdAt: DateTime(400),
+    type: ActivityType.like,
+    user: UserStub.user1,
+  ),
+];
+
+void main() {
+  late MockAlbumActivity activityMock;
+  late MockCurrentAlbumProvider mockCurrentAlbumProvider;
+  late MockCurrentAssetProvider mockCurrentAssetProvider;
+  late List<Override> overrides;
+  late Isar db;
+
+  setUpAll(() async {
+    TestUtils.init();
+    db = await TestUtils.initIsar();
+    Store.init(db);
+    Store.put(StoreKey.currentUser, UserStub.admin);
+    Store.put(StoreKey.serverEndpoint, '');
+    Store.put(StoreKey.accessToken, '');
+  });
+
+  setUp(() async {
+    mockCurrentAlbumProvider = MockCurrentAlbumProvider(AlbumStub.twoAsset);
+    mockCurrentAssetProvider = MockCurrentAssetProvider(AssetStub.image1);
+    activityMock = MockAlbumActivity(_activities);
+    overrides = [
+      albumActivityProvider(
+        AlbumStub.twoAsset.remoteId!,
+        AssetStub.image1.remoteId!,
+      ).overrideWith(() => activityMock),
+      currentAlbumProvider.overrideWith(() => mockCurrentAlbumProvider),
+      currentAssetProvider.overrideWith(() => mockCurrentAssetProvider),
+    ];
+
+    await db.writeTxn(() async {
+      await db.clear();
+      // Save all assets
+      await db.users.put(UserStub.admin);
+      await db.assets.putAll([AssetStub.image1, AssetStub.image2]);
+      await db.albums.put(AlbumStub.twoAsset);
+      await AlbumStub.twoAsset.owner.save();
+      await AlbumStub.twoAsset.assets.save();
+    });
+    expect(db.albums.countSync(), 1);
+    expect(db.assets.countSync(), 2);
+    expect(db.users.countSync(), 1);
+  });
+
+  group("App bar", () {
+    testWidgets(
+      "No title when currentAsset != null",
+      (tester) async {
+        await tester.pumpConsumerWidget(
+          const ActivitiesPage(),
+          overrides: overrides,
+        );
+
+        final listTile = tester.widget<AppBar>(find.byType(AppBar));
+        expect(listTile.title, isNull);
+      },
+    );
+
+    testWidgets(
+      "Album name as title when currentAsset == null",
+      (tester) async {
+        await tester.pumpConsumerWidget(
+          const ActivitiesPage(),
+          overrides: overrides,
+        );
+        await tester.pumpAndSettle();
+
+        mockCurrentAssetProvider.state = null;
+        await tester.pumpAndSettle();
+
+        expect(find.text(AlbumStub.twoAsset.name), findsOneWidget);
+        final listTile = tester.widget<AppBar>(find.byType(AppBar));
+        expect(listTile.title, isNotNull);
+      },
+    );
+  });
+
+  group("Body", () {
+    testWidgets(
+      "Contains a stack with Activity List and Activity Input",
+      (tester) async {
+        await tester.pumpConsumerWidget(
+          const ActivitiesPage(),
+          overrides: overrides,
+        );
+        await tester.pumpAndSettle();
+
+        expect(
+          find.descendant(
+            of: find.byType(Stack),
+            matching: find.byType(ActivityTextField),
+          ),
+          findsOneWidget,
+        );
+
+        expect(
+          find.descendant(
+            of: find.byType(Stack),
+            matching: find.byType(ListView),
+          ),
+          findsOneWidget,
+        );
+      },
+    );
+
+    testWidgets(
+      "List Contains all dismissible activities",
+      (tester) async {
+        await tester.pumpConsumerWidget(
+          const ActivitiesPage(),
+          overrides: overrides,
+        );
+        await tester.pumpAndSettle();
+
+        final listFinder = find.descendant(
+          of: find.byType(Stack),
+          matching: find.byType(ListView),
+        );
+        final listChildren = find.descendant(
+          of: listFinder,
+          matching: find.byType(DismissibleActivity),
+        );
+        expect(listChildren, findsNWidgets(_activities.length));
+      },
+    );
+
+    testWidgets(
+      "Submitting text input adds a comment with the text",
+      (tester) async {
+        await tester.pumpConsumerWidget(
+          const ActivitiesPage(),
+          overrides: overrides,
+        );
+        await tester.pumpAndSettle();
+
+        when(() => activityMock.addComment(any()))
+            .thenAnswer((_) => Future.value());
+
+        final textField = find.byType(TextField);
+        await tester.enterText(textField, 'Test comment');
+        await tester.testTextInput.receiveAction(TextInputAction.done);
+
+        verify(() => activityMock.addComment('Test comment'));
+      },
+    );
+
+    testWidgets(
+      "Owner can remove all activities",
+      (tester) async {
+        await tester.pumpConsumerWidget(
+          const ActivitiesPage(),
+          overrides: overrides,
+        );
+        await tester.pumpAndSettle();
+
+        final deletableActivityFinder = find.byWidgetPredicate(
+          (widget) => widget is DismissibleActivity && widget.onDismiss != null,
+        );
+        expect(deletableActivityFinder, findsNWidgets(_activities.length));
+      },
+    );
+
+    testWidgets(
+      "Non-Owner can remove only their activities",
+      (tester) async {
+        final mockCurrentUser = MockCurrentUserProvider();
+
+        await tester.pumpConsumerWidget(
+          const ActivitiesPage(),
+          overrides: [
+            ...overrides,
+            currentUserProvider.overrideWith((ref) => mockCurrentUser),
+          ],
+        );
+        mockCurrentUser.state = UserStub.user1;
+        await tester.pumpAndSettle();
+
+        final deletableActivityFinder = find.byWidgetPredicate(
+          (widget) => widget is DismissibleActivity && widget.onDismiss != null,
+        );
+        expect(
+          deletableActivityFinder,
+          findsNWidgets(
+            _activities.where((a) => a.user == UserStub.user1).length,
+          ),
+        );
+      },
+    );
+  });
+}
diff --git a/mobile/test/modules/activity/activity_mocks.dart b/mobile/test/modules/activity/activity_mocks.dart
new file mode 100644
index 0000000000..0a3e37216d
--- /dev/null
+++ b/mobile/test/modules/activity/activity_mocks.dart
@@ -0,0 +1,23 @@
+import 'package:immich_mobile/modules/activities/models/activity.model.dart';
+import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
+import 'package:immich_mobile/modules/activities/providers/activity_statistics.provider.dart';
+import 'package:immich_mobile/modules/activities/services/activity.service.dart';
+import 'package:mocktail/mocktail.dart';
+
+class ActivityServiceMock extends Mock implements ActivityService {}
+
+class MockAlbumActivity extends AlbumActivityInternal
+    with Mock
+    implements AlbumActivity {
+  List<Activity>? initActivities;
+  MockAlbumActivity([this.initActivities]);
+
+  @override
+  Future<List<Activity>> build(String albumId, [String? assetId]) async {
+    return initActivities ?? [];
+  }
+}
+
+class ActivityStatisticsMock extends ActivityStatisticsInternal
+    with Mock
+    implements ActivityStatistics {}
diff --git a/mobile/test/modules/activity/activity_provider_test.dart b/mobile/test/modules/activity/activity_provider_test.dart
new file mode 100644
index 0000000000..9e8559839e
--- /dev/null
+++ b/mobile/test/modules/activity/activity_provider_test.dart
@@ -0,0 +1,353 @@
+import 'package:flutter_test/flutter_test.dart';
+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/modules/activities/providers/activity_service.provider.dart';
+import 'package:immich_mobile/modules/activities/providers/activity_statistics.provider.dart';
+import 'package:mocktail/mocktail.dart';
+
+import '../../fixtures/user.stub.dart';
+import '../../test_utils.dart';
+import 'activity_mocks.dart';
+
+final _activities = [
+  Activity(
+    id: '1',
+    createdAt: DateTime(100),
+    type: ActivityType.comment,
+    comment: 'First Activity',
+    assetId: 'asset-2',
+    user: UserStub.admin,
+  ),
+  Activity(
+    id: '2',
+    createdAt: DateTime(200),
+    type: ActivityType.comment,
+    comment: 'Second Activity',
+    user: UserStub.user1,
+  ),
+  Activity(
+    id: '3',
+    createdAt: DateTime(300),
+    type: ActivityType.like,
+    assetId: 'asset-1',
+    user: UserStub.admin,
+  ),
+  Activity(
+    id: '4',
+    createdAt: DateTime(400),
+    type: ActivityType.like,
+    user: UserStub.user1,
+  ),
+];
+
+void main() {
+  late ActivityServiceMock activityMock;
+  late ActivityStatisticsMock activityStatisticsMock;
+  late ProviderContainer container;
+  late AlbumActivityProvider provider;
+  late ListenerMock<AsyncValue<List<Activity>>> listener;
+
+  setUpAll(() {
+    registerFallbackValue(AsyncData<List<Activity>>([..._activities]));
+  });
+
+  setUp(() async {
+    activityMock = ActivityServiceMock();
+    activityStatisticsMock = ActivityStatisticsMock();
+    container = TestUtils.createContainer(
+      overrides: [
+        activityServiceProvider.overrideWith((ref) => activityMock),
+        activityStatisticsProvider('test-album', 'test-asset')
+            .overrideWith(() => activityStatisticsMock),
+      ],
+    );
+
+    // Mock values
+    when(
+      () => activityMock.getAllActivities('test-album', assetId: 'test-asset'),
+    ).thenAnswer((_) async => [..._activities]);
+
+    // Init and wait for providers future to complete
+    provider = albumActivityProvider('test-album', 'test-asset');
+    listener = ListenerMock();
+    container.listen(
+      provider,
+      listener,
+      fireImmediately: true,
+    );
+
+    await container.read(provider.future);
+  });
+
+  test('Returns a list of activity', () async {
+    verifyInOrder([
+      () => listener.call(null, const AsyncLoading()),
+      () => listener.call(
+            const AsyncLoading(),
+            any(
+              that: allOf(
+                [
+                  isA<AsyncData<List<Activity>>>(),
+                  predicate(
+                    (AsyncData<List<Activity>> ad) =>
+                        ad.requireValue.every((e) => _activities.contains(e)),
+                  ),
+                ],
+              ),
+            ),
+          ),
+    ]);
+
+    verifyNoMoreInteractions(listener);
+  });
+
+  group('addLike()', () {
+    test('Like successfully added', () async {
+      final like = Activity(
+        id: '5',
+        createdAt: DateTime(2023),
+        type: ActivityType.like,
+        user: UserStub.admin,
+      );
+
+      when(
+        () => activityMock.addActivity(
+          'test-album',
+          ActivityType.like,
+          assetId: 'test-asset',
+        ),
+      ).thenAnswer((_) async => AsyncData(like));
+
+      await container.read(provider.notifier).addLike();
+
+      verify(
+        () => activityMock.addActivity(
+          'test-album',
+          ActivityType.like,
+          assetId: 'test-asset',
+        ),
+      );
+
+      final activities = await container.read(provider.future);
+      expect(activities, hasLength(5));
+      expect(activities, contains(like));
+
+      // Never bump activity count for new likes
+      verifyNever(() => activityStatisticsMock.addActivity());
+    });
+
+    test('Like failed', () async {
+      final like = Activity(
+        id: '5',
+        createdAt: DateTime(2023),
+        type: ActivityType.like,
+        user: UserStub.admin,
+      );
+      when(
+        () => activityMock.addActivity(
+          'test-album',
+          ActivityType.like,
+          assetId: 'test-asset',
+        ),
+      ).thenAnswer(
+        (_) async => AsyncError(Exception('Mock'), StackTrace.current),
+      );
+
+      await container.read(provider.notifier).addLike();
+
+      verify(
+        () => activityMock.addActivity(
+          'test-album',
+          ActivityType.like,
+          assetId: 'test-asset',
+        ),
+      );
+
+      final activities = await container.read(provider.future);
+      expect(activities, hasLength(4));
+      expect(activities, isNot(contains(like)));
+    });
+  });
+
+  group('removeActivity()', () {
+    test('Like successfully removed', () async {
+      when(() => activityMock.removeActivity('3'))
+          .thenAnswer((_) async => true);
+
+      await container.read(provider.notifier).removeActivity('3');
+
+      verify(
+        () => activityMock.removeActivity('3'),
+      );
+
+      final activities = await container.read(provider.future);
+      expect(activities, hasLength(3));
+      expect(
+        activities,
+        isNot(anyElement(predicate((Activity a) => a.id == '3'))),
+      );
+
+      verifyNever(() => activityStatisticsMock.removeActivity());
+    });
+
+    test('Remove Like failed', () async {
+      when(() => activityMock.removeActivity('3'))
+          .thenAnswer((_) async => false);
+
+      await container.read(provider.notifier).removeActivity('3');
+
+      final activities = await container.read(provider.future);
+      expect(activities, hasLength(4));
+      expect(
+        activities,
+        anyElement(predicate((Activity a) => a.id == '3')),
+      );
+    });
+
+    test('Comment successfully removed', () async {
+      when(() => activityMock.removeActivity('1'))
+          .thenAnswer((_) async => true);
+
+      await container.read(provider.notifier).removeActivity('1');
+
+      final activities = await container.read(provider.future);
+      expect(
+        activities,
+        isNot(anyElement(predicate((Activity a) => a.id == '1'))),
+      );
+
+      verify(() => activityStatisticsMock.removeActivity());
+    });
+  });
+
+  group('addComment()', () {
+    late ActivityStatisticsMock albumActivityStatisticsMock;
+
+    setUp(() {
+      albumActivityStatisticsMock = ActivityStatisticsMock();
+      container = TestUtils.createContainer(
+        overrides: [
+          activityServiceProvider.overrideWith((ref) => activityMock),
+          activityStatisticsProvider('test-album', 'test-asset')
+              .overrideWith(() => activityStatisticsMock),
+          activityStatisticsProvider('test-album')
+              .overrideWith(() => albumActivityStatisticsMock),
+        ],
+      );
+    });
+
+    test('Comment successfully added', () async {
+      final comment = Activity(
+        id: '5',
+        createdAt: DateTime(2023),
+        type: ActivityType.comment,
+        user: UserStub.admin,
+        comment: 'Test-Comment',
+        assetId: 'test-asset',
+      );
+
+      when(
+        () => activityMock.addActivity(
+          'test-album',
+          ActivityType.comment,
+          assetId: 'test-asset',
+          comment: 'Test-Comment',
+        ),
+      ).thenAnswer((_) async => AsyncData(comment));
+      when(() => activityStatisticsMock.build('test-album', 'test-asset'))
+          .thenReturn(4);
+      when(() => albumActivityStatisticsMock.build('test-album')).thenReturn(2);
+
+      await container.read(provider.notifier).addComment('Test-Comment');
+
+      verify(
+        () => activityMock.addActivity(
+          'test-album',
+          ActivityType.comment,
+          assetId: 'test-asset',
+          comment: 'Test-Comment',
+        ),
+      );
+
+      final activities = await container.read(provider.future);
+      expect(activities, hasLength(5));
+      expect(activities, contains(comment));
+
+      verify(() => activityStatisticsMock.addActivity());
+      verify(() => albumActivityStatisticsMock.addActivity());
+    });
+
+    test('Comment successfully added without assetId', () async {
+      final comment = Activity(
+        id: '5',
+        createdAt: DateTime(2023),
+        type: ActivityType.comment,
+        user: UserStub.admin,
+        assetId: 'test-asset',
+        comment: 'Test-Comment',
+      );
+
+      when(
+        () => activityMock.addActivity(
+          'test-album',
+          ActivityType.comment,
+          comment: 'Test-Comment',
+        ),
+      ).thenAnswer((_) async => AsyncData(comment));
+      when(() => albumActivityStatisticsMock.build('test-album')).thenReturn(2);
+      when(() => activityMock.getAllActivities('test-album'))
+          .thenAnswer((_) async => [..._activities]);
+
+      final albumProvider = albumActivityProvider('test-album');
+      await container.read(albumProvider.notifier).addComment('Test-Comment');
+
+      verify(
+        () => activityMock.addActivity(
+          'test-album',
+          ActivityType.comment,
+          assetId: null,
+          comment: 'Test-Comment',
+        ),
+      );
+
+      final activities = await container.read(albumProvider.future);
+      expect(activities, hasLength(5));
+      expect(activities, contains(comment));
+
+      verifyNever(() => activityStatisticsMock.addActivity());
+      verify(() => albumActivityStatisticsMock.addActivity());
+    });
+
+    test('Comment failed', () async {
+      final comment = Activity(
+        id: '5',
+        createdAt: DateTime(2023),
+        type: ActivityType.comment,
+        user: UserStub.admin,
+        comment: 'Test-Comment',
+        assetId: 'test-asset',
+      );
+
+      when(
+        () => activityMock.addActivity(
+          'test-album',
+          ActivityType.comment,
+          assetId: 'test-asset',
+          comment: 'Test-Comment',
+        ),
+      ).thenAnswer(
+        (_) async => AsyncError(Exception('Error'), StackTrace.current),
+      );
+
+      await container.read(provider.notifier).addComment('Test-Comment');
+
+      final activities = await container.read(provider.future);
+      expect(activities, hasLength(4));
+      expect(activities, isNot(contains(comment)));
+
+      verifyNever(() => activityStatisticsMock.addActivity());
+      verifyNever(() => albumActivityStatisticsMock.addActivity());
+    });
+  });
+}
diff --git a/mobile/test/modules/activity/activity_statistics_provider_test.dart b/mobile/test/modules/activity/activity_statistics_provider_test.dart
new file mode 100644
index 0000000000..be147d201d
--- /dev/null
+++ b/mobile/test/modules/activity/activity_statistics_provider_test.dart
@@ -0,0 +1,91 @@
+import 'package:flutter_test/flutter_test.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/activities/providers/activity_service.provider.dart';
+import 'package:immich_mobile/modules/activities/providers/activity_statistics.provider.dart';
+import 'package:mocktail/mocktail.dart';
+
+import '../../test_utils.dart';
+import 'activity_mocks.dart';
+
+void main() {
+  late ActivityServiceMock activityMock;
+  late ProviderContainer container;
+  late ListenerMock<int> listener;
+
+  setUp(() async {
+    activityMock = ActivityServiceMock();
+    container = TestUtils.createContainer(
+      overrides: [
+        activityServiceProvider.overrideWith((ref) => activityMock),
+      ],
+    );
+    listener = ListenerMock();
+  });
+
+  test('Returns the proper count family', () async {
+    when(
+      () => activityMock.getStatistics('test-album', assetId: 'test-asset'),
+    ).thenAnswer((_) async => 5);
+
+    // Read here to make the getStatistics call
+    container.read(activityStatisticsProvider('test-album', 'test-asset'));
+
+    container.listen(
+      activityStatisticsProvider('test-album', 'test-asset'),
+      listener,
+      fireImmediately: true,
+    );
+
+    // Sleep for the getStatistics future to resolve
+    await Future.delayed(const Duration(milliseconds: 1));
+
+    verifyInOrder([
+      () => listener.call(null, 0),
+      () => listener.call(0, 5),
+    ]);
+
+    verifyNoMoreInteractions(listener);
+  });
+
+  test('Adds activity', () async {
+    when(
+      () => activityMock.getStatistics('test-album'),
+    ).thenAnswer((_) async => 10);
+
+    final provider = activityStatisticsProvider('test-album');
+    container.listen(
+      provider,
+      listener,
+      fireImmediately: true,
+    );
+
+    // Sleep for the getStatistics future to resolve
+    await Future.delayed(const Duration(milliseconds: 1));
+
+    container.read(provider.notifier).addActivity();
+    container.read(provider.notifier).addActivity();
+
+    expect(container.read(provider), 12);
+  });
+
+  test('Removes activity', () async {
+    when(
+      () => activityMock.getStatistics('new-album', assetId: 'test-asset'),
+    ).thenAnswer((_) async => 10);
+
+    final provider = activityStatisticsProvider('new-album', 'test-asset');
+    container.listen(
+      provider,
+      listener,
+      fireImmediately: true,
+    );
+
+    // Sleep for the getStatistics future to resolve
+    await Future.delayed(const Duration(milliseconds: 1));
+
+    container.read(provider.notifier).removeActivity();
+    container.read(provider.notifier).removeActivity();
+
+    expect(container.read(provider), 8);
+  });
+}
diff --git a/mobile/test/modules/activity/activity_text_field_test.dart b/mobile/test/modules/activity/activity_text_field_test.dart
new file mode 100644
index 0000000000..c05971b256
--- /dev/null
+++ b/mobile/test/modules/activity/activity_text_field_test.dart
@@ -0,0 +1,199 @@
+@Tags(['widget'])
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:immich_mobile/modules/activities/providers/activity.provider.dart';
+import 'package:immich_mobile/modules/activities/widgets/activity_text_field.dart';
+import 'package:immich_mobile/modules/album/providers/current_album.provider.dart';
+import 'package:immich_mobile/shared/models/store.dart';
+import 'package:immich_mobile/shared/providers/user.provider.dart';
+import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
+import 'package:isar/isar.dart';
+import 'package:mocktail/mocktail.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+import '../../fixtures/album.stub.dart';
+import '../../fixtures/user.stub.dart';
+import '../../test_utils.dart';
+import '../../widget_tester_extensions.dart';
+import '../album/album_mocks.dart';
+import '../shared/shared_mocks.dart';
+import 'activity_mocks.dart';
+
+void main() {
+  late Isar db;
+  late MockCurrentAlbumProvider mockCurrentAlbumProvider;
+  late MockAlbumActivity activityMock;
+  late List<Override> overrides;
+
+  setUpAll(() async {
+    TestUtils.init();
+    db = await TestUtils.initIsar();
+    Store.init(db);
+    Store.put(StoreKey.currentUser, UserStub.admin);
+    Store.put(StoreKey.serverEndpoint, '');
+  });
+
+  setUp(() {
+    mockCurrentAlbumProvider = MockCurrentAlbumProvider(AlbumStub.twoAsset);
+    activityMock = MockAlbumActivity();
+    overrides = [
+      currentAlbumProvider.overrideWith(() => mockCurrentAlbumProvider),
+      albumActivityProvider(AlbumStub.twoAsset.remoteId!)
+          .overrideWith(() => activityMock),
+    ];
+  });
+
+  testWidgets('Returns an Input text field', (tester) async {
+    await tester.pumpConsumerWidget(
+      ActivityTextField(
+        onSubmit: (_) {},
+      ),
+      overrides: overrides,
+    );
+
+    expect(find.byType(TextField), findsOneWidget);
+  });
+
+  testWidgets('No UserCircleAvatar when user == null', (tester) async {
+    final userProvider = MockCurrentUserProvider();
+
+    await tester.pumpConsumerWidget(
+      ActivityTextField(
+        onSubmit: (_) {},
+      ),
+      overrides: [
+        currentUserProvider.overrideWith((ref) => userProvider),
+        ...overrides,
+      ],
+    );
+
+    expect(find.byType(UserCircleAvatar), findsNothing);
+  });
+
+  testWidgets('UserCircleAvatar displayed when user != null', (tester) async {
+    await tester.pumpConsumerWidget(
+      ActivityTextField(
+        onSubmit: (_) {},
+      ),
+      overrides: overrides,
+    );
+
+    expect(find.byType(UserCircleAvatar), findsOneWidget);
+  });
+
+  testWidgets(
+    'Filled icon if likedId != null',
+    (tester) async {
+      await tester.pumpConsumerWidget(
+        ActivityTextField(
+          onSubmit: (_) {},
+          likeId: '1',
+        ),
+        overrides: overrides,
+      );
+
+      expect(
+        find.widgetWithIcon(IconButton, Icons.favorite_rounded),
+        findsOneWidget,
+      );
+      expect(
+        find.widgetWithIcon(IconButton, Icons.favorite_border_rounded),
+        findsNothing,
+      );
+    },
+  );
+
+  testWidgets('Bordered icon if likedId == null', (tester) async {
+    await tester.pumpConsumerWidget(
+      ActivityTextField(
+        onSubmit: (_) {},
+      ),
+      overrides: overrides,
+    );
+
+    expect(
+      find.widgetWithIcon(IconButton, Icons.favorite_border_rounded),
+      findsOneWidget,
+    );
+    expect(
+      find.widgetWithIcon(IconButton, Icons.favorite_rounded),
+      findsNothing,
+    );
+  });
+
+  testWidgets('Adds new like', (tester) async {
+    await tester.pumpConsumerWidget(
+      ActivityTextField(
+        onSubmit: (_) {},
+      ),
+      overrides: overrides,
+    );
+
+    when(() => activityMock.addLike()).thenAnswer((_) => Future.value());
+
+    final suffixIcon = find.byType(IconButton);
+    await tester.tap(suffixIcon);
+
+    verify(() => activityMock.addLike());
+  });
+
+  testWidgets('Removes like if already liked', (tester) async {
+    await tester.pumpConsumerWidget(
+      ActivityTextField(
+        onSubmit: (_) {},
+        likeId: 'test-suffix',
+      ),
+      overrides: overrides,
+    );
+
+    when(() => activityMock.removeActivity(any()))
+        .thenAnswer((_) => Future.value());
+
+    final suffixIcon = find.byType(IconButton);
+    await tester.tap(suffixIcon);
+
+    verify(() => activityMock.removeActivity('test-suffix'));
+  });
+
+  testWidgets('Passes text entered to onSubmit on submit', (tester) async {
+    String? receivedText;
+
+    await tester.pumpConsumerWidget(
+      ActivityTextField(
+        onSubmit: (text) => receivedText = text,
+        likeId: 'test-suffix',
+      ),
+      overrides: overrides,
+    );
+
+    final textField = find.byType(TextField);
+    await tester.enterText(textField, 'This is a test comment');
+    await tester.testTextInput.receiveAction(TextInputAction.done);
+    expect(receivedText, 'This is a test comment');
+  });
+
+  testWidgets('Input disabled when isEnabled false', (tester) async {
+    String? receviedText;
+
+    await tester.pumpConsumerWidget(
+      ActivityTextField(
+        onSubmit: (text) => receviedText = text,
+        isEnabled: false,
+        likeId: 'test-suffix',
+      ),
+      overrides: overrides,
+    );
+
+    final suffixIcon = find.byType(IconButton);
+    await tester.tap(suffixIcon, warnIfMissed: false);
+
+    final textField = find.byType(TextField);
+    await tester.enterText(textField, 'This is a test comment');
+    await tester.testTextInput.receiveAction(TextInputAction.done);
+
+    expect(receviedText, isNull);
+    verifyNever(() => activityMock.addLike());
+    verifyNever(() => activityMock.removeActivity(any()));
+  });
+}
diff --git a/mobile/test/modules/activity/activity_tile_test.dart b/mobile/test/modules/activity/activity_tile_test.dart
new file mode 100644
index 0000000000..f2980dce6f
--- /dev/null
+++ b/mobile/test/modules/activity/activity_tile_test.dart
@@ -0,0 +1,222 @@
+@Tags(['widget'])
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/modules/activities/models/activity.model.dart';
+import 'package:immich_mobile/modules/activities/widgets/activity_tile.dart';
+import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart';
+import 'package:immich_mobile/shared/models/store.dart';
+import 'package:immich_mobile/shared/ui/user_circle_avatar.dart';
+import 'package:isar/isar.dart';
+
+import '../../fixtures/asset.stub.dart';
+import '../../fixtures/user.stub.dart';
+import '../../test_utils.dart';
+import '../../widget_tester_extensions.dart';
+import '../asset_viewer/asset_viewer_mocks.dart';
+
+void main() {
+  late MockCurrentAssetProvider assetProvider;
+  late List<Override> overrides;
+  late Isar db;
+
+  setUpAll(() async {
+    TestUtils.init();
+    db = await TestUtils.initIsar();
+    // For UserCircleAvatar
+    Store.init(db);
+    Store.put(StoreKey.currentUser, UserStub.admin);
+    Store.put(StoreKey.serverEndpoint, '');
+    Store.put(StoreKey.accessToken, '');
+  });
+
+  setUp(() {
+    assetProvider = MockCurrentAssetProvider();
+    overrides = [currentAssetProvider.overrideWith(() => assetProvider)];
+  });
+
+  testWidgets('Returns a ListTile', (tester) async {
+    await tester.pumpConsumerWidget(
+      ActivityTile(
+        Activity(
+          id: '1',
+          createdAt: DateTime(100),
+          type: ActivityType.like,
+          user: UserStub.admin,
+        ),
+      ),
+      overrides: overrides,
+    );
+
+    expect(find.byType(ListTile), findsOneWidget);
+  });
+
+  testWidgets('No trailing widget when activity assetId == null',
+      (tester) async {
+    await tester.pumpConsumerWidget(
+      ActivityTile(
+        Activity(
+          id: '1',
+          createdAt: DateTime(100),
+          type: ActivityType.like,
+          user: UserStub.admin,
+        ),
+      ),
+      overrides: overrides,
+    );
+
+    final listTile = tester.widget<ListTile>(find.byType(ListTile));
+    expect(listTile.trailing, isNull);
+  });
+
+  testWidgets(
+      'Asset Thumbanil as trailing widget when activity assetId != null',
+      (tester) async {
+    await tester.pumpConsumerWidget(
+      ActivityTile(
+        Activity(
+          id: '1',
+          createdAt: DateTime(100),
+          type: ActivityType.like,
+          user: UserStub.admin,
+          assetId: '1',
+        ),
+      ),
+      overrides: overrides,
+    );
+
+    final listTile = tester.widget<ListTile>(find.byType(ListTile));
+    expect(listTile.trailing, isNotNull);
+    // TODO: Validate this to be the common class after migrating ActivityTile#_ActivityAssetThumbnail to a common class
+  });
+
+  testWidgets('No trailing widget when current asset != null', (tester) async {
+    await tester.pumpConsumerWidget(
+      ActivityTile(
+        Activity(
+          id: '1',
+          createdAt: DateTime(100),
+          type: ActivityType.like,
+          user: UserStub.admin,
+          assetId: '1',
+        ),
+      ),
+      overrides: overrides,
+    );
+
+    assetProvider.state = AssetStub.image1;
+    await tester.pumpAndSettle();
+
+    final listTile = tester.widget<ListTile>(find.byType(ListTile));
+    expect(listTile.trailing, isNull);
+  });
+
+  group('Like Activity', () {
+    final activity = Activity(
+      id: '1',
+      createdAt: DateTime(100),
+      type: ActivityType.like,
+      user: UserStub.admin,
+    );
+
+    testWidgets('Like contains filled heart as leading', (tester) async {
+      await tester.pumpConsumerWidget(
+        ActivityTile(activity),
+        overrides: overrides,
+      );
+
+      // Leading widget should not be null
+      final listTile = tester.widget<ListTile>(find.byType(ListTile));
+      expect(listTile.leading, isNotNull);
+
+      // And should have a favorite icon
+      final favoIconFinder = find.widgetWithIcon(
+        listTile.leading!.runtimeType,
+        Icons.favorite_rounded,
+      );
+
+      expect(favoIconFinder, findsOneWidget);
+    });
+
+    testWidgets('Like title is center aligned', (tester) async {
+      await tester.pumpConsumerWidget(
+        ActivityTile(activity),
+        overrides: overrides,
+      );
+
+      final listTile = tester.widget<ListTile>(find.byType(ListTile));
+
+      expect(listTile.titleAlignment, ListTileTitleAlignment.center);
+    });
+
+    testWidgets('No subtitle for likes', (tester) async {
+      await tester.pumpConsumerWidget(
+        ActivityTile(activity),
+        overrides: overrides,
+      );
+
+      final listTile = tester.widget<ListTile>(find.byType(ListTile));
+
+      expect(listTile.subtitle, isNull);
+    });
+  });
+
+  group('Comment Activity', () {
+    final activity = Activity(
+      id: '1',
+      createdAt: DateTime(100),
+      type: ActivityType.comment,
+      comment: 'This is a test comment',
+      user: UserStub.admin,
+    );
+
+    testWidgets('Comment contains User Circle Avatar as leading',
+        (tester) async {
+      await tester.pumpConsumerWidget(
+        ActivityTile(activity),
+        overrides: overrides,
+      );
+
+      final userAvatarFinder = find.byType(UserCircleAvatar);
+      expect(userAvatarFinder, findsOneWidget);
+
+      // Leading widget should not be null
+      final listTile = tester.widget<ListTile>(find.byType(ListTile));
+      expect(listTile.leading, isNotNull);
+
+      // Make sure that the leading widget is the UserCircleAvatar
+      final userAvatar = tester.widget<UserCircleAvatar>(userAvatarFinder);
+      expect(listTile.leading, userAvatar);
+    });
+
+    testWidgets('Comment title is top aligned', (tester) async {
+      await tester.pumpConsumerWidget(
+        ActivityTile(activity),
+        overrides: overrides,
+      );
+
+      final listTile = tester.widget<ListTile>(find.byType(ListTile));
+
+      expect(listTile.titleAlignment, ListTileTitleAlignment.top);
+    });
+
+    testWidgets('Contains comment text as subtitle', (tester) async {
+      await tester.pumpConsumerWidget(
+        ActivityTile(activity),
+        overrides: overrides,
+      );
+
+      final listTile = tester.widget<ListTile>(find.byType(ListTile));
+
+      expect(listTile.subtitle, isNotNull);
+      expect(
+        find.descendant(
+          of: find.byType(ListTile),
+          matching: find.text(activity.comment!),
+        ),
+        findsOneWidget,
+      );
+    });
+  });
+}
diff --git a/mobile/test/modules/activity/dismissible_activity_test.dart b/mobile/test/modules/activity/dismissible_activity_test.dart
new file mode 100644
index 0000000000..0ce204a648
--- /dev/null
+++ b/mobile/test/modules/activity/dismissible_activity_test.dart
@@ -0,0 +1,119 @@
+@Tags(['widget'])
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:immich_mobile/modules/activities/models/activity.model.dart';
+import 'package:immich_mobile/modules/activities/widgets/activity_tile.dart';
+import 'package:immich_mobile/modules/activities/widgets/dismissible_activity.dart';
+import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart';
+import 'package:immich_mobile/shared/ui/confirm_dialog.dart';
+import 'package:riverpod_annotation/riverpod_annotation.dart';
+
+import '../../fixtures/user.stub.dart';
+import '../../test_utils.dart';
+import '../../widget_tester_extensions.dart';
+import '../asset_viewer/asset_viewer_mocks.dart';
+
+final activity = Activity(
+  id: '1',
+  createdAt: DateTime(100),
+  type: ActivityType.like,
+  user: UserStub.admin,
+);
+
+void main() {
+  late MockCurrentAssetProvider assetProvider;
+  late List<Override> overrides;
+
+  setUpAll(() => TestUtils.init());
+
+  setUp(() {
+    assetProvider = MockCurrentAssetProvider();
+    overrides = [currentAssetProvider.overrideWith(() => assetProvider)];
+  });
+
+  testWidgets('Returns a Dismissible', (tester) async {
+    await tester.pumpConsumerWidget(
+      DismissibleActivity('1', ActivityTile(activity)),
+      overrides: overrides,
+    );
+
+    expect(find.byType(Dismissible), findsOneWidget);
+  });
+
+  testWidgets('Dialog displayed when onDismiss is set', (tester) async {
+    await tester.pumpConsumerWidget(
+      DismissibleActivity('1', ActivityTile(activity), onDismiss: (_) {}),
+      overrides: overrides,
+    );
+
+    final dismissible = find.byType(Dismissible);
+    await tester.drag(dismissible, const Offset(500, 0));
+    await tester.pumpAndSettle();
+
+    expect(find.byType(ConfirmDialog), findsOneWidget);
+  });
+
+  testWidgets(
+      'Ok action in ConfirmDialog should call onDismiss with activityId',
+      (tester) async {
+    String? receivedActivityId;
+    await tester.pumpConsumerWidget(
+      DismissibleActivity(
+        '1',
+        ActivityTile(activity),
+        onDismiss: (id) => receivedActivityId = id,
+      ),
+      overrides: overrides,
+    );
+
+    final dismissible = find.byType(Dismissible);
+    await tester.drag(dismissible, const Offset(-500, 0));
+    await tester.pumpAndSettle();
+
+    final okButton = find.text('delete_dialog_ok');
+    await tester.tap(okButton);
+    await tester.pumpAndSettle();
+
+    expect(receivedActivityId, '1');
+  });
+
+  testWidgets('Delete icon for background if onDismiss is set', (tester) async {
+    await tester.pumpConsumerWidget(
+      DismissibleActivity('1', ActivityTile(activity), onDismiss: (_) {}),
+      overrides: overrides,
+    );
+
+    final dismissible = find.byType(Dismissible);
+    await tester.drag(dismissible, const Offset(500, 0));
+    await tester.pumpAndSettle();
+
+    expect(find.byIcon(Icons.delete_sweep_rounded), findsOneWidget);
+  });
+
+  testWidgets('No delete dialog if onDismiss is not set', (tester) async {
+    await tester.pumpConsumerWidget(
+      DismissibleActivity('1', ActivityTile(activity)),
+      overrides: overrides,
+    );
+
+    final dismissible = find.byType(Dismissible);
+    await tester.drag(dismissible, const Offset(500, 0));
+    await tester.pumpAndSettle();
+
+    expect(find.byType(ConfirmDialog), findsNothing);
+  });
+
+  testWidgets('No icon for background if onDismiss is not set', (tester) async {
+    await tester.pumpConsumerWidget(
+      DismissibleActivity('1', ActivityTile(activity)),
+      overrides: overrides,
+    );
+
+    final dismissible = find.byType(Dismissible);
+    await tester.drag(dismissible, const Offset(-500, 0));
+    await tester.pumpAndSettle();
+
+    expect(find.byIcon(Icons.delete_sweep_rounded), findsNothing);
+  });
+}
diff --git a/mobile/test/modules/album/album_mocks.dart b/mobile/test/modules/album/album_mocks.dart
new file mode 100644
index 0000000000..c8218e50df
--- /dev/null
+++ b/mobile/test/modules/album/album_mocks.dart
@@ -0,0 +1,15 @@
+import 'package:immich_mobile/modules/album/providers/current_album.provider.dart';
+import 'package:immich_mobile/shared/models/album.dart';
+import 'package:mocktail/mocktail.dart';
+
+class MockCurrentAlbumProvider extends CurrentAlbum
+    with Mock
+    implements CurrentAlbumInternal {
+  Album? initAlbum;
+  MockCurrentAlbumProvider([this.initAlbum]);
+
+  @override
+  Album? build() {
+    return initAlbum;
+  }
+}
diff --git a/mobile/test/album_sort_by_options_provider_test.dart b/mobile/test/modules/album/album_sort_by_options_provider_test.dart
similarity index 67%
rename from mobile/test/album_sort_by_options_provider_test.dart
rename to mobile/test/modules/album/album_sort_by_options_provider_test.dart
index 30b235166b..b39c495ae5 100644
--- a/mobile/test/album_sort_by_options_provider_test.dart
+++ b/mobile/test/modules/album/album_sort_by_options_provider_test.dart
@@ -1,17 +1,17 @@
-import 'package:collection/collection.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/modules/album/providers/album_sort_by_options.provider.dart';
+import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
 import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
 import 'package:immich_mobile/shared/models/album.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
 import 'package:isar/isar.dart';
 import 'package:mocktail/mocktail.dart';
 
-import 'fixtures/album.stub.dart';
-import 'fixtures/asset.stub.dart';
-import 'mocks/app_settings_provider.mock.dart';
-import 'test_utils.dart';
+import '../../fixtures/album.stub.dart';
+import '../../fixtures/asset.stub.dart';
+import '../../test_utils.dart';
+import '../settings/settings_mocks.dart';
 
 void main() {
   /// Verify the sort modes
@@ -48,15 +48,24 @@ void main() {
       const created = AlbumSortMode.created;
       test("Created time - ASC", () {
         final sorted = created.sortFn(albums, false);
-        expect(sorted.isSortedBy((a) => a.createdAt), true);
+        final sortedList = [
+          AlbumStub.emptyAlbum,
+          AlbumStub.twoAsset,
+          AlbumStub.oneAsset,
+          AlbumStub.sharedWithUser,
+        ];
+        expect(sorted, orderedEquals(sortedList));
       });
 
       test("Created time - DESC", () {
         final sorted = created.sortFn(albums, true);
-        expect(
-          sorted.isSorted((b, a) => a.createdAt.compareTo(b.createdAt)),
-          true,
-        );
+        final sortedList = [
+          AlbumStub.sharedWithUser,
+          AlbumStub.oneAsset,
+          AlbumStub.twoAsset,
+          AlbumStub.emptyAlbum,
+        ];
+        expect(sorted, orderedEquals(sortedList));
       });
     });
 
@@ -64,18 +73,24 @@ void main() {
       const assetCount = AlbumSortMode.assetCount;
       test("Asset Count - ASC", () {
         final sorted = assetCount.sortFn(albums, false);
-        expect(
-          sorted.isSorted((a, b) => a.assetCount.compareTo(b.assetCount)),
-          true,
-        );
+        final sortedList = [
+          AlbumStub.emptyAlbum,
+          AlbumStub.sharedWithUser,
+          AlbumStub.oneAsset,
+          AlbumStub.twoAsset,
+        ];
+        expect(sorted, orderedEquals(sortedList));
       });
 
       test("Asset Count - DESC", () {
         final sorted = assetCount.sortFn(albums, true);
-        expect(
-          sorted.isSorted((b, a) => a.assetCount.compareTo(b.assetCount)),
-          true,
-        );
+        final sortedList = [
+          AlbumStub.twoAsset,
+          AlbumStub.oneAsset,
+          AlbumStub.sharedWithUser,
+          AlbumStub.emptyAlbum,
+        ];
+        expect(sorted, orderedEquals(sortedList));
       });
     });
 
@@ -83,18 +98,24 @@ void main() {
       const lastModified = AlbumSortMode.lastModified;
       test("Last modified - ASC", () {
         final sorted = lastModified.sortFn(albums, false);
-        expect(
-          sorted.isSorted((a, b) => a.modifiedAt.compareTo(b.modifiedAt)),
-          true,
-        );
+        final sortedList = [
+          AlbumStub.twoAsset,
+          AlbumStub.emptyAlbum,
+          AlbumStub.sharedWithUser,
+          AlbumStub.oneAsset,
+        ];
+        expect(sorted, orderedEquals(sortedList));
       });
 
       test("Last modified - DESC", () {
         final sorted = lastModified.sortFn(albums, true);
-        expect(
-          sorted.isSorted((b, a) => a.modifiedAt.compareTo(b.modifiedAt)),
-          true,
-        );
+        final sortedList = [
+          AlbumStub.oneAsset,
+          AlbumStub.sharedWithUser,
+          AlbumStub.emptyAlbum,
+          AlbumStub.twoAsset,
+        ];
+        expect(sorted, orderedEquals(sortedList));
       });
     });
 
@@ -102,18 +123,24 @@ void main() {
       const created = AlbumSortMode.created;
       test("Created - ASC", () {
         final sorted = created.sortFn(albums, false);
-        expect(
-          sorted.isSorted((a, b) => a.createdAt.compareTo(b.createdAt)),
-          true,
-        );
+        final sortedList = [
+          AlbumStub.emptyAlbum,
+          AlbumStub.twoAsset,
+          AlbumStub.oneAsset,
+          AlbumStub.sharedWithUser,
+        ];
+        expect(sorted, orderedEquals(sortedList));
       });
 
       test("Created - DESC", () {
         final sorted = created.sortFn(albums, true);
-        expect(
-          sorted.isSorted((b, a) => a.createdAt.compareTo(b.createdAt)),
-          true,
-        );
+        final sortedList = [
+          AlbumStub.sharedWithUser,
+          AlbumStub.oneAsset,
+          AlbumStub.twoAsset,
+          AlbumStub.emptyAlbum,
+        ];
+        expect(sorted, orderedEquals(sortedList));
       });
     });
 
@@ -122,28 +149,24 @@ void main() {
 
       test("Most Recent - ASC", () {
         final sorted = mostRecent.sortFn(albums, false);
-        expect(
-          sorted,
-          [
-            AlbumStub.sharedWithUser,
-            AlbumStub.twoAsset,
-            AlbumStub.oneAsset,
-            AlbumStub.emptyAlbum,
-          ],
-        );
+        final sortedList = [
+          AlbumStub.sharedWithUser,
+          AlbumStub.twoAsset,
+          AlbumStub.oneAsset,
+          AlbumStub.emptyAlbum,
+        ];
+        expect(sorted, orderedEquals(sortedList));
       });
 
       test("Most Recent - DESC", () {
         final sorted = mostRecent.sortFn(albums, true);
-        expect(
-          sorted,
-          [
-            AlbumStub.emptyAlbum,
-            AlbumStub.oneAsset,
-            AlbumStub.twoAsset,
-            AlbumStub.sharedWithUser,
-          ],
-        );
+        final sortedList = [
+          AlbumStub.emptyAlbum,
+          AlbumStub.oneAsset,
+          AlbumStub.twoAsset,
+          AlbumStub.sharedWithUser,
+        ];
+        expect(sorted, orderedEquals(sortedList));
       });
     });
 
@@ -152,28 +175,24 @@ void main() {
 
       test("Most Oldest - ASC", () {
         final sorted = mostOldest.sortFn(albums, false);
-        expect(
-          sorted,
-          [
-            AlbumStub.twoAsset,
-            AlbumStub.emptyAlbum,
-            AlbumStub.oneAsset,
-            AlbumStub.sharedWithUser,
-          ],
-        );
+        final sortedList = [
+          AlbumStub.twoAsset,
+          AlbumStub.emptyAlbum,
+          AlbumStub.oneAsset,
+          AlbumStub.sharedWithUser,
+        ];
+        expect(sorted, orderedEquals(sortedList));
       });
 
       test("Most Oldest - DESC", () {
         final sorted = mostOldest.sortFn(albums, true);
-        expect(
-          sorted,
-          [
-            AlbumStub.sharedWithUser,
-            AlbumStub.oneAsset,
-            AlbumStub.emptyAlbum,
-            AlbumStub.twoAsset,
-          ],
-        );
+        final sortedList = [
+          AlbumStub.sharedWithUser,
+          AlbumStub.oneAsset,
+          AlbumStub.emptyAlbum,
+          AlbumStub.twoAsset,
+        ];
+        expect(sorted, orderedEquals(sortedList));
       });
     });
   });
@@ -186,7 +205,9 @@ void main() {
     setUp(() async {
       settingsMock = AppSettingsServiceMock();
       container = TestUtils.createContainer(
-        overrides: [getAppSettingsServiceMock(settingsMock)],
+        overrides: [
+          appSettingsServiceProvider.overrideWith((ref) => settingsMock),
+        ],
       );
     });
 
@@ -196,7 +217,7 @@ void main() {
         () => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortOrder),
       ).thenReturn(0);
 
-      expect(AlbumSortMode.created, container.read(albumSortByOptionsProvider));
+      expect(container.read(albumSortByOptionsProvider), AlbumSortMode.created);
     });
 
     test('Returns the correct sort mode with index from Store', () {
@@ -206,8 +227,8 @@ void main() {
       ).thenReturn(3);
 
       expect(
-        AlbumSortMode.lastModified,
         container.read(albumSortByOptionsProvider),
+        AlbumSortMode.lastModified,
       );
     });
 
@@ -230,7 +251,6 @@ void main() {
       ).thenReturn(0);
 
       final listener = ListenerMock<AlbumSortMode>();
-
       container.listen(
         albumSortByOptionsProvider,
         listener,
@@ -265,7 +285,9 @@ void main() {
     setUp(() async {
       settingsMock = AppSettingsServiceMock();
       container = TestUtils.createContainer(
-        overrides: [getAppSettingsServiceMock(settingsMock)],
+        overrides: [
+          appSettingsServiceProvider.overrideWith((ref) => settingsMock),
+        ],
       );
     });
 
@@ -274,7 +296,7 @@ void main() {
         () => settingsMock.getSetting(AppSettingsEnum.selectedAlbumSortReverse),
       ).thenReturn(false);
 
-      expect(false, container.read(albumSortOrderProvider));
+      expect(container.read(albumSortOrderProvider), isFalse);
     });
 
     test('Properly saves the correct order', () {
@@ -294,7 +316,6 @@ void main() {
       ).thenReturn(false);
 
       final listener = ListenerMock<bool>();
-
       container.listen(
         albumSortOrderProvider,
         listener,
diff --git a/mobile/test/modules/asset_viewer/asset_viewer_mocks.dart b/mobile/test/modules/asset_viewer/asset_viewer_mocks.dart
new file mode 100644
index 0000000000..5a4bbd8be1
--- /dev/null
+++ b/mobile/test/modules/asset_viewer/asset_viewer_mocks.dart
@@ -0,0 +1,15 @@
+import 'package:immich_mobile/modules/asset_viewer/providers/current_asset.provider.dart';
+import 'package:immich_mobile/shared/models/asset.dart';
+import 'package:mocktail/mocktail.dart';
+
+class MockCurrentAssetProvider extends CurrentAssetInternal
+    with Mock
+    implements CurrentAsset {
+  Asset? initAsset;
+  MockCurrentAssetProvider([this.initAsset]);
+
+  @override
+  Asset? build() {
+    return initAsset;
+  }
+}
diff --git a/mobile/test/asset_extensions_test.dart b/mobile/test/modules/extensions/asset_extensions_test.dart
similarity index 89%
rename from mobile/test/asset_extensions_test.dart
rename to mobile/test/modules/extensions/asset_extensions_test.dart
index 1e429b5ac1..15aab38fdb 100644
--- a/mobile/test/asset_extensions_test.dart
+++ b/mobile/test/modules/extensions/asset_extensions_test.dart
@@ -49,8 +49,8 @@ void main() {
       final a = makeAsset(id: '1', createdAt: createdAt);
       final (dt, tz) = a.getTZAdjustedTimeAndOffset();
 
-      expect(dt, createdAt);
-      expect(tz, createdAt.timeZoneOffset);
+      expect(createdAt, dt);
+      expect(createdAt.timeZoneOffset, tz);
     });
 
     test('returns createdAt in local if in utc', () {
@@ -59,8 +59,8 @@ void main() {
       final (dt, tz) = a.getTZAdjustedTimeAndOffset();
 
       final localCreatedAt = createdAt.toLocal();
-      expect(dt, localCreatedAt);
-      expect(tz, localCreatedAt.timeZoneOffset);
+      expect(localCreatedAt, dt);
+      expect(localCreatedAt.timeZoneOffset, tz);
     });
   });
 
@@ -73,8 +73,8 @@ void main() {
       final (dt, tz) = a.getTZAdjustedTimeAndOffset();
 
       final dateTimeInUTC = dateTimeOriginal.toUtc();
-      expect(dt, dateTimeInUTC);
-      expect(tz, dateTimeInUTC.timeZoneOffset);
+      expect(dateTimeInUTC, dt);
+      expect(dateTimeInUTC.timeZoneOffset, tz);
     });
 
     test('Returns dateTimeOriginal in UTC from exifInfo with invalid timezone',
@@ -89,8 +89,8 @@ void main() {
       final (dt, tz) = a.getTZAdjustedTimeAndOffset();
 
       final dateTimeInUTC = dateTimeOriginal.toUtc();
-      expect(dt, dateTimeInUTC);
-      expect(tz, dateTimeInUTC.timeZoneOffset);
+      expect(dateTimeInUTC, dt);
+      expect(dateTimeInUTC.timeZoneOffset, tz);
     });
   });
 
@@ -106,8 +106,8 @@ void main() {
 
       final adjustedTime =
           TZDateTime.from(dateTimeOriginal.toUtc(), getLocation(location));
-      expect(dt, adjustedTime);
-      expect(tz, adjustedTime.timeZoneOffset);
+      expect(adjustedTime, dt);
+      expect(adjustedTime.timeZoneOffset, tz);
     });
 
     test('With timezone as offset', () {
@@ -124,8 +124,8 @@ void main() {
       final adjustedTime = dateTimeOriginal.toUtc().add(offsetFromLocation);
 
       // Adds the offset to the actual time and returns the offset separately
-      expect(dt, adjustedTime);
-      expect(tz, offsetFromLocation);
+      expect(adjustedTime, dt);
+      expect(offsetFromLocation, tz);
     });
   });
 }
diff --git a/mobile/test/builtin_extensions_test.dart b/mobile/test/modules/extensions/builtin_extensions_test.dart
similarity index 74%
rename from mobile/test/builtin_extensions_test.dart
rename to mobile/test/modules/extensions/builtin_extensions_test.dart
index 9fc729774a..2de450a952 100644
--- a/mobile/test/builtin_extensions_test.dart
+++ b/mobile/test/modules/extensions/builtin_extensions_test.dart
@@ -11,9 +11,9 @@ void main() {
       );
     });
     test('malformed', () {
-      expect("".toDuration(), null);
-      expect("1:2".toDuration(), null);
-      expect("a:b:c".toDuration(), null);
+      expect("".toDuration(), isNull);
+      expect("1:2".toDuration(), isNull);
+      expect("a:b:c".toDuration(), isNull);
     });
   });
   group('Test uniqueConsecutive', () {
@@ -29,17 +29,17 @@ void main() {
 
     test('noDuplicates', () {
       final a = [1, 2, 3];
-      expect(a.uniqueConsecutive(), [1, 2, 3]);
+      expect(a.uniqueConsecutive(), orderedEquals([1, 2, 3]));
     });
 
     test('unsortedDuplicates', () {
       final a = [1, 2, 1, 3];
-      expect(a.uniqueConsecutive(), [1, 2, 1, 3]);
+      expect(a.uniqueConsecutive(), orderedEquals([1, 2, 1, 3]));
     });
 
     test('sortedDuplicates', () {
       final a = [6, 6, 2, 3, 3, 3, 4, 5, 1, 1];
-      expect(a.uniqueConsecutive(), [6, 2, 3, 4, 5, 1]);
+      expect(a.uniqueConsecutive(), orderedEquals([6, 2, 3, 4, 5, 1]));
     });
 
     test('withKey', () {
@@ -48,7 +48,7 @@ void main() {
         a.uniqueConsecutive(
           compare: (s1, s2) => s1.length.compareTo(s2.length),
         ),
-        ["a", "bb", "ddd"],
+        orderedEquals(["a", "bb", "ddd"]),
       );
     });
   });
diff --git a/mobile/test/asset_grid_data_structure_test.dart b/mobile/test/modules/home/asset_grid_data_structure_test.dart
similarity index 96%
rename from mobile/test/asset_grid_data_structure_test.dart
rename to mobile/test/modules/home/asset_grid_data_structure_test.dart
index 6b8f080638..86433768ac 100644
--- a/mobile/test/asset_grid_data_structure_test.dart
+++ b/mobile/test/modules/home/asset_grid_data_structure_test.dart
@@ -75,7 +75,7 @@ void main() {
       // 5 Assets => 2 Rows
       // Day 1
       // 5 Assets => 2 Rows
-      expect(renderList.elements.length, 4);
+      expect(renderList.elements, hasLength(4));
       expect(
         renderList.elements[0].type,
         RenderAssetGridElementType.monthTitle,
@@ -122,7 +122,7 @@ void main() {
         RenderAssetGridElementType.monthTitle,
       ];
 
-      expect(renderList.elements.length, types.length);
+      expect(renderList.elements, hasLength(types.length));
 
       for (int i = 0; i < renderList.elements.length; i++) {
         expect(renderList.elements[i].type, types[i]);
diff --git a/mobile/test/modules/settings/settings_mocks.dart b/mobile/test/modules/settings/settings_mocks.dart
new file mode 100644
index 0000000000..0fd6948702
--- /dev/null
+++ b/mobile/test/modules/settings/settings_mocks.dart
@@ -0,0 +1,4 @@
+import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
+import 'package:mocktail/mocktail.dart';
+
+class AppSettingsServiceMock extends Mock implements AppSettingsService {}
diff --git a/mobile/test/modules/shared/shared_mocks.dart b/mobile/test/modules/shared/shared_mocks.dart
new file mode 100644
index 0000000000..af88a93eaa
--- /dev/null
+++ b/mobile/test/modules/shared/shared_mocks.dart
@@ -0,0 +1,16 @@
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/shared/models/user.dart';
+import 'package:immich_mobile/shared/providers/user.provider.dart';
+import 'package:immich_mobile/shared/services/hash.service.dart';
+import 'package:mocktail/mocktail.dart';
+
+class MockHashService extends Mock implements HashService {}
+
+class MockCurrentUserProvider extends StateNotifier<User?>
+    with Mock
+    implements CurrentUserProvider {
+  MockCurrentUserProvider() : super(null);
+
+  @override
+  set state(User? user) => super.state = user;
+}
diff --git a/mobile/test/sync_service_test.dart b/mobile/test/modules/shared/sync_service_test.dart
similarity index 84%
rename from mobile/test/sync_service_test.dart
rename to mobile/test/modules/shared/sync_service_test.dart
index 1d00875541..f5caedfd06 100644
--- a/mobile/test/sync_service_test.dart
+++ b/mobile/test/modules/shared/sync_service_test.dart
@@ -1,17 +1,14 @@
 import 'package:flutter/widgets.dart';
 import 'package:flutter_test/flutter_test.dart';
-import 'package:immich_mobile/shared/models/album.dart';
 import 'package:immich_mobile/shared/models/asset.dart';
-import 'package:immich_mobile/shared/models/etag.dart';
-import 'package:immich_mobile/shared/models/exif_info.dart';
-import 'package:immich_mobile/shared/models/logger_message.model.dart';
 import 'package:immich_mobile/shared/models/store.dart';
 import 'package:immich_mobile/shared/models/user.dart';
-import 'package:immich_mobile/shared/services/hash.service.dart';
 import 'package:immich_mobile/shared/services/immich_logger.service.dart';
 import 'package:immich_mobile/shared/services/sync.service.dart';
 import 'package:isar/isar.dart';
-import 'package:mocktail/mocktail.dart';
+
+import '../../test_utils.dart';
+import 'shared_mocks.dart';
 
 void main() {
   Asset makeAsset({
@@ -39,22 +36,6 @@ void main() {
     );
   }
 
-  Isar loadDb() {
-    return Isar.openSync(
-      [
-        ExifInfoSchema,
-        AssetSchema,
-        AlbumSchema,
-        UserSchema,
-        StoreValueSchema,
-        LoggerMessageSchema,
-        ETagSchema,
-      ],
-      maxSizeMiB: 256,
-      directory: ".",
-    );
-  }
-
   group('Test SyncService grouped', () {
     late final Isar db;
     final MockHashService hs = MockHashService();
@@ -67,8 +48,7 @@ void main() {
     );
     setUpAll(() async {
       WidgetsFlutterBinding.ensureInitialized();
-      await Isar.initializeIsarCore(download: true);
-      db = loadDb();
+      db = await TestUtils.initIsar();
       ImmichLogger();
       db.writeTxnSync(() => db.clearSync());
       Store.init(db);
@@ -97,7 +77,7 @@ void main() {
       expect(db.assets.countSync(), 5);
       final bool c1 =
           await s.syncRemoteAssetsToDb(owner, _failDiff, (u) => remoteAssets);
-      expect(c1, false);
+      expect(c1, isFalse);
       expect(db.assets.countSync(), 5);
     });
 
@@ -114,7 +94,7 @@ void main() {
       expect(db.assets.countSync(), 5);
       final bool c1 =
           await s.syncRemoteAssetsToDb(owner, _failDiff, (u) => remoteAssets);
-      expect(c1, true);
+      expect(c1, isTrue);
       expect(db.assets.countSync(), 7);
     });
 
@@ -131,22 +111,22 @@ void main() {
       expect(db.assets.countSync(), 5);
       final bool c1 =
           await s.syncRemoteAssetsToDb(owner, _failDiff, (u) => remoteAssets);
-      expect(c1, true);
+      expect(c1, isTrue);
       expect(db.assets.countSync(), 8);
       final bool c2 =
           await s.syncRemoteAssetsToDb(owner, _failDiff, (u) => remoteAssets);
-      expect(c2, false);
+      expect(c2, isFalse);
       expect(db.assets.countSync(), 8);
       remoteAssets.removeAt(4);
       final bool c3 =
           await s.syncRemoteAssetsToDb(owner, _failDiff, (u) => remoteAssets);
-      expect(c3, true);
+      expect(c3, isTrue);
       expect(db.assets.countSync(), 7);
       remoteAssets.add(makeAsset(checksum: "k", remoteId: "2-1e"));
       remoteAssets.add(makeAsset(checksum: "l", remoteId: "2-2"));
       final bool c4 =
           await s.syncRemoteAssetsToDb(owner, _failDiff, (u) => remoteAssets);
-      expect(c4, true);
+      expect(c4, isTrue);
       expect(db.assets.countSync(), 9);
     });
 
@@ -164,7 +144,7 @@ void main() {
         (user, since) async => (toUpsert, toDelete),
         (user) => throw Exception(),
       );
-      expect(c, true);
+      expect(c, isTrue);
       expect(db.assets.countSync(), 6);
     });
   });
@@ -172,5 +152,3 @@ void main() {
 
 Future<(List<Asset>?, List<String>?)> _failDiff(User user, DateTime time) =>
     Future.value((null, null));
-
-class MockHashService extends Mock implements HashService {}
diff --git a/mobile/test/async_mutex_test.dart b/mobile/test/modules/utils/async_mutex_test.dart
similarity index 100%
rename from mobile/test/async_mutex_test.dart
rename to mobile/test/modules/utils/async_mutex_test.dart
diff --git a/mobile/test/diff_test.dart b/mobile/test/modules/utils/diff_test.dart
similarity index 100%
rename from mobile/test/diff_test.dart
rename to mobile/test/modules/utils/diff_test.dart
diff --git a/mobile/test/test_utils.dart b/mobile/test/test_utils.dart
index 5052f59107..bd359d0400 100644
--- a/mobile/test/test_utils.dart
+++ b/mobile/test/test_utils.dart
@@ -1,3 +1,6 @@
+import 'dart:io';
+
+import 'package:easy_localization/easy_localization.dart';
 import 'package:flutter_test/flutter_test.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/modules/backup/models/backup_album.model.dart';
@@ -14,9 +17,10 @@ import 'package:immich_mobile/shared/models/user.dart';
 import 'package:isar/isar.dart';
 import 'package:mocktail/mocktail.dart';
 
+import 'mock_http_override.dart';
+
 // Listener Mock to test when a provider notifies its listeners
 class ListenerMock<T> extends Mock {
-  // ignore: avoid-declaring-call-method
   void call(T? previous, T next);
 }
 
@@ -26,6 +30,12 @@ final class TestUtils {
   /// Downloads Isar binaries (if required) and initializes a new Isar db
   static Future<Isar> initIsar() async {
     await Isar.initializeIsarCore(download: true);
+
+    final instance = Isar.getInstance();
+    if (instance != null) {
+      return instance;
+    }
+
     final db = await Isar.open(
       [
         StoreValueSchema,
@@ -41,8 +51,9 @@ final class TestUtils {
         IOSDeviceAssetSchema,
       ],
       maxSizeMiB: 256,
-      directory: ".",
+      directory: "test/",
     );
+
     // Clear and close db on test end
     addTearDown(() async {
       await db.writeTxn(() => db.clear());
@@ -68,4 +79,11 @@ final class TestUtils {
 
     return container;
   }
+
+  static void init() {
+    // Turn off easy localization logging
+    EasyLocalization.logger.enableBuildModes = [];
+    WidgetController.hitTestWarningShouldBeFatal = true;
+    HttpOverrides.global = MockHttpOverrides();
+  }
 }
diff --git a/mobile/test/widget_tester_extensions.dart b/mobile/test/widget_tester_extensions.dart
new file mode 100644
index 0000000000..c054a32501
--- /dev/null
+++ b/mobile/test/widget_tester_extensions.dart
@@ -0,0 +1,30 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:hooks_riverpod/hooks_riverpod.dart';
+
+extension PumpConsumerWidget on WidgetTester {
+  /// Wraps the provided [widget] with Material app such that it becomes:
+  ///
+  /// ProviderScope
+  ///   |-MaterialApp
+  ///     |-Material
+  ///       |-[widget]
+  Future<void> pumpConsumerWidget(
+    Widget widget, {
+    Duration? duration,
+    EnginePhase phase = EnginePhase.sendSemanticsUpdate,
+    List<Override> overrides = const [],
+  }) async {
+    return pumpWidget(
+      ProviderScope(
+        overrides: overrides,
+        child: MaterialApp(
+          debugShowCheckedModeBanner: false,
+          home: Material(child: widget),
+        ),
+      ),
+      duration,
+      phase,
+    );
+  }
+}