This commit is contained in:
Manav Rathi
2025-06-24 08:02:12 +05:30
parent 241e1cbf88
commit 86d09d997a
6 changed files with 93 additions and 81 deletions

View File

@@ -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<FileThumbnailProps> = ({
}
};
// See: [Note: Files in trash pseudo collection have deleteBy]
const deleteBy =
activeCollectionID == PseudoCollectionID.trash &&
(file as EnteTrashFile).deleteBy;
return (
<FileThumbnail_
key={`thumb-${file.id}}`}
@@ -1281,17 +1298,13 @@ const FileThumbnail: React.FC<FileThumbnailProps> = ({
<InSelectRangeOverlay />
)}
{activeCollectionID == PseudoCollectionID.trash &&
// TODO(RE):
(file as EnteTrashFile).deleteBy && (
<TileBottomTextOverlay>
<Typography variant="small">
{formattedDateRelative(
(file as EnteTrashFile).deleteBy,
)}
</Typography>
</TileBottomTextOverlay>
)}
{deleteBy && (
<TileBottomTextOverlay>
<Typography variant="small">
{formattedDateRelative(deleteBy)}
</Typography>
</TileBottomTextOverlay>
)}
</FileThumbnail_>
);
};

View File

@@ -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();

View File

@@ -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<GalleryState, GalleryAction> = (
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<GalleryState, GalleryAction> = (
hiddenCollections,
lastSyncedNormalFiles,
lastSyncedHiddenFiles,
trashedFiles: action.trashedFiles,
trashItems,
normalFiles,
hiddenFiles,
archivedCollectionIDs,
@@ -568,7 +570,7 @@ const galleryReducer: React.Reducer<GalleryState, GalleryAction> = (
action.user,
normalCollections,
normalFiles,
action.trashedFiles,
trashItems,
archivedFileIDs,
),
hiddenCollectionSummaries: deriveHiddenCollectionSummaries(
@@ -595,7 +597,7 @@ const galleryReducer: React.Reducer<GalleryState, GalleryAction> = (
state.user!,
normalCollections,
state.normalFiles,
state.trashedFiles,
state.trashItems,
archivedFileIDs,
);
const hiddenCollectionSummaries = deriveHiddenCollectionSummaries(
@@ -665,7 +667,7 @@ const galleryReducer: React.Reducer<GalleryState, GalleryAction> = (
state.user!,
normalCollections,
state.normalFiles,
state.trashedFiles,
state.trashItems,
archivedFileIDs,
);
@@ -812,18 +814,21 @@ const galleryReducer: React.Reducer<GalleryState, GalleryAction> = (
});
}
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<number>,
) => {
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)),
]);
}

View File

@@ -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,
/**

View File

@@ -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) {

View File

@@ -79,15 +79,12 @@ export type RemoteTrashItem = z.infer<typeof RemoteTrashItem>;
* @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<typeof RemoteTrashItem>;
*/
export const pullTrash = async (
collections: Collection[],
onUpdateTrashFiles: ((files: EnteFile[]) => void) | undefined,
onSetTrashedItems: ((trashItems: TrashItem[]) => void) | undefined,
onPruneDeletedFileIDs: (deletedFileIDs: Set<number>) => Promise<void>,
): Promise<void> => {
// 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;