[web] Use reducer for gallery - Part 4/x (#3790)

This commit is contained in:
Manav Rathi
2024-10-21 19:18:29 +05:30
committed by GitHub
12 changed files with 267 additions and 120 deletions

View File

@@ -2,7 +2,7 @@ import log from "@/base/log";
import type { LivePhotoSourceURL, SourceURLs } from "@/media/file";
import { EnteFile } from "@/media/file";
import { FileType } from "@/media/file-type";
import type { GalleryBarMode } from "@/new/photos/components/gallery/BarImpl";
import type { GalleryBarMode } from "@/new/photos/components/gallery/reducer";
import { TRASH_SECTION } from "@/new/photos/services/collection";
import DownloadManager from "@/new/photos/services/download";
import { PHOTOS_PAGES } from "@ente/shared/constants/pages";

View File

@@ -1,7 +1,7 @@
import { SelectionBar } from "@/base/components/Navbar";
import type { Collection } from "@/media/collection";
import type { CollectionSelectorAttributes } from "@/new/photos/components/CollectionSelector";
import type { GalleryBarMode } from "@/new/photos/components/gallery/BarImpl";
import type { GalleryBarMode } from "@/new/photos/components/gallery/reducer";
import {
ALL_SECTION,
ARCHIVE_SECTION,

View File

@@ -1,6 +1,7 @@
import { stashRedirect } from "@/accounts/services/redirect";
import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator";
import { ALL_SECTION } from "@/new/photos/services/collection";
import { createFileCollectionIDs } from "@/new/photos/services/file";
import { getLocalFiles } from "@/new/photos/services/files";
import { AppContext } from "@/new/photos/types/context";
import { VerticallyCentered } from "@ente/shared/components/Container";
@@ -28,7 +29,7 @@ import {
DefaultDeduplicateContext,
} from "types/deduplicate";
import { SelectedState } from "types/gallery";
import { constructFileToCollectionMap, getSelectedFiles } from "utils/file";
import { getSelectedFiles } from "utils/file";
export const DeduplicateContext = createContext<DeduplicateContextType>(
DefaultDeduplicateContext,
@@ -114,7 +115,7 @@ export default function Deduplicate() {
}, [duplicates]);
const fileToCollectionsMap = useMemoSingleThreaded(() => {
return constructFileToCollectionMap(duplicateFiles);
return createFileCollectionIDs(duplicateFiles ?? []);
}, [duplicateFiles]);
const deleteFileHelper = async () => {

View File

@@ -19,10 +19,10 @@ import {
PeopleEmptyState,
SearchResultsHeader,
} from "@/new/photos/components/gallery";
import type { GalleryBarMode } from "@/new/photos/components/gallery/BarImpl";
import {
uniqueFilesByID,
useGalleryReducer,
type GalleryBarMode,
} from "@/new/photos/components/gallery/reducer";
import { usePeopleStateSnapshot } from "@/new/photos/components/utils/ml";
import { shouldShowWhatsNew } from "@/new/photos/services/changelog";
@@ -137,16 +137,10 @@ import {
import { checkSubscriptionPurchase } from "utils/billing";
import {
COLLECTION_OPS_TYPE,
constructCollectionNameMap,
getSelectedCollection,
handleCollectionOps,
} from "utils/collection";
import {
FILE_OPS_TYPE,
constructFileToCollectionMap,
getSelectedFiles,
handleFileOps,
} from "utils/file";
import { FILE_OPS_TYPE, getSelectedFiles, handleFileOps } from "utils/file";
import { getSessionExpiredMessage } from "utils/ui";
import { getLocalFamilyData } from "utils/user/family";
@@ -252,8 +246,6 @@ export default function Gallery() {
const [userIDToEmailMap, setUserIDToEmailMap] =
useState<Map<number, string>>(null);
const [emailList, setEmailList] = useState<string[]>(null);
const [activeCollectionID, setActiveCollectionID] =
useState<number>(undefined);
const [fixCreationTimeView, setFixCreationTimeView] = useState(false);
const [fixCreationTimeAttributes, setFixCreationTimeAttributes] =
useState<FixCreationTimeAttributes>(null);
@@ -283,20 +275,11 @@ export default function Gallery() {
const closeAuthenticateUserModal = () =>
setAuthenticateUserModalView(false);
// True if we're in "search mode". See: [Note: "search mode"].
const [isInSearchMode, setIsInSearchMode] = useState(false);
// The option selected by the user selected from the search bar dropdown.
const [selectedSearchOption, setSelectedSearchOption] = useState<
SearchOption | undefined
>();
// If visible, what should the (sticky) gallery bar show.
const [barMode, setBarMode] = useState<GalleryBarMode>("albums");
// The ID of the currently selected person in the gallery bar (if any).
const [activePersonID, setActivePersonID] = useState<string | undefined>();
const peopleState = usePeopleStateSnapshot();
const [isClipSearchResult, setIsClipSearchResult] =
@@ -339,8 +322,14 @@ export default function Gallery() {
const archivedCollectionIDs = state.archivedCollectionIDs;
const defaultHiddenCollectionIDs = state.defaultHiddenCollectionIDs;
const hiddenFileIDs = state.hiddenFileIDs;
const collectionNameMap = state.allCollectionNameByID;
const fileToCollectionsMap = state.fileCollectionIDs;
const collectionSummaries = state.collectionSummaries;
const hiddenCollectionSummaries = state.hiddenCollectionSummaries;
const barMode = state.barMode ?? "albums";
const activeCollectionID = state.activeCollectionID;
const activePersonID = state.activePersonID;
const isInSearchMode = state.isInSearchMode;
if (process.env.NEXT_PUBLIC_ENTE_WIP_CL) {
console.log("render", { collections, hiddenCollections, files });
@@ -379,7 +368,7 @@ export default function Gallery() {
}
await downloadManager.init(token);
setupSelectAllKeyBoardShortcutHandler();
setActiveCollectionID(ALL_SECTION);
dispatch({ type: "showAll" });
setIsFirstLoad(isFirstLogin());
if (justSignedUp()) {
setPlanModalView(true);
@@ -746,20 +735,6 @@ export default function Gallery() {
};
}, [selectAll, clearSelection]);
const fileToCollectionsMap = useMemoSingleThreaded(() => {
return constructFileToCollectionMap(files);
}, [files]);
const collectionNameMap = useMemo(() => {
if (!collections || !hiddenCollections) {
return new Map();
}
return constructCollectionNameMap([
...collections,
...hiddenCollections,
]);
}, [collections, hiddenCollections]);
const showSessionExpiredMessage = () => {
setDialogMessage(getSessionExpiredMessage(logout));
};
@@ -999,18 +974,24 @@ export default function Gallery() {
) => {
const type = searchOption?.suggestion.type;
if (type == "collection" || type == "person") {
setIsInSearchMode(false);
setSelectedSearchOption(undefined);
if (type == "collection") {
setBarMode("albums");
setActiveCollectionID(searchOption.suggestion.collectionID);
dispatch({
type: "showNormalOrHiddenCollectionSummary",
collectionSummaryID: searchOption.suggestion.collectionID,
});
} else {
setBarMode("people");
setActivePersonID(searchOption.suggestion.person.id);
dispatch({
type: "showPerson",
personID: searchOption.suggestion.person.id,
});
}
} else {
setIsInSearchMode(!!searchOption);
setSelectedSearchOption(undefined);
} else if (searchOption) {
dispatch({ type: "enterSearchMode" });
setSelectedSearchOption(searchOption);
} else {
dispatch({ type: "exitSearch" });
setSelectedSearchOption(undefined);
}
setIsClipSearchResult(type == "clip");
};
@@ -1031,38 +1012,32 @@ export default function Gallery() {
setExportModalView(false);
};
const handleShowCollection = (collectionID: number) => {
setBarMode("albums");
setActiveCollectionID(collectionID);
setIsInSearchMode(false);
};
const handleSetActiveCollectionID = (
collectionSummaryID: number | undefined,
) =>
dispatch({
type: "showNormalOrHiddenCollectionSummary",
collectionSummaryID,
});
const handleShowSearchInput = () => setIsInSearchMode(true);
const handleChangeBarMode = (mode: GalleryBarMode) =>
mode == "people"
? dispatch({ type: "showPeople" })
: dispatch({ type: "showAll" });
const openHiddenSection: GalleryContextType["openHiddenSection"] = (
callback,
) => {
authenticateUser(() => {
setBarMode("hidden-albums");
setActiveCollectionID(HIDDEN_ITEMS_SECTION);
dispatch({ type: "showHidden" });
callback?.();
});
};
const exitHiddenSection = () => {
setBarMode("albums");
setActiveCollectionID(ALL_SECTION);
};
const handleSelectPerson = (person: Person | undefined) => {
setActivePersonID(person?.id);
setBarMode("people");
};
const handleSelectFileInfoPerson = (personID: string) => {
setActivePersonID(personID);
setBarMode("people");
};
const handleSelectPerson = (person: Person | undefined) =>
person
? dispatch({ type: "showPerson", personID: person.id })
: dispatch({ type: "showPeople" });
const handleOpenCollectionSelector = useCallback(
(attributes: CollectionSelectorAttributes) => {
@@ -1091,8 +1066,12 @@ export default function Gallery() {
value={{
...defaultGalleryContext,
showPlanSelectorModal,
setActiveCollectionID,
onShowCollection: handleShowCollection,
setActiveCollectionID: handleSetActiveCollectionID,
onShowCollection: (id) =>
dispatch({
type: "showNormalOrHiddenCollectionSummary",
collectionSummaryID: id,
}),
syncWithRemote,
setBlockingLoad,
photoListHeader,
@@ -1170,7 +1149,7 @@ export default function Gallery() {
>
{barMode == "hidden-albums" ? (
<HiddenSectionNavbarContents
onBack={exitHiddenSection}
onBack={() => dispatch({ type: "showAll" })}
/>
) : (
<NormalNavbarContents
@@ -1178,7 +1157,8 @@ export default function Gallery() {
openSidebar,
openUploader,
isInSearchMode,
onShowSearchInput: handleShowSearchInput,
onShowSearchInput: () =>
dispatch({ type: "enterSearchMode" }),
onSelectSearchOption: handleSelectSearchOption,
onSelectPerson: handleSelectPerson,
}}
@@ -1190,11 +1170,11 @@ export default function Gallery() {
{...{
shouldHide: isInSearchMode,
mode: barMode,
onChangeMode: setBarMode,
onChangeMode: handleChangeBarMode,
collectionSummaries,
activeCollection,
activeCollectionID,
setActiveCollectionID,
setActiveCollectionID: handleSetActiveCollectionID,
hiddenCollectionSummaries,
showPeopleSectionButton,
people: galleryPeopleState?.people ?? [],
@@ -1284,7 +1264,9 @@ export default function Gallery() {
setFilesDownloadProgressAttributesCreator
}
selectable={true}
onSelectPerson={handleSelectFileInfoPerson}
onSelectPerson={(personID) => {
dispatch({ type: "showPerson", personID });
}}
/>
)}
{selected.count > 0 &&

View File

@@ -9,6 +9,10 @@ import {
} from "@/media/file-metadata";
import { FileType } from "@/media/file-type";
import { decodeLivePhoto } from "@/media/live-photo";
import {
createCollectionNameByID,
getCollectionUserFacingName,
} from "@/new/photos/services/collection";
import downloadManager from "@/new/photos/services/download";
import { updateExifIfNeededAndPossible } from "@/new/photos/services/exif-update";
import {
@@ -34,10 +38,6 @@ import {
ExportUIUpdaters,
FileExportNames,
} from "types/export";
import {
constructCollectionNameMap,
getCollectionUserFacingName,
} from "utils/collection";
import { getAllLocalCollections } from "../collectionService";
import { migrateExport } from "./migration";
@@ -330,7 +330,7 @@ class ExportService {
convertCollectionIDExportNameObjectToMap(
exportRecord.collectionExportNames,
);
const collectionIDNameMap = constructCollectionNameMap(collections);
const collectionIDNameMap = createCollectionNameByID(collections);
const renamedCollections = getRenamedExportedCollections(
collections,

View File

@@ -11,8 +11,8 @@ import {
import { EnteFile } from "@/media/file";
import { ItemVisibility } from "@/media/file-metadata";
import {
DEFAULT_HIDDEN_COLLECTION_USER_FACING_NAME,
findDefaultHiddenCollectionIDs,
isDefaultHiddenCollection,
isHiddenCollection,
isIncomingShare,
} from "@/new/photos/services/collection";
@@ -391,26 +391,6 @@ export function getHiddenCollections(collections: Collection[]): Collection[] {
return collections.filter((collection) => isHiddenCollection(collection));
}
export function constructCollectionNameMap(
collections: Collection[],
): Map<number, string> {
return new Map<number, string>(
(collections ?? []).map((collection) => [
collection.id,
getCollectionUserFacingName(collection),
]),
);
}
const DEFAULT_HIDDEN_COLLECTION_USER_FACING_NAME = "Hidden";
export const getCollectionUserFacingName = (collection: Collection) => {
if (isDefaultHiddenCollection(collection)) {
return DEFAULT_HIDDEN_COLLECTION_USER_FACING_NAME;
}
return collection.name;
};
export const getOrCreateAlbum = async (
albumName: string,
existingCollections: Collection[],

View File

@@ -517,17 +517,6 @@ export function getIDBasedSortedFiles(files: EnteFile[]) {
return files.sort((a, b) => a.id - b.id);
}
export function constructFileToCollectionMap(files: EnteFile[]) {
const fileToCollectionsMap = new Map<number, number[]>();
(files ?? []).forEach((file) => {
if (!fileToCollectionsMap.get(file.id)) {
fileToCollectionsMap.set(file.id, []);
}
fileToCollectionsMap.get(file.id).push(file.collectionID);
});
return fileToCollectionsMap;
}
export const shouldShowAvatar = (file: EnteFile, user: User) => {
if (!file || !user) {
return false;

View File

@@ -3,7 +3,7 @@ import type { LivePhotoSourceURL, SourceURLs } from "@/media/file";
import { EnteFile } from "@/media/file";
import { FileType } from "@/media/file-type";
import type { SelectionContext } from "@/new/photos/components/gallery";
import type { GalleryBarMode } from "@/new/photos/components/gallery/BarImpl";
import type { GalleryBarMode } from "@/new/photos/components/gallery/reducer";
import { ensure } from "@/utils/ensure";
import { SetSelectedState } from "types/gallery";

View File

@@ -42,11 +42,7 @@ import {
type ListChildComponentProps,
areEqual,
} from "react-window";
/**
* Specifies what the bar is displaying currently.
*/
export type GalleryBarMode = "albums" | "hidden-albums" | "people";
import type { GalleryBarMode } from "./reducer";
export interface GalleryBarImplProps {
/**

View File

@@ -5,7 +5,10 @@ import {
} from "@/media/collection";
import type { EnteFile } from "@/media/file";
import { mergeMetadata } from "@/media/file";
import { isHiddenCollection } from "@/new/photos/services/collection";
import {
createCollectionNameByID,
isHiddenCollection,
} from "@/new/photos/services/collection";
import { splitByPredicate } from "@/utils/array";
import { ensure } from "@/utils/ensure";
import type { User } from "@ente/shared/user/types";
@@ -26,6 +29,7 @@ import type {
CollectionSummaryType,
} from "../../services/collection/ui";
import {
createFileCollectionIDs,
getLatestVersionFiles,
groupFilesByCollectionID,
} from "../../services/file";
@@ -38,6 +42,25 @@ import {
import type { Person } from "../../services/ml/people";
import type { FamilyData } from "../../services/user";
/**
* Specifies what the bar at the top of the gallery is displaying currently.
*/
export type GalleryBarMode = "albums" | "hidden-albums" | "people";
/**
* Specifies what the gallery is currently displaying.
*
* TODO: An experiment at consolidating state.
*/
export type GalleryFocus =
| {
type: "albums" | "hidden-albums";
activeCollectionID: number;
activeCollection: Collection | undefined;
activeCollectionSummary: CollectionSummary;
}
| { type: "people"; activePersonID: string; activePerson: Person };
/**
* Derived UI state backing the gallery.
*
@@ -109,6 +132,17 @@ export interface GalleryState {
* File IDs of all the files that the user has marked as a favorite.
*/
favoriteFileIDs: Set<number>;
/**
* User visible collection names indexed by collection IDs for fast lookup.
*
* This map will contain entries for all (both normal and hidden)
* collections.
*/
allCollectionNameByID: Map<number, string>;
/**
* A list of collection IDs to which a file belongs, indexed by file ID.
*/
fileCollectionIDs: Map<number, number[]>;
/*--< Derived UI state >--*/
@@ -124,6 +158,18 @@ export interface GalleryState {
/*--< Transient UI state >--*/
/**
* If visible, what should the (sticky) gallery bar show.
*/
barMode: GalleryBarMode | undefined;
/**
* The section / area, and the item within it, that the gallery is currently
* showing.
*/
focus: GalleryFocus | undefined;
activeCollectionID: number | undefined;
activePersonID: string | undefined;
filteredData: EnteFile[];
/**
* The currently selected person, if any.
@@ -136,6 +182,28 @@ export interface GalleryState {
* The list of people to show.
*/
people: Person[] | undefined;
/**
* `true` if we are in "search mode".
*
* We will always be in search mode if we are showing search results, but we
* also may be in search mode earlier on smaller screens, where the search
* input is only shown on entering search mode. See: [Note: "Search mode"].
*
* That is, {@link isInSearchMode} may be true even when
* {@link searchResults} is undefined.
*/
isInSearchMode: boolean;
/**
* List of files that match the selected search option.
*
* This will be set only if we are showing search results.
*
* The search dropdown shows a list of options ("suggestions") that match
* the user's search term. If the user selects from one of these options,
* then we run a search to find all files that match that suggestion, and
* set this value to the result.
*/
searchResults: EnteFile[] | undefined;
}
export type GalleryAction =
@@ -169,7 +237,18 @@ export type GalleryAction =
| { type: "uploadFile"; file: EnteFile }
| { type: "resetHiddenFiles"; hiddenFiles: EnteFile[] }
| { type: "fetchHiddenFiles"; hiddenFiles: EnteFile[] }
| { type: "setTrashedFiles"; trashedFiles: EnteFile[] };
| { type: "setTrashedFiles"; trashedFiles: EnteFile[] }
| { type: "showAll" }
| { type: "showHidden" }
| {
type: "showNormalOrHiddenCollectionSummary";
collectionSummaryID: number | undefined;
}
| { type: "showPeople" }
| { type: "showPerson"; personID: string }
| { type: "searchResults"; searchResults: EnteFile[] }
| { type: "enterSearchMode" }
| { type: "exitSearch" };
const initialGalleryState: GalleryState = {
user: undefined,
@@ -183,11 +262,19 @@ const initialGalleryState: GalleryState = {
defaultHiddenCollectionIDs: new Set(),
hiddenFileIDs: new Set(),
favoriteFileIDs: new Set(),
allCollectionNameByID: new Map(),
fileCollectionIDs: new Map(),
collectionSummaries: new Map(),
hiddenCollectionSummaries: new Map(),
barMode: undefined,
focus: undefined,
activeCollectionID: undefined,
activePersonID: undefined,
filteredData: [],
activePerson: undefined,
people: [],
isInSearchMode: false,
searchResults: undefined,
};
const galleryReducer: React.Reducer<GalleryState, GalleryAction> = (
@@ -222,6 +309,10 @@ const galleryReducer: React.Reducer<GalleryState, GalleryAction> = (
collections,
action.files,
),
allCollectionNameByID: createCollectionNameByID(
action.allCollections,
),
fileCollectionIDs: createFileCollectionIDs(action.files),
collectionSummaries: deriveCollectionSummaries(
action.user,
collections,
@@ -255,6 +346,9 @@ const galleryReducer: React.Reducer<GalleryState, GalleryAction> = (
action.collections,
state.files,
),
allCollectionNameByID: createCollectionNameByID(
action.collections.concat(state.hiddenCollections),
),
collectionSummaries: deriveCollectionSummaries(
ensure(state.user),
action.collections,
@@ -280,6 +374,9 @@ const galleryReducer: React.Reducer<GalleryState, GalleryAction> = (
action.collections,
state.files,
),
allCollectionNameByID: createCollectionNameByID(
action.collections.concat(action.hiddenCollections),
),
collectionSummaries: deriveCollectionSummaries(
ensure(state.user),
action.collections,
@@ -303,6 +400,7 @@ const galleryReducer: React.Reducer<GalleryState, GalleryAction> = (
state.collections,
files,
),
fileCollectionIDs: createFileCollectionIDs(action.files),
collectionSummaries: deriveCollectionSummaries(
ensure(state.user),
state.collections,
@@ -325,6 +423,7 @@ const galleryReducer: React.Reducer<GalleryState, GalleryAction> = (
state.collections,
files,
),
fileCollectionIDs: createFileCollectionIDs(action.files),
collectionSummaries: deriveCollectionSummaries(
ensure(state.user),
state.collections,
@@ -343,6 +442,7 @@ const galleryReducer: React.Reducer<GalleryState, GalleryAction> = (
state.collections,
files,
),
fileCollectionIDs: createFileCollectionIDs(files),
// TODO: Consider batching this instead of doing it per file
// upload to speed up uploads. Perf test first though.
collectionSummaries: deriveCollectionSummaries(
@@ -399,6 +499,65 @@ const galleryReducer: React.Reducer<GalleryState, GalleryAction> = (
state.archivedCollectionIDs,
),
};
case "showAll":
return {
...state,
barMode: "albums",
activeCollectionID: ALL_SECTION,
isInSearchMode: false,
searchResults: undefined,
};
case "showHidden":
return {
...state,
barMode: "hidden-albums",
activeCollectionID: HIDDEN_ITEMS_SECTION,
isInSearchMode: false,
searchResults: undefined,
};
case "showNormalOrHiddenCollectionSummary":
return {
...state,
barMode:
action.collectionSummaryID !== undefined &&
state.hiddenCollectionSummaries.has(
action.collectionSummaryID,
)
? "hidden-albums"
: "albums",
activeCollectionID: action.collectionSummaryID ?? ALL_SECTION,
isInSearchMode: false,
searchResults: undefined,
};
case "showPeople":
return {
...state,
barMode: "people",
activePersonID: undefined,
isInSearchMode: false,
searchResults: undefined,
};
case "showPerson":
return {
...state,
barMode: "people",
activePersonID: action.personID,
isInSearchMode: false,
searchResults: undefined,
};
case "enterSearchMode":
return { ...state, isInSearchMode: true };
case "searchResults":
return {
...state,
searchResults: action.searchResults,
};
case "exitSearch":
return {
...state,
isInSearchMode: false,
searchResults: undefined,
};
}
};

View File

@@ -41,3 +41,30 @@ export const isHiddenCollection = (collection: Collection) =>
// TODO: Need to audit the types
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
collection.magicMetadata?.data.visibility === ItemVisibility.hidden;
// TODO: Does this need localizations?
export const DEFAULT_HIDDEN_COLLECTION_USER_FACING_NAME = "Hidden";
/**
* Return the "user facing" name of the given collection.
*
* Usually this is the same as the collection name, but it might be a different
* string for special collections like default hidden collections.
*/
export const getCollectionUserFacingName = (collection: Collection) => {
if (isDefaultHiddenCollection(collection)) {
return DEFAULT_HIDDEN_COLLECTION_USER_FACING_NAME;
}
return collection.name;
};
/**
* Return a map of the (user-facing) collection name, indexed by collection ID.
*/
export const createCollectionNameByID = (allCollections: Collection[]) =>
new Map<number, string>(
allCollections.map((collection) => [
collection.id,
getCollectionUserFacingName(collection),
]),
);

View File

@@ -14,6 +14,19 @@ export const groupFilesByCollectionID = (files: EnteFile[]) =>
return result;
}, new Map<number, EnteFile[]>());
/**
* 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<number, number[]>());
export function getLatestVersionFiles(files: EnteFile[]) {
const latestVersionFiles = new Map<string, EnteFile>();
files.forEach((file) => {