From 37bc453de64db28abd057556ae57590874317595 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 23 Jun 2025 15:50:10 +0530 Subject: [PATCH 1/6] Parse res --- web/packages/gallery/services/upload/remote.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/web/packages/gallery/services/upload/remote.ts b/web/packages/gallery/services/upload/remote.ts index b402bdc43a..22c62569d2 100644 --- a/web/packages/gallery/services/upload/remote.ts +++ b/web/packages/gallery/services/upload/remote.ts @@ -7,7 +7,7 @@ import { type PublicAlbumsCredentials, } from "ente-base/http"; import { apiURL, uploaderOrigin } from "ente-base/origins"; -import { type RemoteEnteFile, type RemoteFileMetadata } from "ente-media/file"; +import { RemoteEnteFile, type RemoteFileMetadata } from "ente-media/file"; import type { RemoteMagicMetadata } from "ente-media/magic-metadata"; import { nullToUndefined } from "ente-utils/transform"; import { z } from "zod/v4"; @@ -480,8 +480,7 @@ export const postEnteFile = async ( body: JSON.stringify(postFileRequest), }); ensureOk(res); - // TODO(RE): - return (await res.json()) as RemoteEnteFile; + return RemoteEnteFile.parse(await res.json()); }; /** @@ -498,6 +497,5 @@ export const postPublicAlbumsEnteFile = async ( body: JSON.stringify(postFileRequest), }); ensureOk(res); - // TODO(RE): - return (await res.json()) as RemoteEnteFile; + return RemoteEnteFile.parse(await res.json()); }; From 498a60d7522b0c0395a3e617688c9467e8e37bd8 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 23 Jun 2025 16:20:46 +0530 Subject: [PATCH 2/6] Discr union --- .../photos/src/components/UploadProgress.tsx | 24 +++--- .../photos/src/services/upload-manager.ts | 72 +++++++---------- web/apps/photos/src/services/watch.ts | 79 ++++++++++--------- web/packages/gallery/services/upload/index.ts | 19 ++--- .../gallery/services/upload/upload-service.ts | 39 +++++---- 5 files changed, 110 insertions(+), 123 deletions(-) diff --git a/web/apps/photos/src/components/UploadProgress.tsx b/web/apps/photos/src/components/UploadProgress.tsx index 4af28ab8e0..9b762c5189 100644 --- a/web/apps/photos/src/components/UploadProgress.tsx +++ b/web/apps/photos/src/components/UploadProgress.tsx @@ -45,7 +45,7 @@ import { ListItemKeySelector, } from "react-window"; import type { - FinishedUploadResult, + FinishedUploadType, InProgressUpload, SegregatedFinishedUploads, UploadCounter, @@ -321,11 +321,11 @@ function UploadProgressDialog() { {uploadPhase == "uploading" && } @@ -335,12 +335,12 @@ function UploadProgressDialog() { )} } /> @@ -482,20 +482,20 @@ const NotUploadSectionHeader = styled("div")( ); interface ResultSectionProps { - uploadResult: FinishedUploadResult; + resultType: FinishedUploadType; sectionTitle: string; sectionInfo?: React.ReactNode; } const ResultSection: React.FC = ({ - uploadResult, + resultType, sectionTitle, sectionInfo, }) => { const { finishedUploads, uploadFileNames } = useContext( UploadProgressContext, ); - const fileList = finishedUploads.get(uploadResult); + const fileList = finishedUploads.get(resultType); if (!fileList?.length) { return <>; diff --git a/web/apps/photos/src/services/upload-manager.ts b/web/apps/photos/src/services/upload-manager.ts index 61f4e23a31..3ce335d739 100644 --- a/web/apps/photos/src/services/upload-manager.ts +++ b/web/apps/photos/src/services/upload-manager.ts @@ -29,11 +29,7 @@ import UploadService, { } from "ente-gallery/services/upload/upload-service"; import { processVideoNewUpload } from "ente-gallery/services/video"; import type { Collection } from "ente-media/collection"; -import { - decryptRemoteFile, - type EnteFile, - type RemoteEnteFile, -} from "ente-media/file"; +import { decryptRemoteFile, type EnteFile } from "ente-media/file"; import { fileCreationTime, type ParsedMetadata, @@ -67,22 +63,17 @@ export interface InProgressUpload { } /** - * A variant of {@link UploadResult} used when segregating finished uploads in - * the UI. "addedSymlink" is treated as "uploaded", everything else remains as - * it were. + * A variant of {@link UploadResult}'s {@link type} values used when segregating + * finished uploads in the UI. "addedSymlink" is treated as "uploaded", + * everything else remains as it were. */ -export type FinishedUploadResult = Exclude; - -export interface FinishedUpload { - localFileID: FileID; - result: FinishedUploadResult; -} +export type FinishedUploadType = Exclude; export type InProgressUploads = Map; -export type FinishedUploads = Map; +export type FinishedUploads = Map; -export type SegregatedFinishedUploads = Map; +export type SegregatedFinishedUploads = Map; export interface ProgressUpdater { setPercentComplete: React.Dispatch>; @@ -145,7 +136,7 @@ class UIService { this.setTotalFileCount(count); this.filesUploadedCount = 0; this.inProgressUploads = new Map(); - this.finishedUploads = new Map(); + this.finishedUploads = new Map(); this.updateProgressBarUI(); } @@ -189,8 +180,8 @@ class UIService { this.updateProgressBarUI(); } - moveFileToResultList(key: number, uploadResult: FinishedUploadResult) { - this.finishedUploads.set(key, uploadResult); + moveFileToResultList(key: number, type: FinishedUploadType) { + this.finishedUploads.set(key, type); this.inProgressUploads.delete(key); this.updateProgressBarUI(); } @@ -523,7 +514,7 @@ class UploadManager { uiService.setFileProgress(localID, 0); await wait(0); - const { uploadResult, uploadedFile } = await upload( + const uploadResult = await upload( uploadableItem, this.uploaderName, this.existingFiles, @@ -532,13 +523,12 @@ class UploadManager { uploadContext, ); - const finalUploadResult = await this.postUploadTask( + const finishedUploadType = await this.postUploadTask( uploadableItem, uploadResult, - uploadedFile, ); - uiService.moveFileToResultList(localID, finalUploadResult); + uiService.moveFileToResultList(localID, finishedUploadType); uiService.increaseFileUploaded(); UploadService.reducePendingUploadCount(); } @@ -547,29 +537,29 @@ class UploadManager { private async postUploadTask( uploadableItem: UploadableUploadItem, uploadResult: UploadResult, - uploadedFile: RemoteEnteFile | EnteFile | undefined, - ): Promise { - log.info(`Upload ${uploadableItem.fileName} | ${uploadResult}`); - const finishedUploadResult = - uploadResult == "addedSymlink" ? "uploaded" : uploadResult; + ): Promise { + const type = uploadResult.type; + log.info(`Upload ${uploadableItem.fileName} | ${type}`); + try { const processableUploadItem = await markUploadedAndObtainProcessableItem(uploadableItem); let decryptedFile: EnteFile; - switch (uploadResult) { + switch (uploadResult.type) { case "failed": case "blocked": + // Retriable. this.failedItems.push(uploadableItem); break; case "alreadyUploaded": case "addedSymlink": - decryptedFile = uploadedFile as EnteFile; + decryptedFile = uploadResult.file; break; case "uploaded": case "uploadedWithStaticThumbnail": decryptedFile = await decryptRemoteFile( - uploadedFile as RemoteEnteFile, + uploadResult.file, uploadableItem.collection.key, ); break; @@ -578,23 +568,21 @@ class UploadManager { case "tooLarge": // no-op break; - default: - throw new Error(`Invalid Upload Result ${uploadResult}`); } if ( [ "addedSymlink", "uploaded", "uploadedWithStaticThumbnail", - ].includes(uploadResult) + ].includes(type) ) { const uploadItem = uploadableItem.uploadItem ?? uploadableItem.livePhotoAssets.image; if ( uploadItem && - (uploadResult == "uploaded" || - uploadResult == "uploadedWithStaticThumbnail") + (type == "uploaded" || + type == "uploadedWithStaticThumbnail") ) { indexNewUpload(decryptedFile, processableUploadItem); processVideoNewUpload(decryptedFile, processableUploadItem); @@ -602,17 +590,11 @@ class UploadManager { this.updateExistingFiles(decryptedFile); } - if (isDesktop) { - if (watcher.isUploadRunning()) { - await watcher.onFileUpload( - uploadResult, - uploadableItem, - uploadedFile, - ); - } + if (isDesktop && watcher.isUploadRunning()) { + await watcher.onFileUpload(uploadableItem, uploadResult); } - return finishedUploadResult; + return type == "addedSymlink" ? "uploaded" : type; } catch (e) { log.error("Post file upload action failed", e); return "failed"; diff --git a/web/apps/photos/src/services/watch.ts b/web/apps/photos/src/services/watch.ts index cde7a69fad..6997596239 100644 --- a/web/apps/photos/src/services/watch.ts +++ b/web/apps/photos/src/services/watch.ts @@ -326,47 +326,54 @@ class FolderWatcher { * {@link upload} gets uploaded. */ async onFileUpload( - fileUploadResult: UploadResult, item: UploadItemWithCollection, - file: FolderWatchUploadedFile, + uploadResult: UploadResult, ) { // Re the usage of ensureString: For desktop watch, the only possibility // for a UploadItem is for it to be a string (the absolute path to a // file on disk). - if ( - [ - "addedSymlink", - "uploaded", - "uploadedWithStaticThumbnail", - "alreadyUploaded", - ].includes(fileUploadResult) - ) { - if (item.isLivePhoto) { - this.uploadedFileForPath.set( - ensureString(item.livePhotoAssets.image), - file, - ); - this.uploadedFileForPath.set( - ensureString(item.livePhotoAssets.video), - file, - ); - } else { - this.uploadedFileForPath.set( - ensureString(item.uploadItem), - file, - ); - } - } else if (["unsupported", "tooLarge"].includes(fileUploadResult)) { - if (item.isLivePhoto) { - this.unUploadableFilePaths.add( - ensureString(item.livePhotoAssets.image), - ); - this.unUploadableFilePaths.add( - ensureString(item.livePhotoAssets.video), - ); - } else { - this.unUploadableFilePaths.add(ensureString(item.uploadItem)); - } + switch (uploadResult.type) { + case "alreadyUploaded": + case "addedSymlink": + case "uploaded": + case "uploadedWithStaticThumbnail": + { + // Done. + if (item.isLivePhoto) { + this.uploadedFileForPath.set( + ensureString(item.livePhotoAssets.image), + uploadResult.file, + ); + this.uploadedFileForPath.set( + ensureString(item.livePhotoAssets.video), + uploadResult.file, + ); + } else { + this.uploadedFileForPath.set( + ensureString(item.uploadItem), + uploadResult.file, + ); + } + } + break; + case "unsupported": + case "tooLarge": + { + // Non-retriable error. + if (item.isLivePhoto) { + this.unUploadableFilePaths.add( + ensureString(item.livePhotoAssets.image), + ); + this.unUploadableFilePaths.add( + ensureString(item.livePhotoAssets.video), + ); + } else { + this.unUploadableFilePaths.add( + ensureString(item.uploadItem), + ); + } + } + break; } } diff --git a/web/packages/gallery/services/upload/index.ts b/web/packages/gallery/services/upload/index.ts index 7af420aff0..b100323ccf 100644 --- a/web/packages/gallery/services/upload/index.ts +++ b/web/packages/gallery/services/upload/index.ts @@ -4,6 +4,7 @@ import { customAPIOrigin } from "ente-base/origins"; import type { ZipItem } from "ente-base/types/ipc"; import { exportMetadataDirectoryName } from "ente-gallery/export-dirs"; import type { Collection } from "ente-media/collection"; +import type { EnteFile, RemoteEnteFile } from "ente-media/file"; import { nullToUndefined } from "ente-utils/transform"; import { z } from "zod/v4"; @@ -432,15 +433,15 @@ export type UploadPhase = | "done"; export type UploadResult = - | "failed" - | "alreadyUploaded" - | "unsupported" - | "blocked" - | "tooLarge" - | "largerThanAvailableStorage" - | "uploaded" - | "uploadedWithStaticThumbnail" - | "addedSymlink"; + | { type: "unsupported" } + | { type: "tooLarge" } + | { type: "largerThanAvailableStorage" } + | { type: "blocked" } + | { type: "failed" } + | { type: "alreadyUploaded"; file: EnteFile } + | { type: "addedSymlink"; file: EnteFile } + | { type: "uploadedWithStaticThumbnail"; file: RemoteEnteFile } + | { type: "uploaded"; file: RemoteEnteFile }; /** * Return true to disable the upload of files via Cloudflare Workers. diff --git a/web/packages/gallery/services/upload/upload-service.ts b/web/packages/gallery/services/upload/upload-service.ts index 04ba1a22b4..d462eaf07a 100644 --- a/web/packages/gallery/services/upload/upload-service.ts +++ b/web/packages/gallery/services/upload/upload-service.ts @@ -24,7 +24,7 @@ import { isFileTypeNotSupportedError, } from "ente-gallery/utils/detect-type"; import { readStream } from "ente-gallery/utils/native-stream"; -import type { EnteFile, RemoteEnteFile } from "ente-media/file"; +import type { EnteFile } from "ente-media/file"; import { fileFileName, metadataHash, @@ -42,8 +42,13 @@ import { import { addToCollection } from "ente-new/photos/services/collection"; import { mergeUint8Arrays } from "ente-utils/array"; import { ensureInteger, ensureNumber } from "ente-utils/ensure"; -import type { UploadableUploadItem, UploadItem, UploadPathPrefix } from "."; -import { type LivePhotoAssets, type UploadResult } from "."; +import type { + UploadableUploadItem, + UploadItem, + UploadPathPrefix, + UploadResult, +} from "."; +import { type LivePhotoAssets } from "."; import { tryParseEpochMicrosecondsFromFileName } from "./date"; import { matchJSONMetadata, type ParsedMetadataJSON } from "./metadata-json"; import { @@ -635,11 +640,6 @@ interface UploadContext { updateUploadProgress: (fileLocalID: number, percentage: number) => void; } -interface UploadResponse { - uploadResult: UploadResult; - uploadedFile?: RemoteEnteFile | EnteFile; -} - /** * Upload the given {@link UploadableUploadItem} * @@ -657,7 +657,7 @@ export const upload = async ( parsedMetadataJSONMap: Map, worker: CryptoWorker, uploadContext: UploadContext, -): Promise => { +): Promise => { const { abortIfCancelled } = uploadContext; log.info(`Upload ${fileName} | start`); @@ -685,7 +685,7 @@ export const upload = async ( } catch (e) { if (isFileTypeNotSupportedError(e)) { log.error(`Not uploading ${fileName}`, e); - return { uploadResult: "unsupported" }; + return { type: "unsupported" }; } throw e; } @@ -693,7 +693,7 @@ export const upload = async ( const { fileTypeInfo, fileSize, lastModifiedMs } = assetDetails; const maxFileSize = 10 * 1024 * 1024 * 1024; /* 10 GB */ - if (fileSize >= maxFileSize) return { uploadResult: "tooLarge" }; + if (fileSize >= maxFileSize) return { type: "tooLarge" }; abortIfCancelled(); @@ -717,16 +717,13 @@ export const upload = async ( (f) => f.collectionID == collection.id, ); if (matchInSameCollection) { - return { - uploadResult: "alreadyUploaded", - uploadedFile: matchInSameCollection, - }; + return { type: "alreadyUploaded", file: matchInSameCollection }; } else { // Any of the matching files can be used to add a symlink. const symlink = Object.assign({}, anyMatch); symlink.collectionID = collection.id; await addToCollection(collection, [symlink]); - return { uploadResult: "addedSymlink", uploadedFile: symlink }; + return { type: "addedSymlink", file: symlink }; } } @@ -778,10 +775,10 @@ export const upload = async ( ); return { - uploadResult: metadata.hasStaticThumbnail + type: metadata.hasStaticThumbnail ? "uploadedWithStaticThumbnail" : "uploaded", - uploadedFile, + file: uploadedFile, }; } catch (e) { if (isUploadCancelledError(e)) { @@ -799,11 +796,11 @@ export const upload = async ( /* file specific */ case eTagMissingErrorMessage: - return { uploadResult: "blocked" }; + return { type: "blocked" }; case fileTooLargeErrorMessage: - return { uploadResult: "largerThanAvailableStorage" }; + return { type: "largerThanAvailableStorage" }; default: - return { uploadResult: "failed" }; + return { type: "failed" }; } } }; From 1559ae7f422757ef27a62949c7aad6d9fb3ca33e Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 23 Jun 2025 16:27:54 +0530 Subject: [PATCH 3/6] Simplify --- web/apps/photos/src/services/upload-manager.ts | 7 ++----- web/apps/photos/src/services/watch.ts | 10 +++------- web/packages/gallery/services/upload/index.ts | 6 +++--- web/packages/gallery/services/upload/upload-service.ts | 4 ++-- 4 files changed, 10 insertions(+), 17 deletions(-) diff --git a/web/apps/photos/src/services/upload-manager.ts b/web/apps/photos/src/services/upload-manager.ts index 3ce335d739..629ce4db1b 100644 --- a/web/apps/photos/src/services/upload-manager.ts +++ b/web/apps/photos/src/services/upload-manager.ts @@ -29,7 +29,7 @@ import UploadService, { } from "ente-gallery/services/upload/upload-service"; import { processVideoNewUpload } from "ente-gallery/services/video"; import type { Collection } from "ente-media/collection"; -import { decryptRemoteFile, type EnteFile } from "ente-media/file"; +import { type EnteFile } from "ente-media/file"; import { fileCreationTime, type ParsedMetadata, @@ -558,10 +558,7 @@ class UploadManager { break; case "uploaded": case "uploadedWithStaticThumbnail": - decryptedFile = await decryptRemoteFile( - uploadResult.file, - uploadableItem.collection.key, - ); + decryptedFile = uploadResult.file; break; case "largerThanAvailableStorage": case "unsupported": diff --git a/web/apps/photos/src/services/watch.ts b/web/apps/photos/src/services/watch.ts index 6997596239..e475302644 100644 --- a/web/apps/photos/src/services/watch.ts +++ b/web/apps/photos/src/services/watch.ts @@ -14,6 +14,7 @@ import type { } from "ente-base/types/ipc"; import { type UploadResult } from "ente-gallery/services/upload"; import type { UploadAsset } from "ente-gallery/services/upload/upload-service"; +import type { EnteFile } from "ente-media/file"; import { getLocalFiles, groupFilesByCollectionID, @@ -22,11 +23,6 @@ import { ensureString } from "ente-utils/ensure"; import { removeFromCollection } from "./collectionService"; import { type UploadItemWithCollection, uploadManager } from "./upload-manager"; -interface FolderWatchUploadedFile { - id: number; - collectionID: number; -} - /** * Watch for file system folders and automatically update the corresponding Ente * collections. @@ -53,7 +49,7 @@ class FolderWatcher { * A map from file paths to the (fileID, collectionID) of the file that was * uploaded (or symlinked) as part of the most recent upload attempt. */ - private uploadedFileForPath = new Map(); + private uploadedFileForPath = new Map(); /** * A set of file paths that could not be uploaded in the most recent upload * attempt. These are the uploads that failed due to a permanent error that @@ -415,7 +411,7 @@ class FolderWatcher { const syncedFiles: FolderWatch["syncedFiles"] = []; const ignoredFiles: FolderWatch["ignoredFiles"] = []; - const markSynced = (file: FolderWatchUploadedFile, path: string) => { + const markSynced = (file: EnteFile, path: string) => { syncedFiles.push({ path, uploadedFileID: file.id, diff --git a/web/packages/gallery/services/upload/index.ts b/web/packages/gallery/services/upload/index.ts index b100323ccf..7020f0e816 100644 --- a/web/packages/gallery/services/upload/index.ts +++ b/web/packages/gallery/services/upload/index.ts @@ -4,7 +4,7 @@ import { customAPIOrigin } from "ente-base/origins"; import type { ZipItem } from "ente-base/types/ipc"; import { exportMetadataDirectoryName } from "ente-gallery/export-dirs"; import type { Collection } from "ente-media/collection"; -import type { EnteFile, RemoteEnteFile } from "ente-media/file"; +import type { EnteFile } from "ente-media/file"; import { nullToUndefined } from "ente-utils/transform"; import { z } from "zod/v4"; @@ -440,8 +440,8 @@ export type UploadResult = | { type: "failed" } | { type: "alreadyUploaded"; file: EnteFile } | { type: "addedSymlink"; file: EnteFile } - | { type: "uploadedWithStaticThumbnail"; file: RemoteEnteFile } - | { type: "uploaded"; file: RemoteEnteFile }; + | { type: "uploadedWithStaticThumbnail"; file: EnteFile } + | { type: "uploaded"; file: EnteFile }; /** * Return true to disable the upload of files via Cloudflare Workers. diff --git a/web/packages/gallery/services/upload/upload-service.ts b/web/packages/gallery/services/upload/upload-service.ts index d462eaf07a..af4bf38c88 100644 --- a/web/packages/gallery/services/upload/upload-service.ts +++ b/web/packages/gallery/services/upload/upload-service.ts @@ -24,7 +24,7 @@ import { isFileTypeNotSupportedError, } from "ente-gallery/utils/detect-type"; import { readStream } from "ente-gallery/utils/native-stream"; -import type { EnteFile } from "ente-media/file"; +import { decryptRemoteFile, type EnteFile } from "ente-media/file"; import { fileFileName, metadataHash, @@ -778,7 +778,7 @@ export const upload = async ( type: metadata.hasStaticThumbnail ? "uploadedWithStaticThumbnail" : "uploaded", - file: uploadedFile, + file: await decryptRemoteFile(uploadedFile, collection.key), }; } catch (e) { if (isUploadCancelledError(e)) { From 17648c582a6272ed974a6ccc6730d375ae8f390d Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 23 Jun 2025 16:35:37 +0530 Subject: [PATCH 4/6] Simplify 2 --- .../photos/src/services/upload-manager.ts | 52 ++++++------------- web/packages/gallery/services/upload/index.ts | 4 +- 2 files changed, 17 insertions(+), 39 deletions(-) diff --git a/web/apps/photos/src/services/upload-manager.ts b/web/apps/photos/src/services/upload-manager.ts index 629ce4db1b..46564076a9 100644 --- a/web/apps/photos/src/services/upload-manager.ts +++ b/web/apps/photos/src/services/upload-manager.ts @@ -540,51 +540,32 @@ class UploadManager { ): Promise { const type = uploadResult.type; log.info(`Upload ${uploadableItem.fileName} | ${type}`); - try { const processableUploadItem = await markUploadedAndObtainProcessableItem(uploadableItem); - let decryptedFile: EnteFile; switch (uploadResult.type) { case "failed": case "blocked": - // Retriable. + // Retriable error. this.failedItems.push(uploadableItem); break; - case "alreadyUploaded": + case "addedSymlink": - decryptedFile = uploadResult.file; + this.updateExistingFiles(uploadResult.file); break; + case "uploaded": case "uploadedWithStaticThumbnail": - decryptedFile = uploadResult.file; + { + const { file } = uploadResult; + + indexNewUpload(file, processableUploadItem); + processVideoNewUpload(file, processableUploadItem); + + this.updateExistingFiles(file); + } break; - case "largerThanAvailableStorage": - case "unsupported": - case "tooLarge": - // no-op - break; - } - if ( - [ - "addedSymlink", - "uploaded", - "uploadedWithStaticThumbnail", - ].includes(type) - ) { - const uploadItem = - uploadableItem.uploadItem ?? - uploadableItem.livePhotoAssets.image; - if ( - uploadItem && - (type == "uploaded" || - type == "uploadedWithStaticThumbnail") - ) { - indexNewUpload(decryptedFile, processableUploadItem); - processVideoNewUpload(decryptedFile, processableUploadItem); - } - this.updateExistingFiles(decryptedFile); } if (isDesktop && watcher.isUploadRunning()) { @@ -620,12 +601,9 @@ class UploadManager { return this.uploaderName; } - private updateExistingFiles(decryptedFile: EnteFile) { - if (!decryptedFile) { - throw Error("decrypted file can't be undefined"); - } - this.existingFiles.push(decryptedFile); - this.onUploadFile(decryptedFile); + private updateExistingFiles(file: EnteFile) { + this.existingFiles.push(file); + this.onUploadFile(file); } /** diff --git a/web/packages/gallery/services/upload/index.ts b/web/packages/gallery/services/upload/index.ts index 7020f0e816..da3d7891d2 100644 --- a/web/packages/gallery/services/upload/index.ts +++ b/web/packages/gallery/services/upload/index.ts @@ -279,8 +279,8 @@ export type UploadableUploadItem = ClusteredUploadItem & { }; /** - * Result of {@link markUploadedAndObtainPostProcessableItem}; see the - * documentation of that function for the meaning and cases of this type. + * Result of {@link markUploadedAndObtainProcessableItem}; see the documentation + * of that function for the meaning and cases of this type. */ export type ProcessableUploadItem = File | TimestampedFileSystemUploadItem; From 4b82516909efad5b9019e82ecb0623bc1d301b2f Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 23 Jun 2025 16:54:42 +0530 Subject: [PATCH 5/6] Move --- web/apps/photos/src/components/FileList.tsx | 2 +- web/apps/photos/src/pages/gallery.tsx | 2 +- web/apps/photos/src/pages/shared-albums.tsx | 2 +- .../photos/src/services/collectionService.ts | 7 +- .../src/services/publicCollectionService.ts | 2 +- web/apps/photos/src/services/watch.ts | 6 +- web/apps/photos/tests/upload.test.ts | 2 +- web/packages/gallery/services/video.ts | 6 +- web/packages/gallery/utils/files.ts | 75 +++++++++ .../new/photos/components/gallery/reducer.ts | 28 +++- .../new/photos/services/collections.ts | 2 - web/packages/new/photos/services/files.ts | 158 +----------------- .../new/photos/services/search/index.ts | 2 +- web/packages/new/photos/services/trash.ts | 68 ++++++++ 14 files changed, 177 insertions(+), 185 deletions(-) create mode 100644 web/packages/gallery/utils/files.ts diff --git a/web/apps/photos/src/components/FileList.tsx b/web/apps/photos/src/components/FileList.tsx index d5f460fa98..b7ad2c63ca 100644 --- a/web/apps/photos/src/components/FileList.tsx +++ b/web/apps/photos/src/components/FileList.tsx @@ -26,7 +26,7 @@ import { } from "ente-new/photos/components/PlaceholderThumbnails"; import { TileBottomTextOverlay } from "ente-new/photos/components/Tiles"; import { PseudoCollectionID } from "ente-new/photos/services/collection-summary"; -import { enteFileDeletionDate } from "ente-new/photos/services/files"; +import { enteFileDeletionDate } from "ente-new/photos/services/trash"; import { t } from "i18next"; import memoize from "memoize-one"; import { GalleryContext } from "pages/gallery"; diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index 6ac6e94c99..4332299a89 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -75,7 +75,6 @@ import { } from "ente-new/photos/services/collection-summary"; import exportService from "ente-new/photos/services/export"; import { updateFilesVisibility } from "ente-new/photos/services/file"; -import { getLocalTrashedFiles } from "ente-new/photos/services/files"; import { savedCollections, savedHiddenFiles, @@ -92,6 +91,7 @@ import { preCollectionAndFilesSync, syncCollectionAndFiles, } from "ente-new/photos/services/sync"; +import { getLocalTrashedFiles } from "ente-new/photos/services/trash"; import { initUserDetailsOrTriggerSync, redirectToCustomerPortal, diff --git a/web/apps/photos/src/pages/shared-albums.tsx b/web/apps/photos/src/pages/shared-albums.tsx index 40e0dbe9bf..12a2b2d3e2 100644 --- a/web/apps/photos/src/pages/shared-albums.tsx +++ b/web/apps/photos/src/pages/shared-albums.tsx @@ -47,6 +47,7 @@ import { FullScreenDropZone } from "ente-gallery/components/FullScreenDropZone"; import { downloadManager } from "ente-gallery/services/download"; import { extractCollectionKeyFromShareURL } from "ente-gallery/services/share"; import { updateShouldDisableCFUploadProxy } from "ente-gallery/services/upload"; +import { sortFiles } from "ente-gallery/utils/files"; import type { Collection } from "ente-media/collection"; import { type EnteFile } from "ente-media/file"; import { verifyPublicAlbumPassword } from "ente-new/albums/services/publicCollection"; @@ -56,7 +57,6 @@ import { } from "ente-new/photos/components/gallery/ListHeader"; import { isHiddenCollection } from "ente-new/photos/services/collection"; import { PseudoCollectionID } from "ente-new/photos/services/collection-summary"; -import { sortFiles } from "ente-new/photos/services/files"; import { usePhotosAppContext } from "ente-new/photos/types/context"; import { CustomError, parseSharingErrorCodes } from "ente-shared/error"; import { t } from "i18next"; diff --git a/web/apps/photos/src/services/collectionService.ts b/web/apps/photos/src/services/collectionService.ts index 2bccb2a56b..95b58ca57d 100644 --- a/web/apps/photos/src/services/collectionService.ts +++ b/web/apps/photos/src/services/collectionService.ts @@ -2,6 +2,7 @@ import type { User } from "ente-accounts/services/user"; import { ensureLocalUser } from "ente-accounts/services/user"; import log from "ente-base/log"; import { apiURL } from "ente-base/origins"; +import { groupFilesByCollectionID, sortFiles } from "ente-gallery/utils/files"; import { Collection } from "ente-media/collection"; import { EnteFile } from "ente-media/file"; import { @@ -18,11 +19,7 @@ import { CollectionsSortBy, } from "ente-new/photos/services/collection-summary"; import { getLocalCollections } from "ente-new/photos/services/collections"; -import { - getLocalFiles, - groupFilesByCollectionID, - sortFiles, -} from "ente-new/photos/services/files"; +import { getLocalFiles } from "ente-new/photos/services/files"; import HTTPService from "ente-shared/network/HTTPService"; import { getData } from "ente-shared/storage/localStorage"; import { getToken } from "ente-shared/storage/localStorage/helpers"; diff --git a/web/apps/photos/src/services/publicCollectionService.ts b/web/apps/photos/src/services/publicCollectionService.ts index e30de3b4b0..075419e947 100644 --- a/web/apps/photos/src/services/publicCollectionService.ts +++ b/web/apps/photos/src/services/publicCollectionService.ts @@ -3,6 +3,7 @@ import log from "ente-base/log"; import { apiURL } from "ente-base/origins"; import { transformFilesIfNeeded } from "ente-gallery/services/files-db"; import { type MagicMetadataCore } from "ente-gallery/services/magic-metadata"; +import { sortFiles } from "ente-gallery/utils/files"; import type { Collection, CollectionPublicMagicMetadataData, @@ -10,7 +11,6 @@ import type { import type { EnteFile, RemoteEnteFile } from "ente-media/file"; import { decryptRemoteFile } from "ente-media/file"; import { savedPublicCollections } from "ente-new/albums/services/public-albums-fdb"; -import { sortFiles } from "ente-new/photos/services/files"; import { CustomError, parseSharingErrorCodes } from "ente-shared/error"; import HTTPService from "ente-shared/network/HTTPService"; import localForage from "ente-shared/storage/localForage"; diff --git a/web/apps/photos/src/services/watch.ts b/web/apps/photos/src/services/watch.ts index e475302644..b02155ffd1 100644 --- a/web/apps/photos/src/services/watch.ts +++ b/web/apps/photos/src/services/watch.ts @@ -14,11 +14,9 @@ import type { } from "ente-base/types/ipc"; import { type UploadResult } from "ente-gallery/services/upload"; import type { UploadAsset } from "ente-gallery/services/upload/upload-service"; +import { groupFilesByCollectionID } from "ente-gallery/utils/files"; import type { EnteFile } from "ente-media/file"; -import { - getLocalFiles, - groupFilesByCollectionID, -} from "ente-new/photos/services/files"; +import { getLocalFiles } from "ente-new/photos/services/files"; import { ensureString } from "ente-utils/ensure"; import { removeFromCollection } from "./collectionService"; import { type UploadItemWithCollection, uploadManager } from "./upload-manager"; diff --git a/web/apps/photos/tests/upload.test.ts b/web/apps/photos/tests/upload.test.ts index 327add692d..17eacfd919 100644 --- a/web/apps/photos/tests/upload.test.ts +++ b/web/apps/photos/tests/upload.test.ts @@ -7,6 +7,7 @@ import { matchJSONMetadata, metadataJSONMapKeyForJSON, } from "ente-gallery/services/upload/metadata-json"; +import { groupFilesByCollectionID } from "ente-gallery/utils/files"; import { fileCreationTime, fileFileName, @@ -14,7 +15,6 @@ import { } from "ente-media/file-metadata"; import { FileType } from "ente-media/file-type"; import { getLocalCollections } from "ente-new/photos/services/collections"; -import { groupFilesByCollectionID } from "ente-new/photos/services/files"; import { savedNormalFiles } from "ente-new/photos/services/photos-fdb"; import { getUserDetailsV2 } from "ente-new/photos/services/user-details"; diff --git a/web/packages/gallery/services/video.ts b/web/packages/gallery/services/video.ts index b352c5ba20..aff911a726 100644 --- a/web/packages/gallery/services/video.ts +++ b/web/packages/gallery/services/video.ts @@ -9,13 +9,11 @@ import { getKV, getKVB, getKVN, setKV } from "ente-base/kv"; import log from "ente-base/log"; import { apiURL } from "ente-base/origins"; import { ensureAuthToken } from "ente-base/token"; +import { uniqueFilesByID } from "ente-gallery/utils/files"; import { fileLogID, type EnteFile } from "ente-media/file"; import { FileType } from "ente-media/file-type"; import { updateFilePublicMagicMetadata } from "ente-new/photos/services/file"; -import { - getLocalTrashFileIDs, - uniqueFilesByID, -} from "ente-new/photos/services/files"; +import { getLocalTrashFileIDs } from "ente-new/photos/services/files"; import { savedFiles } from "ente-new/photos/services/photos-fdb"; import { gunzip, gzip } from "ente-new/photos/utils/gzip"; import { randomSample } from "ente-utils/array"; diff --git a/web/packages/gallery/utils/files.ts b/web/packages/gallery/utils/files.ts new file mode 100644 index 0000000000..ec3807c265 --- /dev/null +++ b/web/packages/gallery/utils/files.ts @@ -0,0 +1,75 @@ +import type { EnteFile } from "ente-media/file"; +import { fileCreationTime } from "ente-media/file-metadata"; + +/** + * Sort the given list of {@link EnteFile}s in place. + * + * Like the JavaScript Array#sort, this method modifies the {@link files} + * argument. It sorts {@link files} in place, and then returns a reference to + * the same mutated array. + * + * By default, files are sorted so that the newest one is first. The optional + * {@link sortAsc} flag can be set to `true` to sort them so that the oldest one + * is first. + */ +export const sortFiles = (files: EnteFile[], sortAsc = false) => { + // Sort based on the time of creation time of the file. + // + // For files with same creation time, sort based on the time of last + // modification. + const factor = sortAsc ? -1 : 1; + return files.sort((a, b) => { + const at = fileCreationTime(a); + const bt = fileCreationTime(b); + return at == bt + ? factor * + (b.metadata.modificationTime - a.metadata.modificationTime) + : factor * (bt - at); + }); +}; + +/** + * [Note: Collection file] + * + * File IDs themselves are unique across all the files for the user (in fact, + * they're unique across all the files in an Ente instance). + * + * However, we can have multiple entries for the same file ID in our local + * database and/or remote responses because the unit of account is not file, but + * a "Collection File" – a collection and file pair. + * + * For example, if the same file is symlinked into two collections, then we will + * have two "Collection File" entries for it, both with the same file ID, but + * with different collection IDs. + * + * This function returns files such that only one of these entries is returned. + * The entry that is returned is arbitrary in general, this function just picks + * the first one for each unique file ID. + * + * If this function is invoked on a list on which {@link sortFiles} has already + * been called, which by default sorts such that the newest file is first, then + * this function's behaviour would be to return the newest file from among + * multiple files with the same ID but different collections. + */ +export const uniqueFilesByID = (files: EnteFile[]) => { + const seen = new Set(); + return files.filter(({ id }) => { + if (seen.has(id)) return false; + seen.add(id); + return true; + }); +}; + +/** + * Segment the given {@link files} into lists indexed by their collection ID. + * + * Order is preserved. + */ +export const groupFilesByCollectionID = (files: EnteFile[]) => + files.reduce((result, file) => { + const id = file.collectionID; + let cfs = result.get(id); + if (!cfs) result.set(id, (cfs = [])); + cfs.push(file); + return result; + }, new Map()); diff --git a/web/packages/new/photos/components/gallery/reducer.ts b/web/packages/new/photos/components/gallery/reducer.ts index 53e74e7d70..ca75a66dd6 100644 --- a/web/packages/new/photos/components/gallery/reducer.ts +++ b/web/packages/new/photos/components/gallery/reducer.ts @@ -3,6 +3,11 @@ import { isArchivedCollection, isPinnedCollection, } from "ente-gallery/services/magic-metadata"; +import { + groupFilesByCollectionID, + sortFiles, + uniqueFilesByID, +} from "ente-gallery/utils/files"; import { collectionTypes, type Collection } from "ente-media/collection"; import type { EnteFile } from "ente-media/file"; import { @@ -14,6 +19,8 @@ import { createCollectionNameByID, isHiddenCollection, } from "ente-new/photos/services/collection"; +import { getLatestVersionFiles } from "ente-new/photos/services/files"; +import { type TrashedEnteFile } from "ente-new/photos/services/trash"; import { splitByPredicate } from "ente-utils/array"; import { includes } from "ente-utils/type-guards"; import { t } from "i18next"; @@ -28,14 +35,6 @@ import { type CollectionSummary, type CollectionSummaryType, } from "../../services/collection-summary"; -import { - createFileCollectionIDs, - getLatestVersionFiles, - groupFilesByCollectionID, - sortFiles, - uniqueFilesByID, - type TrashedEnteFile, -} from "../../services/files"; import type { PeopleState, Person } from "../../services/ml/people"; import type { SearchSuggestion } from "../../services/search/types"; import type { FamilyData } from "../../services/user-details"; @@ -1251,6 +1250,19 @@ const deriveFavoriteFileIDs = ( return favoriteFileIDs; }; +/** + * Construct a map from file IDs to the list of collections (IDs) to which the + * file belongs. + */ +const createFileCollectionIDs = (files: EnteFile[]) => + files.reduce((result, file) => { + const id = file.id; + let fs = result.get(id); + if (!fs) result.set(id, (fs = [])); + fs.push(file.collectionID); + return result; + }, new Map()); + /** * Compute normal (non-hidden) collection summaries from their dependencies. */ diff --git a/web/packages/new/photos/services/collections.ts b/web/packages/new/photos/services/collections.ts index 908fe9effd..38e7c63798 100644 --- a/web/packages/new/photos/services/collections.ts +++ b/web/packages/new/photos/services/collections.ts @@ -11,8 +11,6 @@ import { getLocalTrash, getTrashedFiles, TRASH, -} from "ente-new/photos/services/files"; -import { type EncryptedTrashItem, type Trash, } from "ente-new/photos/services/trash"; diff --git a/web/packages/new/photos/services/files.ts b/web/packages/new/photos/services/files.ts index f3bcaacf44..7a9133fe95 100644 --- a/web/packages/new/photos/services/files.ts +++ b/web/packages/new/photos/services/files.ts @@ -1,5 +1,4 @@ import { blobCache } from "ente-base/blob-cache"; -import { dateFromEpochMicroseconds } from "ente-base/date"; import log from "ente-base/log"; import { apiURL } from "ente-base/origins"; import type { Collection } from "ente-media/collection"; @@ -8,10 +7,8 @@ import { type EnteFile, type RemoteEnteFile, } from "ente-media/file"; -import { fileCreationTime, metadataHash } from "ente-media/file-metadata"; -import { type Trash } from "ente-new/photos/services/trash"; +import { metadataHash } from "ente-media/file-metadata"; import HTTPService from "ente-shared/network/HTTPService"; -import localForage from "ente-shared/storage/localForage"; import { getToken } from "ente-shared/storage/localStorage/helpers"; import { getCollectionLastSyncTime, @@ -151,130 +148,6 @@ const removeDeletedCollectionFiles = ( return files; }; -/** - * Sort the given list of {@link EnteFile}s in place. - * - * Like the JavaScript Array#sort, this method modifies the {@link files} - * argument. It sorts {@link files} in place, and then returns a reference to - * the same mutated array. - * - * By default, files are sorted so that the newest one is first. The optional - * {@link sortAsc} flag can be set to `true` to sort them so that the oldest one - * is first. - */ -export const sortFiles = (files: EnteFile[], sortAsc = false) => { - // Sort based on the time of creation time of the file. - // - // For files with same creation time, sort based on the time of last - // modification. - const factor = sortAsc ? -1 : 1; - return files.sort((a, b) => { - const at = fileCreationTime(a); - const bt = fileCreationTime(b); - return at == bt - ? factor * - (b.metadata.modificationTime - a.metadata.modificationTime) - : factor * (bt - at); - }); -}; - -/** - * [Note: Collection file] - * - * File IDs themselves are unique across all the files for the user (in fact, - * they're unique across all the files in an Ente instance). - * - * However, we can have multiple entries for the same file ID in our local - * database and/or remote responses because the unit of account is not file, but - * a "Collection File" – a collection and file pair. - * - * For example, if the same file is symlinked into two collections, then we will - * have two "Collection File" entries for it, both with the same file ID, but - * with different collection IDs. - * - * This function returns files such that only one of these entries is returned. - * The entry that is returned is arbitrary in general, this function just picks - * the first one for each unique file ID. - * - * If this function is invoked on a list on which {@link sortFiles} has already - * been called, which by default sorts such that the newest file is first, then - * this function's behaviour would be to return the newest file from among - * multiple files with the same ID but different collections. - */ -export const uniqueFilesByID = (files: EnteFile[]) => { - const seen = new Set(); - return files.filter(({ id }) => { - if (seen.has(id)) return false; - seen.add(id); - return true; - }); -}; - -export const TRASH = "file-trash"; - -export async function getLocalTrash() { - const trash = (await localForage.getItem(TRASH)) ?? []; - return trash; -} - -export async function getLocalTrashedFiles() { - return getTrashedFiles(await getLocalTrash()); -} - -export type TrashedEnteFile = EnteFile & { - /** - * `true` if this file is in trash (i.e. it has been deleted by the user, - * and will be permanently deleted after 30 days of being moved to trash). - */ - isTrashed?: boolean; - /** - * If {@link isTrashed} is `true`, then {@link deleteBy} contains the epoch - * microseconds when this file will be permanently deleted. - */ - deleteBy?: number; -}; - -/** - * Return the date when the file will be deleted permanently. Only valid for - * files that are in the user's trash. - * - * This is a convenience wrapper over the {@link deleteBy} property of a file, - * converting that epoch microsecond value into a JavaScript date. - */ -export const enteFileDeletionDate = (file: TrashedEnteFile) => - dateFromEpochMicroseconds(file.deleteBy); - -export function getTrashedFiles(trash: Trash): TrashedEnteFile[] { - return sortTrashFiles( - trash.map((trashedFile) => ({ - ...trashedFile.file, - updationTime: trashedFile.updatedAt, - deleteBy: trashedFile.deleteBy, - isTrashed: true, - })), - ); -} - -/** - * Return the IDs of all the files that are part of the trash as per our local - * database. - */ -export const getLocalTrashFileIDs = () => - getLocalTrash().then((trash) => new Set(trash.map((f) => f.file.id))); - -const sortTrashFiles = (files: TrashedEnteFile[]) => { - return files.sort((a, b) => { - if (a.deleteBy === b.deleteBy) { - const at = fileCreationTime(a); - const bt = fileCreationTime(b); - return at == bt - ? b.metadata.modificationTime - a.metadata.modificationTime - : bt - at; - } - return (a.deleteBy ?? 0) - (b.deleteBy ?? 0); - }); -}; - /** * Clear cached thumbnails for existing files if the thumbnail data has changed. * @@ -295,7 +168,7 @@ const sortTrashFiles = (files: TrashedEnteFile[]) => { * * @param newFiles The {@link EnteFile}s which we got in the diff response. */ -export const clearCachedThumbnailsIfChanged = async ( +const clearCachedThumbnailsIfChanged = async ( existingFiles: EnteFile[], newFiles: EnteFile[], ) => { @@ -328,33 +201,6 @@ export const clearCachedThumbnailsIfChanged = async ( } }; -/** - * Segment the given {@link files} into lists indexed by their collection ID. - * - * Order is preserved. - */ -export const groupFilesByCollectionID = (files: EnteFile[]) => - files.reduce((result, file) => { - const id = file.collectionID; - let cfs = result.get(id); - if (!cfs) result.set(id, (cfs = [])); - cfs.push(file); - return result; - }, new Map()); - -/** - * Construct a map from file IDs to the list of collections (IDs) to which the - * file belongs. - */ -export const createFileCollectionIDs = (files: EnteFile[]) => - files.reduce((result, file) => { - const id = file.id; - let fs = result.get(id); - if (!fs) result.set(id, (fs = [])); - fs.push(file.collectionID); - return result; - }, new Map()); - export function getLatestVersionFiles(files: EnteFile[]) { const latestVersionFiles = new Map(); files.forEach((file) => { diff --git a/web/packages/new/photos/services/search/index.ts b/web/packages/new/photos/services/search/index.ts index 4e30266995..d06447638b 100644 --- a/web/packages/new/photos/services/search/index.ts +++ b/web/packages/new/photos/services/search/index.ts @@ -1,9 +1,9 @@ import log from "ente-base/log"; import { ensureMasterKeyFromSession } from "ente-base/session"; import { ComlinkWorker } from "ente-base/worker/comlink-worker"; +import { uniqueFilesByID } from "ente-gallery/utils/files"; import { FileType } from "ente-media/file-type"; import i18n, { t } from "i18next"; -import { uniqueFilesByID } from "../files"; import { clipMatches, isMLEnabled, isMLSupported } from "../ml"; import type { NamedPerson } from "../ml/people"; import type { diff --git a/web/packages/new/photos/services/trash.ts b/web/packages/new/photos/services/trash.ts index 7ddb27ed03..8770c1dbd4 100644 --- a/web/packages/new/photos/services/trash.ts +++ b/web/packages/new/photos/services/trash.ts @@ -1,4 +1,7 @@ +import { dateFromEpochMicroseconds } from "ente-base/date"; import type { EnteFile, RemoteEnteFile } from "ente-media/file"; +import { fileCreationTime } from "ente-media/file-metadata"; +import localForage from "ente-shared/storage/localForage"; export interface TrashItem extends Omit { file: EnteFile; @@ -29,3 +32,68 @@ export interface EncryptedTrashItem { } export type Trash = TrashItem[]; + +export const TRASH = "file-trash"; + +export async function getLocalTrash() { + const trash = (await localForage.getItem(TRASH)) ?? []; + return trash; +} + +export async function getLocalTrashedFiles() { + return getTrashedFiles(await getLocalTrash()); +} + +export type TrashedEnteFile = EnteFile & { + /** + * `true` if this file is in trash (i.e. it has been deleted by the user, + * and will be permanently deleted after 30 days of being moved to trash). + */ + isTrashed?: boolean; + /** + * If {@link isTrashed} is `true`, then {@link deleteBy} contains the epoch + * microseconds when this file will be permanently deleted. + */ + deleteBy?: number; +}; + +/** + * Return the date when the file will be deleted permanently. Only valid for + * files that are in the user's trash. + * + * This is a convenience wrapper over the {@link deleteBy} property of a file, + * converting that epoch microsecond value into a JavaScript date. + */ +export const enteFileDeletionDate = (file: TrashedEnteFile) => + dateFromEpochMicroseconds(file.deleteBy); + +export function getTrashedFiles(trash: Trash): TrashedEnteFile[] { + return sortTrashFiles( + trash.map((trashedFile) => ({ + ...trashedFile.file, + updationTime: trashedFile.updatedAt, + deleteBy: trashedFile.deleteBy, + isTrashed: true, + })), + ); +} + +const sortTrashFiles = (files: TrashedEnteFile[]) => { + return files.sort((a, b) => { + if (a.deleteBy === b.deleteBy) { + const at = fileCreationTime(a); + const bt = fileCreationTime(b); + return at == bt + ? b.metadata.modificationTime - a.metadata.modificationTime + : bt - at; + } + return (a.deleteBy ?? 0) - (b.deleteBy ?? 0); + }); +}; + +/** + * Return the IDs of all the files that are part of the trash as per our local + * database. + */ +export const getLocalTrashFileIDs = () => + getLocalTrash().then((trash) => new Set(trash.map((f) => f.file.id))); From fa137dcccc5636440eb7eb5acfa58fe2b070a7ca Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 23 Jun 2025 17:28:12 +0530 Subject: [PATCH 6/6] Move to use site --- web/apps/photos/src/components/FileList.tsx | 24 +++++----- web/packages/base/date.ts | 17 ------- web/packages/base/i18n-date.ts | 23 +++++++-- web/packages/gallery/services/video.ts | 2 +- .../new/photos/components/gallery/reducer.ts | 4 +- web/packages/new/photos/services/ml/worker.ts | 2 +- web/packages/new/photos/services/trash.ts | 47 +++++++------------ 7 files changed, 52 insertions(+), 67 deletions(-) diff --git a/web/apps/photos/src/components/FileList.tsx b/web/apps/photos/src/components/FileList.tsx index b7ad2c63ca..8166ef2108 100644 --- a/web/apps/photos/src/components/FileList.tsx +++ b/web/apps/photos/src/components/FileList.tsx @@ -26,7 +26,7 @@ import { } from "ente-new/photos/components/PlaceholderThumbnails"; import { TileBottomTextOverlay } from "ente-new/photos/components/Tiles"; import { PseudoCollectionID } from "ente-new/photos/services/collection-summary"; -import { enteFileDeletionDate } from "ente-new/photos/services/trash"; +import { type EnteTrashFile } from "ente-new/photos/services/trash"; import { t } from "i18next"; import memoize from "memoize-one"; import { GalleryContext } from "pages/gallery"; @@ -355,21 +355,19 @@ export const FileList: React.FC = ({ const groupByTime = (timeStampList: TimeStampListItem[]) => { let listItemIndex = 0; - let currentDate; + let lastCreationTime: number | undefined; annotatedFiles.forEach((item, index) => { + const creationTime = fileCreationTime(item.file) / 1000; if ( - !currentDate || - !isSameDay( - new Date(fileCreationTime(item.file) / 1000), - new Date(currentDate), - ) + !lastCreationTime || + !isSameDay(new Date(creationTime), new Date(lastCreationTime)) ) { - currentDate = fileCreationTime(item.file) / 1000; + lastCreationTime = creationTime; timeStampList.push({ tag: "date", date: item.timelineDateString, - id: currentDate.toString(), + id: lastCreationTime.toString(), }); timeStampList.push({ tag: "file", @@ -1285,12 +1283,12 @@ const FileThumbnail: React.FC = ({ {activeCollectionID == PseudoCollectionID.trash && // TODO(RE): - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - file.isTrashed && ( + (file as EnteTrashFile).deleteBy && ( - {formattedDateRelative(enteFileDeletionDate(file))} + {formattedDateRelative( + (file as EnteTrashFile).deleteBy, + )} )} diff --git a/web/packages/base/date.ts b/web/packages/base/date.ts index c44fc5b234..1aea5aa2b1 100644 --- a/web/packages/base/date.ts +++ b/web/packages/base/date.ts @@ -1,20 +1,3 @@ -/** - * Convert an epoch microsecond value to a JavaScript date. - * - * [Note: Remote timestamps are epoch microseconds] - * - * This is a convenience API for dealing with optional epoch microseconds in - * various data structures. Remote talks in terms of epoch microseconds, but - * JavaScript dates are underlain by epoch milliseconds, and this does a - * conversion, with a convenience of short circuiting undefined values. - */ -export const dateFromEpochMicroseconds = ( - epochMicroseconds: number | undefined, -) => - epochMicroseconds === undefined - ? undefined - : new Date(epochMicroseconds / 1000); - /** * Return `true` if both the given dates have the same day. */ diff --git a/web/packages/base/i18n-date.ts b/web/packages/base/i18n-date.ts index bd49fb118c..05cc4fd8af 100644 --- a/web/packages/base/i18n-date.ts +++ b/web/packages/base/i18n-date.ts @@ -57,11 +57,14 @@ export const formattedTime = (date: Date) => _timeFormat.format(date); * @param dateOrEpochMicroseconds A JavaScript Date or a numeric epoch * microseconds value. * + * [Note: Remote timestamps are epoch microseconds] + * + * Remote talks in terms of epoch microseconds, while JavaScript dates are + * underlain by epoch milliseconds. + * * As a convenience, this function can be either be directly passed a JavaScript * date, or it can be given the raw epoch microseconds value and it'll convert * internally. - * - * See: [Note: Remote timestamps are epoch microseconds] */ export const formattedDateTime = (dateOrEpochMicroseconds: Date | number) => _formattedDateTime(toDate(dateOrEpochMicroseconds)); @@ -74,7 +77,19 @@ const toDate = (dm: Date | number) => let _relativeTimeFormat: Intl.RelativeTimeFormat | undefined; -export const formattedDateRelative = (date: Date) => { +/** + * Return a locale aware relative version of the given date. + * + * Example: "in 23 days" + * + * @param dateOrEpochMicroseconds A JavaScript Date or a numeric epoch + * microseconds value. + * + * See: [Note: Remote timestamps are epoch microseconds] + */ +export const formattedDateRelative = ( + dateOrEpochMicroseconds: Date | number, +) => { const units: [Intl.RelativeTimeFormatUnit, number][] = [ ["year", 24 * 60 * 60 * 1000 * 365], ["month", (24 * 60 * 60 * 1000 * 365) / 12], @@ -84,6 +99,8 @@ export const formattedDateRelative = (date: Date) => { ["second", 1000], ]; + const date = toDate(dateOrEpochMicroseconds); + // Math.abs accounts for both past and future scenarios. const elapsed = Math.abs(date.getTime() - Date.now()); diff --git a/web/packages/gallery/services/video.ts b/web/packages/gallery/services/video.ts index aff911a726..bfa039ff2d 100644 --- a/web/packages/gallery/services/video.ts +++ b/web/packages/gallery/services/video.ts @@ -13,8 +13,8 @@ import { uniqueFilesByID } from "ente-gallery/utils/files"; import { fileLogID, type EnteFile } from "ente-media/file"; import { FileType } from "ente-media/file-type"; import { updateFilePublicMagicMetadata } from "ente-new/photos/services/file"; -import { getLocalTrashFileIDs } from "ente-new/photos/services/files"; import { savedFiles } from "ente-new/photos/services/photos-fdb"; +import { getLocalTrashFileIDs } from "ente-new/photos/services/trash"; import { gunzip, gzip } from "ente-new/photos/utils/gzip"; import { randomSample } from "ente-utils/array"; import { ensurePrecondition } from "ente-utils/ensure"; diff --git a/web/packages/new/photos/components/gallery/reducer.ts b/web/packages/new/photos/components/gallery/reducer.ts index ca75a66dd6..0df6b60637 100644 --- a/web/packages/new/photos/components/gallery/reducer.ts +++ b/web/packages/new/photos/components/gallery/reducer.ts @@ -20,7 +20,7 @@ import { isHiddenCollection, } from "ente-new/photos/services/collection"; import { getLatestVersionFiles } from "ente-new/photos/services/files"; -import { type TrashedEnteFile } from "ente-new/photos/services/trash"; +import { type EnteTrashFile } from "ente-new/photos/services/trash"; import { splitByPredicate } from "ente-utils/array"; import { includes } from "ente-utils/type-guards"; import { t } from "i18next"; @@ -424,7 +424,7 @@ export type GalleryAction = collections: Collection[]; normalFiles: EnteFile[]; hiddenFiles: EnteFile[]; - trashedFiles: TrashedEnteFile[]; + trashedFiles: EnteTrashFile[]; } | { type: "setCollections"; diff --git a/web/packages/new/photos/services/ml/worker.ts b/web/packages/new/photos/services/ml/worker.ts index 57cc238840..0b21ecb389 100644 --- a/web/packages/new/photos/services/ml/worker.ts +++ b/web/packages/new/photos/services/ml/worker.ts @@ -8,8 +8,8 @@ import type { ElectronMLWorker } from "ente-base/types/ipc"; import { isNetworkDownloadError } from "ente-gallery/services/download"; import type { ProcessableUploadItem } from "ente-gallery/services/upload"; import { fileLogID, type EnteFile } from "ente-media/file"; +import { getLocalTrashFileIDs } from "ente-new/photos/services/trash"; import { wait } from "ente-utils/promise"; -import { getLocalTrashFileIDs } from "../files"; import { savedFiles } from "../photos-fdb"; import { createImageBitmapAndData, diff --git a/web/packages/new/photos/services/trash.ts b/web/packages/new/photos/services/trash.ts index 8770c1dbd4..f14c794e0e 100644 --- a/web/packages/new/photos/services/trash.ts +++ b/web/packages/new/photos/services/trash.ts @@ -1,4 +1,3 @@ -import { dateFromEpochMicroseconds } from "ente-base/date"; import type { EnteFile, RemoteEnteFile } from "ente-media/file"; import { fileCreationTime } from "ente-media/file-metadata"; import localForage from "ente-shared/storage/localForage"; @@ -44,42 +43,31 @@ export async function getLocalTrashedFiles() { return getTrashedFiles(await getLocalTrash()); } -export type TrashedEnteFile = EnteFile & { +/** + * A file augmented with the date when it will be permanently deleted. + */ +export type EnteTrashFile = EnteFile & { /** - * `true` if this file is in trash (i.e. it has been deleted by the user, - * and will be permanently deleted after 30 days of being moved to trash). - */ - isTrashed?: boolean; - /** - * If {@link isTrashed} is `true`, then {@link deleteBy} contains the epoch - * microseconds when this file will be permanently deleted. + * Timestamp (epoch microseconds) when this file, which is already in trash, + * will be permanently deleted. + * + * On being deleted by the user, files move to trash, will be permanently + * deleted after 30 days of being moved to trash) */ deleteBy?: number; }; -/** - * Return the date when the file will be deleted permanently. Only valid for - * files that are in the user's trash. - * - * This is a convenience wrapper over the {@link deleteBy} property of a file, - * converting that epoch microsecond value into a JavaScript date. - */ -export const enteFileDeletionDate = (file: TrashedEnteFile) => - dateFromEpochMicroseconds(file.deleteBy); - -export function getTrashedFiles(trash: Trash): TrashedEnteFile[] { - return sortTrashFiles( - trash.map((trashedFile) => ({ - ...trashedFile.file, - updationTime: trashedFile.updatedAt, - deleteBy: trashedFile.deleteBy, - isTrashed: true, +export const getTrashedFiles = (trash: Trash): EnteTrashFile[] => + sortTrashFiles( + trash.map(({ file, updatedAt, deleteBy }) => ({ + ...file, + updationTime: updatedAt, + deleteBy, })), ); -} -const sortTrashFiles = (files: TrashedEnteFile[]) => { - return files.sort((a, b) => { +const sortTrashFiles = (files: EnteTrashFile[]) => + files.sort((a, b) => { if (a.deleteBy === b.deleteBy) { const at = fileCreationTime(a); const bt = fileCreationTime(b); @@ -89,7 +77,6 @@ const sortTrashFiles = (files: TrashedEnteFile[]) => { } return (a.deleteBy ?? 0) - (b.deleteBy ?? 0); }); -}; /** * Return the IDs of all the files that are part of the trash as per our local