From 86d09d997a5e73fd50b8cb32cb69adbbca2e2862 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Tue, 24 Jun 2025 08:02:12 +0530 Subject: [PATCH] refactor --- web/apps/photos/src/components/FileList.tsx | 37 ++++++---- web/apps/photos/src/pages/gallery.tsx | 8 +-- .../new/photos/components/gallery/reducer.ts | 71 ++++++++++++------- .../new/photos/services/collection-summary.ts | 9 ++- web/packages/new/photos/services/sync.ts | 9 +-- web/packages/new/photos/services/trash.ts | 40 ++--------- 6 files changed, 93 insertions(+), 81 deletions(-) diff --git a/web/apps/photos/src/components/FileList.tsx b/web/apps/photos/src/components/FileList.tsx index 8166ef2108..705677778c 100644 --- a/web/apps/photos/src/components/FileList.tsx +++ b/web/apps/photos/src/components/FileList.tsx @@ -26,7 +26,6 @@ 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 { type EnteTrashFile } from "ente-new/photos/services/trash"; import { t } from "i18next"; import memoize from "memoize-one"; import { GalleryContext } from "pages/gallery"; @@ -93,6 +92,19 @@ export interface FileListAnnotatedFile { timelineDateString: string; } +/** + * A file augmented with the date when it will be permanently deleted. + * + * See: [Note: Files in trash pseudo collection have deleteBy] + */ +export type EnteTrashFile = EnteFile & { + /** + * Timestamp (epoch microseconds) when the trash item (and its corresponding + * {@link EnteFile}) will be permanently deleted. + */ + deleteBy?: number; +}; + export interface FileListProps { /** The height we should occupy (needed since the list is virtualized). */ height: number; @@ -1228,6 +1240,11 @@ const FileThumbnail: React.FC = ({ } }; + // See: [Note: Files in trash pseudo collection have deleteBy] + const deleteBy = + activeCollectionID == PseudoCollectionID.trash && + (file as EnteTrashFile).deleteBy; + return ( = ({ )} - {activeCollectionID == PseudoCollectionID.trash && - // TODO(RE): - (file as EnteTrashFile).deleteBy && ( - - - {formattedDateRelative( - (file as EnteTrashFile).deleteBy, - )} - - - )} + {deleteBy && ( + + + {formattedDateRelative(deleteBy)} + + + )} ); }; diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index 4332299a89..1dd03b2767 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -79,6 +79,7 @@ import { savedCollections, savedHiddenFiles, savedNormalFiles, + savedTrashItems, } from "ente-new/photos/services/photos-fdb"; import { filterSearchableFiles, @@ -91,7 +92,6 @@ import { preCollectionAndFilesSync, syncCollectionAndFiles, } from "ente-new/photos/services/sync"; -import { getLocalTrashedFiles } from "ente-new/photos/services/trash"; import { initUserDetailsOrTriggerSync, redirectToCustomerPortal, @@ -332,7 +332,7 @@ const Page: React.FC = () => { collections: await savedCollections(), normalFiles: await savedNormalFiles(), hiddenFiles: await savedHiddenFiles(), - trashedFiles: await getLocalTrashedFiles(), + trashItems: await savedTrashItems(), }); await syncWithRemote({ force: true }); setIsFirstLoad(false); @@ -557,8 +557,8 @@ const Page: React.FC = () => { dispatch({ type: "setHiddenFiles", files }), onFetchHiddenFiles: (files) => dispatch({ type: "fetchHiddenFiles", files }), - onResetTrashedFiles: (files) => - dispatch({ type: "setTrashedFiles", files }), + onSetTrashedItems: (trashItems) => + dispatch({ type: "setTrashItems", trashItems }), }); if (didUpdateFiles) { exportService.onLocalFilesUpdated(); diff --git a/web/packages/new/photos/components/gallery/reducer.ts b/web/packages/new/photos/components/gallery/reducer.ts index 0df6b60637..11e70b20a7 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 EnteTrashFile } from "ente-new/photos/services/trash"; +import { sortTrashItems, type TrashItem } from "ente-new/photos/services/trash"; import { splitByPredicate } from "ente-utils/array"; import { includes } from "ente-utils/type-guards"; import { t } from "i18next"; @@ -146,11 +146,12 @@ export interface GalleryState { */ lastSyncedHiddenFiles: EnteFile[]; /** - * The user's files that are in trash. + * The items in the user's trash. * - * The list is sorted so that newer files are first. + * The items are sorted in ascending order of their time to deletion. For + * more details about the sorting order, see {@link sortTrashItems}. */ - trashedFiles: EnteFile[]; + trashItems: TrashItem[]; /** * Latest snapshot of people related state, as reported by * {@link usePeopleStateSnapshot}. @@ -424,7 +425,7 @@ export type GalleryAction = collections: Collection[]; normalFiles: EnteFile[]; hiddenFiles: EnteFile[]; - trashedFiles: EnteTrashFile[]; + trashItems: TrashItem[]; } | { type: "setCollections"; @@ -438,7 +439,7 @@ export type GalleryAction = | { type: "uploadNormalFile"; file: EnteFile } | { type: "setHiddenFiles"; files: EnteFile[] } | { type: "fetchHiddenFiles"; files: EnteFile[] } - | { type: "setTrashedFiles"; files: EnteFile[] } + | { type: "setTrashItems"; trashItems: TrashItem[] } | { type: "setPeopleState"; peopleState: PeopleState | undefined } | { type: "markTempDeleted"; files: EnteFile[] } | { type: "clearTempDeleted" } @@ -473,7 +474,7 @@ const initialGalleryState: GalleryState = { hiddenCollections: [], lastSyncedNormalFiles: [], lastSyncedHiddenFiles: [], - trashedFiles: [], + trashItems: [], peopleState: undefined, normalFiles: [], hiddenFiles: [], @@ -515,6 +516,7 @@ const galleryReducer: React.Reducer = ( case "mount": { const lastSyncedNormalFiles = sortFiles(action.normalFiles); const lastSyncedHiddenFiles = sortFiles(action.hiddenFiles); + const trashItems = sortTrashItems(action.trashItems); // During mount there are no unsynced updates, and we can directly // use the provided files. @@ -546,7 +548,7 @@ const galleryReducer: React.Reducer = ( hiddenCollections, lastSyncedNormalFiles, lastSyncedHiddenFiles, - trashedFiles: action.trashedFiles, + trashItems, normalFiles, hiddenFiles, archivedCollectionIDs, @@ -568,7 +570,7 @@ const galleryReducer: React.Reducer = ( action.user, normalCollections, normalFiles, - action.trashedFiles, + trashItems, archivedFileIDs, ), hiddenCollectionSummaries: deriveHiddenCollectionSummaries( @@ -595,7 +597,7 @@ const galleryReducer: React.Reducer = ( state.user!, normalCollections, state.normalFiles, - state.trashedFiles, + state.trashItems, archivedFileIDs, ); const hiddenCollectionSummaries = deriveHiddenCollectionSummaries( @@ -665,7 +667,7 @@ const galleryReducer: React.Reducer = ( state.user!, normalCollections, state.normalFiles, - state.trashedFiles, + state.trashItems, archivedFileIDs, ); @@ -812,18 +814,21 @@ const galleryReducer: React.Reducer = ( }); } - case "setTrashedFiles": + case "setTrashItems": { + const trashItems = sortTrashItems(action.trashItems); + return stateByUpdatingFilteredFiles({ ...state, - trashedFiles: action.files, + trashItems, normalCollectionSummaries: deriveNormalCollectionSummaries( state.user!, state.normalCollections, state.normalFiles, - action.files, + trashItems, state.archivedFileIDs, ), }); + } case "setPeopleState": { const peopleState = action.peopleState; @@ -1270,7 +1275,7 @@ const deriveNormalCollectionSummaries = ( user: User, normalCollections: Collection[], normalFiles: EnteFile[], - trashedFiles: EnteFile[], + trashItems: TrashItem[], archivedFileIDs: Set, ) => { const normalCollectionSummaries = createCollectionSummaries( @@ -1305,7 +1310,10 @@ const deriveNormalCollectionSummaries = ( name: t("section_all"), }); normalCollectionSummaries.set(PseudoCollectionID.trash, { - ...pseudoCollectionOptionsForFiles(trashedFiles), + ...pseudoCollectionOptionsForLatestFileAndCount( + trashItems[0]?.file, + trashItems.length, + ), id: PseudoCollectionID.trash, name: t("section_trash"), type: "trash", @@ -1327,11 +1335,17 @@ const deriveNormalCollectionSummaries = ( return normalCollectionSummaries; }; -const pseudoCollectionOptionsForFiles = (files: EnteFile[]) => ({ - coverFile: files[0], - latestFile: files[0], - fileCount: files.length, - updationTime: files[0]?.updationTime, +const pseudoCollectionOptionsForFiles = (files: EnteFile[]) => + pseudoCollectionOptionsForLatestFileAndCount(files[0], files.length); + +const pseudoCollectionOptionsForLatestFileAndCount = ( + file: EnteFile | undefined, + fileCount: number, +) => ({ + coverFile: file, + latestFile: file, + fileCount, + updationTime: file?.updationTime, }); /** @@ -1738,7 +1752,7 @@ const stateForUpdatedNormalFiles = ( state.user!, state.normalCollections, normalFiles, - state.trashedFiles, + state.trashItems, state.archivedFileIDs, ), pendingSearchSuggestions: enqueuePendingSearchSuggestionsIfNeeded( @@ -1788,7 +1802,7 @@ const stateByUpdatingFilteredFiles = (state: GalleryState) => { } else if (state.view?.type == "albums") { const filteredFiles = deriveAlbumsFilteredFiles( state.normalFiles, - state.trashedFiles, + state.trashItems, state.hiddenFileIDs, state.archivedCollectionIDs, state.archivedFileIDs, @@ -1822,7 +1836,7 @@ const stateByUpdatingFilteredFiles = (state: GalleryState) => { */ const deriveAlbumsFilteredFiles = ( normalFiles: GalleryState["normalFiles"], - trashedFiles: GalleryState["trashedFiles"], + trashItems: GalleryState["trashItems"], hiddenFileIDs: GalleryState["hiddenFileIDs"], archivedCollectionIDs: GalleryState["archivedCollectionIDs"], archivedFileIDs: GalleryState["archivedFileIDs"], @@ -1833,9 +1847,16 @@ const deriveAlbumsFilteredFiles = ( const activeCollectionSummaryID = view.activeCollectionSummaryID; // Trash is dealt with separately. + // + // [Note: Files in trash pseudo collection have deleteBy] + // + // When showing the trash pseudo collection, each file in the files array is + // in fact an instance of `EnteTrashFile` - it has an additional (and + // optional) `deleteBy` property. The types don't reflect this. + if (activeCollectionSummaryID == PseudoCollectionID.trash) { return uniqueFilesByID([ - ...trashedFiles, + ...trashItems.map(({ file, deleteBy }) => ({ ...file, deleteBy })), ...normalFiles.filter((file) => tempDeletedFileIDs.has(file.id)), ]); } diff --git a/web/packages/new/photos/services/collection-summary.ts b/web/packages/new/photos/services/collection-summary.ts index 4c4c207e99..1be023b8f5 100644 --- a/web/packages/new/photos/services/collection-summary.ts +++ b/web/packages/new/photos/services/collection-summary.ts @@ -38,8 +38,13 @@ export const PseudoCollectionID = { /** * Trash. * - * This pseudo-collection contains files that are in the user's trash - - * files that have been deleted, but have not yet been deleted permanently. + * This pseudo-collection contains items that are in the user's trash. Each + * items corresponds to a file that has been deleted, but has not yet been + * deleted permanently. + * + * As a special case, when the trash pseudo collection is being shown, then + * the corresponding files array will have {@link EnteTrashFile} items + * instead of normal {@link EnteFile} ones. */ trash: -2, /** diff --git a/web/packages/new/photos/services/sync.ts b/web/packages/new/photos/services/sync.ts index a88cc64d8b..7498cac089 100644 --- a/web/packages/new/photos/services/sync.ts +++ b/web/packages/new/photos/services/sync.ts @@ -16,7 +16,7 @@ import { import { searchDataSync } from "ente-new/photos/services/search"; import { syncSettings } from "ente-new/photos/services/settings"; import { splitByPredicate } from "ente-utils/array"; -import { pullTrash } from "./trash"; +import { pullTrash, type TrashItem } from "./trash"; /** * Called during a full sync, before doing the collection and file sync. @@ -99,9 +99,10 @@ interface SyncCallectionAndFilesOpts { */ onFetchHiddenFiles: (files: EnteFile[]) => void; /** - * Called when saved trashed files were replaced by the given {@link files}. + * Called when saved trashed items were replaced by the given + * {@link trashItems}. */ - onResetTrashedFiles: (files: EnteFile[]) => void; + onSetTrashedItems: (trashItems: TrashItem[]) => void; } /** @@ -144,7 +145,7 @@ export const syncCollectionAndFiles = async ( ); await pullTrash( collections, - opts?.onResetTrashedFiles, + opts?.onSetTrashedItems, videoPrunePermanentlyDeletedFileIDsIfNeeded, ); if (didUpdateNormalFiles || didUpdateHiddenFiles) { diff --git a/web/packages/new/photos/services/trash.ts b/web/packages/new/photos/services/trash.ts index 88dd5993a1..e9ae8a598e 100644 --- a/web/packages/new/photos/services/trash.ts +++ b/web/packages/new/photos/services/trash.ts @@ -79,15 +79,12 @@ export type RemoteTrashItem = z.infer; * @param collections All the (non-deleted) collections that we know about * locally. * - * @param onUpdateTrashFiles A callback invoked when the locally persisted trash + * @param onSetTrashedItems A callback invoked when the locally persisted trash * items are updated. This can be used for the UI to also update its state. * * This callback can be invoked multiple times during the pull (once for each - * batch that gets pulled and processed). - * - * Each time, it gets passed a list of all the items that are present in trash, - * sorted in ascending order of their time to deletion. For more details about - * the sorting order, see {@link sortTrashItems}. + * batch that gets pulled and processed). Each time, it gets passed a list of + * all the items that are present in trash (in an arbitrary order). * * @param onPruneDeletedFileIDs A callback invoked when files that were * previously in trash have now been permanently deleted. This can be used by @@ -99,7 +96,7 @@ export type RemoteTrashItem = z.infer; */ export const pullTrash = async ( collections: Collection[], - onUpdateTrashFiles: ((files: EnteFile[]) => void) | undefined, + onSetTrashedItems: ((trashItems: TrashItem[]) => void) | undefined, onPruneDeletedFileIDs: (deletedFileIDs: Set) => Promise, ): Promise => { // Data structures: @@ -169,7 +166,7 @@ export const pullTrash = async ( } const trashItems = [...trashItemsByID.values()]; - onUpdateTrashFiles?.(getTrashedFiles(trashItems)); + onSetTrashedItems?.(trashItems); await saveTrashItems(trashItems); await saveTrashLastUpdatedAt(sinceTime); if (deletedFileIDs.size) await onPruneDeletedFileIDs(deletedFileIDs); @@ -214,31 +211,6 @@ const getTrashDiff = async (sinceTime: number) => { return TrashDiffResponse.parse(await res.json()); }; -/** - * Return all the trash items present locally after sorting them. - */ -export async function getLocalTrashedFiles() { - return getTrashedFiles(await savedTrashItems()); -} - -/** - * A file augmented with the date when it will be permanently deleted. - */ -export type EnteTrashFile = EnteFile & { - /** - * Timestamp (epoch microseconds) when the trash item (and its corresponding - * {@link EnteFile}) will be permanently deleted. - */ - deleteBy?: number; -}; - -const getTrashedFiles = (trashItems: TrashItem[]): EnteTrashFile[] => - sortTrashItems(trashItems).map(({ file, updatedAt, deleteBy }) => ({ - ...file, - updationTime: updatedAt, - deleteBy, - })); - /** * Sort trash items such that the items which will be permanently deleted * earlier are first. @@ -250,7 +222,7 @@ const getTrashedFiles = (trashItems: TrashItem[]): EnteTrashFile[] => * the same time to deletion, the ordering is in descending order of the item's * file's modification or creation date. */ -const sortTrashItems = (trashItems: TrashItem[]) => +export const sortTrashItems = (trashItems: TrashItem[]) => trashItems.sort((a, b) => { if (a.deleteBy == b.deleteBy) { const af = a.file;