From d5707a030bbbcb126f556b4d9b241e35d2d91dd5 Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Tue, 1 Jul 2025 16:51:22 +0530 Subject: [PATCH 01/36] fix: rearrange the priority of execution --- mobile/lib/services/home_widget_service.dart | 12 ++++++------ mobile/lib/ui/tabs/home_widget.dart | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mobile/lib/services/home_widget_service.dart b/mobile/lib/services/home_widget_service.dart index 36aa3abe59..45730d83e6 100644 --- a/mobile/lib/services/home_widget_service.dart +++ b/mobile/lib/services/home_widget_service.dart @@ -62,9 +62,9 @@ class HomeWidgetService { } void _initializeWidgetServices(SharedPreferences prefs) { - MemoryHomeWidgetService.instance.init(prefs); - PeopleHomeWidgetService.instance.init(prefs); AlbumHomeWidgetService.instance.init(prefs); + PeopleHomeWidgetService.instance.init(prefs); + MemoryHomeWidgetService.instance.init(prefs); } void setAppGroupID(String id) { @@ -72,9 +72,9 @@ class HomeWidgetService { } Future initHomeWidget() async { - await MemoryHomeWidgetService.instance.initMemoryHomeWidget(); - await PeopleHomeWidgetService.instance.initPeopleHomeWidget(); await AlbumHomeWidgetService.instance.initAlbumHomeWidget(); + await PeopleHomeWidgetService.instance.initPeopleHomeWidget(); + await MemoryHomeWidgetService.instance.initMemoryHomeWidget(); } Future updateWidget({ @@ -222,9 +222,9 @@ class HomeWidgetService { } await Future.wait([ - MemoryHomeWidgetService.instance.clearWidget(), - PeopleHomeWidgetService.instance.clearWidget(), AlbumHomeWidgetService.instance.clearWidget(), + PeopleHomeWidgetService.instance.clearWidget(), + MemoryHomeWidgetService.instance.clearWidget(), ]); try { diff --git a/mobile/lib/ui/tabs/home_widget.dart b/mobile/lib/ui/tabs/home_widget.dart index 33c9d8708b..61b4ba895e 100644 --- a/mobile/lib/ui/tabs/home_widget.dart +++ b/mobile/lib/ui/tabs/home_widget.dart @@ -279,9 +279,9 @@ class _HomeWidgetState extends State { await Future.delayed(const Duration(milliseconds: 5000)); _logger.info("Syncing home widget"); - await MemoryHomeWidgetService.instance.checkPendingMemorySync(); - await PeopleHomeWidgetService.instance.checkPendingPeopleSync(); await AlbumHomeWidgetService.instance.checkPendingAlbumsSync(); + await PeopleHomeWidgetService.instance.checkPendingPeopleSync(); + await MemoryHomeWidgetService.instance.checkPendingMemorySync(); } final Map _linkedPublicAlbums = {}; From 4e692fce1f987e7c6e0b8c2e4e5cb00aa901c8dd Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Tue, 1 Jul 2025 17:01:44 +0530 Subject: [PATCH 02/36] fix: collection service is not initialized --- mobile/lib/main.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 2e59b78812..7153486480 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -163,6 +163,7 @@ Future _runMinimally(String taskId, TimeLogger tlog) async { LocalFileUpdateService.instance.init(prefs); await LocalSyncService.instance.init(prefs); RemoteSyncService.instance.init(prefs); + await FavoritesService.instance.initFav(); await SyncService.instance.init(prefs); // Misc Services From 27a400743cf4a9b93d511c99642730c57855f640 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 1 Jul 2025 17:14:31 +0530 Subject: [PATCH 03/36] Sketch --- .../new/albums/services/public-albums-fdb.ts | 50 +++++++++++++++++-- .../new/photos/services/photos-fdb.ts | 8 ++- 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/web/packages/new/albums/services/public-albums-fdb.ts b/web/packages/new/albums/services/public-albums-fdb.ts index 7c25ff2502..bbed0cbd3d 100644 --- a/web/packages/new/albums/services/public-albums-fdb.ts +++ b/web/packages/new/albums/services/public-albums-fdb.ts @@ -5,6 +5,7 @@ import { LocalCollections, LocalEnteFiles, + LocalTimestamp, transformFilesIfNeeded, } from "ente-gallery/services/files-db"; import { type Collection } from "ente-media/collection"; @@ -91,7 +92,8 @@ type ES = LocalSavedPublicCollectionFilesEntry[]; * * Use {@link savePublicCollectionFiles} to update the database. * - * @param accessToken The access token of the public album whose files we want. + * @param accessToken The access token that identifies the public album under + * consideration. */ export const savedPublicCollectionFiles = async ( accessToken: string, @@ -108,8 +110,8 @@ export const savedPublicCollectionFiles = async ( * * This is the setter corresponding to {@link savedPublicCollectionFiles}. * - * @param accessToken The access token of the public album whose files we want - * to replace. + * @param accessToken The access token that identifies the public album under + * consideration. * * @param files The files to save. */ @@ -125,6 +127,44 @@ export const savePublicCollectionFiles = async ( ]); }; +/** + * Return the locally persisted "last sync time" for a public collection that we + * have pulled from remote. This can be used to perform a paginated delta pull + * from the saved time onwards. + * + * Use {@link savePublic CollectionLastSyncTime} to update the value saved in + * the database, and {@link removePublicCollectionLastSyncTime} to remove the + * saved value from the database. + * + * @param accessToken The access token that identifies the public album under + * consideration. + */ +export const savedPublicCollectionLastSyncTime = async (accessToken: string) => + LocalTimestamp.parse( + await localForage.getItem(`public-${accessToken}-time`), + ); + +/** + * Update the locally persisted timestamp that will be returned by subsequent + * calls to {@link savedPublicCollectionLastSyncTime}. + */ +export const savePublicCollectionLastSyncTime = async ( + accessToken: string, + time: number, +) => { + await localForage.setItem(`public-${accessToken}-time`, time); +}; + +/** + * Remove the locally persisted timestamp, if any, previously saved for a + * collection using {@link savedPublicCollectionLastSyncTime}. + */ +export const removePublicCollectionLastSyncTime = async ( + accessToken: string, +) => { + await localForage.removeItem(`public-${accessToken}-time`); +}; + const LocalUploaderName = z.string().nullish().transform(nullToUndefined); /** @@ -143,8 +183,8 @@ const LocalUploaderName = z.string().nullish().transform(nullToUndefined); * public collection, in the local database so that it can prefill it the next * time there is an upload from the same client. * - * @param accessToken The access token of the public album whose persisted - * uploader name we we want. + * @param accessToken The access token that identifies the public album under + * consideration. */ export const savedPublicCollectionUploaderName = async (accessToken: string) => LocalUploaderName.parse( diff --git a/web/packages/new/photos/services/photos-fdb.ts b/web/packages/new/photos/services/photos-fdb.ts index 8c549c2a2c..4c5329d94e 100644 --- a/web/packages/new/photos/services/photos-fdb.ts +++ b/web/packages/new/photos/services/photos-fdb.ts @@ -188,8 +188,12 @@ export const saveCollectionFiles = async (files: EnteFile[]) => { }; /** - * Return the locally persisted {@link updationTime} of the latest file from the - * given {@link collection} that we have pulled from remote. + * Return the locally persisted "last sync time" for a collection that we have + * pulled from remote. This can be used to perform a paginated delta pull from + * the saved time onwards. + * + * > Specifically, this is the {@link updationTime} of the latest file from the + * > {@link collection}, or the the collection itself if it is fully synced. * * Use {@link saveCollectionLastSyncTime} to update the value saved in the * database, and {@link removeCollectionIDLastSyncTime} to remove the saved From 436a5811cb3f85dc776266e85b78f176ec07c84b Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 1 Jul 2025 17:16:37 +0530 Subject: [PATCH 04/36] Use --- .../src/services/publicCollectionService.ts | 26 ++++--------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/web/apps/photos/src/services/publicCollectionService.ts b/web/apps/photos/src/services/publicCollectionService.ts index 8ade970689..0396a506f3 100644 --- a/web/apps/photos/src/services/publicCollectionService.ts +++ b/web/apps/photos/src/services/publicCollectionService.ts @@ -9,10 +9,13 @@ import type { import type { EnteFile, RemoteEnteFile } from "ente-media/file"; import { decryptRemoteFile } from "ente-media/file"; import { + removePublicCollectionLastSyncTime, savedPublicCollectionFiles, + savedPublicCollectionLastSyncTime, savedPublicCollections, saveLastPublicCollectionReferralCode, savePublicCollectionFiles, + savePublicCollectionLastSyncTime, } from "ente-new/albums/services/public-albums-fdb"; import { CustomError, parseSharingErrorCodes } from "ente-shared/error"; import HTTPService from "ente-shared/network/HTTPService"; @@ -25,9 +28,6 @@ const PUBLIC_COLLECTIONS_TABLE = "public-collections"; // eslint-disable-next-line @typescript-eslint/no-unnecessary-template-expression export const getPublicCollectionUID = (token: string) => `${token}`; -const getPublicCollectionLastSyncTimeKey = (collectionUID: string) => - `public-${collectionUID}-time`; - const getPublicCollectionPasswordKey = (collectionUID: string) => `public-${collectionUID}-passkey`; @@ -86,20 +86,6 @@ const dedupeCollections = (collections: Collection[]) => { }); }; -const getPublicCollectionLastSyncTime = async (collectionUID: string) => - (await localForage.getItem( - getPublicCollectionLastSyncTimeKey(collectionUID), - )) ?? 0; - -const savePublicCollectionLastSyncTime = async ( - collectionUID: string, - time: number, -) => - await localForage.setItem( - getPublicCollectionLastSyncTimeKey(collectionUID), - time, - ); - export const syncPublicFiles = async ( token: string, passwordToken: string, @@ -118,7 +104,7 @@ export const syncPublicFiles = async ( return sortFiles(files, sortAsc); } const lastSyncTime = - await getPublicCollectionLastSyncTime(collectionUID); + (await savedPublicCollectionLastSyncTime(collectionUID)) ?? 0; if (collection.updationTime === lastSyncTime) { return sortFiles(files, sortAsc); } @@ -318,9 +304,7 @@ export const removePublicCollectionWithFiles = async ( export const removePublicFiles = async (collectionUID: string) => { await localForage.removeItem(getPublicCollectionPasswordKey(collectionUID)); - await localForage.removeItem( - getPublicCollectionLastSyncTimeKey(collectionUID), - ); + await removePublicCollectionLastSyncTime(collectionUID); const publicCollectionFiles = (await localForage.getItem( From a538e852bd185f5f1d473688a15e638cd8cabb14 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 1 Jul 2025 17:27:34 +0530 Subject: [PATCH 05/36] Update --- .../src/services/publicCollectionService.ts | 15 +------- .../new/albums/services/public-albums-fdb.ts | 38 ++++++++++++++----- 2 files changed, 31 insertions(+), 22 deletions(-) diff --git a/web/apps/photos/src/services/publicCollectionService.ts b/web/apps/photos/src/services/publicCollectionService.ts index 0396a506f3..35af9787b3 100644 --- a/web/apps/photos/src/services/publicCollectionService.ts +++ b/web/apps/photos/src/services/publicCollectionService.ts @@ -9,6 +9,7 @@ import type { import type { EnteFile, RemoteEnteFile } from "ente-media/file"; import { decryptRemoteFile } from "ente-media/file"; import { + removePublicCollectionFiles, removePublicCollectionLastSyncTime, savedPublicCollectionFiles, savedPublicCollectionLastSyncTime, @@ -21,7 +22,6 @@ import { CustomError, parseSharingErrorCodes } from "ente-shared/error"; import HTTPService from "ente-shared/network/HTTPService"; import localForage from "ente-shared/storage/localForage"; -const PUBLIC_COLLECTION_FILES_TABLE = "public-collection-files"; const PUBLIC_COLLECTIONS_TABLE = "public-collections"; // Fix this once we can trust the types. @@ -305,16 +305,5 @@ export const removePublicCollectionWithFiles = async ( export const removePublicFiles = async (collectionUID: string) => { await localForage.removeItem(getPublicCollectionPasswordKey(collectionUID)); await removePublicCollectionLastSyncTime(collectionUID); - - const publicCollectionFiles = - (await localForage.getItem( - PUBLIC_COLLECTION_FILES_TABLE, - )) ?? []; - await localForage.setItem( - PUBLIC_COLLECTION_FILES_TABLE, - publicCollectionFiles.filter( - (collectionFiles) => - collectionFiles.collectionUID !== collectionUID, - ), - ); + await removePublicCollectionFiles(collectionUID); }; diff --git a/web/packages/new/albums/services/public-albums-fdb.ts b/web/packages/new/albums/services/public-albums-fdb.ts index bbed0cbd3d..c3670252a4 100644 --- a/web/packages/new/albums/services/public-albums-fdb.ts +++ b/web/packages/new/albums/services/public-albums-fdb.ts @@ -84,13 +84,11 @@ type LocalSavedPublicCollectionFilesEntry = z.infer< typeof LocalSavedPublicCollectionFilesEntry >; -// A purely synactic and local alias to avoid the code from looking scary. -type ES = LocalSavedPublicCollectionFilesEntry[]; - /** * Return all files for a public collection present in our local database. * - * Use {@link savePublicCollectionFiles} to update the database. + * Use {@link savePublicCollectionFiles} to update the list of files in the + * database, and {@link removePublicCollectionFiles} to remove them. * * @param accessToken The access token that identifies the public album under * consideration. @@ -98,11 +96,23 @@ type ES = LocalSavedPublicCollectionFilesEntry[]; export const savedPublicCollectionFiles = async ( accessToken: string, ): Promise => { + const entry = (await pcfEntries()).find( + (e) => e.collectionUID == accessToken, + ); + return transformFilesIfNeeded(entry ? entry.files : []); +}; + +/** + * A convenience routine to read the DB entries for "public-collection-files". + */ +const pcfEntries = async () => { + // A local alias to avoid the code from looking scary. + type ES = LocalSavedPublicCollectionFilesEntry[]; + // See: [Note: Avoiding Zod parsing for large DB arrays] for why we use an // (implied) cast here instead of parsing using the Zod schema. const entries = await localForage.getItem("public-collection-files"); - const entry = (entries ?? []).find((e) => e.collectionUID == accessToken); - return transformFilesIfNeeded(entry ? entry.files : []); + return entries ?? []; }; /** @@ -119,11 +129,21 @@ export const savePublicCollectionFiles = async ( accessToken: string, files: EnteFile[], ): Promise => { - // See: [Note: Avoiding Zod parsing for large DB arrays]. - const entries = await localForage.getItem("public-collection-files"); await localForage.setItem("public-collection-files", [ { collectionUID: accessToken, files }, - ...(entries ?? []).filter((e) => e.collectionUID != accessToken), + ...(await pcfEntries()).filter((e) => e.collectionUID != accessToken), + ]); +}; + +/** + * Remove the list of files, in any, in our local database for the given + * collection (identified by its {@link accessToken}). + */ +export const removePublicCollectionFiles = async ( + accessToken: string, +): Promise => { + await localForage.setItem("public-collection-files", [ + ...(await pcfEntries()).filter((e) => e.collectionUID != accessToken), ]); }; From b8547305337d105baaf9d5f4df6311d317e90b9f Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 1 Jul 2025 17:39:00 +0530 Subject: [PATCH 06/36] Sketch --- .../new/albums/services/public-albums-fdb.ts | 56 ++++++++++++++++--- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/web/packages/new/albums/services/public-albums-fdb.ts b/web/packages/new/albums/services/public-albums-fdb.ts index c3670252a4..2b4d916133 100644 --- a/web/packages/new/albums/services/public-albums-fdb.ts +++ b/web/packages/new/albums/services/public-albums-fdb.ts @@ -38,6 +38,46 @@ export const savedPublicCollections = async (): Promise => export const savePublicCollections = (collections: Collection[]) => localForage.setItem("public-collections", collections); +/** + * Return the saved public collection with the given {@link key} if present in + * our local database. + * + * Use {@link savePublicCollection} to save collections in our local database. + * + * @param key The collection key that can be used to identify the public album + * we want from amongst all the locally saved public albums. + */ +export const savedPublicCollection = async ( + collectionKey: string, +): Promise => + savedPublicCollections().then((cs) => + cs.find((c) => c.key == collectionKey), + ); + +/** + * Save a public collection to our local database. + * + * The collection can later be retrieved using {@link savedPublicCollection}. + * The collection can be removed using {@link removePublicCollection}. + */ +export const savePublicCollection = async (collection: Collection) => { + const collections = await savedPublicCollections(); + await savePublicCollections([ + collection, + ...collections.filter((c) => c.id != collection.id), + ]); +}; + +/** + * Remove a public collection from our local database. + */ +export const removePublicCollection = async (collection: Collection) => { + const collections = await savedPublicCollections(); + await savePublicCollections([ + ...collections.filter((c) => c.id != collection.id), + ]); +}; + const LocalReferralCode = z.string().nullish().transform(nullToUndefined); /** @@ -90,8 +130,8 @@ type LocalSavedPublicCollectionFilesEntry = z.infer< * Use {@link savePublicCollectionFiles} to update the list of files in the * database, and {@link removePublicCollectionFiles} to remove them. * - * @param accessToken The access token that identifies the public album under - * consideration. + * @param accessToken The access token that identifies the public album whose + * files we want. */ export const savedPublicCollectionFiles = async ( accessToken: string, @@ -120,8 +160,8 @@ const pcfEntries = async () => { * * This is the setter corresponding to {@link savedPublicCollectionFiles}. * - * @param accessToken The access token that identifies the public album under - * consideration. + * @param accessToken The access token that identifies the public album whose + * files we want to update. * * @param files The files to save. */ @@ -156,8 +196,8 @@ export const removePublicCollectionFiles = async ( * the database, and {@link removePublicCollectionLastSyncTime} to remove the * saved value from the database. * - * @param accessToken The access token that identifies the public album under - * consideration. + * @param accessToken The access token that identifies the public album whose + * last sync time we want. */ export const savedPublicCollectionLastSyncTime = async (accessToken: string) => LocalTimestamp.parse( @@ -203,8 +243,8 @@ const LocalUploaderName = z.string().nullish().transform(nullToUndefined); * public collection, in the local database so that it can prefill it the next * time there is an upload from the same client. * - * @param accessToken The access token that identifies the public album under - * consideration. + * @param accessToken The access token that identifies the public album whose + * saved uploader name we want. */ export const savedPublicCollectionUploaderName = async (accessToken: string) => LocalUploaderName.parse( From 5034fb4496fc57b7a9ebdd9d11a597a3413c6921 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 1 Jul 2025 17:43:14 +0530 Subject: [PATCH 07/36] Update --- web/apps/photos/src/pages/shared-albums.tsx | 4 +- .../src/services/publicCollectionService.ts | 43 ++----------------- .../new/albums/services/public-albums-fdb.ts | 12 ++++-- 3 files changed, 13 insertions(+), 46 deletions(-) diff --git a/web/apps/photos/src/pages/shared-albums.tsx b/web/apps/photos/src/pages/shared-albums.tsx index 23230f49ce..7d73991222 100644 --- a/web/apps/photos/src/pages/shared-albums.tsx +++ b/web/apps/photos/src/pages/shared-albums.tsx @@ -52,6 +52,7 @@ import type { Collection } from "ente-media/collection"; import { type EnteFile } from "ente-media/file"; import { savedLastPublicCollectionReferralCode, + savedPublicCollectionByKey, savedPublicCollectionFiles, } from "ente-new/albums/services/public-albums-fdb"; import { verifyPublicAlbumPassword } from "ente-new/albums/services/public-collection"; @@ -68,7 +69,6 @@ import { useRouter } from "next/router"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { type FileWithPath } from "react-dropzone"; import { - getLocalPublicCollection, getLocalPublicCollectionPassword, getPublicCollection, getPublicCollectionUID, @@ -216,7 +216,7 @@ export default function PublicCollectionGallery() { } collectionKey.current = ck; url.current = window.location.href; - const localCollection = await getLocalPublicCollection( + const localCollection = await savedPublicCollectionByKey( collectionKey.current, ); const accessToken = t; diff --git a/web/apps/photos/src/services/publicCollectionService.ts b/web/apps/photos/src/services/publicCollectionService.ts index 35af9787b3..c9b8765c97 100644 --- a/web/apps/photos/src/services/publicCollectionService.ts +++ b/web/apps/photos/src/services/publicCollectionService.ts @@ -9,12 +9,13 @@ import type { import type { EnteFile, RemoteEnteFile } from "ente-media/file"; import { decryptRemoteFile } from "ente-media/file"; import { + removePublicCollectionByKey, removePublicCollectionFiles, removePublicCollectionLastSyncTime, savedPublicCollectionFiles, savedPublicCollectionLastSyncTime, - savedPublicCollections, saveLastPublicCollectionReferralCode, + savePublicCollection, savePublicCollectionFiles, savePublicCollectionLastSyncTime, } from "ente-new/albums/services/public-albums-fdb"; @@ -22,8 +23,6 @@ import { CustomError, parseSharingErrorCodes } from "ente-shared/error"; import HTTPService from "ente-shared/network/HTTPService"; import localForage from "ente-shared/storage/localForage"; -const PUBLIC_COLLECTIONS_TABLE = "public-collections"; - // Fix this once we can trust the types. // eslint-disable-next-line @typescript-eslint/no-unnecessary-template-expression export const getPublicCollectionUID = (token: string) => `${token}`; @@ -56,36 +55,6 @@ export const savePublicCollectionPassword = async ( ); }; -export const getLocalPublicCollection = async (collectionKey: string) => { - const localCollections = await savedPublicCollections(); - const publicCollection = - localCollections.find( - (localSavedPublicCollection) => - localSavedPublicCollection.key === collectionKey, - ) || null; - return publicCollection; -}; - -export const savePublicCollection = async (collection: Collection) => { - const publicCollections = await savedPublicCollections(); - await localForage.setItem( - PUBLIC_COLLECTIONS_TABLE, - dedupeCollections([collection, ...publicCollections]), - ); -}; - -const dedupeCollections = (collections: Collection[]) => { - const keySet = new Set([]); - return collections.filter((collection) => { - if (!keySet.has(collection.key)) { - keySet.add(collection.key); - return true; - } else { - return false; - } - }); -}; - export const syncPublicFiles = async ( token: string, passwordToken: string, @@ -292,13 +261,7 @@ export const removePublicCollectionWithFiles = async ( collectionUID: string, collectionKey: string, ) => { - const publicCollections = await savedPublicCollections(); - await localForage.setItem( - PUBLIC_COLLECTIONS_TABLE, - publicCollections.filter( - (collection) => collection.key !== collectionKey, - ), - ); + await removePublicCollectionByKey(collectionKey); await removePublicFiles(collectionUID); }; diff --git a/web/packages/new/albums/services/public-albums-fdb.ts b/web/packages/new/albums/services/public-albums-fdb.ts index 2b4d916133..91217655e9 100644 --- a/web/packages/new/albums/services/public-albums-fdb.ts +++ b/web/packages/new/albums/services/public-albums-fdb.ts @@ -47,7 +47,7 @@ export const savePublicCollections = (collections: Collection[]) => * @param key The collection key that can be used to identify the public album * we want from amongst all the locally saved public albums. */ -export const savedPublicCollection = async ( +export const savedPublicCollectionByKey = async ( collectionKey: string, ): Promise => savedPublicCollections().then((cs) => @@ -69,12 +69,16 @@ export const savePublicCollection = async (collection: Collection) => { }; /** - * Remove a public collection from our local database. + * Remove a public collection, identified using its collection key, from our + * local database. + * + * @param key The collection key that can be used to identify the public album + * we want to remove. */ -export const removePublicCollection = async (collection: Collection) => { +export const removePublicCollectionByKey = async (collectionKey: string) => { const collections = await savedPublicCollections(); await savePublicCollections([ - ...collections.filter((c) => c.id != collection.id), + ...collections.filter((c) => c.key != collectionKey), ]); }; From fe2f066733989a35f18091637d38bb4ffb574527 Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Tue, 1 Jul 2025 17:52:54 +0530 Subject: [PATCH 08/36] fix: albums logic --- mobile/lib/services/album_home_widget_service.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/lib/services/album_home_widget_service.dart b/mobile/lib/services/album_home_widget_service.dart index af06b70fef..b46d81342e 100644 --- a/mobile/lib/services/album_home_widget_service.dart +++ b/mobile/lib/services/album_home_widget_service.dart @@ -157,7 +157,7 @@ class AlbumHomeWidgetService { final collection = CollectionsService.instance.getCollectionByID(albumId); if (collection != null && !collection.isDeleted && - collection.isHidden()) { + !collection.isHidden()) { albums.add(collection); } } From 7ded133cafb2e6c9dcbdf037e23e7ef9bc1ce9f4 Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Tue, 1 Jul 2025 17:57:56 +0530 Subject: [PATCH 09/36] chore: fix expandedHeight scaling --- mobile/lib/ui/settings/widgets/albums_widget_settings.dart | 3 +-- mobile/lib/ui/settings/widgets/memories_widget_settings.dart | 2 +- mobile/lib/ui/settings/widgets/people_widget_settings.dart | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/mobile/lib/ui/settings/widgets/albums_widget_settings.dart b/mobile/lib/ui/settings/widgets/albums_widget_settings.dart index de1e0270a4..e6cbf9b867 100644 --- a/mobile/lib/ui/settings/widgets/albums_widget_settings.dart +++ b/mobile/lib/ui/settings/widgets/albums_widget_settings.dart @@ -103,7 +103,6 @@ class _AlbumsWidgetSettingsState extends State { await AlbumHomeWidgetService.instance .updateSelectedAlbums(albums); Navigator.pop(context); - } : null, isDisabled: _selectedAlbums.albums.isEmpty, @@ -123,7 +122,7 @@ class _AlbumsWidgetSettingsState extends State { flexibleSpaceTitle: TitleBarTitleWidget( title: S.of(context).albums, ), - expandedHeight: 120, + expandedHeight: MediaQuery.textScalerOf(context).scale(120), flexibleSpaceCaption: hasInstalledAny ? S.of(context).albumsWidgetDesc : context.l10n.addAlbumWidgetPrompt, diff --git a/mobile/lib/ui/settings/widgets/memories_widget_settings.dart b/mobile/lib/ui/settings/widgets/memories_widget_settings.dart index 5b22d0627d..7deecfdf73 100644 --- a/mobile/lib/ui/settings/widgets/memories_widget_settings.dart +++ b/mobile/lib/ui/settings/widgets/memories_widget_settings.dart @@ -104,7 +104,7 @@ class _MemoriesWidgetSettingsState extends State { flexibleSpaceTitle: TitleBarTitleWidget( title: S.of(context).memories, ), - expandedHeight: 120, + expandedHeight: MediaQuery.textScalerOf(context).scale(120), flexibleSpaceCaption: hasInstalledAny ? S.of(context).memoriesWidgetDesc : context.l10n.addMemoriesWidgetPrompt, diff --git a/mobile/lib/ui/settings/widgets/people_widget_settings.dart b/mobile/lib/ui/settings/widgets/people_widget_settings.dart index b760eade80..fc78d8332f 100644 --- a/mobile/lib/ui/settings/widgets/people_widget_settings.dart +++ b/mobile/lib/ui/settings/widgets/people_widget_settings.dart @@ -88,7 +88,7 @@ class _PeopleWidgetSettingsState extends State { flexibleSpaceTitle: TitleBarTitleWidget( title: S.of(context).people, ), - expandedHeight: 120, + expandedHeight: MediaQuery.textScalerOf(context).scale(120), flexibleSpaceCaption: hasInstalledAny ? S.of(context).peopleWidgetDesc : context.l10n.addPeopleWidgetPrompt, From 54dde95545460b7ab316dba41c6ebf574aabc7e2 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 1 Jul 2025 17:59:08 +0530 Subject: [PATCH 10/36] Sketch --- .../new/albums/services/public-albums-fdb.ts | 46 +++++++++++++++++-- 1 file changed, 42 insertions(+), 4 deletions(-) diff --git a/web/packages/new/albums/services/public-albums-fdb.ts b/web/packages/new/albums/services/public-albums-fdb.ts index 91217655e9..4464f76be4 100644 --- a/web/packages/new/albums/services/public-albums-fdb.ts +++ b/web/packages/new/albums/services/public-albums-fdb.ts @@ -82,7 +82,10 @@ export const removePublicCollectionByKey = async (collectionKey: string) => { ]); }; -const LocalReferralCode = z.string().nullish().transform(nullToUndefined); +/** + * Zod schema for a nullish string, with `null` transformed to `undefined`. + */ +const LocalString = z.string().nullish().transform(nullToUndefined); /** * Return the last saved referral code present in our local database. @@ -100,7 +103,7 @@ const LocalReferralCode = z.string().nullish().transform(nullToUndefined); * out a new value using {@link saveLastPublicCollectionReferralCode}. */ export const savedLastPublicCollectionReferralCode = async () => - LocalReferralCode.parse(await localForage.getItem("public-referral-code")); + LocalString.parse(await localForage.getItem("public-referral-code")); /** * Update the referral code present in our local database. @@ -229,7 +232,42 @@ export const removePublicCollectionLastSyncTime = async ( await localForage.removeItem(`public-${accessToken}-time`); }; -const LocalUploaderName = z.string().nullish().transform(nullToUndefined); +/** + * Return the password JWT, if any, present in our local database for the given + * public collection (as identified by its {@link accessToken}). + * + * Use {@link savedPublicCollectionPasswordJWT} to save the value, and + * {@link removePublicCollectionPasswordJWT} to remove it. + */ +export const savedPublicCollectionPasswordJWT = async (accessToken: string) => + LocalString.parse( + await localForage.getItem(`public-${accessToken}-passkey`), + ); + +/** + * Update the password JWT in our local database for the given public + * collection (as identified by its {@link accessToken}). + * + * This is the setter corresponding to {@link savedPublicCollectionPasswordJWT}. + */ +export const savePublicCollectionPasswordJWT = async ( + accessToken: string, + passwordJWT: string, +) => { + await localForage.setItem(`public-${accessToken}-passkey`, passwordJWT); +}; + +/** + * Remove the password JWT in our local database for the given public + * collection (as identified by its {@link accessToken}). + * + * This is the setter corresponding to {@link savedPublicCollectionPasswordJWT}. + */ +export const removePublicCollectionPasswordJWT = async ( + accessToken: string, +) => { + await localForage.removeItem(`public-${accessToken}-passkey`); +}; /** * Return the previously saved uploader name, if any, present in our local @@ -251,7 +289,7 @@ const LocalUploaderName = z.string().nullish().transform(nullToUndefined); * saved uploader name we want. */ export const savedPublicCollectionUploaderName = async (accessToken: string) => - LocalUploaderName.parse( + LocalString.parse( await localForage.getItem(`public-${accessToken}-uploaderName`), ); From 738088e8a5d7bff89a3a2829d71ee4332626d82a Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 1 Jul 2025 18:09:34 +0530 Subject: [PATCH 11/36] Update --- web/apps/photos/src/pages/shared-albums.tsx | 15 ++++++---- .../src/services/publicCollectionService.ts | 27 ++--------------- .../new/albums/services/public-albums-fdb.ts | 29 ++++++++++--------- 3 files changed, 26 insertions(+), 45 deletions(-) diff --git a/web/apps/photos/src/pages/shared-albums.tsx b/web/apps/photos/src/pages/shared-albums.tsx index 7d73991222..34d82c9f34 100644 --- a/web/apps/photos/src/pages/shared-albums.tsx +++ b/web/apps/photos/src/pages/shared-albums.tsx @@ -51,9 +51,12 @@ import { sortFiles } from "ente-gallery/utils/file"; import type { Collection } from "ente-media/collection"; import { type EnteFile } from "ente-media/file"; import { + removePublicCollectionAccessTokenJWT, savedLastPublicCollectionReferralCode, + savedPublicCollectionAccessTokenJWT, savedPublicCollectionByKey, savedPublicCollectionFiles, + savePublicCollectionAccessTokenJWT, } from "ente-new/albums/services/public-albums-fdb"; import { verifyPublicAlbumPassword } from "ente-new/albums/services/public-collection"; import { @@ -69,12 +72,10 @@ import { useRouter } from "next/router"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { type FileWithPath } from "react-dropzone"; import { - getLocalPublicCollectionPassword, getPublicCollection, getPublicCollectionUID, removePublicCollectionWithFiles, removePublicFiles, - savePublicCollectionPassword, syncPublicFiles, } from "services/publicCollectionService"; import { uploadManager } from "services/upload-manager"; @@ -230,13 +231,12 @@ export default function PublicCollectionGallery() { const isPasswordProtected = localCollection?.publicURLs?.[0]?.passwordEnabled; setIsPasswordProtected(isPasswordProtected); - const collectionUID = getPublicCollectionUID(accessToken); const localFiles = await savedPublicCollectionFiles(accessToken); const localPublicFiles = sortFiles(localFiles, sortAsc); setPublicFiles(localPublicFiles); accessTokenJWT = - await getLocalPublicCollectionPassword(collectionUID); + await savedPublicCollectionAccessTokenJWT(accessToken); } credentials.current = { accessToken, accessTokenJWT }; downloadManager.setPublicAlbumsCredentials(credentials.current); @@ -316,7 +316,7 @@ export default function PublicCollectionGallery() { if (!isPasswordProtected && credentials.current.accessTokenJWT) { credentials.current.accessTokenJWT = undefined; downloadManager.setPublicAlbumsCredentials(credentials.current); - savePublicCollectionPassword(collectionUID, null); + removePublicCollectionAccessTokenJWT(collectionUID); } if ( @@ -395,7 +395,10 @@ export default function PublicCollectionGallery() { const collectionUID = getPublicCollectionUID( credentials.current.accessToken, ); - await savePublicCollectionPassword(collectionUID, accessTokenJWT); + await savePublicCollectionAccessTokenJWT( + collectionUID, + accessTokenJWT, + ); } catch (e) { log.error("Failed to verifyLinkPassword", e); if (isHTTP401Error(e)) { diff --git a/web/apps/photos/src/services/publicCollectionService.ts b/web/apps/photos/src/services/publicCollectionService.ts index c9b8765c97..e51653efd0 100644 --- a/web/apps/photos/src/services/publicCollectionService.ts +++ b/web/apps/photos/src/services/publicCollectionService.ts @@ -9,6 +9,7 @@ import type { import type { EnteFile, RemoteEnteFile } from "ente-media/file"; import { decryptRemoteFile } from "ente-media/file"; import { + removePublicCollectionAccessTokenJWT, removePublicCollectionByKey, removePublicCollectionFiles, removePublicCollectionLastSyncTime, @@ -21,40 +22,16 @@ import { } from "ente-new/albums/services/public-albums-fdb"; import { CustomError, parseSharingErrorCodes } from "ente-shared/error"; import HTTPService from "ente-shared/network/HTTPService"; -import localForage from "ente-shared/storage/localForage"; // Fix this once we can trust the types. // eslint-disable-next-line @typescript-eslint/no-unnecessary-template-expression export const getPublicCollectionUID = (token: string) => `${token}`; -const getPublicCollectionPasswordKey = (collectionUID: string) => - `public-${collectionUID}-passkey`; - export interface LocalSavedPublicCollectionFiles { collectionUID: string; files: EnteFile[]; } -export const getLocalPublicCollectionPassword = async ( - collectionUID: string, -): Promise => { - return ( - (await localForage.getItem( - getPublicCollectionPasswordKey(collectionUID), - )) || "" - ); -}; - -export const savePublicCollectionPassword = async ( - collectionUID: string, - passToken: string, -): Promise => { - return await localForage.setItem( - getPublicCollectionPasswordKey(collectionUID), - passToken, - ); -}; - export const syncPublicFiles = async ( token: string, passwordToken: string, @@ -266,7 +243,7 @@ export const removePublicCollectionWithFiles = async ( }; export const removePublicFiles = async (collectionUID: string) => { - await localForage.removeItem(getPublicCollectionPasswordKey(collectionUID)); + await removePublicCollectionAccessTokenJWT(collectionUID); await removePublicCollectionLastSyncTime(collectionUID); await removePublicCollectionFiles(collectionUID); }; diff --git a/web/packages/new/albums/services/public-albums-fdb.ts b/web/packages/new/albums/services/public-albums-fdb.ts index 4464f76be4..5a20984e1b 100644 --- a/web/packages/new/albums/services/public-albums-fdb.ts +++ b/web/packages/new/albums/services/public-albums-fdb.ts @@ -19,7 +19,7 @@ import { z } from "zod/v4"; * * Use {@link savePublicCollections} to update the database. */ -export const savedPublicCollections = async (): Promise => +const savedPublicCollections = async (): Promise => // TODO: // // See: [Note: strict mode migration] @@ -35,7 +35,7 @@ export const savedPublicCollections = async (): Promise => * * This is the setter corresponding to {@link savedPublicCollections}. */ -export const savePublicCollections = (collections: Collection[]) => +const savePublicCollections = (collections: Collection[]) => localForage.setItem("public-collections", collections); /** @@ -233,24 +233,27 @@ export const removePublicCollectionLastSyncTime = async ( }; /** - * Return the password JWT, if any, present in our local database for the given - * public collection (as identified by its {@link accessToken}). + * Return the access token JWT, if any, present in our local database for the + * given public collection (as identified by its {@link accessToken}). * - * Use {@link savedPublicCollectionPasswordJWT} to save the value, and - * {@link removePublicCollectionPasswordJWT} to remove it. + * Use {@link savePublicCollectionAccessTokenJWT} to save the value, and + * {@link removePublicCollectionAccessTokenJWT} to remove it. */ -export const savedPublicCollectionPasswordJWT = async (accessToken: string) => +export const savedPublicCollectionAccessTokenJWT = async ( + accessToken: string, +) => LocalString.parse( await localForage.getItem(`public-${accessToken}-passkey`), ); /** - * Update the password JWT in our local database for the given public + * Update the access token JWT in our local database for the given public * collection (as identified by its {@link accessToken}). * - * This is the setter corresponding to {@link savedPublicCollectionPasswordJWT}. + * This is the setter corresponding to + * {@link savedPublicCollectionAccessTokenJWT}. */ -export const savePublicCollectionPasswordJWT = async ( +export const savePublicCollectionAccessTokenJWT = async ( accessToken: string, passwordJWT: string, ) => { @@ -258,12 +261,10 @@ export const savePublicCollectionPasswordJWT = async ( }; /** - * Remove the password JWT in our local database for the given public + * Remove the access token JWT in our local database for the given public * collection (as identified by its {@link accessToken}). - * - * This is the setter corresponding to {@link savedPublicCollectionPasswordJWT}. */ -export const removePublicCollectionPasswordJWT = async ( +export const removePublicCollectionAccessTokenJWT = async ( accessToken: string, ) => { await localForage.removeItem(`public-${accessToken}-passkey`); From 09466f05c60f1734f4d8f7f0134e293fa378c5d3 Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Tue, 1 Jul 2025 18:10:24 +0530 Subject: [PATCH 12/36] fix: get top two faces logic --- .../services/people_home_widget_service.dart | 22 ++++++++++++++++--- mobile/lib/services/search_service.dart | 13 +++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/mobile/lib/services/people_home_widget_service.dart b/mobile/lib/services/people_home_widget_service.dart index ec53641dd9..4eb205c27b 100644 --- a/mobile/lib/services/people_home_widget_service.dart +++ b/mobile/lib/services/people_home_widget_service.dart @@ -131,7 +131,7 @@ class PeopleHomeWidgetService { Future checkPeopleChanged() async { final havePeopleChanged = await peopleChangedLock.synchronized(() async { - final peopleIds = getSelectedPeople() ?? []; + final peopleIds = await _getEffectiveSelections(); final currentHash = await _calculateHash(peopleIds); final lastHash = getPeopleLastHash(); @@ -204,6 +204,22 @@ class PeopleHomeWidgetService { await _refreshPeopleWidget(); } + Future> _getEffectiveSelections() async { + var selection = getSelectedPeople(); + + if ((selection?.isEmpty ?? true) && + getPeopleStatus() == WidgetStatus.syncedAll) { + selection = await SearchService.instance.getTopTwoFaces(); + if (selection.isEmpty) { + await clearWidget(); + return []; + } + await setSelectedPeople(selection); + } + + return selection ?? []; + } + Future _calculateHash(List peopleIds) async { return await entityService.getHashForIds(peopleIds); } @@ -226,7 +242,7 @@ class PeopleHomeWidgetService { } // Check if selected people or hash exist - final peopleIds = getSelectedPeople() ?? []; + final peopleIds = await _getEffectiveSelections(); final hash = await _calculateHash(peopleIds); final noSelectionOrHashEmpty = peopleIds.isEmpty || hash.isEmpty; @@ -304,7 +320,7 @@ class PeopleHomeWidgetService { } Future _updatePeopleWidgetCache() async { - final peopleIds = getSelectedPeople() ?? []; + final peopleIds = await _getEffectiveSelections(); final peopleWithFiles = await _getPeople(peopleIds); if (peopleWithFiles.isEmpty) { diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index 2b747087f5..3530e407c1 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -663,6 +663,19 @@ class SearchService { return searchResults; } + Future> getTopTwoFaces() async { + final searchFilter = await SectionType.face.getData(null).then( + (value) => (value as List).where( + (element) => (element.params[kPersonParamID] as String?) != null, + ), + ); + + return searchFilter + .take(2) + .map((e) => e.params[kPersonParamID] as String) + .toList(); + } + Future> getLocationResults(String query) async { final locationTagEntities = (await locationService.getLocationTags()); final Map, List> result = {}; From e47d6a8ece65611615d1c23a426c07b23ac09423 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 1 Jul 2025 18:23:34 +0530 Subject: [PATCH 13/36] Update --- .../Collections/CollectionShare.tsx | 108 ++++++++---------- 1 file changed, 46 insertions(+), 62 deletions(-) diff --git a/web/apps/photos/src/components/Collections/CollectionShare.tsx b/web/apps/photos/src/components/Collections/CollectionShare.tsx index d49f9e389e..0bd3871d59 100644 --- a/web/apps/photos/src/components/Collections/CollectionShare.tsx +++ b/web/apps/photos/src/components/Collections/CollectionShare.tsx @@ -68,7 +68,6 @@ import { } from "ente-new/photos/services/collection"; import type { CollectionSummary } from "ente-new/photos/services/collection-summary"; import { usePhotosAppContext } from "ente-new/photos/types/context"; -import { CustomError, parseSharingErrorCodes } from "ente-shared/error"; import { wait } from "ente-utils/promise"; import { useFormik } from "formik"; import { t } from "i18next"; @@ -300,25 +299,6 @@ const SharingDetails: React.FC = ({ ); }; -const handleSharingErrors = (error) => { - const parsedError = parseSharingErrorCodes(error); - let errorMessage = ""; - switch (parsedError.message) { - case CustomError.BAD_REQUEST: - errorMessage = t("sharing_album_not_allowed"); - break; - case CustomError.SUBSCRIPTION_NEEDED: - errorMessage = t("sharing_disabled_for_free_accounts"); - break; - case CustomError.NOT_FOUND: - errorMessage = t("sharing_user_does_not_exist"); - break; - default: - errorMessage = `${t("generic_error_retry")} ${parsedError.message}`; - } - return errorMessage; -}; - type EmailShareProps = { onRootClose: () => void; wrap: (f: () => Promise) => () => void; @@ -956,51 +936,55 @@ const ManageParticipant: React.FC = ({ onClose(); }; - const handleRoleChange = (role: string) => () => { - if (role !== selectedParticipant.role) { - changeRolePermission(selectedParticipant.email, role); - } - }; + const confirmChangeRolePermission = useCallback( + ( + selectedEmail: string, + newRole: CollectionNewParticipantRole, + action: () => Promise, + ) => { + let message: React.ReactNode; + let buttonText: string; - const updateCollectionRole = async (selectedEmail, newRole) => { - try { - await shareCollection(collection, selectedEmail, newRole); - selectedParticipant.role = newRole; - await onRemotePull({ silent: true }); - } catch (e) { - log.error(handleSharingErrors(e), e); - } - }; + if (newRole == "VIEWER") { + message = ( + + ); - const changeRolePermission = (selectedEmail, newRole) => { - let contentText; - let buttonText; + buttonText = t("confirm_convert_to_viewer"); + } else if (newRole == "COLLABORATOR") { + message = t("change_permission_to_collaborator", { + selectedEmail, + }); + buttonText = t("confirm_convert_to_collaborator"); + } - if (newRole == "VIEWER") { - contentText = ( - - ); - - buttonText = t("confirm_convert_to_viewer"); - } else if (newRole == "COLLABORATOR") { - contentText = t("change_permission_to_collaborator", { - selectedEmail, + showMiniDialog({ + title: t("change_permission_title"), + message: message, + continue: { text: buttonText, color: "critical", action }, }); - buttonText = t("confirm_convert_to_collaborator"); - } + }, + [showMiniDialog], + ); - showMiniDialog({ - title: t("change_permission_title"), - message: contentText, - continue: { - text: buttonText, - color: "critical", - action: () => updateCollectionRole(selectedEmail, newRole), - }, - }); + const updateCollectionRole = async ( + selectedEmail: string, + newRole: CollectionNewParticipantRole, + ) => { + await shareCollection(collection, selectedEmail, newRole); + selectedParticipant.role = newRole; + await onRemotePull({ silent: true }); + }; + + const createOnRoleChange = (role: CollectionNewParticipantRole) => () => { + if (role == selectedParticipant.role) return; + const { email } = selectedParticipant; + confirmChangeRolePermission(email, role, () => + updateCollectionRole(email, role), + ); }; const removeParticipant = () => { @@ -1044,7 +1028,7 @@ const ManageParticipant: React.FC = ({ } endIcon={ @@ -1057,7 +1041,7 @@ const ManageParticipant: React.FC = ({ } endIcon={ From 68efbc2bed7e788013d5f1e56b2c350962bd2e2e Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Tue, 1 Jul 2025 18:27:01 +0530 Subject: [PATCH 14/36] fix: don't select first two people by default --- .../ui/viewer/search/result/people_section_all_page.dart | 8 -------- 1 file changed, 8 deletions(-) diff --git a/mobile/lib/ui/viewer/search/result/people_section_all_page.dart b/mobile/lib/ui/viewer/search/result/people_section_all_page.dart index 33c671f515..d78eb4b9ba 100644 --- a/mobile/lib/ui/viewer/search/result/people_section_all_page.dart +++ b/mobile/lib/ui/viewer/search/result/people_section_all_page.dart @@ -399,14 +399,6 @@ class _PeopleSectionAllWidgetState extends State { results.removeWhere( (element) => element.params[kPersonParamID] == null, ); - if (widget.selectedPeople?.personIds.isEmpty ?? false) { - widget.selectedPeople!.select( - results - .take(2) - .map((e) => e.params[kPersonParamID] as String) - .toSet(), - ); - } } _isLoaded = true; return results; From fb06332272ed5d740c1ee4f5bbf9860706c2720a Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Tue, 1 Jul 2025 18:35:59 +0530 Subject: [PATCH 15/36] fix: home widget sync in background --- mobile/lib/main.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 7153486480..bc3e007f98 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -10,6 +10,7 @@ import 'package:flutter/material.dart'; import "package:flutter/rendering.dart"; import "package:flutter/services.dart"; import "package:flutter_displaymode/flutter_displaymode.dart"; +import "package:intl/date_symbol_data_local.dart"; import 'package:logging/logging.dart'; import "package:media_kit/media_kit.dart"; import "package:package_info_plus/package_info_plus.dart"; @@ -116,6 +117,11 @@ Future _homeWidgetSync([bool isBackground = false]) async { return; } + if (isBackground) { + final locale = await getLocale(); + await initializeDateFormatting(locale?.languageCode ?? "en"); + } + try { await HomeWidgetService.instance.initHomeWidget(); } catch (e, s) { From f5276240272687ff31064b928c5133b98df78764 Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Tue, 1 Jul 2025 18:37:30 +0530 Subject: [PATCH 16/36] fix: only pick from non cache for memories when has any widgets --- mobile/lib/services/memories_cache_service.dart | 3 ++- mobile/lib/services/memory_home_widget_service.dart | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/mobile/lib/services/memories_cache_service.dart b/mobile/lib/services/memories_cache_service.dart index 46fd63058f..51ea8710bb 100644 --- a/mobile/lib/services/memories_cache_service.dart +++ b/mobile/lib/services/memories_cache_service.dart @@ -433,6 +433,7 @@ class MemoriesCacheService { required bool onThisDay, required bool pastYears, required bool smart, + required bool hasAnyWidgets, }) async { if (!onThisDay && !pastYears && !smart) { _logger.info( @@ -440,7 +441,7 @@ class MemoriesCacheService { ); return []; } - final allMemories = await getMemories(onlyUseCache: true); + final allMemories = await getMemories(onlyUseCache: !hasAnyWidgets); if (onThisDay && pastYears && smart) { return allMemories; } diff --git a/mobile/lib/services/memory_home_widget_service.dart b/mobile/lib/services/memory_home_widget_service.dart index 00482c8308..d98e895f95 100644 --- a/mobile/lib/services/memory_home_widget_service.dart +++ b/mobile/lib/services/memory_home_widget_service.dart @@ -222,6 +222,7 @@ class MemoryHomeWidgetService { onThisDay: onThisDayValue, pastYears: lastYearValue, smart: smartMemoryValue, + hasAnyWidgets: await countHomeWidgets() > 0, ); return memories; From 645bb485a7689cd48b52f2b702fecf8c9ea5ce05 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 1 Jul 2025 18:43:17 +0530 Subject: [PATCH 17/36] photosd-v1.7.14 --- desktop/CHANGELOG.md | 3 +-- desktop/package.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/desktop/CHANGELOG.md b/desktop/CHANGELOG.md index 22d6ad2309..2d895f0df5 100644 --- a/desktop/CHANGELOG.md +++ b/desktop/CHANGELOG.md @@ -1,9 +1,8 @@ # CHANGELOG -## v1.7.14 (Unreleased) +## v1.7.14 - Increase file size limit to 10 GB. -- . ## v1.7.13 diff --git a/desktop/package.json b/desktop/package.json index ae2bfafeb2..e6e0e4ec0c 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "ente", - "version": "1.7.14-beta", + "version": "1.7.14", "private": true, "description": "Desktop client for Ente Photos", "repository": "github:ente-io/photos-desktop", From 910a64dc1ccd319cfbbd6894510bdfb188f10d8c Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Tue, 1 Jul 2025 19:11:13 +0530 Subject: [PATCH 18/36] fix: refresh tap --- mobile/lib/services/album_home_widget_service.dart | 2 +- mobile/lib/services/memory_home_widget_service.dart | 2 +- mobile/lib/services/people_home_widget_service.dart | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mobile/lib/services/album_home_widget_service.dart b/mobile/lib/services/album_home_widget_service.dart index b46d81342e..3f05e6ddf1 100644 --- a/mobile/lib/services/album_home_widget_service.dart +++ b/mobile/lib/services/album_home_widget_service.dart @@ -254,7 +254,7 @@ class AlbumHomeWidgetService { Future _refreshAlbumsWidget() async { // only refresh if widget was synced without issues - if (getAlbumsStatus() == WidgetStatus.syncedAll) return; + if (await countHomeWidgets() == 0) return; await _refreshWidget(message: "Refreshing from existing album set"); } diff --git a/mobile/lib/services/memory_home_widget_service.dart b/mobile/lib/services/memory_home_widget_service.dart index d98e895f95..ca69b75eb4 100644 --- a/mobile/lib/services/memory_home_widget_service.dart +++ b/mobile/lib/services/memory_home_widget_service.dart @@ -182,7 +182,7 @@ class MemoryHomeWidgetService { Future _refreshMemoriesWidget() async { // only refresh if widget was synced without issues - if (getMemoriesStatus() == WidgetStatus.syncedAll) return; + if (await countHomeWidgets() == 0) return; await _refreshWidget(message: "Refreshing from existing memory set"); } diff --git a/mobile/lib/services/people_home_widget_service.dart b/mobile/lib/services/people_home_widget_service.dart index 4eb205c27b..c204e140b1 100644 --- a/mobile/lib/services/people_home_widget_service.dart +++ b/mobile/lib/services/people_home_widget_service.dart @@ -256,7 +256,7 @@ class PeopleHomeWidgetService { Future _refreshPeopleWidget() async { // only refresh if widget was synced without issues - if (getPeopleStatus() == WidgetStatus.syncedAll) return; + if (await countHomeWidgets() == 0) return; await _refreshWidget(message: "Refreshing from existing people set"); } From 035f40dc0aad1b67ba42b504e7ce86fa7f42d93b Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 1 Jul 2025 19:20:35 +0530 Subject: [PATCH 19/36] Start next release cycle --- desktop/CHANGELOG.md | 4 ++++ desktop/package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/desktop/CHANGELOG.md b/desktop/CHANGELOG.md index 2d895f0df5..2cff804323 100644 --- a/desktop/CHANGELOG.md +++ b/desktop/CHANGELOG.md @@ -1,5 +1,9 @@ # CHANGELOG +## v1.7.15 (Unreleased) + +- . + ## v1.7.14 - Increase file size limit to 10 GB. diff --git a/desktop/package.json b/desktop/package.json index e6e0e4ec0c..9dc9e4f2e2 100644 --- a/desktop/package.json +++ b/desktop/package.json @@ -1,6 +1,6 @@ { "name": "ente", - "version": "1.7.14", + "version": "1.7.15-beta", "private": true, "description": "Desktop client for Ente Photos", "repository": "github:ente-io/photos-desktop", From 98364405c66d3acefeff4c0b8c023dca12a3818a Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 1 Jul 2025 19:42:19 +0530 Subject: [PATCH 20/36] Sketch 1 --- .../new/albums/services/public-collection.ts | 48 ++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) diff --git a/web/packages/new/albums/services/public-collection.ts b/web/packages/new/albums/services/public-collection.ts index 83100af109..b1ca98b0e4 100644 --- a/web/packages/new/albums/services/public-collection.ts +++ b/web/packages/new/albums/services/public-collection.ts @@ -4,7 +4,11 @@ import { ensureOk, } from "ente-base/http"; import { apiURL } from "ente-base/origins"; -import { RemoteCollection, type PublicURL } from "ente-media/collection"; +import { + RemoteCollection, + type Collection, + type PublicURL, +} from "ente-media/collection"; import { z } from "zod/v4"; /** @@ -68,3 +72,45 @@ export const PublicCollectionInfo = z.object({ */ referralCode: z.string(), }); + +/** + * Fetch a public collection from remote using its access key, decrypt it using + * the provided key, save the collection in our local database for subsequent + * use, and return it. + * + * This function modifies local state. + * + * @param accessToken A public collection access key obtained from the "t=" + * query parameter of the public URL. + * + * The access key serves to both identify the public collection, and also + * authenticate the request. See: [Note: Public album access token]. + * + * @param collectionKey The base64 encoded key that can be used to decrypt the + * collection obtained from remote. + * + * The collection key is obtained from the fragment portion of the public URL + * (the fragment is a client side only portion that can be used to have local + * secrets that are not sent by the browser to the server). + */ +export const fetchAndSavePublicCollection = ( + accessToken: string, + collectionKey: string, +): Promise => { + throw new Error("TODO"); +}; + +/** + * Fetch information from remote about a public collection using its access key. + * + * Remote only, does not modify local state. + * + * @param accessToken A public collection access key. + */ +const getPublicCollectionInfo = (accessToken: string) => { + const res = await fetch(await apiURL("/public-collection/info"), { + headers: authenticatedPublicAlbumsRequestHeaders({ accessToken }), + }); + ensureOk(res); + return PublicCollectionInfo.parse(await res.json()); +}; From 60b044e61acf7e503dcb56efa5f3ca78d9d0f81a Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 1 Jul 2025 19:44:43 +0530 Subject: [PATCH 21/36] Impl --- .../new/albums/services/public-collection.ts | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/web/packages/new/albums/services/public-collection.ts b/web/packages/new/albums/services/public-collection.ts index b1ca98b0e4..679fa19a81 100644 --- a/web/packages/new/albums/services/public-collection.ts +++ b/web/packages/new/albums/services/public-collection.ts @@ -5,11 +5,16 @@ import { } from "ente-base/http"; import { apiURL } from "ente-base/origins"; import { + decryptRemoteCollection, RemoteCollection, type Collection, type PublicURL, } from "ente-media/collection"; import { z } from "zod/v4"; +import { + saveLastPublicCollectionReferralCode, + savePublicCollection, +} from "./public-albums-fdb"; /** * Verify with remote that the password entered by the user is the same as the @@ -93,11 +98,20 @@ export const PublicCollectionInfo = z.object({ * (the fragment is a client side only portion that can be used to have local * secrets that are not sent by the browser to the server). */ -export const fetchAndSavePublicCollection = ( +export const fetchAndSavePublicCollection = async ( accessToken: string, collectionKey: string, ): Promise => { - throw new Error("TODO"); + const collectionInfo = await getPublicCollectionInfo(accessToken); + const collection = await decryptRemoteCollection( + collectionInfo.collection, + collectionKey, + ); + + await savePublicCollection(collection); + await saveLastPublicCollectionReferralCode(collectionInfo.referralCode); + + return collection; }; /** @@ -107,7 +121,7 @@ export const fetchAndSavePublicCollection = ( * * @param accessToken A public collection access key. */ -const getPublicCollectionInfo = (accessToken: string) => { +const getPublicCollectionInfo = async (accessToken: string) => { const res = await fetch(await apiURL("/public-collection/info"), { headers: authenticatedPublicAlbumsRequestHeaders({ accessToken }), }); From b752af70469e01e853315c96172ee28b8be6d1a2 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 1 Jul 2025 19:50:17 +0530 Subject: [PATCH 22/36] Swap --- web/apps/photos/src/pages/shared-albums.tsx | 15 ++-- .../src/services/publicCollectionService.ts | 75 +------------------ .../new/albums/services/public-collection.ts | 41 +++++----- 3 files changed, 31 insertions(+), 100 deletions(-) diff --git a/web/apps/photos/src/pages/shared-albums.tsx b/web/apps/photos/src/pages/shared-albums.tsx index 34d82c9f34..81cb4fe472 100644 --- a/web/apps/photos/src/pages/shared-albums.tsx +++ b/web/apps/photos/src/pages/shared-albums.tsx @@ -58,7 +58,10 @@ import { savedPublicCollectionFiles, savePublicCollectionAccessTokenJWT, } from "ente-new/albums/services/public-albums-fdb"; -import { verifyPublicAlbumPassword } from "ente-new/albums/services/public-collection"; +import { + fetchAndSavePublicCollection, + verifyPublicAlbumPassword, +} from "ente-new/albums/services/public-collection"; import { GalleryItemsHeaderAdapter, GalleryItemsSummary, @@ -72,7 +75,6 @@ import { useRouter } from "next/router"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { type FileWithPath } from "react-dropzone"; import { - getPublicCollection, getPublicCollectionUID, removePublicCollectionWithFiles, removePublicFiles, @@ -299,10 +301,11 @@ export default function PublicCollectionGallery() { try { showLoadingBar(); setLoading(true); - const [collection, userReferralCode] = await getPublicCollection( - credentials.current.accessToken, - collectionKey.current, - ); + const { collection, referralCode: userReferralCode } = + await fetchAndSavePublicCollection( + credentials.current.accessToken, + collectionKey.current, + ); referralCode.current = userReferralCode; setPublicCollection(collection); diff --git a/web/apps/photos/src/services/publicCollectionService.ts b/web/apps/photos/src/services/publicCollectionService.ts index e51653efd0..360ad750dc 100644 --- a/web/apps/photos/src/services/publicCollectionService.ts +++ b/web/apps/photos/src/services/publicCollectionService.ts @@ -1,11 +1,7 @@ -import { sharedCryptoWorker } from "ente-base/crypto"; import log from "ente-base/log"; import { apiURL } from "ente-base/origins"; import { sortFiles } from "ente-gallery/utils/file"; -import type { - Collection, - CollectionPublicMagicMetadataData, -} from "ente-media/collection"; +import type { Collection } from "ente-media/collection"; import type { EnteFile, RemoteEnteFile } from "ente-media/file"; import { decryptRemoteFile } from "ente-media/file"; import { @@ -15,8 +11,6 @@ import { removePublicCollectionLastSyncTime, savedPublicCollectionFiles, savedPublicCollectionLastSyncTime, - saveLastPublicCollectionReferralCode, - savePublicCollection, savePublicCollectionFiles, savePublicCollectionLastSyncTime, } from "ente-new/albums/services/public-albums-fdb"; @@ -167,73 +161,6 @@ const getPublicFiles = async ( } }; -export interface MagicMetadataCore { - version: number; - count: number; - header: string; - data: T; -} - -export const getPublicCollection = async ( - token: string, - collectionKey: string, -): Promise<[Collection, string]> => { - try { - if (!token) { - return; - } - const resp = await HTTPService.get( - await apiURL("/public-collection/info"), - null, - { "X-Auth-Access-Token": token }, - ); - const fetchedCollection = resp.data.collection; - const referralCode = resp.data.referralCode ?? ""; - - const cryptoWorker = await sharedCryptoWorker(); - - const collectionName = (fetchedCollection.name = - fetchedCollection.name || - new TextDecoder().decode( - await cryptoWorker.decryptBoxBytes( - { - encryptedData: fetchedCollection.encryptedName, - nonce: fetchedCollection.nameDecryptionNonce, - }, - collectionKey, - ), - )); - - let collectionPublicMagicMetadata: MagicMetadataCore; - if (fetchedCollection.pubMagicMetadata?.data) { - collectionPublicMagicMetadata = { - ...fetchedCollection.pubMagicMetadata, - data: await cryptoWorker.decryptMetadataJSON( - { - encryptedData: fetchedCollection.pubMagicMetadata.data, - decryptionHeader: - fetchedCollection.pubMagicMetadata.header, - }, - collectionKey, - ), - }; - } - - const collection = { - ...fetchedCollection, - name: collectionName, - key: collectionKey, - pubMagicMetadata: collectionPublicMagicMetadata, - }; - await savePublicCollection(collection); - await saveLastPublicCollectionReferralCode(referralCode); - return [collection, referralCode]; - } catch (e) { - log.error("failed to get public collection", e); - throw e; - } -}; - export const removePublicCollectionWithFiles = async ( collectionUID: string, collectionKey: string, diff --git a/web/packages/new/albums/services/public-collection.ts b/web/packages/new/albums/services/public-collection.ts index 679fa19a81..c4aa363ddd 100644 --- a/web/packages/new/albums/services/public-collection.ts +++ b/web/packages/new/albums/services/public-collection.ts @@ -63,21 +63,6 @@ export const verifyPublicAlbumPassword = async ( return z.object({ jwtToken: z.string() }).parse(await res.json()).jwtToken; }; -// TODO(RE): Use me -export const PublicCollectionInfo = z.object({ - collection: RemoteCollection, - /** - * A referral code of the owner of the public album. - * - * [Note: Public albums referral code] - * - * The information of a public collection contains the referral code of the - * person who shared the album. This allows both the viewer and the sharer - * to gain storage bonus. - */ - referralCode: z.string(), -}); - /** * Fetch a public collection from remote using its access key, decrypt it using * the provided key, save the collection in our local database for subsequent @@ -101,19 +86,35 @@ export const PublicCollectionInfo = z.object({ export const fetchAndSavePublicCollection = async ( accessToken: string, collectionKey: string, -): Promise => { - const collectionInfo = await getPublicCollectionInfo(accessToken); +): Promise<{ collection: Collection; referralCode: string }> => { + const { collection: remoteCollection, referralCode } = + await getPublicCollectionInfo(accessToken); + const collection = await decryptRemoteCollection( - collectionInfo.collection, + remoteCollection, collectionKey, ); await savePublicCollection(collection); - await saveLastPublicCollectionReferralCode(collectionInfo.referralCode); + await saveLastPublicCollectionReferralCode(referralCode); - return collection; + return { collection, referralCode }; }; +const PublicCollectionInfo = z.object({ + collection: RemoteCollection, + /** + * A referral code of the owner of the public album. + * + * [Note: Public albums referral code] + * + * The information of a public collection contains the referral code of the + * person who shared the album. This allows both the viewer and the sharer + * to gain storage bonus. + */ + referralCode: z.string(), +}); + /** * Fetch information from remote about a public collection using its access key. * From 21738995ccd27023213e4114d9c7256c9971d0e9 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 1 Jul 2025 20:03:44 +0530 Subject: [PATCH 23/36] Sketch 1 --- web/apps/photos/src/pages/shared-albums.tsx | 2 +- .../new/albums/services/public-collection.ts | 33 +++++++++++++++++++ web/packages/new/photos/services/pull.ts | 3 +- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/web/apps/photos/src/pages/shared-albums.tsx b/web/apps/photos/src/pages/shared-albums.tsx index 81cb4fe472..0d2de6617b 100644 --- a/web/apps/photos/src/pages/shared-albums.tsx +++ b/web/apps/photos/src/pages/shared-albums.tsx @@ -163,7 +163,7 @@ export default function PublicCollectionGallery() { }, []); const onAddPhotos = useMemo(() => { - return publicCollection?.publicURLs?.[0]?.enableCollect + return publicCollection?.publicURLs[0]?.enableCollect ? () => setUploadTypeSelectorView(true) : undefined; }, [publicCollection]); diff --git a/web/packages/new/albums/services/public-collection.ts b/web/packages/new/albums/services/public-collection.ts index c4aa363ddd..8d776ce4d6 100644 --- a/web/packages/new/albums/services/public-collection.ts +++ b/web/packages/new/albums/services/public-collection.ts @@ -2,6 +2,7 @@ import { deriveKey } from "ente-base/crypto"; import { authenticatedPublicAlbumsRequestHeaders, ensureOk, + type PublicAlbumsCredentials, } from "ente-base/http"; import { apiURL } from "ente-base/origins"; import { @@ -10,6 +11,7 @@ import { type Collection, type PublicURL, } from "ente-media/collection"; +import type { EnteFile } from "ente-media/file"; import { z } from "zod/v4"; import { saveLastPublicCollectionReferralCode, @@ -129,3 +131,34 @@ const getPublicCollectionInfo = async (accessToken: string) => { ensureOk(res); return PublicCollectionInfo.parse(await res.json()); }; + +/** + * Pull any changes to the files belonging to the given collection, updating our + * local database and also calling the provided callback. + * + * This function modifies local state. + * + * The pull uses a persisted timestamp for the most recent change we've already + * fetched, and will be only fetch the delta of changes since the last pull. The + * files are fetched in a paginated manner, so the provided callback can get + * called multiple times during the pull (one for each page). + * + * @param credentials A public collection access key and an optional password + * unlocked access token JWT. The credentials serve to both identify the + * collection, and authenticate the request. + * + * @param collection The public collection corresponding to the credentials. + * + * @param onSetFiles A callback that is invoked each time a new batch of updates + * to the collection's files is fetched and processed. THe callback is called + * the consolidated list of files after applying the updates received so far. + * + * This callback can get called multiple times during the pull. + */ +export const pullPublicCollectionFiles = ( + credentials: PublicAlbumsCredentials, + collection: Collection, + onSetFiles: (files: EnteFile[]) => void, +) => { + throw new Error("X"); +}; diff --git a/web/packages/new/photos/services/pull.ts b/web/packages/new/photos/services/pull.ts index 485b6d9d3a..21e276a6ca 100644 --- a/web/packages/new/photos/services/pull.ts +++ b/web/packages/new/photos/services/pull.ts @@ -79,7 +79,8 @@ interface PullFilesOpts { } /** - * Pull the latest collections, collections files and trash items from remote. + * Pull the latest collections, collections files and trash items from remote, + * updating our local database and also calling the provided callbacks. * * This is a subset of a full remote pull, independently exposed for use at * times when we only want to pull the file related information (e.g. we just From 7edb1fab7b42458f7d09ca6ff1507d5bfde4f501 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 1 Jul 2025 20:31:34 +0530 Subject: [PATCH 24/36] Sketch --- web/packages/media/file.ts | 1 + .../new/albums/services/public-collection.ts | 66 ++++++++++++++++++- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/web/packages/media/file.ts b/web/packages/media/file.ts index 3eb09a229f..12ab7bbed4 100644 --- a/web/packages/media/file.ts +++ b/web/packages/media/file.ts @@ -322,6 +322,7 @@ export type RemoteEnteFile = z.infer; * a provided timestamp. * * - "/collections/v2/diff" + * - "/public-collection/diff" * - "/cast/diff" */ export const FileDiffResponse = z.object({ diff --git a/web/packages/new/albums/services/public-collection.ts b/web/packages/new/albums/services/public-collection.ts index 8d776ce4d6..8d8a81ee76 100644 --- a/web/packages/new/albums/services/public-collection.ts +++ b/web/packages/new/albums/services/public-collection.ts @@ -11,11 +11,19 @@ import { type Collection, type PublicURL, } from "ente-media/collection"; -import type { EnteFile } from "ente-media/file"; +import { + decryptRemoteFile, + FileDiffResponse, + type EnteFile, +} from "ente-media/file"; import { z } from "zod/v4"; import { + savedPublicCollectionFiles, + savedPublicCollectionLastSyncTime, saveLastPublicCollectionReferralCode, savePublicCollection, + savePublicCollectionFiles, + savePublicCollectionLastSyncTime, } from "./public-albums-fdb"; /** @@ -155,10 +163,62 @@ const getPublicCollectionInfo = async (accessToken: string) => { * * This callback can get called multiple times during the pull. */ -export const pullPublicCollectionFiles = ( +export const pullPublicCollectionFiles = async ( credentials: PublicAlbumsCredentials, collection: Collection, onSetFiles: (files: EnteFile[]) => void, ) => { - throw new Error("X"); + const { accessToken } = credentials; + + const files = await savedPublicCollectionFiles(accessToken); + const filesByID = new Map(files.map((f) => [f.id, f])); + + let sinceTime = (await savedPublicCollectionLastSyncTime(accessToken)) ?? 0; + + while (true) { + const { diff, hasMore } = await getPublicCollectionDiff( + credentials, + sinceTime, + ); + if (!diff.length) break; + for (const change of diff) { + sinceTime = Math.max(sinceTime, change.updationTime); + if (change.isDeleted) { + filesByID.delete(change.id); + } else { + filesByID.set( + change.id, + await decryptRemoteFile(change, collection.key), + ); + } + } + + const files = [...filesByID.values()]; + await savePublicCollectionFiles(accessToken, files); + await savePublicCollectionLastSyncTime(accessToken, sinceTime); + onSetFiles(files); + + if (!hasMore) break; + } +}; + +/** + * Fetch the public collection diff to obtain updates to the collection + * (identified by its {@link credentials}) since {@link sinceTime}. + * + * Remote only, does not modify local state. + * + * @param credentials + * @param number + */ +const getPublicCollectionDiff = async ( + credentials: PublicAlbumsCredentials, + sinceTime: number, +) => { + const res = await fetch( + await apiURL("/public-collection/diff", { sinceTime }), + { headers: authenticatedPublicAlbumsRequestHeaders(credentials) }, + ); + ensureOk(res); + return FileDiffResponse.parse(await res.json()); }; From 54aec6e0db54c36b37e93bc5dd9d7d4aa895129f Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 1 Jul 2025 20:40:23 +0530 Subject: [PATCH 25/36] Touch ups --- web/apps/photos/src/pages/shared-albums.tsx | 31 +++++++++---------- .../new/albums/services/public-collection.ts | 17 +++++++--- 2 files changed, 28 insertions(+), 20 deletions(-) diff --git a/web/apps/photos/src/pages/shared-albums.tsx b/web/apps/photos/src/pages/shared-albums.tsx index 0d2de6617b..0cc1434874 100644 --- a/web/apps/photos/src/pages/shared-albums.tsx +++ b/web/apps/photos/src/pages/shared-albums.tsx @@ -91,6 +91,9 @@ import { downloadSelectedFiles, getSelectedFiles } from "utils/file"; import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery"; export default function PublicCollectionGallery() { + const { showMiniDialog } = useBaseContext(); + const { showLoadingBar, hideLoadingBar } = usePhotosAppContext(); + const credentials = useRef(undefined); const collectionKey = useRef(null); const url = useRef(null); @@ -98,12 +101,10 @@ export default function PublicCollectionGallery() { const [publicFiles, setPublicFiles] = useState(null); const [publicCollection, setPublicCollection] = useState(null); const [errorMessage, setErrorMessage] = useState(null); - const { showMiniDialog } = useBaseContext(); - const { showLoadingBar, hideLoadingBar } = usePhotosAppContext(); const [loading, setLoading] = useState(true); + const [isPasswordProtected, setIsPasswordProtected] = useState(false); + const router = useRouter(); - const [isPasswordProtected, setIsPasswordProtected] = - useState(false); const [photoListHeader, setPhotoListHeader] = useState(null); @@ -219,24 +220,22 @@ export default function PublicCollectionGallery() { } collectionKey.current = ck; url.current = window.location.href; - const localCollection = await savedPublicCollectionByKey( + const collection = await savedPublicCollectionByKey( collectionKey.current, ); const accessToken = t; let accessTokenJWT: string | undefined; - if (localCollection) { + if (collection) { referralCode.current = await savedLastPublicCollectionReferralCode(); - const sortAsc: boolean = - localCollection?.pubMagicMetadata?.data.asc ?? false; - setPublicCollection(localCollection); - const isPasswordProtected = - localCollection?.publicURLs?.[0]?.passwordEnabled; - setIsPasswordProtected(isPasswordProtected); - const localFiles = - await savedPublicCollectionFiles(accessToken); - const localPublicFiles = sortFiles(localFiles, sortAsc); - setPublicFiles(localPublicFiles); + setPublicCollection(collection); + const sortAsc = + collection.pubMagicMetadata?.data.asc ?? false; + setIsPasswordProtected( + !!collection.publicURLs[0]?.passwordEnabled, + ); + const files = await savedPublicCollectionFiles(accessToken); + setPublicFiles(sortFiles(files, sortAsc)); accessTokenJWT = await savedPublicCollectionAccessTokenJWT(accessToken); } diff --git a/web/packages/new/albums/services/public-collection.ts b/web/packages/new/albums/services/public-collection.ts index 8d8a81ee76..d7d089b80b 100644 --- a/web/packages/new/albums/services/public-collection.ts +++ b/web/packages/new/albums/services/public-collection.ts @@ -5,6 +5,7 @@ import { type PublicAlbumsCredentials, } from "ente-base/http"; import { apiURL } from "ente-base/origins"; +import { sortFiles } from "ente-gallery/utils/file"; import { decryptRemoteCollection, RemoteCollection, @@ -157,11 +158,17 @@ const getPublicCollectionInfo = async (accessToken: string) => { * * @param collection The public collection corresponding to the credentials. * + * This function assumes that collection has already been pulled from remote and + * is at its latest, remote, value. This assumption is used to skip fetching + * files if the collection has not changed on remote (any updates to the files + * will also increase the updation time of the collection that contains them). + * * @param onSetFiles A callback that is invoked each time a new batch of updates * to the collection's files is fetched and processed. THe callback is called * the consolidated list of files after applying the updates received so far. * - * This callback can get called multiple times during the pull. + * This callback can get called multiple times during the pull. The callback can + * also never get called if no changes were pulled. */ export const pullPublicCollectionFiles = async ( credentials: PublicAlbumsCredentials, @@ -169,12 +176,14 @@ export const pullPublicCollectionFiles = async ( onSetFiles: (files: EnteFile[]) => void, ) => { const { accessToken } = credentials; + const sortAsc = collection.pubMagicMetadata?.data.asc ?? false; + + let sinceTime = (await savedPublicCollectionLastSyncTime(accessToken)) ?? 0; + if (sinceTime == collection.updationTime) return; const files = await savedPublicCollectionFiles(accessToken); const filesByID = new Map(files.map((f) => [f.id, f])); - let sinceTime = (await savedPublicCollectionLastSyncTime(accessToken)) ?? 0; - while (true) { const { diff, hasMore } = await getPublicCollectionDiff( credentials, @@ -193,7 +202,7 @@ export const pullPublicCollectionFiles = async ( } } - const files = [...filesByID.values()]; + const files = sortFiles([...filesByID.values()], sortAsc); await savePublicCollectionFiles(accessToken, files); await savePublicCollectionLastSyncTime(accessToken, sinceTime); onSetFiles(files); From 2fba8a2705e38e051441d66d2f3845cd7e6b8745 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 2 Jul 2025 07:29:24 +0530 Subject: [PATCH 26/36] Extract sort --- web/apps/photos/src/pages/shared-albums.tsx | 43 +++++++++++++------ .../new/albums/services/public-collection.ts | 15 ++++--- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/web/apps/photos/src/pages/shared-albums.tsx b/web/apps/photos/src/pages/shared-albums.tsx index 0cc1434874..4d700952cf 100644 --- a/web/apps/photos/src/pages/shared-albums.tsx +++ b/web/apps/photos/src/pages/shared-albums.tsx @@ -98,8 +98,12 @@ export default function PublicCollectionGallery() { const collectionKey = useRef(null); const url = useRef(null); const referralCode = useRef(""); - const [publicFiles, setPublicFiles] = useState(null); - const [publicCollection, setPublicCollection] = useState(null); + const [publicCollection, setPublicCollection] = useState< + Collection | undefined + >(undefined); + const [publicFiles, setPublicFiles] = useState( + undefined, + ); const [errorMessage, setErrorMessage] = useState(null); const [loading, setLoading] = useState(true); const [isPasswordProtected, setIsPasswordProtected] = useState(false); @@ -229,13 +233,15 @@ export default function PublicCollectionGallery() { referralCode.current = await savedLastPublicCollectionReferralCode(); setPublicCollection(collection); - const sortAsc = - collection.pubMagicMetadata?.data.asc ?? false; setIsPasswordProtected( !!collection.publicURLs[0]?.passwordEnabled, ); - const files = await savedPublicCollectionFiles(accessToken); - setPublicFiles(sortFiles(files, sortAsc)); + setPublicFiles( + sortFilesForCollection( + await savedPublicCollectionFiles(accessToken), + collection, + ), + ); accessTokenJWT = await savedPublicCollectionAccessTokenJWT(accessToken); } @@ -330,7 +336,10 @@ export default function PublicCollectionGallery() { credentials.current.accessToken, credentials.current.accessTokenJWT, collection, - setPublicFiles, + (files) => + setPublicFiles( + sortFilesForCollection(files, collection), + ), ); } catch (e) { const parsedError = parseSharingErrorCodes(e); @@ -365,8 +374,8 @@ export default function PublicCollectionGallery() { collectionUID, collectionKey.current, ); - setPublicCollection(null); - setPublicFiles(null); + setPublicCollection(undefined); + setPublicFiles(undefined); } else { log.error("Public album remote pull failed", e); } @@ -425,6 +434,11 @@ export default function PublicCollectionGallery() { }); }; + const handleUploadFile = (file: EnteFile) => + setPublicFiles( + sortFilesForCollection([...publicFiles, file], publicCollection), + ); + const downloadFilesHelper = async () => { try { const selectedFiles = getSelectedFiles(selected, publicFiles); @@ -543,9 +557,7 @@ export default function PublicCollectionGallery() { uploadTypeSelectorIntent="collect" uploadTypeSelectorView={uploadTypeSelectorView} onRemotePull={publicAlbumsRemotePull} - onUploadFile={(file) => - setPublicFiles(sortFiles([...publicFiles, file])) - } + onUploadFile={handleUploadFile} closeUploadTypeSelector={closeUploadTypeSelectorView} onShowSessionExpiredDialog={showPublicLinkExpiredMessage} {...{ dragAndDropFiles }} @@ -559,6 +571,13 @@ export default function PublicCollectionGallery() { ); } +/** + * Sort the given {@link files} using {@link sortFiles}, using the ascending + * ordering preference if specified in the given {@link collection}'s metadata. + */ +const sortFilesForCollection = (files: EnteFile[], collection?: Collection) => + sortFiles(files, collection?.pubMagicMetadata?.data.asc ?? false); + const EnteLogoLink = styled("a")(({ theme }) => ({ // Remove the excess space at the top. svg: { verticalAlign: "middle" }, diff --git a/web/packages/new/albums/services/public-collection.ts b/web/packages/new/albums/services/public-collection.ts index d7d089b80b..cddc3c89f5 100644 --- a/web/packages/new/albums/services/public-collection.ts +++ b/web/packages/new/albums/services/public-collection.ts @@ -5,7 +5,6 @@ import { type PublicAlbumsCredentials, } from "ente-base/http"; import { apiURL } from "ente-base/origins"; -import { sortFiles } from "ente-gallery/utils/file"; import { decryptRemoteCollection, RemoteCollection, @@ -164,11 +163,13 @@ const getPublicCollectionInfo = async (accessToken: string) => { * will also increase the updation time of the collection that contains them). * * @param onSetFiles A callback that is invoked each time a new batch of updates - * to the collection's files is fetched and processed. THe callback is called + * to the collection's files is fetched and processed. The callback is called * the consolidated list of files after applying the updates received so far. * + * The provided files are in an arbitrary order, and must be sorted before use. + * * This callback can get called multiple times during the pull. The callback can - * also never get called if no changes were pulled. + * also never get called if no changes were pulled (or needed to be pulled). */ export const pullPublicCollectionFiles = async ( credentials: PublicAlbumsCredentials, @@ -176,9 +177,13 @@ export const pullPublicCollectionFiles = async ( onSetFiles: (files: EnteFile[]) => void, ) => { const { accessToken } = credentials; - const sortAsc = collection.pubMagicMetadata?.data.asc ?? false; let sinceTime = (await savedPublicCollectionLastSyncTime(accessToken)) ?? 0; + + // Prior to reaching here, we would've already fetched the latest + // collection. If the updation time of the collection is the same as the + // last sync time, then we know there were no new updates (since updates to + // files also increase the updation time of their containing collection). if (sinceTime == collection.updationTime) return; const files = await savedPublicCollectionFiles(accessToken); @@ -202,7 +207,7 @@ export const pullPublicCollectionFiles = async ( } } - const files = sortFiles([...filesByID.values()], sortAsc); + const files = [...filesByID.values()]; await savePublicCollectionFiles(accessToken, files); await savePublicCollectionLastSyncTime(accessToken, sinceTime); onSetFiles(files); From c60288b0df9f4c6159315d5e0850df6132330d1c Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 2 Jul 2025 07:33:39 +0530 Subject: [PATCH 27/36] Simplify --- web/apps/photos/src/pages/shared-albums.tsx | 39 ++--- .../src/services/publicCollectionService.ts | 152 ------------------ .../new/albums/services/public-collection.ts | 5 +- 3 files changed, 22 insertions(+), 174 deletions(-) diff --git a/web/apps/photos/src/pages/shared-albums.tsx b/web/apps/photos/src/pages/shared-albums.tsx index 4d700952cf..d85900880d 100644 --- a/web/apps/photos/src/pages/shared-albums.tsx +++ b/web/apps/photos/src/pages/shared-albums.tsx @@ -59,7 +59,8 @@ import { savePublicCollectionAccessTokenJWT, } from "ente-new/albums/services/public-albums-fdb"; import { - fetchAndSavePublicCollection, + pullCollection, + pullPublicCollectionFiles, verifyPublicAlbumPassword, } from "ente-new/albums/services/public-collection"; import { @@ -78,7 +79,6 @@ import { getPublicCollectionUID, removePublicCollectionWithFiles, removePublicFiles, - syncPublicFiles, } from "services/publicCollectionService"; import { uploadManager } from "services/upload-manager"; import { @@ -307,7 +307,7 @@ export default function PublicCollectionGallery() { showLoadingBar(); setLoading(true); const { collection, referralCode: userReferralCode } = - await fetchAndSavePublicCollection( + await pullCollection( credentials.current.accessToken, collectionKey.current, ); @@ -327,14 +327,12 @@ export default function PublicCollectionGallery() { removePublicCollectionAccessTokenJWT(collectionUID); } - if ( - !isPasswordProtected || - (isPasswordProtected && credentials.current.accessTokenJWT) - ) { + if (isPasswordProtected && !credentials.current.accessTokenJWT) { + await removePublicFiles(collectionUID); + } else { try { - await syncPublicFiles( - credentials.current.accessToken, - credentials.current.accessTokenJWT, + await pullPublicCollectionFiles( + credentials.current, collection, (files) => setPublicFiles( @@ -342,21 +340,26 @@ export default function PublicCollectionGallery() { ), ); } catch (e) { - const parsedError = parseSharingErrorCodes(e); - if (parsedError.message === CustomError.TOKEN_EXPIRED) { - // passwordToken has expired, sharer has changed the password, - // so,clearing local cache token value to prompt user to re-enter password + // If we reached the try block and attempted to pull files, + // it means the accessToken itself is very likely valid + // (since the `pullCollection` succeeded just a moment ago). + // + // So a 401 Unauthorized now indicates that the + // accessTokenJWT is no longer valid since the sharer has + // changed the password. + // + // Clear the locally cached accessTokenJWT and ask the user + // to reenter the password. + if (isHTTP401Error(e)) { credentials.current.accessTokenJWT = undefined; downloadManager.setPublicAlbumsCredentials( credentials.current, ); + } else { + throw e; } } } - - if (isPasswordProtected && !credentials.current.accessTokenJWT) { - await removePublicFiles(collectionUID); - } } catch (e) { const parsedError = parseSharingErrorCodes(e); if ( diff --git a/web/apps/photos/src/services/publicCollectionService.ts b/web/apps/photos/src/services/publicCollectionService.ts index 360ad750dc..6ee913d032 100644 --- a/web/apps/photos/src/services/publicCollectionService.ts +++ b/web/apps/photos/src/services/publicCollectionService.ts @@ -1,166 +1,14 @@ -import log from "ente-base/log"; -import { apiURL } from "ente-base/origins"; -import { sortFiles } from "ente-gallery/utils/file"; -import type { Collection } from "ente-media/collection"; -import type { EnteFile, RemoteEnteFile } from "ente-media/file"; -import { decryptRemoteFile } from "ente-media/file"; import { removePublicCollectionAccessTokenJWT, removePublicCollectionByKey, removePublicCollectionFiles, removePublicCollectionLastSyncTime, - savedPublicCollectionFiles, - savedPublicCollectionLastSyncTime, - savePublicCollectionFiles, - savePublicCollectionLastSyncTime, } from "ente-new/albums/services/public-albums-fdb"; -import { CustomError, parseSharingErrorCodes } from "ente-shared/error"; -import HTTPService from "ente-shared/network/HTTPService"; // Fix this once we can trust the types. // eslint-disable-next-line @typescript-eslint/no-unnecessary-template-expression export const getPublicCollectionUID = (token: string) => `${token}`; -export interface LocalSavedPublicCollectionFiles { - collectionUID: string; - files: EnteFile[]; -} - -export const syncPublicFiles = async ( - token: string, - passwordToken: string, - collection: Collection, - setPublicFiles: (files: EnteFile[]) => void, -) => { - try { - let files: EnteFile[] = []; - const sortAsc = collection?.pubMagicMetadata?.data.asc ?? false; - const collectionUID = getPublicCollectionUID(token); - const localFiles = await savedPublicCollectionFiles(collectionUID); - - files = [...files, ...localFiles]; - try { - if (!token) { - return sortFiles(files, sortAsc); - } - const lastSyncTime = - (await savedPublicCollectionLastSyncTime(collectionUID)) ?? 0; - if (collection.updationTime === lastSyncTime) { - return sortFiles(files, sortAsc); - } - const fetchedFiles = await getPublicFiles( - token, - passwordToken, - collection, - lastSyncTime, - files, - setPublicFiles, - ); - - files = [...files, ...fetchedFiles]; - const latestVersionFiles = new Map(); - files.forEach((file) => { - const uid = `${file.collectionID}-${file.id}`; - if ( - !latestVersionFiles.has(uid) || - latestVersionFiles.get(uid).updationTime < file.updationTime - ) { - latestVersionFiles.set(uid, file); - } - }); - files = []; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - for (const [_, file] of latestVersionFiles) { - // TODO(RE): - if ("isDeleted" in file && file.isDeleted) { - continue; - } - files.push(file); - } - await savePublicCollectionFiles(collectionUID, files); - await savePublicCollectionLastSyncTime( - collectionUID, - collection.updationTime, - ); - setPublicFiles([...sortFiles(files, sortAsc)]); - } catch (e) { - const parsedError = parseSharingErrorCodes(e); - log.error("failed to sync shared collection files", e); - if (parsedError.message === CustomError.TOKEN_EXPIRED) { - throw e; - } - } - return [...sortFiles(files, sortAsc)]; - } catch (e) { - log.error("failed to get local or sync shared collection files", e); - throw e; - } -}; - -const getPublicFiles = async ( - token: string, - passwordToken: string, - collection: Collection, - sinceTime: number, - files: EnteFile[], - setPublicFiles: (files: EnteFile[]) => void, -): Promise => { - try { - let decryptedFiles: EnteFile[] = []; - let time = sinceTime; - let resp; - const sortAsc = collection?.pubMagicMetadata?.data.asc ?? false; - do { - if (!token) { - break; - } - resp = await HTTPService.get( - await apiURL("/public-collection/diff"), - { sinceTime: time }, - { - "X-Auth-Access-Token": token, - ...(passwordToken && { - "X-Auth-Access-Token-JWT": passwordToken, - }), - }, - ); - decryptedFiles = [ - ...decryptedFiles, - ...(await Promise.all( - resp.data.diff.map(async (file: RemoteEnteFile) => { - if (!file.isDeleted) { - return await decryptRemoteFile( - file, - collection.key, - ); - } else { - return file; - } - }) as Promise[], - )), - ]; - - if (resp.data.diff.length) { - time = resp.data.diff.slice(-1)[0].updationTime; - } - setPublicFiles( - sortFiles( - [...(files || []), ...decryptedFiles].filter( - // TODO(RE): - // (item) => !item.isDeleted, - (file) => !("isDeleted" in file && file.isDeleted), - ), - sortAsc, - ), - ); - } while (resp.data.hasMore); - return decryptedFiles; - } catch (e) { - log.error("Get public files failed", e); - throw e; - } -}; - export const removePublicCollectionWithFiles = async ( collectionUID: string, collectionKey: string, diff --git a/web/packages/new/albums/services/public-collection.ts b/web/packages/new/albums/services/public-collection.ts index cddc3c89f5..f6cf9ccfc2 100644 --- a/web/packages/new/albums/services/public-collection.ts +++ b/web/packages/new/albums/services/public-collection.ts @@ -93,7 +93,7 @@ export const verifyPublicAlbumPassword = async ( * (the fragment is a client side only portion that can be used to have local * secrets that are not sent by the browser to the server). */ -export const fetchAndSavePublicCollection = async ( +export const pullCollection = async ( accessToken: string, collectionKey: string, ): Promise<{ collection: Collection; referralCode: string }> => { @@ -221,9 +221,6 @@ export const pullPublicCollectionFiles = async ( * (identified by its {@link credentials}) since {@link sinceTime}. * * Remote only, does not modify local state. - * - * @param credentials - * @param number */ const getPublicCollectionDiff = async ( credentials: PublicAlbumsCredentials, From 028a5cf82741a4c1855c44c935a773049d0affaf Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 2 Jul 2025 08:43:04 +0530 Subject: [PATCH 28/36] Update --- web/apps/photos/src/pages/shared-albums.tsx | 47 ++++++++----------- .../src/services/publicCollectionService.ts | 24 ---------- .../new/albums/services/public-collection.ts | 17 +++++++ 3 files changed, 36 insertions(+), 52 deletions(-) delete mode 100644 web/apps/photos/src/services/publicCollectionService.ts diff --git a/web/apps/photos/src/pages/shared-albums.tsx b/web/apps/photos/src/pages/shared-albums.tsx index d85900880d..36788d936e 100644 --- a/web/apps/photos/src/pages/shared-albums.tsx +++ b/web/apps/photos/src/pages/shared-albums.tsx @@ -52,6 +52,7 @@ import type { Collection } from "ente-media/collection"; import { type EnteFile } from "ente-media/file"; import { removePublicCollectionAccessTokenJWT, + removePublicCollectionByKey, savedLastPublicCollectionReferralCode, savedPublicCollectionAccessTokenJWT, savedPublicCollectionByKey, @@ -61,6 +62,7 @@ import { import { pullCollection, pullPublicCollectionFiles, + removePublicCollectionFileData, verifyPublicAlbumPassword, } from "ente-new/albums/services/public-collection"; import { @@ -75,11 +77,6 @@ import { t } from "i18next"; import { useRouter } from "next/router"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { type FileWithPath } from "react-dropzone"; -import { - getPublicCollectionUID, - removePublicCollectionWithFiles, - removePublicFiles, -} from "services/publicCollectionService"; import { uploadManager } from "services/upload-manager"; import { SelectedState, @@ -208,6 +205,10 @@ export default function PublicCollectionGallery() { { shallow: true }, ); } + /** + * Determine credentials, read the locally cached state, then start + * pulling the latest from remote. + */ const main = async () => { let redirectingToWebsite = false; try { @@ -300,35 +301,30 @@ export default function PublicCollectionGallery() { * both our local database and component state. */ const publicAlbumsRemotePull = useCallback(async () => { - const collectionUID = getPublicCollectionUID( - credentials.current.accessToken, - ); + const accessToken = credentials.current.accessToken; try { showLoadingBar(); setLoading(true); const { collection, referralCode: userReferralCode } = - await pullCollection( - credentials.current.accessToken, - collectionKey.current, - ); + await pullCollection(accessToken, collectionKey.current); referralCode.current = userReferralCode; setPublicCollection(collection); const isPasswordProtected = - collection?.publicURLs?.[0]?.passwordEnabled; + !!collection.publicURLs[0]?.passwordEnabled; setIsPasswordProtected(isPasswordProtected); setErrorMessage(null); - // Remove the locally saved outdated password token if the sharer - // has disabled password protection on the link. + // Remove the locally cached accessTokenJWT if the sharer has + // disabled password protection on the link. if (!isPasswordProtected && credentials.current.accessTokenJWT) { credentials.current.accessTokenJWT = undefined; downloadManager.setPublicAlbumsCredentials(credentials.current); - removePublicCollectionAccessTokenJWT(collectionUID); + removePublicCollectionAccessTokenJWT(accessToken); } if (isPasswordProtected && !credentials.current.accessTokenJWT) { - await removePublicFiles(collectionUID); + await removePublicCollectionFileData(accessToken); } else { try { await pullPublicCollectionFiles( @@ -371,12 +367,9 @@ export default function PublicCollectionGallery() { ? t("link_request_limit_exceeded") : t("link_expired_message"), ); - // share has been disabled - // local cache should be cleared - removePublicCollectionWithFiles( - collectionUID, - collectionKey.current, - ); + // Sharing has been disabled. Clear out local cache. + await removePublicCollectionFileData(accessToken); + await removePublicCollectionByKey(collectionKey.current); setPublicCollection(undefined); setPublicFiles(undefined); } else { @@ -399,18 +392,16 @@ export default function PublicCollectionGallery() { setFieldError, ) => { try { + const accessToken = credentials.current.accessToken; const accessTokenJWT = await verifyPublicAlbumPassword( publicCollection.publicURLs[0]!, password, - credentials.current.accessToken, + accessToken, ); credentials.current.accessTokenJWT = accessTokenJWT; downloadManager.setPublicAlbumsCredentials(credentials.current); - const collectionUID = getPublicCollectionUID( - credentials.current.accessToken, - ); await savePublicCollectionAccessTokenJWT( - collectionUID, + accessToken, accessTokenJWT, ); } catch (e) { diff --git a/web/apps/photos/src/services/publicCollectionService.ts b/web/apps/photos/src/services/publicCollectionService.ts deleted file mode 100644 index 6ee913d032..0000000000 --- a/web/apps/photos/src/services/publicCollectionService.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { - removePublicCollectionAccessTokenJWT, - removePublicCollectionByKey, - removePublicCollectionFiles, - removePublicCollectionLastSyncTime, -} from "ente-new/albums/services/public-albums-fdb"; - -// Fix this once we can trust the types. -// eslint-disable-next-line @typescript-eslint/no-unnecessary-template-expression -export const getPublicCollectionUID = (token: string) => `${token}`; - -export const removePublicCollectionWithFiles = async ( - collectionUID: string, - collectionKey: string, -) => { - await removePublicCollectionByKey(collectionKey); - await removePublicFiles(collectionUID); -}; - -export const removePublicFiles = async (collectionUID: string) => { - await removePublicCollectionAccessTokenJWT(collectionUID); - await removePublicCollectionLastSyncTime(collectionUID); - await removePublicCollectionFiles(collectionUID); -}; diff --git a/web/packages/new/albums/services/public-collection.ts b/web/packages/new/albums/services/public-collection.ts index f6cf9ccfc2..5000f364b0 100644 --- a/web/packages/new/albums/services/public-collection.ts +++ b/web/packages/new/albums/services/public-collection.ts @@ -18,6 +18,9 @@ import { } from "ente-media/file"; import { z } from "zod/v4"; import { + removePublicCollectionAccessTokenJWT, + removePublicCollectionFiles, + removePublicCollectionLastSyncTime, savedPublicCollectionFiles, savedPublicCollectionLastSyncTime, saveLastPublicCollectionReferralCode, @@ -233,3 +236,17 @@ const getPublicCollectionDiff = async ( ensureOk(res); return FileDiffResponse.parse(await res.json()); }; + +/** + * Remove the files, sync time and accessTokenJWT associated with the given + * collection (identified by its {@link accessToken}). + * + * This function modifies local state. + */ +export const removePublicCollectionFileData = async (accessToken: string) => { + await Promise.all([ + removePublicCollectionAccessTokenJWT(accessToken), + removePublicCollectionLastSyncTime(accessToken), + removePublicCollectionFiles(accessToken), + ]); +}; From ce020d4398d05201a73dd16100788399226ec830 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 2 Jul 2025 08:59:03 +0530 Subject: [PATCH 29/36] Use --- web/apps/photos/src/pages/shared-albums.tsx | 33 +++++++++++++++------ 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/web/apps/photos/src/pages/shared-albums.tsx b/web/apps/photos/src/pages/shared-albums.tsx index 36788d936e..92a49006ab 100644 --- a/web/apps/photos/src/pages/shared-albums.tsx +++ b/web/apps/photos/src/pages/shared-albums.tsx @@ -41,7 +41,11 @@ import { useIsTouchscreen, } from "ente-base/components/utils/hooks"; import { useBaseContext } from "ente-base/context"; -import { isHTTP401Error, PublicAlbumsCredentials } from "ente-base/http"; +import { + isHTTP401Error, + isHTTPErrorWithStatus, + PublicAlbumsCredentials, +} from "ente-base/http"; import log from "ente-base/log"; import { FullScreenDropZone } from "ente-gallery/components/FullScreenDropZone"; import { downloadManager } from "ente-gallery/services/download"; @@ -72,7 +76,6 @@ import { import { isHiddenCollection } from "ente-new/photos/services/collection"; import { PseudoCollectionID } from "ente-new/photos/services/collection-summary"; import { usePhotosAppContext } from "ente-new/photos/types/context"; -import { CustomError, parseSharingErrorCodes } from "ente-shared/error"; import { t } from "i18next"; import { useRouter } from "next/router"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; @@ -88,7 +91,7 @@ import { downloadSelectedFiles, getSelectedFiles } from "utils/file"; import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery"; export default function PublicCollectionGallery() { - const { showMiniDialog } = useBaseContext(); + const { showMiniDialog, onGenericError } = useBaseContext(); const { showLoadingBar, hideLoadingBar } = usePhotosAppContext(); const credentials = useRef(undefined); @@ -302,9 +305,9 @@ export default function PublicCollectionGallery() { */ const publicAlbumsRemotePull = useCallback(async () => { const accessToken = credentials.current.accessToken; + showLoadingBar(); + setLoading(true); try { - showLoadingBar(); - setLoading(true); const { collection, referralCode: userReferralCode } = await pullCollection(accessToken, collectionKey.current); referralCode.current = userReferralCode; @@ -357,13 +360,22 @@ export default function PublicCollectionGallery() { } } } catch (e) { - const parsedError = parseSharingErrorCodes(e); + // The 410 Gone or 429 Rate limited can arise from either the + // collection pull or the files pull since they're part of the + // remote's access token check sequence. + // + // In practice, it almost always will be a consequence of the + // collection pull since it happens first. + // + // The 401 Unauthorized can only arise from the collection pull + // since we already handle that separately for the files pull. if ( - parsedError.message === CustomError.TOKEN_EXPIRED || - parsedError.message === CustomError.TOO_MANY_REQUESTS + isHTTPErrorWithStatus(e, 401) || + isHTTPErrorWithStatus(e, 410) || + isHTTPErrorWithStatus(e, 429) ) { setErrorMessage( - parsedError.message === CustomError.TOO_MANY_REQUESTS + isHTTPErrorWithStatus(e, 429) ? t("link_request_limit_exceeded") : t("link_expired_message"), ); @@ -374,6 +386,9 @@ export default function PublicCollectionGallery() { setPublicFiles(undefined); } else { log.error("Public album remote pull failed", e); + // Don't use the `setErrorMessage`, show a dialog instead, + // because this might be a transient network error. + onGenericError(e); } } finally { hideLoadingBar(); From d04ee0aa713794895dfa4b74c3f3dd65eaee555c Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 2 Jul 2025 09:06:09 +0530 Subject: [PATCH 30/36] Prune --- web/apps/auth/src/pages/_app.tsx | 4 +- web/apps/photos/src/pages/_app.tsx | 4 +- web/packages/accounts/pages/verify.tsx | 10 - web/packages/shared/error/index.ts | 52 ----- web/packages/shared/network/HTTPService.ts | 223 --------------------- web/packages/shared/package.json | 1 - web/yarn.lock | 57 ------ 7 files changed, 2 insertions(+), 349 deletions(-) delete mode 100644 web/packages/shared/network/HTTPService.ts diff --git a/web/apps/auth/src/pages/_app.tsx b/web/apps/auth/src/pages/_app.tsx index 9aada5ae8a..ec70ea527b 100644 --- a/web/apps/auth/src/pages/_app.tsx +++ b/web/apps/auth/src/pages/_app.tsx @@ -3,7 +3,7 @@ import { CssBaseline } from "@mui/material"; import { ThemeProvider } from "@mui/material/styles"; import { accountLogout } from "ente-accounts/services/logout"; import type { User } from "ente-accounts/services/user"; -import { clientPackageName, staticAppTitle } from "ente-base/app"; +import { staticAppTitle } from "ente-base/app"; import { CustomHead } from "ente-base/components/Head"; import { LoadingIndicator, @@ -19,7 +19,6 @@ import { import { authTheme } from "ente-base/components/utils/theme"; import { BaseContext, deriveBaseContext } from "ente-base/context"; import { logStartupBanner } from "ente-base/log-web"; -import HTTPService from "ente-shared/network/HTTPService"; import { getData } from "ente-shared/storage/localStorage"; import { t } from "i18next"; import type { AppProps } from "next/app"; @@ -35,7 +34,6 @@ const App: React.FC = ({ Component, pageProps }) => { useEffect(() => { const user = getData("user") as User | undefined | null; logStartupBanner(user?.id); - HTTPService.setHeaders({ "X-Client-Package": clientPackageName }); }, []); const logout = useCallback(() => { diff --git a/web/apps/photos/src/pages/_app.tsx b/web/apps/photos/src/pages/_app.tsx index 5824cb1890..67ce61f0c3 100644 --- a/web/apps/photos/src/pages/_app.tsx +++ b/web/apps/photos/src/pages/_app.tsx @@ -4,7 +4,7 @@ import { CssBaseline, Typography } from "@mui/material"; import { styled, ThemeProvider } from "@mui/material/styles"; import { useNotification } from "components/utils/hooks-app"; import type { User } from "ente-accounts/services/user"; -import { clientPackageName, isDesktop, staticAppTitle } from "ente-base/app"; +import { isDesktop, staticAppTitle } from "ente-base/app"; import { CenteredRow } from "ente-base/components/containers"; import { CustomHead } from "ente-base/components/Head"; import { @@ -39,7 +39,6 @@ import { runMigrations } from "ente-new/photos/services/migration"; import { initML, isMLSupported } from "ente-new/photos/services/ml"; import { getFamilyPortalRedirectURL } from "ente-new/photos/services/user-details"; import { PhotosAppContext } from "ente-new/photos/types/context"; -import HTTPService from "ente-shared/network/HTTPService"; import { getData, isLocalStorageAndIndexedDBMismatch, @@ -71,7 +70,6 @@ const App: React.FC = ({ Component, pageProps }) => { useEffect(() => { const user = getData("user") as User | undefined | null; logStartupBanner(user?.id); - HTTPService.setHeaders({ "X-Client-Package": clientPackageName }); void isLocalStorageAndIndexedDBMismatch().then((mismatch) => { if (mismatch) { log.error("Logging out (IndexedDB and local storage mismatch)"); diff --git a/web/packages/accounts/pages/verify.tsx b/web/packages/accounts/pages/verify.tsx index c8f8b26ad0..fba3a9ad1e 100644 --- a/web/packages/accounts/pages/verify.tsx +++ b/web/packages/accounts/pages/verify.tsx @@ -1,5 +1,4 @@ import { Box, Typography } from "@mui/material"; -import { HttpStatusCode } from "axios"; import { AccountsPageContents, AccountsPageFooter, @@ -39,7 +38,6 @@ import { isDevBuild } from "ente-base/env"; import { isHTTPErrorWithStatus } from "ente-base/http"; import log from "ente-base/log"; import { clearSessionStorage } from "ente-base/session"; -import { ApiError } from "ente-shared/error"; import localForage from "ente-shared/storage/localForage"; import { getData, setData, setLSUser } from "ente-shared/storage/localStorage"; import { @@ -169,14 +167,6 @@ const Page: React.FC = () => { setFieldError(t("invalid_code_error")); } else if (isHTTPErrorWithStatus(e, 410)) { setFieldError(t("expired_code_error")); - } else if (e instanceof ApiError) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison - if (e?.httpStatusCode === HttpStatusCode.Unauthorized) { - setFieldError(t("invalid_code_error")); - // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison - } else if (e?.httpStatusCode === HttpStatusCode.Gone) { - setFieldError(t("expired_code_error")); - } } else { log.error("OTT verification failed", e); throw e; diff --git a/web/packages/shared/error/index.ts b/web/packages/shared/error/index.ts index 788df9acb1..f776ae6056 100644 --- a/web/packages/shared/error/index.ts +++ b/web/packages/shared/error/index.ts @@ -1,26 +1,3 @@ -import { HttpStatusCode } from "axios"; - -export interface ApiErrorResponse { - code: string; - message: string; -} - -export class ApiError extends Error { - httpStatusCode: number; - errCode: string; - - constructor(message: string, errCode: string, httpStatus: number) { - super(message); - this.name = "ApiError"; - this.errCode = errCode; - this.httpStatusCode = httpStatus; - } -} - -export function isApiErrorResponse(object: any): object is ApiErrorResponse { - return object && "code" in object && "message" in object; -} - export const CustomError = { TOKEN_EXPIRED: "token expired", TOO_MANY_REQUESTS: "too many requests", @@ -32,32 +9,3 @@ export const CustomError = { EXPORT_FOLDER_DOES_NOT_EXIST: "export folder does not exist", TWO_FACTOR_ENABLED: "two factor enabled", }; - -export const parseSharingErrorCodes = (error: any) => { - let parsedMessage = null; - if (error instanceof ApiError) { - switch (error.httpStatusCode) { - case HttpStatusCode.BadRequest: - parsedMessage = CustomError.BAD_REQUEST; - break; - case HttpStatusCode.PaymentRequired: - parsedMessage = CustomError.SUBSCRIPTION_NEEDED; - break; - case HttpStatusCode.NotFound: - parsedMessage = CustomError.NOT_FOUND; - break; - case HttpStatusCode.Unauthorized: - case HttpStatusCode.Gone: - parsedMessage = CustomError.TOKEN_EXPIRED; - break; - case HttpStatusCode.TooManyRequests: - parsedMessage = CustomError.TOO_MANY_REQUESTS; - break; - default: - parsedMessage = `Something went wrong (statusCode:${error.httpStatusCode})`; - } - } else { - parsedMessage = error.message; - } - return new Error(parsedMessage); -}; diff --git a/web/packages/shared/network/HTTPService.ts b/web/packages/shared/network/HTTPService.ts deleted file mode 100644 index 66c5851317..0000000000 --- a/web/packages/shared/network/HTTPService.ts +++ /dev/null @@ -1,223 +0,0 @@ -import axios, { type AxiosRequestConfig, type AxiosResponse } from "axios"; -import log from "ente-base/log"; -import { ApiError, isApiErrorResponse } from "../error"; - -type IHTTPHeaders = Record; - -type IQueryPrams = Record; - -/** - * Service to manage all HTTP calls. - */ -class HTTPService { - constructor() { - axios.interceptors.response.use( - (response) => Promise.resolve(response), - (error) => { - const config = error.config as AxiosRequestConfig; - if (error.response) { - const response = error.response as AxiosResponse; - let apiError: ApiError; - // The request was made and the server responded with a status code - // that falls out of the range of 2xx - if (isApiErrorResponse(response.data)) { - const responseData = response.data; - log.error( - `HTTP Service Error - ${JSON.stringify({ - url: config?.url, - method: config?.method, - xRequestId: response.headers["x-request-id"], - httpStatus: response.status, - errMessage: responseData.message, - errCode: responseData.code, - })}`, - error, - ); - apiError = new ApiError( - responseData.message, - responseData.code, - response.status, - ); - } else { - if (response.status >= 400 && response.status < 500) { - apiError = new ApiError( - "client error", - "", - response.status, - ); - } else { - apiError = new ApiError( - "server error", - "", - response.status, - ); - } - } - log.error( - `HTTP Service Error - ${JSON.stringify({ - url: config.url, - method: config.method, - cfRay: response.headers["cf-ray"], - xRequestId: response.headers["x-request-id"], - httpStatus: response.status, - })}`, - apiError, - ); - throw apiError; - } else if (error.request) { - // The request was made but no response was received - // `error.request` is an instance of XMLHttpRequest in the browser and an instance of - // http.ClientRequest in node.js - log.info( - `request failed - no response (${config.method} ${config.url}`, - ); - return Promise.reject(error); - } else { - // Something happened in setting up the request that - // triggered an Error - log.info( - `request failed - axios error (${config.method} ${config.url}`, - ); - return Promise.reject(error); - } - }, - ); - } - - /** - * header object to be append to all api calls. - */ - private headers: IHTTPHeaders = { "content-type": "application/json" }; - - /** - * Sets the headers to the given object. - */ - public setHeaders(headers: IHTTPHeaders) { - this.headers = { ...this.headers, ...headers }; - } - - /** - * Adds a header to list of headers. - */ - public appendHeader(key: string, value: string) { - this.headers = { ...this.headers, [key]: value }; - } - - /** - * Removes the given header. - */ - public removeHeader(key: string) { - this.headers[key] = undefined; - } - - /** - * Returns axios interceptors. - */ - public getInterceptors() { - return axios.interceptors; - } - - /** - * Generic HTTP request. - * This is done so that developer can use any functionality - * provided by axios. Here, only the set headers are spread - * over what was sent in config. - */ - public async request(config: AxiosRequestConfig, customConfig?: any) { - config.headers = { - ...this.headers, - // eslint-disable-next-line @typescript-eslint/no-misused-spread - ...config.headers, - }; - if (customConfig?.cancel) { - config.cancelToken = new axios.CancelToken( - (c) => (customConfig.cancel.exec = c), - ); - } - return await axios({ ...config, ...customConfig }); - } - - /** - * Get request. - */ - public get( - url: string, - params?: IQueryPrams, - headers?: IHTTPHeaders, - customConfig?: any, - ) { - return this.request( - { headers, method: "GET", params, url }, - customConfig, - ); - } - - /** - * Post request - */ - public post( - url: string, - data?: any, - params?: IQueryPrams, - headers?: IHTTPHeaders, - customConfig?: any, - ) { - return this.request( - { data, headers, method: "POST", params, url }, - customConfig, - ); - } - - /** - * Patch request - */ - public patch( - url: string, - data?: any, - params?: IQueryPrams, - headers?: IHTTPHeaders, - customConfig?: any, - ) { - return this.request( - { data, headers, method: "PATCH", params, url }, - customConfig, - ); - } - - /** - * Put request - */ - public put( - url: string, - data: any, - params?: IQueryPrams, - headers?: IHTTPHeaders, - customConfig?: any, - ) { - return this.request( - { data, headers, method: "PUT", params, url }, - customConfig, - ); - } - - /** - * Delete request - */ - public delete( - url: string, - data: any, - params?: IQueryPrams, - headers?: IHTTPHeaders, - customConfig?: any, - ) { - return this.request( - { data, headers, method: "DELETE", params, url }, - customConfig, - ); - } -} - -// Creates a Singleton Service. -// This will help me maintain common headers / functionality -// at a central place. -export default new HTTPService(); diff --git a/web/packages/shared/package.json b/web/packages/shared/package.json index afe14de210..1a016ce083 100644 --- a/web/packages/shared/package.json +++ b/web/packages/shared/package.json @@ -3,7 +3,6 @@ "version": "0.0.0", "private": true, "dependencies": { - "axios": "^1.9.0", "ente-base": "*" }, "devDependencies": { diff --git a/web/yarn.lock b/web/yarn.lock index 17d47637a6..e717930e12 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -1469,11 +1469,6 @@ arraybuffer.prototype.slice@^1.0.4: get-intrinsic "^1.2.6" is-array-buffer "^3.0.4" -asynckit@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" - integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== - attr-accept@^2.2.2: version "2.2.5" resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.5.tgz#d7061d958e6d4f97bf8665c68b75851a0713ab5e" @@ -1486,15 +1481,6 @@ available-typed-arrays@^1.0.7: dependencies: possible-typed-array-names "^1.0.0" -axios@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-1.9.0.tgz#25534e3b72b54540077d33046f77e3b8d7081901" - integrity sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg== - dependencies: - follow-redirects "^1.15.6" - form-data "^4.0.0" - proxy-from-env "^1.1.0" - babel-plugin-macros@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1" @@ -1670,13 +1656,6 @@ color@^4.2.3: color-convert "^2.0.1" color-string "^1.9.0" -combined-stream@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" - integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== - dependencies: - delayed-stream "~1.0.0" - comlink@^4.4.2: version "4.4.2" resolved "https://registry.yarnpkg.com/comlink/-/comlink-4.4.2.tgz#cbbcd82742fbebc06489c28a183eedc5c60a2bca" @@ -1817,11 +1796,6 @@ define-properties@^1.1.3, define-properties@^1.2.1: has-property-descriptors "^1.0.0" object-keys "^1.1.1" -delayed-stream@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" - integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== - detect-indent@^7.0.1: version "7.0.1" resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-7.0.1.tgz#cbb060a12842b9c4d333f1cac4aa4da1bb66bc25" @@ -2287,11 +2261,6 @@ flatted@^3.2.9: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.2.tgz#adba1448a9841bec72b42c532ea23dbbedef1a27" integrity sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA== -follow-redirects@^1.15.6: - version "1.15.9" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1" - integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ== - for-each@^0.3.3: version "0.3.3" resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" @@ -2299,15 +2268,6 @@ for-each@^0.3.3: dependencies: is-callable "^1.1.3" -form-data@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.1.tgz#ba1076daaaa5bfd7e99c1a6cb02aa0a5cff90d48" - integrity sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw== - dependencies: - asynckit "^0.4.0" - combined-stream "^1.0.8" - mime-types "^2.1.12" - formik@^2.4.6: version "2.4.6" resolved "https://registry.yarnpkg.com/formik/-/formik-2.4.6.tgz#4da75ca80f1a827ab35b08fd98d5a76e928c9686" @@ -3051,18 +3011,6 @@ micromatch@^4.0.4: braces "^3.0.3" picomatch "^2.3.1" -mime-db@1.52.0: - version "1.52.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" - integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== - -mime-types@^2.1.12: - version "2.1.35" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" - integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== - dependencies: - mime-db "1.52.0" - mimic-function@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/mimic-function/-/mimic-function-5.0.1.tgz#acbe2b3349f99b9deaca7fb70e48b83e94e67076" @@ -3397,11 +3345,6 @@ prop-types@^15.6.0, prop-types@^15.6.2, prop-types@^15.8.1: object-assign "^4.1.1" react-is "^16.13.1" -proxy-from-env@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" - integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== - punycode@^2.1.0: version "2.3.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" From 3c7b1c6c5e28cad9e54965e60078fdc9b91a0ffd Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 2 Jul 2025 09:15:02 +0530 Subject: [PATCH 31/36] [mob] Update change log order --- mobile/lib/services/update_service.dart | 2 +- .../notification/update/change_log_page.dart | 23 +++++++++++-------- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/mobile/lib/services/update_service.dart b/mobile/lib/services/update_service.dart index e144548a79..4c7b1dc881 100644 --- a/mobile/lib/services/update_service.dart +++ b/mobile/lib/services/update_service.dart @@ -14,7 +14,7 @@ import 'package:url_launcher/url_launcher_string.dart'; class UpdateService { static const kUpdateAvailableShownTimeKey = "update_available_shown_time_key"; static const changeLogVersionKey = "update_change_log_key"; - static const currentChangeLogVersion = 29; + static const currentChangeLogVersion = 30; LatestVersionInfo? _latestVersion; final _logger = Logger("UpdateService"); diff --git a/mobile/lib/ui/notification/update/change_log_page.dart b/mobile/lib/ui/notification/update/change_log_page.dart index 42ddaa4407..e386546fb2 100644 --- a/mobile/lib/ui/notification/update/change_log_page.dart +++ b/mobile/lib/ui/notification/update/change_log_page.dart @@ -1,3 +1,5 @@ +import "dart:io"; + import 'package:flutter/material.dart'; import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; @@ -101,29 +103,30 @@ class _ChangeLogPageState extends State { final List items = []; items.addAll([ ChangeLogEntry( - context.l10n.cLTitle1, - context.l10n.cLDesc1, - ), - ChangeLogEntry( - context.l10n.cLTitle2, - context.l10n.cLDesc2, + context.l10n.cLTitle4, + context.l10n.cLDesc4, ), ChangeLogEntry( context.l10n.cLTitle3, context.l10n.cLDesc3, ), - ChangeLogEntry( - context.l10n.cLTitle4, - context.l10n.cLDesc4, - ), ChangeLogEntry( context.l10n.cLTitle5, context.l10n.cLDesc5, ), + if (!Platform.isAndroid) + ChangeLogEntry( + context.l10n.cLTitle2, + context.l10n.cLDesc2, + ), ChangeLogEntry( context.l10n.cLTitle6, context.l10n.cLDesc6, ), + ChangeLogEntry( + context.l10n.cLTitle1, + context.l10n.cLDesc1, + ), ]); return Container( From ddcfd2ff4320ba3eb33603347605d9376918485f Mon Sep 17 00:00:00 2001 From: ashilkn Date: Wed, 2 Jul 2025 09:15:38 +0530 Subject: [PATCH 32/36] Update launch.json in mobile docs to include cronetHttpNoPlay=true for android --- mobile/docs/vscode/launch.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/mobile/docs/vscode/launch.json b/mobile/docs/vscode/launch.json index 437d6ac30a..80d910083b 100644 --- a/mobile/docs/vscode/launch.json +++ b/mobile/docs/vscode/launch.json @@ -10,7 +10,12 @@ "type": "dart", "flutterMode": "debug", "program": "mobile/lib/main.dart", - "args": ["--flavor", "independent"] + "args": [ + "--flavor", + "independent", + "--dart-define", + "cronetHttpNoPlay=true" + ] }, { "name": "Photos Android Local", @@ -24,7 +29,9 @@ "--dart-define", "endpoint=http://localhost:8080", "--dart-define", - "web-family=http://localhost:3003" + "web-family=http://localhost:3003", + "--dart-define", + "cronetHttpNoPlay=true" ] }, { From ffb19c3a6507d3580766f11f315b1951434c1057 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Wed, 2 Jul 2025 09:17:03 +0530 Subject: [PATCH 33/36] Prune --- .../accounts/components/VerifyMasterPasswordForm.tsx | 10 +++++----- .../components/utils/second-factor-choice.ts | 9 +++++++++ web/packages/accounts/pages/credentials.tsx | 12 +++++++----- web/packages/new/photos/components/Export.tsx | 2 +- web/packages/new/photos/services/export.ts | 8 +++++++- web/packages/shared/error/index.ts | 11 ----------- 6 files changed, 29 insertions(+), 23 deletions(-) delete mode 100644 web/packages/shared/error/index.ts diff --git a/web/packages/accounts/components/VerifyMasterPasswordForm.tsx b/web/packages/accounts/components/VerifyMasterPasswordForm.tsx index 73d0b529ba..f5ed68553a 100644 --- a/web/packages/accounts/components/VerifyMasterPasswordForm.tsx +++ b/web/packages/accounts/components/VerifyMasterPasswordForm.tsx @@ -8,10 +8,10 @@ import { LoadingButton } from "ente-base/components/mui/LoadingButton"; import { ShowHidePasswordInputAdornment } from "ente-base/components/mui/PasswordInputAdornment"; import { sharedCryptoWorker } from "ente-base/crypto"; import log from "ente-base/log"; -import { CustomError } from "ente-shared/error"; import { useFormik } from "formik"; import { t } from "i18next"; import { useCallback, useState } from "react"; +import { twoFactorEnabledErrorMessage } from "./utils/second-factor-choice"; export interface VerifyMasterPasswordFormProps { /** @@ -30,9 +30,9 @@ export interface VerifyMasterPasswordFormProps { * used for reauthenticating the user after they've already logged in, then * this function will not be provided. * - * This function can throw an `CustomError.TWO_FACTOR_ENABLED` to signal to - * the form that some other form of second factor is enabled and the user - * has been redirected to a two factor verification page. + * @throws A Error with message {@link twoFactorEnabledErrorMessage} to + * signal to the form that some other form of second factor is enabled and + * the user has been redirected to a two factor verification page. * * @throws A Error with message * {@link srpVerificationUnauthorizedErrorMessage} to signal that either @@ -154,7 +154,7 @@ export const VerifyMasterPasswordForm: React.FC< } catch (e) { if (e instanceof Error) { switch (e.message) { - case CustomError.TWO_FACTOR_ENABLED: + case twoFactorEnabledErrorMessage: // Two factor enabled, user has been redirected to // the two-factor verification page. return; diff --git a/web/packages/accounts/components/utils/second-factor-choice.ts b/web/packages/accounts/components/utils/second-factor-choice.ts index 1557255ae3..e2e2ca19a3 100644 --- a/web/packages/accounts/components/utils/second-factor-choice.ts +++ b/web/packages/accounts/components/utils/second-factor-choice.ts @@ -8,6 +8,15 @@ import { useModalVisibility } from "ente-base/components/utils/modal"; import { useCallback, useMemo, useRef } from "react"; import type { SecondFactorType } from "../SecondFactorChoice"; +/** + * The message of the {@link Error} that is thrown when the user has enabled a + * second factor so further authentication is needed during the login sequence. + * + * TODO: This is not really an error but rather is a code flow flag; consider + * not using exceptions for flow control. + */ +export const twoFactorEnabledErrorMessage = "two factor enabled"; + /** * A convenience hook for keeping track of the state and logic that is needed * after password verification to determine which second factor (if any) we diff --git a/web/packages/accounts/pages/credentials.tsx b/web/packages/accounts/pages/credentials.tsx index c03b269ae0..975437b049 100644 --- a/web/packages/accounts/pages/credentials.tsx +++ b/web/packages/accounts/pages/credentials.tsx @@ -6,7 +6,10 @@ import { } from "ente-accounts/components/LoginComponents"; import { SecondFactorChoice } from "ente-accounts/components/SecondFactorChoice"; import { sessionExpiredDialogAttributes } from "ente-accounts/components/utils/dialog"; -import { useSecondFactorChoiceIfNeeded } from "ente-accounts/components/utils/second-factor-choice"; +import { + twoFactorEnabledErrorMessage, + useSecondFactorChoiceIfNeeded, +} from "ente-accounts/components/utils/second-factor-choice"; import { VerifyMasterPasswordForm, type VerifyMasterPasswordFormProps, @@ -47,7 +50,6 @@ import { unstashKeyEncryptionKeyFromSession, updateSessionFromElectronSafeStorageIfNeeded, } from "ente-base/session"; -import { CustomError } from "ente-shared/error"; import { getData, setData, setLSUser } from "ente-shared/storage/localStorage"; import { getToken, @@ -205,7 +207,7 @@ const Page: React.FC = () => { ); setPasskeyVerificationData({ passkeySessionID, url }); openPasskeyVerificationURL({ passkeySessionID, url }); - throw Error(CustomError.TWO_FACTOR_ENABLED); + throw new Error(twoFactorEnabledErrorMessage); } else if (twoFactorSessionID) { await stashKeyEncryptionKeyInSessionStore(kek); const user = getData("user"); @@ -215,7 +217,7 @@ const Page: React.FC = () => { isTwoFactorEnabled: true, }); void router.push("/two-factor/verify"); - throw Error(CustomError.TWO_FACTOR_ENABLED); + throw new Error(twoFactorEnabledErrorMessage); } else { const user = getData("user"); await setLSUser({ @@ -231,7 +233,7 @@ const Page: React.FC = () => { } catch (e) { if ( e instanceof Error && - e.message != CustomError.TWO_FACTOR_ENABLED + e.message != twoFactorEnabledErrorMessage ) { log.error("getKeyAttributes failed", e); } diff --git a/web/packages/new/photos/components/Export.tsx b/web/packages/new/photos/components/Export.tsx index d008f00289..2fa2f6d42f 100644 --- a/web/packages/new/photos/components/Export.tsx +++ b/web/packages/new/photos/components/Export.tsx @@ -39,7 +39,6 @@ import log from "ente-base/log"; import { type EnteFile } from "ente-media/file"; import { fileFileName } from "ente-media/file-metadata"; import { ItemCard, PreviewItemTile } from "ente-new/photos/components/Tiles"; -import { CustomError } from "ente-shared/error"; import { t } from "i18next"; import React, { memo, useCallback, useEffect, useState } from "react"; import { Trans } from "react-i18next"; @@ -50,6 +49,7 @@ import { type ListItemKeySelector, } from "react-window"; import exportService, { + CustomError, ExportStage, selectAndPrepareExportDirectory, type ExportOpts, diff --git a/web/packages/new/photos/services/export.ts b/web/packages/new/photos/services/export.ts index c8df22cedb..aef25fb6e3 100644 --- a/web/packages/new/photos/services/export.ts +++ b/web/packages/new/photos/services/export.ts @@ -31,13 +31,19 @@ import { safeDirectoryName, safeFileName, } from "ente-new/photos/utils/native-fs"; -import { CustomError } from "ente-shared/error"; import { getData, setData } from "ente-shared/storage/localStorage"; import { PromiseQueue } from "ente-utils/promise"; import i18n from "i18next"; import { migrateExport, type ExportRecord } from "./export-migration"; import { savedCollectionFiles, savedCollections } from "./photos-fdb"; +// TODO: Audit the uses of these constants +export const CustomError = { + UPDATE_EXPORTED_RECORD_FAILED: "update file exported record failed", + EXPORT_STOPPED: "export stopped", + EXPORT_FOLDER_DOES_NOT_EXIST: "export folder does not exist", +}; + /** Name of the JSON file in which we keep the state of the export. */ const exportRecordFileName = "export_status.json"; diff --git a/web/packages/shared/error/index.ts b/web/packages/shared/error/index.ts deleted file mode 100644 index f776ae6056..0000000000 --- a/web/packages/shared/error/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -export const CustomError = { - TOKEN_EXPIRED: "token expired", - TOO_MANY_REQUESTS: "too many requests", - BAD_REQUEST: "bad request", - SUBSCRIPTION_NEEDED: "subscription not present", - NOT_FOUND: "not found ", - UPDATE_EXPORTED_RECORD_FAILED: "update file exported record failed", - EXPORT_STOPPED: "export stopped", - EXPORT_FOLDER_DOES_NOT_EXIST: "export folder does not exist", - TWO_FACTOR_ENABLED: "two factor enabled", -}; From 162a2efe71fa85c692f5e002d027679029c72610 Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Wed, 2 Jul 2025 12:29:42 +0530 Subject: [PATCH 34/36] fix: move fav init to album service --- mobile/lib/main.dart | 1 - mobile/lib/services/album_home_widget_service.dart | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index bc3e007f98..a9294e45ff 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -169,7 +169,6 @@ Future _runMinimally(String taskId, TimeLogger tlog) async { LocalFileUpdateService.instance.init(prefs); await LocalSyncService.instance.init(prefs); RemoteSyncService.instance.init(prefs); - await FavoritesService.instance.initFav(); await SyncService.instance.init(prefs); // Misc Services diff --git a/mobile/lib/services/album_home_widget_service.dart b/mobile/lib/services/album_home_widget_service.dart index 3f05e6ddf1..3e32d93818 100644 --- a/mobile/lib/services/album_home_widget_service.dart +++ b/mobile/lib/services/album_home_widget_service.dart @@ -12,6 +12,7 @@ import 'package:photos/models/collection/collection.dart'; import 'package:photos/models/collection/collection_items.dart'; import 'package:photos/models/file/file.dart'; import 'package:photos/service_locator.dart'; +import "package:photos/services/app_lifecycle_service.dart"; import 'package:photos/services/collections_service.dart'; import 'package:photos/services/favorites_service.dart'; import 'package:photos/services/home_widget_service.dart'; @@ -294,9 +295,13 @@ class AlbumHomeWidgetService { // If no albums selected, use favorites as default if (selectedAlbumIds == null || selectedAlbumIds.isEmpty) { + if (!AppLifecycleService.instance.isForeground) { + await FavoritesService.instance.initFav(); + } final favoriteId = await FavoritesService.instance.getFavoriteCollectionID(); if (favoriteId != null) { + await updateSelectedAlbums([favoriteId.toString()]); return [favoriteId]; } } From 97bdcffd9d317bb42bcbf0120650fc0d7ebd48c3 Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Wed, 2 Jul 2025 13:12:33 +0530 Subject: [PATCH 35/36] fix: isBackground check --- mobile/lib/main.dart | 2 +- .../services/album_home_widget_service.dart | 19 +++++++++---------- mobile/lib/services/home_widget_service.dart | 4 ++-- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index a9294e45ff..404a60da91 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -123,7 +123,7 @@ Future _homeWidgetSync([bool isBackground = false]) async { } try { - await HomeWidgetService.instance.initHomeWidget(); + await HomeWidgetService.instance.initHomeWidget(isBackground); } catch (e, s) { _logger.severe("Error in syncing home widget", e, s); } diff --git a/mobile/lib/services/album_home_widget_service.dart b/mobile/lib/services/album_home_widget_service.dart index 3e32d93818..9513427e1f 100644 --- a/mobile/lib/services/album_home_widget_service.dart +++ b/mobile/lib/services/album_home_widget_service.dart @@ -12,7 +12,6 @@ import 'package:photos/models/collection/collection.dart'; import 'package:photos/models/collection/collection_items.dart'; import 'package:photos/models/file/file.dart'; import 'package:photos/service_locator.dart'; -import "package:photos/services/app_lifecycle_service.dart"; import 'package:photos/services/collections_service.dart'; import 'package:photos/services/favorites_service.dart'; import 'package:photos/services/home_widget_service.dart'; @@ -66,9 +65,9 @@ class AlbumHomeWidgetService { await _prefs.setString(ALBUMS_LAST_HASH_KEY, hash); } - Future initAlbumHomeWidget() async { + Future initAlbumHomeWidget(bool isBg) async { await HomeWidgetService.instance.computeLock.synchronized(() async { - if (await _hasAnyBlockers()) { + if (await _hasAnyBlockers(isBg)) { await clearWidget(); return; } @@ -134,13 +133,13 @@ class AlbumHomeWidgetService { _logger.info("Checking pending albums sync"); if (await _shouldUpdateWidgetCache()) { - await initAlbumHomeWidget(); + await initAlbumHomeWidget(false); } } Future _refreshOnSelection() async { final lastHash = getAlbumsLastHash(); - final selectedAlbumIds = await _getEffectiveSelectedAlbumIds(); + final selectedAlbumIds = await _getEffectiveSelectedAlbumIds(false); final currentHash = _calculateHash(selectedAlbumIds); if (lastHash != null && currentHash == lastHash) { _logger.info("No changes detected in albums"); @@ -148,7 +147,7 @@ class AlbumHomeWidgetService { } await setSelectionChange(true); - await initAlbumHomeWidget(); + await initAlbumHomeWidget(false); } List getAlbumsByIds(List albumIds) { @@ -233,7 +232,7 @@ class AlbumHomeWidgetService { return hash; } - Future _hasAnyBlockers() async { + Future _hasAnyBlockers([bool isBg = false]) async { // Check if first import is completed final hasCompletedFirstImport = LocalSyncService.instance.hasCompletedFirstImport(); @@ -242,7 +241,7 @@ class AlbumHomeWidgetService { } // Check if selected albums exist - final selectedAlbumIds = await _getEffectiveSelectedAlbumIds(); + final selectedAlbumIds = await _getEffectiveSelectedAlbumIds(isBg); final albums = getAlbumsByIds(selectedAlbumIds); if (albums.isEmpty) { @@ -290,12 +289,12 @@ class AlbumHomeWidgetService { return true; } - Future> _getEffectiveSelectedAlbumIds() async { + Future> _getEffectiveSelectedAlbumIds([bool isBg = false]) async { final selectedAlbumIds = getSelectedAlbumIds(); // If no albums selected, use favorites as default if (selectedAlbumIds == null || selectedAlbumIds.isEmpty) { - if (!AppLifecycleService.instance.isForeground) { + if (isBg) { await FavoritesService.instance.initFav(); } final favoriteId = diff --git a/mobile/lib/services/home_widget_service.dart b/mobile/lib/services/home_widget_service.dart index 45730d83e6..3c86fbe51b 100644 --- a/mobile/lib/services/home_widget_service.dart +++ b/mobile/lib/services/home_widget_service.dart @@ -71,8 +71,8 @@ class HomeWidgetService { hw.HomeWidget.setAppGroupId(id).ignore(); } - Future initHomeWidget() async { - await AlbumHomeWidgetService.instance.initAlbumHomeWidget(); + Future initHomeWidget([bool isBg = false]) async { + await AlbumHomeWidgetService.instance.initAlbumHomeWidget(isBg); await PeopleHomeWidgetService.instance.initPeopleHomeWidget(); await MemoryHomeWidgetService.instance.initMemoryHomeWidget(); } From 06a30078da5d87aa84daed26f3a1fdb6108ebae8 Mon Sep 17 00:00:00 2001 From: ashilkn Date: Wed, 2 Jul 2025 13:41:09 +0530 Subject: [PATCH 36/36] Decrease threshold of Pixels needed in image to categorize it as a 'too large image' and decrease the extent of compression from 16MP to 50MP of such images. Large images are rendered in lower resolution so that the app doesn't crash --- mobile/lib/ui/viewer/file/zoomable_image.dart | 38 +++++++------------ 1 file changed, 14 insertions(+), 24 deletions(-) diff --git a/mobile/lib/ui/viewer/file/zoomable_image.dart b/mobile/lib/ui/viewer/file/zoomable_image.dart index 45f4f2b016..f29315a2f2 100644 --- a/mobile/lib/ui/viewer/file/zoomable_image.dart +++ b/mobile/lib/ui/viewer/file/zoomable_image.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; import 'dart:typed_data' show Uint8List; import 'package:flutter/material.dart'; @@ -69,7 +70,7 @@ class _ZoomableImageState extends State { // This is to prevent the app from crashing when loading 200MP images // https://github.com/flutter/flutter/issues/110331 - bool get isTooLargeImage => _photo.width * _photo.height > 160000000; + bool get isTooLargeImage => _photo.width * _photo.height > 100000000; //100MP @override void initState() { @@ -385,23 +386,18 @@ class _ZoomableImageState extends State { ImageProvider imageProvider; if (isTooLargeImage) { _logger.info( - "Handling very large image (${_photo.width}x${_photo.height}) to prevent crash", + "Handling very large image (${_photo.width}x${_photo.height}) by decreasing resolution to 50MP to prevent crash", ); final aspectRatio = _photo.width / _photo.height; - int targetWidth, targetHeight; - if (aspectRatio > 1) { - targetWidth = 4096; - targetHeight = (targetWidth / aspectRatio).round(); - } else { - targetHeight = 4096; - targetWidth = (targetHeight * aspectRatio).round(); - } + const maxPixels = 50000000; + final targetHeight = sqrt(maxPixels / aspectRatio); + final targetWidth = aspectRatio * targetHeight; imageProvider = Image.file( file, gaplessPlayback: true, - cacheWidth: targetWidth, - cacheHeight: targetHeight, + cacheWidth: targetWidth.round(), + cacheHeight: targetHeight.round(), ).image; } else { imageProvider = Image.file( @@ -482,23 +478,17 @@ class _ZoomableImageState extends State { Uint8List? compressedFile; if (isTooLargeImage) { _logger.info( - "Compressing very large image (${_photo.width}x${_photo.height}) more aggressively", + "Compressing very large image (${_photo.width}x${_photo.height}) more aggressively down to 50MP", ); final aspectRatio = _photo.width / _photo.height; - int targetWidth, targetHeight; - - if (aspectRatio > 1) { - targetWidth = 4096; - targetHeight = (targetWidth / aspectRatio).round(); - } else { - targetHeight = 4096; - targetWidth = (targetHeight * aspectRatio).round(); - } + const maxPixels = 50000000; + final targetHeight = sqrt(maxPixels / aspectRatio); + final targetWidth = aspectRatio * targetHeight; compressedFile = await FlutterImageCompress.compressWithFile( file.path, - minWidth: targetWidth, - minHeight: targetHeight, + minWidth: targetWidth.round(), + minHeight: targetHeight.round(), quality: 85, ); } else {