diff --git a/mobile/lib/domain/services/timeline.service.dart b/mobile/lib/domain/services/timeline.service.dart index 4cc58b0fe7..4ac9860b2e 100644 --- a/mobile/lib/domain/services/timeline.service.dart +++ b/mobile/lib/domain/services/timeline.service.dart @@ -85,8 +85,12 @@ class TimelineFactory { TimelineService fromAssetsWithBuckets(List assets, TimelineOrigin type) => TimelineService(_timelineRepository.fromAssetsWithBuckets(assets, type)); - TimelineService map(List userIds, TimelineMapOptions options) => - TimelineService(_timelineRepository.map(userIds, options, groupBy)); + /// Creates a TimelineService for serving geographical map queries, such assets within bounded locations + TimelineService geographicMap( + List userIds, + TimelineMapOptions Function() currentOptions, + Stream optionsStream, + ) => TimelineService(_timelineRepository.geographicMap(userIds, currentOptions, optionsStream, groupBy)); } class TimelineService { diff --git a/mobile/lib/infrastructure/repositories/timeline.repository.dart b/mobile/lib/infrastructure/repositories/timeline.repository.dart index 593d93dc40..8b7d312780 100644 --- a/mobile/lib/infrastructure/repositories/timeline.repository.dart +++ b/mobile/lib/infrastructure/repositories/timeline.repository.dart @@ -509,9 +509,21 @@ class DriftTimelineRepository extends DriftDatabaseRepository { return query.map((row) => row.toDto()).get(); } - TimelineQuery map(List userIds, TimelineMapOptions options, GroupAssetsBy groupBy) => ( - bucketSource: () => _watchMapBucket(userIds, options, groupBy: groupBy), - assetSource: (offset, count) => _getMapBucketAssets(userIds, options, offset: offset, count: count), + /// Creates a geographic map query that can dynamically filter on changing [TimelineMapOptions] + /// (most notably the active map bounds) + TimelineQuery geographicMap( + List userIds, + TimelineMapOptions Function() currentOptions, + Stream optionsStream, + GroupAssetsBy groupBy, + ) => ( + bucketSource: () => Stream.value(currentOptions()) + .followedBy(optionsStream) + .switchMap( + // Any error would kill the stream for all options; make sure the stream stays alive + (options) => _watchMapBucket(userIds, options, groupBy: groupBy).handleError((_) {}), + ), + assetSource: (offset, count) => _getMapBucketAssets(userIds, currentOptions(), offset: offset, count: count), origin: TimelineOrigin.map, ); diff --git a/mobile/lib/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart index 945bdc9584..4412f6dccf 100644 --- a/mobile/lib/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/bottom_sheet/map_bottom_sheet.widget.dart @@ -35,7 +35,6 @@ class _ScopedMapTimeline extends StatelessWidget { @override Widget build(BuildContext context) { - // TODO: this causes the timeline to switch to flicker to "loading" state and back. This is both janky and inefficient. return ProviderScope( overrides: [ timelineServiceProvider.overrideWith((ref) { @@ -44,13 +43,16 @@ class _ScopedMapTimeline extends StatelessWidget { throw Exception('User must be logged in to access archive'); } - final users = ref.watch(mapStateProvider).withPartners - ? ref.watch(timelineUsersProvider).valueOrNull ?? [user.id] - : [user.id]; + final withPartners = ref.watch(mapStateProvider.select((s) => s.withPartners)); + final users = withPartners ? ref.watch(timelineUsersProvider).valueOrNull ?? [user.id] : [user.id]; final timelineService = ref .watch(timelineFactoryProvider) - .map(users, ref.watch(mapStateProvider).toOptions()); + .geographicMap( + users, + () => ref.read(mapStateProvider).toOptions(), + ref.read(mapStateProvider.notifier).optionsStream, + ); ref.onDispose(timelineService.dispose); return timelineService; }), diff --git a/mobile/lib/presentation/widgets/map/map.state.dart b/mobile/lib/presentation/widgets/map/map.state.dart index 57ce619771..d14433cea2 100644 --- a/mobile/lib/presentation/widgets/map/map.state.dart +++ b/mobile/lib/presentation/widgets/map/map.state.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/domain/models/events.model.dart'; @@ -63,11 +65,16 @@ class MapState { class MapStateNotifier extends Notifier { MapStateNotifier(); + final StreamController _optionsController = StreamController.broadcast(); + + Stream get optionsStream => _optionsController.stream; + bool setBounds(LatLngBounds bounds) { if (state.bounds == bounds) { return false; } state = state.copyWith(bounds: bounds); + _optionsController.add(state.toOptions()); return true; } @@ -82,12 +89,14 @@ class MapStateNotifier extends Notifier { void switchFavoriteOnly(bool isFavoriteOnly) { ref.read(settingsProvider).write(.mapShowFavoriteOnly, isFavoriteOnly); state = state.copyWith(onlyFavorites: isFavoriteOnly); + _optionsController.add(state.toOptions()); EventStream.shared.emit(const MapMarkerReloadEvent()); } void switchIncludeArchived(bool isIncludeArchived) { ref.read(settingsProvider).write(.mapIncludeArchived, isIncludeArchived); state = state.copyWith(includeArchived: isIncludeArchived); + _optionsController.add(state.toOptions()); EventStream.shared.emit(const MapMarkerReloadEvent()); } @@ -100,11 +109,13 @@ class MapStateNotifier extends Notifier { void setRelativeTime(int relativeDays) { ref.read(settingsProvider).write(.mapRelativeDate, relativeDays); state = state.copyWith(relativeDays: relativeDays); + _optionsController.add(state.toOptions()); EventStream.shared.emit(const MapMarkerReloadEvent()); } @override MapState build() { + ref.onDispose(_optionsController.close); final mapConfig = ref.read(appConfigProvider.select((config) => config.map)); return MapState( themeMode: mapConfig.themeMode,