From 9fa9ad05b13c5c06f1bf0dec3a4f28ea3ad508c3 Mon Sep 17 00:00:00 2001 From: Fynn Petersen-Frey <10599762+fyfrey@users.noreply.github.com> Date: Mon, 13 Nov 2023 16:54:41 +0100 Subject: [PATCH] feat(mobile): unify partner assets on timeline (#4974) * feat(mobile): unify partner assets on timeline * skip non-owned assets in bulk actions * add message when trying to delete partner assets --- mobile/assets/i18n/en-US.json | 4 + .../providers/archive_asset_provider.dart | 15 +-- .../providers/render_list.provider.dart | 15 +-- .../favorite/providers/favorite_provider.dart | 15 +-- mobile/lib/modules/home/views/home_page.dart | 101 +++++++++++++----- .../partner/providers/partner.provider.dart | 14 ++- .../partner/services/partner.service.dart | 16 +++ .../partner/views/partner_detail_page.dart | 42 ++++++++ .../providers/trashed_asset.provider.dart | 11 +- mobile/lib/shared/models/user.dart | 11 +- mobile/lib/shared/models/user.g.dart | 48 ++------- .../shared/providers/app_state.provider.dart | 1 + .../lib/shared/providers/asset.provider.dart | 54 ++++++---- .../lib/shared/providers/user.provider.dart | 31 ++++++ mobile/lib/shared/services/user.service.dart | 6 +- mobile/lib/utils/migration.dart | 2 + mobile/lib/utils/renderlist_generator.dart | 26 +++++ 17 files changed, 274 insertions(+), 138 deletions(-) create mode 100644 mobile/lib/utils/renderlist_generator.dart diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 672ab28fb5..ff43095476 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -175,6 +175,10 @@ "home_page_favorite_err_local": "Can not favorite local assets yet, skipping", "home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).", "home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping", + "home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping", + "home_page_album_err_partner": "Can not add partner assets to an album yet, skipping", + "home_page_archive_err_partner": "Can not archive partner assets, skipping", + "home_page_delete_err_partner": "Can not delete partner assets, skipping", "image_viewer_page_state_provider_download_error": "Download Error", "image_viewer_page_state_provider_download_success": "Download Success", "image_viewer_page_state_provider_share_error": "Share Error", diff --git a/mobile/lib/modules/archive/providers/archive_asset_provider.dart b/mobile/lib/modules/archive/providers/archive_asset_provider.dart index 328b3c7b58..579ace7c50 100644 --- a/mobile/lib/modules/archive/providers/archive_asset_provider.dart +++ b/mobile/lib/modules/archive/providers/archive_asset_provider.dart @@ -1,15 +1,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.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/asset.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:immich_mobile/shared/providers/user.provider.dart'; +import 'package:immich_mobile/utils/renderlist_generator.dart'; import 'package:isar/isar.dart'; -final archiveProvider = StreamProvider((ref) async* { +final archiveProvider = StreamProvider((ref) { final user = ref.watch(currentUserProvider); - if (user == null) return; + if (user == null) return const Stream.empty(); final query = ref .watch(dbProvider) .assets @@ -19,11 +18,5 @@ final archiveProvider = StreamProvider((ref) async* { .isArchivedEqualTo(true) .isTrashedEqualTo(false) .sortByFileCreatedAt(); - final settings = ref.watch(appSettingsServiceProvider); - final groupBy = - GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)]; - yield await RenderList.fromQuery(query, groupBy); - await for (final _ in query.watchLazy()) { - yield await RenderList.fromQuery(query, groupBy); - } + return renderListGenerator(query, ref); }); diff --git a/mobile/lib/modules/asset_viewer/providers/render_list.provider.dart b/mobile/lib/modules/asset_viewer/providers/render_list.provider.dart index 04532ce1b8..c2e8782bbd 100644 --- a/mobile/lib/modules/asset_viewer/providers/render_list.provider.dart +++ b/mobile/lib/modules/asset_viewer/providers/render_list.provider.dart @@ -3,6 +3,7 @@ import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structu 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/asset.dart'; +import 'package:immich_mobile/utils/renderlist_generator.dart'; import 'package:isar/isar.dart'; final renderListProvider = @@ -17,16 +18,6 @@ final renderListProvider = final renderListQueryProvider = StreamProvider.family?>( - (ref, query) async* { - if (query == null) { - return; - } - final settings = ref.watch(appSettingsServiceProvider); - final groupBy = GroupAssetsBy - .values[settings.getSetting(AppSettingsEnum.groupAssetsBy)]; - yield await RenderList.fromQuery(query, groupBy); - await for (final _ in query.watchLazy()) { - yield await RenderList.fromQuery(query, groupBy); - } - }, + (ref, query) => + query == null ? const Stream.empty() : renderListGenerator(query, ref), ); diff --git a/mobile/lib/modules/favorite/providers/favorite_provider.dart b/mobile/lib/modules/favorite/providers/favorite_provider.dart index 427d2c88b0..0da6b3f8aa 100644 --- a/mobile/lib/modules/favorite/providers/favorite_provider.dart +++ b/mobile/lib/modules/favorite/providers/favorite_provider.dart @@ -1,15 +1,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.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/asset.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:immich_mobile/shared/providers/user.provider.dart'; +import 'package:immich_mobile/utils/renderlist_generator.dart'; import 'package:isar/isar.dart'; -final favoriteAssetsProvider = StreamProvider((ref) async* { +final favoriteAssetsProvider = StreamProvider((ref) { final user = ref.watch(currentUserProvider); - if (user == null) return; + if (user == null) return const Stream.empty(); final query = ref .watch(dbProvider) .assets @@ -19,11 +18,5 @@ final favoriteAssetsProvider = StreamProvider((ref) async* { .isFavoriteEqualTo(true) .isTrashedEqualTo(false) .sortByFileCreatedAt(); - final settings = ref.watch(appSettingsServiceProvider); - final groupBy = - GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)]; - yield await RenderList.fromQuery(query, groupBy); - await for (final _ in query.watchLazy()) { - yield await RenderList.fromQuery(query, groupBy); - } + return renderListGenerator(query, ref); }); diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart index eb26ea077c..07397ecd12 100644 --- a/mobile/lib/modules/home/views/home_page.dart +++ b/mobile/lib/modules/home/views/home_page.dart @@ -44,6 +44,7 @@ class HomePage extends HookConsumerWidget { final sharedAlbums = ref.watch(sharedAlbumProvider); final albumService = ref.watch(albumServiceProvider); final currentUser = ref.watch(currentUserProvider); + final timelineUsers = ref.watch(timelineUsersIdsProvider); final trashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); @@ -55,6 +56,7 @@ class HomePage extends HookConsumerWidget { () { ref.read(websocketProvider.notifier).connect(); Future(() => ref.read(assetProvider.notifier).getAllAsset()); + ref.read(assetProvider.notifier).getPartnerAssets(); ref.read(albumProvider.notifier).getAllAlbums(); ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); ref.read(serverInfoProvider.notifier).getServerInfo(); @@ -84,20 +86,49 @@ class HomePage extends HookConsumerWidget { SelectionAssetState.fromSelection(selectedAssets); } - List remoteOnlySelection({String? localErrorMessage}) { - final Set assets = selection.value; + errorBuilder(String? msg) => msg != null && msg.isNotEmpty + ? () => ImmichToast.show( + context: context, + msg: msg, + gravity: ToastGravity.BOTTOM, + ) + : null; + + Iterable remoteOnly( + Iterable assets, { + void Function()? errorCallback, + }) { final bool onlyRemote = assets.every((e) => e.isRemote); if (!onlyRemote) { - if (localErrorMessage != null && localErrorMessage.isNotEmpty) { - ImmichToast.show( - context: context, - msg: localErrorMessage, - gravity: ToastGravity.BOTTOM, - ); - } - return assets.where((a) => a.isRemote).toList(); + if (errorCallback != null) errorCallback(); + return assets.where((a) => a.isRemote); } - return assets.toList(); + return assets; + } + + Iterable ownedOnly( + Iterable assets, { + void Function()? errorCallback, + }) { + if (currentUser == null) return []; + final userId = currentUser.isarId; + final bool onlyOwned = assets.every((e) => e.ownerId == userId); + if (!onlyOwned) { + if (errorCallback != null) errorCallback(); + return assets.where((a) => a.ownerId == userId); + } + return assets; + } + + Iterable ownedRemoteSelection({ + String? localErrorMessage, + String? ownerErrorMessage, + }) { + final assets = selection.value; + return remoteOnly( + ownedOnly(assets, errorCallback: errorBuilder(ownerErrorMessage)), + errorCallback: errorBuilder(localErrorMessage), + ); } void onShareAssets(bool shareLocal) { @@ -105,7 +136,7 @@ class HomePage extends HookConsumerWidget { if (shareLocal) { handleShareAssets(ref, context, selection.value.toList()); } else { - final ids = remoteOnlySelection().map((e) => e.remoteId!); + final ids = ownedRemoteSelection().map((e) => e.remoteId!); context.autoPush(SharedLinkEditRoute(assetsList: ids.toList())); } processing.value = false; @@ -115,11 +146,12 @@ class HomePage extends HookConsumerWidget { void onFavoriteAssets() async { processing.value = true; try { - final remoteAssets = remoteOnlySelection( + final remoteAssets = ownedRemoteSelection( localErrorMessage: 'home_page_favorite_err_local'.tr(), + ownerErrorMessage: 'home_page_favorite_err_partner'.tr(), ); if (remoteAssets.isNotEmpty) { - await handleFavoriteAssets(ref, context, remoteAssets); + await handleFavoriteAssets(ref, context, remoteAssets.toList()); } } finally { processing.value = false; @@ -130,10 +162,11 @@ class HomePage extends HookConsumerWidget { void onArchiveAsset() async { processing.value = true; try { - final remoteAssets = remoteOnlySelection( + final remoteAssets = ownedRemoteSelection( localErrorMessage: 'home_page_archive_err_local'.tr(), + ownerErrorMessage: 'home_page_archive_err_partner'.tr(), ); - await handleArchiveAssets(ref, context, remoteAssets); + await handleArchiveAssets(ref, context, remoteAssets.toList()); } finally { processing.value = false; selectionEnabledHook.value = false; @@ -143,12 +176,16 @@ class HomePage extends HookConsumerWidget { void onDelete() async { processing.value = true; try { + final toDelete = ownedOnly( + selection.value, + errorCallback: errorBuilder('home_page_delete_err_partner'.tr()), + ).toList(); await ref .read(assetProvider.notifier) - .deleteAssets(selection.value, force: !trashEnabled); + .deleteAssets(toDelete, force: !trashEnabled); - final hasRemote = selection.value.any((a) => a.isRemote); - final assetOrAssets = selection.value.length > 1 ? 'assets' : 'asset'; + final hasRemote = toDelete.any((a) => a.isRemote); + final assetOrAssets = toDelete.length > 1 ? 'assets' : 'asset'; final trashOrRemoved = !trashEnabled ? 'deleted permanently' : 'trashed'; if (hasRemote) { @@ -180,8 +217,9 @@ class HomePage extends HookConsumerWidget { void onAddToAlbum(Album album) async { processing.value = true; try { - final Iterable assets = remoteOnlySelection( + final Iterable assets = ownedRemoteSelection( localErrorMessage: "home_page_add_to_album_err_local".tr(), + ownerErrorMessage: "home_page_album_err_partner".tr(), ); if (assets.isEmpty) { return; @@ -228,8 +266,9 @@ class HomePage extends HookConsumerWidget { void onCreateNewAlbum() async { processing.value = true; try { - final Iterable assets = remoteOnlySelection( + final Iterable assets = ownedRemoteSelection( localErrorMessage: "home_page_add_to_album_err_local".tr(), + ownerErrorMessage: "home_page_album_err_partner".tr(), ); if (assets.isEmpty) { return; @@ -270,6 +309,9 @@ class HomePage extends HookConsumerWidget { Future refreshAssets() async { final fullRefresh = refreshCount.value > 0; await ref.read(assetProvider.notifier).getAllAsset(clear: fullRefresh); + if (timelineUsers.length > 1) { + await ref.read(assetProvider.notifier).getPartnerAssets(); + } if (fullRefresh) { // refresh was forced: user requested another refresh within 2 seconds refreshCount.value = 0; @@ -330,7 +372,13 @@ class HomePage extends HookConsumerWidget { bottom: false, child: Stack( children: [ - ref.watch(assetsProvider(currentUser?.isarId)).when( + ref + .watch( + timelineUsers.length > 1 + ? multiUserAssetsProvider(timelineUsers) + : assetsProvider(currentUser?.isarId), + ) + .when( data: (data) => data.isEmpty ? buildLoadingIndicator() : ImmichAssetGrid( @@ -338,11 +386,10 @@ class HomePage extends HookConsumerWidget { listener: selectionListener, selectionActive: selectionEnabledHook.value, onRefresh: refreshAssets, - topWidget: (currentUser != null && - currentUser.memoryEnabled != null && - currentUser.memoryEnabled!) - ? const MemoryLane() - : const SizedBox(), + topWidget: + (currentUser != null && currentUser.memoryEnabled) + ? const MemoryLane() + : const SizedBox(), showStack: true, ), error: (error, _) => Center(child: Text(error.toString())), diff --git a/mobile/lib/modules/partner/providers/partner.provider.dart b/mobile/lib/modules/partner/providers/partner.provider.dart index d484356168..3123382bb2 100644 --- a/mobile/lib/modules/partner/providers/partner.provider.dart +++ b/mobile/lib/modules/partner/providers/partner.provider.dart @@ -2,21 +2,31 @@ import 'dart:async'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/album/providers/suggested_shared_users.provider.dart'; +import 'package:immich_mobile/modules/partner/services/partner.service.dart'; import 'package:immich_mobile/shared/models/user.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:isar/isar.dart'; class PartnerSharedWithNotifier extends StateNotifier> { - PartnerSharedWithNotifier(Isar db) : super([]) { + PartnerSharedWithNotifier(Isar db, this._ps) : super([]) { final query = db.users.filter().isPartnerSharedWithEqualTo(true); query.findAll().then((partners) => state = partners); query.watch().listen((partners) => state = partners); } + + Future updatePartner(User partner, {required bool inTimeline}) { + return _ps.updatePartner(partner, inTimeline: inTimeline); + } + + final PartnerService _ps; } final partnerSharedWithProvider = StateNotifierProvider>((ref) { - return PartnerSharedWithNotifier(ref.watch(dbProvider)); + return PartnerSharedWithNotifier( + ref.watch(dbProvider), + ref.watch(partnerServiceProvider), + ); }); class PartnerSharedByNotifier extends StateNotifier> { diff --git a/mobile/lib/modules/partner/services/partner.service.dart b/mobile/lib/modules/partner/services/partner.service.dart index 0605e56efb..32e500353b 100644 --- a/mobile/lib/modules/partner/services/partner.service.dart +++ b/mobile/lib/modules/partner/services/partner.service.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:immich_mobile/shared/services/api.service.dart'; import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; +import 'package:openapi/api.dart'; final partnerServiceProvider = Provider( (ref) => PartnerService( @@ -69,4 +70,19 @@ class PartnerService { } return false; } + + Future updatePartner(User partner, {required bool inTimeline}) async { + try { + final dto = await _apiService.partnerApi + .updatePartner(partner.id, UpdatePartnerDto(inTimeline: inTimeline)); + if (dto != null) { + partner.inTimeline = dto.inTimeline ?? partner.inTimeline; + await _db.writeTxn(() => _db.users.put(partner)); + return true; + } + } catch (e) { + _log.warning("failed to update partner ${partner.id}:\n$e"); + } + return false; + } } diff --git a/mobile/lib/modules/partner/views/partner_detail_page.dart b/mobile/lib/modules/partner/views/partner_detail_page.dart index 2aecdb35cc..f7b1580b20 100644 --- a/mobile/lib/modules/partner/views/partner_detail_page.dart +++ b/mobile/lib/modules/partner/views/partner_detail_page.dart @@ -2,9 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/immich_asset_grid.dart'; +import 'package:immich_mobile/modules/partner/providers/partner.provider.dart'; import 'package:immich_mobile/shared/models/user.dart'; import 'package:immich_mobile/shared/providers/asset.provider.dart'; import 'package:immich_mobile/shared/ui/immich_loading_indicator.dart'; +import 'package:immich_mobile/shared/ui/immich_toast.dart'; class PartnerDetailPage extends HookConsumerWidget { const PartnerDetailPage({Key? key, required this.partner}) : super(key: key); @@ -14,6 +16,8 @@ class PartnerDetailPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final assets = ref.watch(assetsProvider(partner.isarId)); + final inTimeline = useState(partner.inTimeline); + bool toggleInProcess = false; useEffect( () { @@ -23,11 +27,49 @@ class PartnerDetailPage extends HookConsumerWidget { [], ); + void toggleInTimeline() async { + if (toggleInProcess) return; + toggleInProcess = true; + try { + final ok = await ref + .read(partnerSharedWithProvider.notifier) + .updatePartner(partner, inTimeline: !inTimeline.value); + if (ok) { + inTimeline.value = !inTimeline.value; + final action = inTimeline.value ? "shown on" : "hidden from"; + ImmichToast.show( + context: context, + toastType: ToastType.success, + durationInSecond: 1, + msg: "${partner.name}'s assets $action your timeline", + ); + } else { + ImmichToast.show( + context: context, + toastType: ToastType.error, + durationInSecond: 1, + msg: "Failed to toggle the timeline setting", + ); + } + } finally { + toggleInProcess = false; + } + } + return Scaffold( appBar: AppBar( title: Text(partner.name), elevation: 0, centerTitle: false, + actions: [ + IconButton( + onPressed: toggleInTimeline, + icon: Icon( + inTimeline.value ? Icons.collections : Icons.collections_outlined, + ), + tooltip: "Show/hide photos on your main timeline", + ), + ], ), body: assets.when( data: (renderList) => renderList.isEmpty diff --git a/mobile/lib/modules/trash/providers/trashed_asset.provider.dart b/mobile/lib/modules/trash/providers/trashed_asset.provider.dart index d0ad41b5fa..04f8d5f165 100644 --- a/mobile/lib/modules/trash/providers/trashed_asset.provider.dart +++ b/mobile/lib/modules/trash/providers/trashed_asset.provider.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/shared/models/asset.dart'; import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:immich_mobile/shared/providers/user.provider.dart'; import 'package:immich_mobile/shared/services/sync.service.dart'; +import 'package:immich_mobile/utils/renderlist_generator.dart'; import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; @@ -107,9 +108,9 @@ final trashProvider = StateNotifierProvider((ref) { ); }); -final trashedAssetsProvider = StreamProvider((ref) async* { +final trashedAssetsProvider = StreamProvider((ref) { final user = ref.read(currentUserProvider); - if (user == null) return; + if (user == null) return const Stream.empty(); final query = ref .watch(dbProvider) .assets @@ -117,9 +118,5 @@ final trashedAssetsProvider = StreamProvider((ref) async* { .ownerIdEqualTo(user.isarId) .isTrashedEqualTo(true) .sortByFileCreatedAt(); - const groupBy = GroupAssetsBy.none; - yield await RenderList.fromQuery(query, groupBy); - await for (final _ in query.watchLazy()) { - yield await RenderList.fromQuery(query, groupBy); - } + return renderListGeneratorWithGroupBy(query, GroupAssetsBy.none); }); diff --git a/mobile/lib/shared/models/user.dart b/mobile/lib/shared/models/user.dart index a247fb21e3..e9e5004da0 100644 --- a/mobile/lib/shared/models/user.dart +++ b/mobile/lib/shared/models/user.dart @@ -31,7 +31,8 @@ class User { isPartnerSharedWith = false, profileImagePath = dto.profileImagePath, isAdmin = dto.isAdmin, - memoryEnabled = dto.memoriesEnabled; + memoryEnabled = dto.memoriesEnabled ?? false, + inTimeline = false; User.fromPartnerDto(PartnerResponseDto dto) : id = dto.id, @@ -42,8 +43,8 @@ class User { isPartnerSharedWith = false, profileImagePath = dto.profileImagePath, isAdmin = dto.isAdmin, - memoryEnabled = dto.memoriesEnabled, - inTimeline = dto.inTimeline; + memoryEnabled = dto.memoriesEnabled ?? false, + inTimeline = dto.inTimeline ?? false; @Index(unique: true, replace: false, type: IndexType.hash) String id; @@ -54,8 +55,8 @@ class User { bool isPartnerSharedWith; bool isAdmin; String profileImagePath; - bool? memoryEnabled; - bool? inTimeline; + bool memoryEnabled; + bool inTimeline; @Backlink(to: 'owner') final IsarLinks albums = IsarLinks(); diff --git a/mobile/lib/shared/models/user.g.dart b/mobile/lib/shared/models/user.g.dart index f5167c2a82..82420a1753 100644 --- a/mobile/lib/shared/models/user.g.dart +++ b/mobile/lib/shared/models/user.g.dart @@ -151,11 +151,11 @@ User _userDeserialize( final object = User( email: reader.readString(offsets[0]), id: reader.readString(offsets[1]), - inTimeline: reader.readBoolOrNull(offsets[2]), + inTimeline: reader.readBoolOrNull(offsets[2]) ?? false, isAdmin: reader.readBool(offsets[3]), isPartnerSharedBy: reader.readBoolOrNull(offsets[4]) ?? false, isPartnerSharedWith: reader.readBoolOrNull(offsets[5]) ?? false, - memoryEnabled: reader.readBoolOrNull(offsets[6]), + memoryEnabled: reader.readBoolOrNull(offsets[6]) ?? true, name: reader.readString(offsets[7]), profileImagePath: reader.readStringOrNull(offsets[8]) ?? '', updatedAt: reader.readDateTime(offsets[9]), @@ -175,7 +175,7 @@ P _userDeserializeProp

( case 1: return (reader.readString(offset)) as P; case 2: - return (reader.readBoolOrNull(offset)) as P; + return (reader.readBoolOrNull(offset) ?? false) as P; case 3: return (reader.readBool(offset)) as P; case 4: @@ -183,7 +183,7 @@ P _userDeserializeProp

( case 5: return (reader.readBoolOrNull(offset) ?? false) as P; case 6: - return (reader.readBoolOrNull(offset)) as P; + return (reader.readBoolOrNull(offset) ?? true) as P; case 7: return (reader.readString(offset)) as P; case 8: @@ -638,24 +638,8 @@ extension UserQueryFilter on QueryBuilder { }); } - QueryBuilder inTimelineIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'inTimeline', - )); - }); - } - - QueryBuilder inTimelineIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'inTimeline', - )); - }); - } - QueryBuilder inTimelineEqualTo( - bool? value) { + bool value) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.equalTo( property: r'inTimeline', @@ -745,24 +729,8 @@ extension UserQueryFilter on QueryBuilder { }); } - QueryBuilder memoryEnabledIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'memoryEnabled', - )); - }); - } - - QueryBuilder memoryEnabledIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'memoryEnabled', - )); - }); - } - QueryBuilder memoryEnabledEqualTo( - bool? value) { + bool value) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.equalTo( property: r'memoryEnabled', @@ -1540,7 +1508,7 @@ extension UserQueryProperty on QueryBuilder { }); } - QueryBuilder inTimelineProperty() { + QueryBuilder inTimelineProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'inTimeline'); }); @@ -1564,7 +1532,7 @@ extension UserQueryProperty on QueryBuilder { }); } - QueryBuilder memoryEnabledProperty() { + QueryBuilder memoryEnabledProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'memoryEnabled'); }); diff --git a/mobile/lib/shared/providers/app_state.provider.dart b/mobile/lib/shared/providers/app_state.provider.dart index e2813d86d8..d2d3f1e1e5 100644 --- a/mobile/lib/shared/providers/app_state.provider.dart +++ b/mobile/lib/shared/providers/app_state.provider.dart @@ -54,6 +54,7 @@ class AppStateNotiifer extends StateNotifier { switch (ref.read(tabProvider)) { case TabEnum.home: ref.read(assetProvider.notifier).getAllAsset(); + ref.read(assetProvider.notifier).getPartnerAssets(); case TabEnum.search: // nothing to do case TabEnum.sharing: diff --git a/mobile/lib/shared/providers/asset.provider.dart b/mobile/lib/shared/providers/asset.provider.dart index 1694e1f261..50c6018016 100644 --- a/mobile/lib/shared/providers/asset.provider.dart +++ b/mobile/lib/shared/providers/asset.provider.dart @@ -8,12 +8,11 @@ import 'package:immich_mobile/shared/providers/db.provider.dart'; import 'package:immich_mobile/shared/providers/user.provider.dart'; import 'package:immich_mobile/shared/services/asset.service.dart'; import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.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/asset.dart'; import 'package:immich_mobile/shared/services/sync.service.dart'; import 'package:immich_mobile/shared/services/user.service.dart'; import 'package:immich_mobile/utils/db.dart'; +import 'package:immich_mobile/utils/renderlist_generator.dart'; import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -251,26 +250,23 @@ final assetWatcher = return db.assets.watchObject(asset.id, fireImmediately: true); }); -final assetsProvider = - StreamProvider.family((ref, userId) async* { - if (userId == null) return; - final query = ref - .watch(dbProvider) - .assets - .where() - .ownerIdEqualToAnyChecksum(userId) - .filter() - .isArchivedEqualTo(false) - .isTrashedEqualTo(false) - .stackParentIdIsNull() - .sortByFileCreatedAtDesc(); - final settings = ref.watch(appSettingsServiceProvider); - final groupBy = - GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)]; - yield await RenderList.fromQuery(query, groupBy); - await for (final _ in query.watchLazy()) { - yield await RenderList.fromQuery(query, groupBy); - } +final assetsProvider = StreamProvider.family((ref, userId) { + if (userId == null) return const Stream.empty(); + final query = _commonFilterAndSort( + _assets(ref).where().ownerIdEqualToAnyChecksum(userId), + ); + return renderListGenerator(query, ref); +}); + +final multiUserAssetsProvider = + StreamProvider.family>((ref, userIds) { + if (userIds.isEmpty) return const Stream.empty(); + final query = _commonFilterAndSort( + _assets(ref) + .where() + .anyOf(userIds, (q, u) => q.ownerIdEqualToAnyChecksum(u)), + ); + return renderListGenerator(query, ref); }); QueryBuilder? getRemoteAssetQuery(WidgetRef ref) { @@ -289,3 +285,17 @@ QueryBuilder? getRemoteAssetQuery(WidgetRef ref) { .stackParentIdIsNull() .sortByFileCreatedAtDesc(); } + +IsarCollection _assets(StreamProviderRef ref) => + ref.watch(dbProvider).assets; + +QueryBuilder _commonFilterAndSort( + QueryBuilder query, +) { + return query + .filter() + .isArchivedEqualTo(false) + .isTrashedEqualTo(false) + .stackParentIdIsNull() + .sortByFileCreatedAtDesc(); +} diff --git a/mobile/lib/shared/providers/user.provider.dart b/mobile/lib/shared/providers/user.provider.dart index df8ff328dd..61c77b24b7 100644 --- a/mobile/lib/shared/providers/user.provider.dart +++ b/mobile/lib/shared/providers/user.provider.dart @@ -3,6 +3,8 @@ import 'dart:async'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/models/user.dart'; +import 'package:immich_mobile/shared/providers/db.provider.dart'; +import 'package:isar/isar.dart'; class CurrentUserProvider extends StateNotifier { CurrentUserProvider() : super(null) { @@ -24,3 +26,32 @@ final currentUserProvider = StateNotifierProvider((ref) { return CurrentUserProvider(); }); + +class TimelineUserIdsProvider extends StateNotifier> { + TimelineUserIdsProvider(Isar db, User? currentUser) : super([]) { + final query = db.users + .filter() + .inTimelineEqualTo(true) + .or() + .isarIdEqualTo(currentUser?.isarId ?? Isar.autoIncrement) + .isarIdProperty(); + query.findAll().then((users) => state = users); + streamSub = query.watch().listen((users) => state = users); + } + + late final StreamSubscription> streamSub; + + @override + void dispose() { + streamSub.cancel(); + super.dispose(); + } +} + +final timelineUsersIdsProvider = + StateNotifierProvider>((ref) { + return TimelineUserIdsProvider( + ref.watch(dbProvider), + ref.watch(currentUserProvider), + ); +}); diff --git a/mobile/lib/shared/services/user.service.dart b/mobile/lib/shared/services/user.service.dart index 16f7b7b0e9..4d398c3a88 100644 --- a/mobile/lib/shared/services/user.service.dart +++ b/mobile/lib/shared/services/user.service.dart @@ -99,7 +99,11 @@ class UserService { users, sharedWith, compare: (User a, User b) => a.id.compareTo(b.id), - both: (User a, User b) => a.isPartnerSharedWith = true, + both: (User a, User b) { + a.isPartnerSharedWith = true; + a.inTimeline = b.inTimeline; + return true; + }, onlyFirst: (_) {}, onlySecond: (_) {}, ); diff --git a/mobile/lib/utils/migration.dart b/mobile/lib/utils/migration.dart index 724f3a8722..2356c73143 100644 --- a/mobile/lib/utils/migration.dart +++ b/mobile/lib/utils/migration.dart @@ -11,6 +11,8 @@ Future migrateDatabaseIfNeeded(Isar db) async { await _migrateTo(db, 2); case 2: await _migrateTo(db, 3); + case 3: + await _migrateTo(db, 4); } } diff --git a/mobile/lib/utils/renderlist_generator.dart b/mobile/lib/utils/renderlist_generator.dart new file mode 100644 index 0000000000..c68957a2a1 --- /dev/null +++ b/mobile/lib/utils/renderlist_generator.dart @@ -0,0 +1,26 @@ +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.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/asset.dart'; +import 'package:isar/isar.dart'; + +Stream renderListGenerator( + QueryBuilder query, + StreamProviderRef ref, +) { + final settings = ref.watch(appSettingsServiceProvider); + final groupBy = + GroupAssetsBy.values[settings.getSetting(AppSettingsEnum.groupAssetsBy)]; + return renderListGeneratorWithGroupBy(query, groupBy); +} + +Stream renderListGeneratorWithGroupBy( + QueryBuilder query, + GroupAssetsBy groupBy, +) async* { + yield await RenderList.fromQuery(query, groupBy); + await for (final _ in query.watchLazy()) { + yield await RenderList.fromQuery(query, groupBy); + } +}