diff --git a/web/apps/photos/src/components/Collections/CollectionHeader.tsx b/web/apps/photos/src/components/Collections/CollectionHeader.tsx index 0a5a18d92c..599c7348ec 100644 --- a/web/apps/photos/src/components/Collections/CollectionHeader.tsx +++ b/web/apps/photos/src/components/Collections/CollectionHeader.tsx @@ -13,6 +13,10 @@ import type { CollectionSummary, CollectionSummaryType, } from "@/new/photos/services/collection/ui"; +import { + isArchivedCollection, + isPinnedCollection, +} from "@/new/photos/services/magic-metadata"; import { AppContext } from "@/new/photos/types/context"; import { HorizontalFlex } from "@ente/shared/components/Container"; import OverflowMenu, { @@ -54,7 +58,6 @@ import { HIDDEN_ITEMS_SECTION, isHiddenCollection, } from "utils/collection"; -import { isArchivedCollection, isPinnedCollection } from "utils/magicMetadata"; interface CollectionHeaderProps { collectionSummary: CollectionSummary; diff --git a/web/apps/photos/src/components/Sidebar/index.tsx b/web/apps/photos/src/components/Sidebar/index.tsx index 0fc85dad15..1ae907e4d1 100644 --- a/web/apps/photos/src/components/Sidebar/index.tsx +++ b/web/apps/photos/src/components/Sidebar/index.tsx @@ -10,6 +10,11 @@ import { savedLogs } from "@/base/log-web"; import { customAPIHost } from "@/base/origins"; import { downloadString } from "@/base/utils/web"; import { downloadAppDialogAttributes } from "@/new/photos/components/utils/download"; +import { + ARCHIVE_SECTION, + DUMMY_UNCATEGORIZED_COLLECTION, + TRASH_SECTION, +} from "@/new/photos/services/collection"; import type { CollectionSummaries } from "@/new/photos/services/collection/ui"; import { AppContext, useAppContext } from "@/new/photos/types/context"; import { initiateEmail, openURL } from "@/new/photos/utils/web"; @@ -71,11 +76,6 @@ import { isSubscriptionCancelled, isSubscriptionPastDue, } from "utils/billing"; -import { - ARCHIVE_SECTION, - DUMMY_UNCATEGORIZED_COLLECTION, - TRASH_SECTION, -} from "utils/collection"; import { isFamilyAdmin, isPartOfFamily } from "utils/user/family"; import { testUpload } from "../../../tests/upload.test"; import { MemberSubscriptionManage } from "../MemberSubscriptionManage"; diff --git a/web/apps/photos/src/components/pages/gallery/SelectedFileOptions.tsx b/web/apps/photos/src/components/pages/gallery/SelectedFileOptions.tsx index e164f24f80..453c6d8684 100644 --- a/web/apps/photos/src/components/pages/gallery/SelectedFileOptions.tsx +++ b/web/apps/photos/src/components/pages/gallery/SelectedFileOptions.tsx @@ -2,6 +2,11 @@ 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 { + ALL_SECTION, + ARCHIVE_SECTION, + TRASH_SECTION, +} from "@/new/photos/services/collection"; import { AppContext } from "@/new/photos/types/context"; import { FluidContainer } from "@ente/shared/components/Container"; import ClockIcon from "@mui/icons-material/AccessTime"; @@ -20,12 +25,7 @@ import VisibilityOutlined from "@mui/icons-material/VisibilityOutlined"; import { Box, IconButton, Stack, Tooltip } from "@mui/material"; import { t } from "i18next"; import { useContext } from "react"; -import { - ALL_SECTION, - ARCHIVE_SECTION, - COLLECTION_OPS_TYPE, - TRASH_SECTION, -} from "utils/collection"; +import { COLLECTION_OPS_TYPE } from "utils/collection"; import { FILE_OPS_TYPE } from "utils/file"; import { formatNumber } from "utils/number/format"; import { getTrashFilesMessage } from "utils/ui"; diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index 19d0c96024..204252d36d 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -62,7 +62,6 @@ import { clearKeys, getKey, } from "@ente/shared/storage/sessionStorage"; -import type { User } from "@ente/shared/user/types"; import ArrowBack from "@mui/icons-material/ArrowBack"; import FileUploadOutlinedIcon from "@mui/icons-material/FileUploadOutlined"; import MenuIcon from "@mui/icons-material/Menu"; @@ -150,7 +149,7 @@ import { getUniqueFiles, handleFileOps, } from "utils/file"; -import { isArchivedFile } from "utils/magicMetadata"; +import { isArchivedFile } from "@/new/photos/services/magic-metadata"; import { getSessionExpiredMessage } from "utils/ui"; import { getLocalFamilyData } from "utils/user/family"; @@ -873,62 +872,6 @@ export default function Gallery() { }; }; - const setDerivativeState = ( - user: User, - collections: Collection[], - hiddenCollections: Collection[], - files: EnteFile[], - trashedFiles: EnteFile[], - hiddenFiles: EnteFile[], - ) => { - let favItemIds = new Set(); - for (const collection of collections) { - if (collection.type === CollectionType.favorites) { - favItemIds = new Set( - files - .filter((file) => file.collectionID === collection.id) - .map((file): number => file.id), - ); - break; - } - } - setFavItemIds(favItemIds); - const archivedCollections = getArchivedCollections(collections); - setArchivedCollections(archivedCollections); - const defaultHiddenCollectionIDs = - getDefaultHiddenCollectionIDs(hiddenCollections); - setDefaultHiddenCollectionIDs(defaultHiddenCollectionIDs); - const hiddenFileIds = new Set(hiddenFiles.map((f) => f.id)); - setHiddenFileIds(hiddenFileIds); - const collectionSummaries = getCollectionSummaries( - user, - collections, - files, - ); - const sectionSummaries = getSectionSummaries( - files, - trashedFiles, - archivedCollections, - ); - const hiddenCollectionSummaries = getCollectionSummaries( - user, - hiddenCollections, - hiddenFiles, - ); - const hiddenItemsSummaries = getHiddenItemsSummary( - hiddenFiles, - hiddenCollections, - ); - hiddenCollectionSummaries.set( - HIDDEN_ITEMS_SECTION, - hiddenItemsSummaries, - ); - setCollectionSummaries( - mergeMaps(collectionSummaries, sectionSummaries), - ); - setHiddenCollectionSummaries(hiddenCollectionSummaries); - }; - const setFilesDownloadProgressAttributesCreator: SetFilesDownloadProgressAttributesCreator = (folderName, collectionID, isHidden) => { const id = filesDownloadProgressAttributesList?.length ?? 0; diff --git a/web/apps/photos/src/services/collectionService.ts b/web/apps/photos/src/services/collectionService.ts index 40576bc8aa..0e1f5c75dc 100644 --- a/web/apps/photos/src/services/collectionService.ts +++ b/web/apps/photos/src/services/collectionService.ts @@ -31,6 +31,12 @@ import { CollectionsSortBy, } from "@/new/photos/services/collection/ui"; import { getLocalFiles, sortFiles } from "@/new/photos/services/files"; +import { + isArchivedCollection, + isArchivedFile, + isPinnedCollection, + updateMagicMetadata, +} from "@/new/photos/services/magic-metadata"; import { batch } from "@/utils/array"; import { CustomError } from "@ente/shared/error"; import HTTPService from "@ente/shared/network/HTTPService"; @@ -60,12 +66,6 @@ import { isValidMoveTarget, } from "utils/collection"; import { getUniqueFiles, groupFilesBasedOnCollectionID } from "utils/file"; -import { - isArchivedCollection, - isArchivedFile, - isPinnedCollection, - updateMagicMetadata, -} from "utils/magicMetadata"; import { UpdateMagicMetadataRequest } from "./fileService"; import { getPublicKey } from "./userService"; @@ -1107,156 +1107,6 @@ function compareCollectionsLatestFile(first: EnteFile, second: EnteFile) { } } -export function getCollectionSummaries( - user: User, - collections: Collection[], - files: EnteFile[], -): CollectionSummaries { - const collectionSummaries: CollectionSummaries = new Map(); - const collectionLatestFiles = getCollectionLatestFiles(files); - const collectionCoverFiles = getCollectionCoverFiles(files, collections); - const collectionFilesCount = getCollectionsFileCount(files); - - let hasUncategorizedCollection = false; - for (const collection of collections) { - if ( - !hasUncategorizedCollection && - collection.type === CollectionType.uncategorized - ) { - hasUncategorizedCollection = true; - } - let type: CollectionSummaryType; - if (isIncomingShare(collection, user)) { - if (isIncomingCollabShare(collection, user)) { - type = "incomingShareCollaborator"; - } else { - type = "incomingShareViewer"; - } - } else if (isOutgoingShare(collection, user)) { - type = "outgoingShare"; - } else if (isSharedOnlyViaLink(collection)) { - type = "sharedOnlyViaLink"; - } else if (isArchivedCollection(collection)) { - type = "archived"; - } else if (isDefaultHiddenCollection(collection)) { - type = "defaultHidden"; - } else if (isPinnedCollection(collection)) { - type = "pinned"; - } else { - // Directly use the collection type - // TODO: The constants can be aligned once collection type goes from - // an enum to an union. - switch (collection.type) { - case CollectionType.folder: - type = "folder"; - break; - case CollectionType.favorites: - type = "favorites"; - break; - case CollectionType.album: - type = "album"; - break; - case CollectionType.uncategorized: - type = "uncategorized"; - break; - } - } - - let CollectionSummaryItemName: string; - if (type == "uncategorized") { - CollectionSummaryItemName = t("section_uncategorized"); - } else if (type == "favorites") { - CollectionSummaryItemName = t("favorites"); - } else { - CollectionSummaryItemName = collection.name; - } - - collectionSummaries.set(collection.id, { - id: collection.id, - name: CollectionSummaryItemName, - latestFile: collectionLatestFiles.get(collection.id), - coverFile: collectionCoverFiles.get(collection.id), - fileCount: collectionFilesCount.get(collection.id) ?? 0, - updationTime: collection.updationTime, - type: type, - order: collection.magicMetadata?.data?.order ?? 0, - }); - } - if (!hasUncategorizedCollection) { - collectionSummaries.set( - DUMMY_UNCATEGORIZED_COLLECTION, - getDummyUncategorizedCollectionSummary(), - ); - } - - return collectionSummaries; -} - -function getCollectionsFileCount(files: EnteFile[]): Map { - const collectionIDToFileMap = groupFilesBasedOnCollectionID(files); - const collectionFilesCount = new Map(); - for (const [id, files] of collectionIDToFileMap) { - collectionFilesCount.set(id, files.length); - } - return collectionFilesCount; -} - -export function getSectionSummaries( - files: EnteFile[], - trashedFiles: EnteFile[], - archivedCollections: Set, -): CollectionSummaries { - const collectionSummaries: CollectionSummaries = new Map(); - collectionSummaries.set( - ALL_SECTION, - getAllSectionSummary(files, archivedCollections), - ); - collectionSummaries.set( - TRASH_SECTION, - getTrashedCollectionSummary(trashedFiles), - ); - collectionSummaries.set(ARCHIVE_SECTION, getArchivedSectionSummary(files)); - - return collectionSummaries; -} - -function getAllSectionSummary( - files: EnteFile[], - archivedCollections: Set, -): CollectionSummary { - const allSectionFiles = getAllSectionVisibleFiles( - files, - archivedCollections, - ); - return { - id: ALL_SECTION, - name: t("section_all"), - type: "all", - coverFile: allSectionFiles?.[0], - latestFile: allSectionFiles?.[0], - fileCount: allSectionFiles?.length || 0, - updationTime: allSectionFiles?.[0]?.updationTime, - }; -} - -function getAllSectionVisibleFiles( - files: EnteFile[], - archivedCollections: Set, -): EnteFile[] { - const allSectionVisibleFiles = getUniqueFiles( - files.filter((file) => { - if ( - isArchivedFile(file) || - archivedCollections.has(file.collectionID) - ) { - return false; - } - return true; - }), - ); - return allSectionVisibleFiles; -} - export function getDummyUncategorizedCollectionSummary(): CollectionSummary { return { id: DUMMY_UNCATEGORIZED_COLLECTION, @@ -1269,47 +1119,7 @@ export function getDummyUncategorizedCollectionSummary(): CollectionSummary { }; } -export function getArchivedSectionSummary( - files: EnteFile[], -): CollectionSummary { - const archivedFiles = getUniqueFiles( - files.filter((file) => isArchivedFile(file)), - ); - return { - id: ARCHIVE_SECTION, - name: t("section_archive"), - type: "archive", - coverFile: null, - latestFile: archivedFiles?.[0], - fileCount: archivedFiles?.length, - updationTime: archivedFiles?.[0]?.updationTime, - }; -} -export function getHiddenItemsSummary( - hiddenFiles: EnteFile[], - hiddenCollections: Collection[], -): CollectionSummary { - const defaultHiddenCollectionIds = new Set( - hiddenCollections - .filter((collection) => isDefaultHiddenCollection(collection)) - .map((collection) => collection.id), - ); - const hiddenItems = getUniqueFiles( - hiddenFiles.filter((file) => - defaultHiddenCollectionIds.has(file.collectionID), - ), - ); - return { - id: HIDDEN_ITEMS_SECTION, - name: t("hidden_items"), - type: "hiddenItems", - coverFile: hiddenItems?.[0], - latestFile: hiddenItems?.[0], - fileCount: hiddenItems?.length, - updationTime: hiddenItems?.[0]?.updationTime, - }; -} export function getTrashedCollectionSummary( trashedFiles: EnteFile[], diff --git a/web/apps/photos/src/services/upload/upload-service.ts b/web/apps/photos/src/services/upload/upload-service.ts index f8df56ee2f..113d42b788 100644 --- a/web/apps/photos/src/services/upload/upload-service.ts +++ b/web/apps/photos/src/services/upload/upload-service.ts @@ -26,6 +26,10 @@ import { FileType, type FileTypeInfo } from "@/media/file-type"; import { encodeLivePhoto } from "@/media/live-photo"; import { extractExif } from "@/new/photos/services/exif"; import * as ffmpeg from "@/new/photos/services/ffmpeg"; +import { + getNonEmptyMagicMetadataProps, + updateMagicMetadata, +} from "@/new/photos/services/magic-metadata"; import type { UploadItem } from "@/new/photos/services/upload/types"; import { RANDOM_PERCENTAGE_PROGRESS_FOR_PUT, @@ -40,10 +44,6 @@ import { PublicUploadProps, type LivePhotoAssets, } from "services/upload/uploadManager"; -import { - getNonEmptyMagicMetadataProps, - updateMagicMetadata, -} from "utils/magicMetadata"; import * as convert from "xml-js"; import { tryParseEpochMicrosecondsFromFileName } from "./date"; import publicUploadHttpClient from "./publicUploadHttpClient"; diff --git a/web/apps/photos/src/utils/collection.ts b/web/apps/photos/src/utils/collection.ts index 3fc69b77d5..0966442105 100644 --- a/web/apps/photos/src/utils/collection.ts +++ b/web/apps/photos/src/utils/collection.ts @@ -10,7 +10,12 @@ import { } from "@/media/collection"; import { EnteFile } from "@/media/file"; import { ItemVisibility } from "@/media/file-metadata"; +import { getDefaultHiddenCollectionIDs, isDefaultHiddenCollection, isIncomingShare } from "@/new/photos/services/collection"; import { getAllLocalFiles, getLocalFiles } from "@/new/photos/services/files"; +import { + isArchivedCollection, + updateMagicMetadata, +} from "@/new/photos/services/magic-metadata"; import { safeDirectoryName } from "@/new/photos/utils/native-fs"; import { CustomError } from "@ente/shared/error"; import { LS_KEYS, getData } from "@ente/shared/storage/localStorage"; @@ -33,13 +38,8 @@ import { } from "services/collectionService"; import { SetFilesDownloadProgressAttributes } from "types/gallery"; import { downloadFilesWithProgress } from "utils/file"; -import { isArchivedCollection, updateMagicMetadata } from "utils/magicMetadata"; -export const ARCHIVE_SECTION = -1; -export const TRASH_SECTION = -2; -export const DUMMY_UNCATEGORIZED_COLLECTION = -3; -export const HIDDEN_ITEMS_SECTION = -4; -export const ALL_SECTION = 0; + export enum COLLECTION_OPS_TYPE { ADD, @@ -336,14 +336,6 @@ export const getArchivedCollections = (collections: Collection[]) => { ); }; -export const getDefaultHiddenCollectionIDs = (collections: Collection[]) => { - return new Set( - collections - .filter(isDefaultHiddenCollection) - .map((collection) => collection.id), - ); -}; - export const getUserOwnedCollections = (collections: Collection[]) => { const user: User = getData(LS_KEYS.USER); if (!user?.id) { @@ -352,37 +344,19 @@ export const getUserOwnedCollections = (collections: Collection[]) => { return collections.filter((collection) => collection.owner.id === user.id); }; -export const isDefaultHiddenCollection = (collection: Collection) => - collection.magicMetadata?.data.subType === SUB_TYPE.DEFAULT_HIDDEN; - export const isHiddenCollection = (collection: Collection) => collection.magicMetadata?.data.visibility === ItemVisibility.hidden; export const isQuickLinkCollection = (collection: Collection) => collection.magicMetadata?.data.subType === SUB_TYPE.QUICK_LINK_COLLECTION; -export function isOutgoingShare(collection: Collection, user: User): boolean { - return collection.owner.id === user.id && collection.sharees?.length > 0; -} -export function isIncomingShare(collection: Collection, user: User) { - return collection.owner.id !== user.id; -} export function isIncomingViewerShare(collection: Collection, user: User) { const sharee = collection.sharees?.find((sharee) => sharee.id === user.id); return sharee?.role === COLLECTION_ROLE.VIEWER; } -export function isIncomingCollabShare(collection: Collection, user: User) { - const sharee = collection.sharees?.find((sharee) => sharee.id === user.id); - return sharee?.role === COLLECTION_ROLE.COLLABORATOR; -} - -export function isSharedOnlyViaLink(collection: Collection) { - return collection.publicURLs?.length && !collection.sharees?.length; -} - export function isValidMoveTarget( sourceCollectionID: number, targetCollection: Collection, diff --git a/web/apps/photos/src/utils/file/index.ts b/web/apps/photos/src/utils/file/index.ts index 9fffe73d50..7a841779d3 100644 --- a/web/apps/photos/src/utils/file/index.ts +++ b/web/apps/photos/src/utils/file/index.ts @@ -17,6 +17,10 @@ import { FileType } from "@/media/file-type"; import { decodeLivePhoto } from "@/media/live-photo"; import DownloadManager from "@/new/photos/services/download"; import { updateExifIfNeededAndPossible } from "@/new/photos/services/exif-update"; +import { + isArchivedFile, + updateMagicMetadata, +} from "@/new/photos/services/magic-metadata"; import { detectFileTypeInfo } from "@/new/photos/utils/detect-type"; import { safeFileName } from "@/new/photos/utils/native-fs"; import { writeStream } from "@/new/photos/utils/native-stream"; @@ -39,7 +43,6 @@ import { SetFilesDownloadProgressAttributes, SetFilesDownloadProgressAttributesCreator, } from "types/gallery"; -import { isArchivedFile, updateMagicMetadata } from "utils/magicMetadata"; export enum FILE_OPS_TYPE { DOWNLOAD, @@ -260,20 +263,6 @@ export async function getFileFromURL(fileURL: string, name: string) { return fileFile; } -export function getUniqueFiles(files: EnteFile[]) { - const idSet = new Set(); - const uniqueFiles = files.filter((file) => { - if (!idSet.has(file.id)) { - idSet.add(file.id); - return true; - } else { - return false; - } - }); - - return uniqueFiles; -} - export async function downloadFilesWithProgress( files: EnteFile[], downloadDirPath: string, diff --git a/web/packages/new/photos/components/gallery/reducer.ts b/web/packages/new/photos/components/gallery/reducer.ts new file mode 100644 index 0000000000..a83c270a34 --- /dev/null +++ b/web/packages/new/photos/components/gallery/reducer.ts @@ -0,0 +1,370 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/** + * @file code that really belongs to pages/gallery.tsx itself (or related + * files), but it written here in a separate file so that we can write in this + * package that has TypeScript strict mode enabled. + * + * Once the original gallery.tsx is strict mode, this code can be inlined back + * there. + */ + +import { CollectionType, type Collection } from "@/media/collection"; +import type { EnteFile } from "@/media/file"; +import type { User } from "@ente/shared/user/types"; +import { t } from "i18next"; +import React, { useReducer } from "react"; +import { + ALL_SECTION, + ARCHIVE_SECTION, + DUMMY_UNCATEGORIZED_COLLECTION, + getDefaultHiddenCollectionIDs, + HIDDEN_ITEMS_SECTION, + isDefaultHiddenCollection, + isIncomingCollabShare, + isIncomingShare, + TRASH_SECTION, +} from "../../services/collection"; +import type { + CollectionSummaries, + CollectionSummary, + CollectionSummaryType, +} from "../../services/collection/ui"; +import { + isArchivedCollection, + isArchivedFile, + isPinnedCollection, +} from "../../services/magic-metadata"; +import type { Person } from "../../services/ml/people"; + +/** + * Derived UI state backing the gallery. + * + * This might be different from the actual different from the actual underlying + * state since there might be unsynced data (hidden or deleted that have not yet + * been synced with remote) that should be temporarily taken into account for + * the UI state until the operation completes. + */ +export interface GalleryState { + filteredData: EnteFile[]; + /** + * The currently selected person, if any. + * + * Whenever this is present, it is guaranteed to be one of the items from + * within {@link people}. + */ + activePerson: Person | undefined; + /** + * The list of people to show. + */ + people: Person[] | undefined; +} + +// TODO: dummy actions for gradual migration to reducers +export type GalleryAction = + | { + type: "set"; + filteredData: EnteFile[]; + galleryPeopleState: + | { activePerson: Person | undefined; people: Person[] } + | undefined; + } + | { type: "dummy" }; + +const initialGalleryState: GalleryState = { + filteredData: [], + activePerson: undefined, + people: [], +}; + +const galleryReducer: React.Reducer = ( + state, + action, +) => { + switch (action.type) { + case "dummy": + return state; + case "set": + return { + ...state, + filteredData: action.filteredData, + activePerson: action.galleryPeopleState?.activePerson, + people: action.galleryPeopleState?.people, + }; + } +}; + +export const useGalleryReducer = () => + useReducer(galleryReducer, initialGalleryState); + +export const setDerivativeState = ( + user: User, + collections: Collection[], + hiddenCollections: Collection[], + files: EnteFile[], + trashedFiles: EnteFile[], + hiddenFiles: EnteFile[], +) => { + let favItemIds = new Set(); + for (const collection of collections) { + if (collection.type === CollectionType.favorites) { + favItemIds = new Set( + files + .filter((file) => file.collectionID === collection.id) + .map((file): number => file.id), + ); + break; + } + } + setFavItemIds(favItemIds); + const archivedCollections = getArchivedCollections(collections); + setArchivedCollections(archivedCollections); + const defaultHiddenCollectionIDs = + getDefaultHiddenCollectionIDs(hiddenCollections); + setDefaultHiddenCollectionIDs(defaultHiddenCollectionIDs); + const hiddenFileIds = new Set(hiddenFiles.map((f) => f.id)); + setHiddenFileIds(hiddenFileIds); + const collectionSummaries = getCollectionSummaries( + user, + collections, + files, + ); + const sectionSummaries = getSectionSummaries( + files, + trashedFiles, + archivedCollections, + ); + const hiddenCollectionSummaries = getCollectionSummaries( + user, + hiddenCollections, + hiddenFiles, + ); + const hiddenItemsSummaries = getHiddenItemsSummary( + hiddenFiles, + hiddenCollections, + ); + hiddenCollectionSummaries.set(HIDDEN_ITEMS_SECTION, hiddenItemsSummaries); + setCollectionSummaries(mergeMaps(collectionSummaries, sectionSummaries)); + setHiddenCollectionSummaries(hiddenCollectionSummaries); +}; + +export function getUniqueFiles(files: EnteFile[]) { + const idSet = new Set(); + const uniqueFiles = files.filter((file) => { + if (!idSet.has(file.id)) { + idSet.add(file.id); + return true; + } else { + return false; + } + }); + + return uniqueFiles; +} + +export const getArchivedCollections = (collections: Collection[]) => { + return new Set( + collections + .filter(isArchivedCollection) + .map((collection) => collection.id), + ); +}; + +export function getCollectionSummaries( + user: User, + collections: Collection[], + files: EnteFile[], +): CollectionSummaries { + const collectionSummaries: CollectionSummaries = new Map(); + const collectionLatestFiles = getCollectionLatestFiles(files); + const collectionCoverFiles = getCollectionCoverFiles(files, collections); + const collectionFilesCount = getCollectionsFileCount(files); + + let hasUncategorizedCollection = false; + for (const collection of collections) { + if ( + !hasUncategorizedCollection && + collection.type === CollectionType.uncategorized + ) { + hasUncategorizedCollection = true; + } + let type: CollectionSummaryType; + if (isIncomingShare(collection, user)) { + if (isIncomingCollabShare(collection, user)) { + type = "incomingShareCollaborator"; + } else { + type = "incomingShareViewer"; + } + } else if (isOutgoingShare(collection, user)) { + type = "outgoingShare"; + } else if (isSharedOnlyViaLink(collection)) { + type = "sharedOnlyViaLink"; + } else if (isArchivedCollection(collection)) { + type = "archived"; + } else if (isDefaultHiddenCollection(collection)) { + type = "defaultHidden"; + } else if (isPinnedCollection(collection)) { + type = "pinned"; + } else { + // Directly use the collection type + // TODO: The constants can be aligned once collection type goes from + // an enum to an union. + switch (collection.type) { + case CollectionType.folder: + type = "folder"; + break; + case CollectionType.favorites: + type = "favorites"; + break; + case CollectionType.album: + type = "album"; + break; + case CollectionType.uncategorized: + type = "uncategorized"; + break; + } + } + + let CollectionSummaryItemName: string; + if (type == "uncategorized") { + CollectionSummaryItemName = t("section_uncategorized"); + } else if (type == "favorites") { + CollectionSummaryItemName = t("favorites"); + } else { + CollectionSummaryItemName = collection.name; + } + + collectionSummaries.set(collection.id, { + id: collection.id, + name: CollectionSummaryItemName, + latestFile: collectionLatestFiles.get(collection.id), + coverFile: collectionCoverFiles.get(collection.id), + fileCount: collectionFilesCount.get(collection.id) ?? 0, + updationTime: collection.updationTime, + type: type, + order: collection.magicMetadata?.data?.order ?? 0, + }); + } + if (!hasUncategorizedCollection) { + collectionSummaries.set( + DUMMY_UNCATEGORIZED_COLLECTION, + getDummyUncategorizedCollectionSummary(), + ); + } + + return collectionSummaries; +} + +export function isOutgoingShare(collection: Collection, user: User): boolean { + return collection.owner.id === user.id && collection.sharees?.length > 0; +} + +export function isSharedOnlyViaLink(collection: Collection) { + return collection.publicURLs?.length && !collection.sharees?.length; +} + +export function getHiddenItemsSummary( + hiddenFiles: EnteFile[], + hiddenCollections: Collection[], +): CollectionSummary { + const defaultHiddenCollectionIds = new Set( + hiddenCollections + .filter((collection) => isDefaultHiddenCollection(collection)) + .map((collection) => collection.id), + ); + const hiddenItems = getUniqueFiles( + hiddenFiles.filter((file) => + defaultHiddenCollectionIds.has(file.collectionID), + ), + ); + return { + id: HIDDEN_ITEMS_SECTION, + name: t("hidden_items"), + type: "hiddenItems", + coverFile: hiddenItems?.[0], + latestFile: hiddenItems?.[0], + fileCount: hiddenItems?.length, + updationTime: hiddenItems?.[0]?.updationTime, + }; +} + +export function getSectionSummaries( + files: EnteFile[], + trashedFiles: EnteFile[], + archivedCollections: Set, +): CollectionSummaries { + const collectionSummaries: CollectionSummaries = new Map(); + collectionSummaries.set( + ALL_SECTION, + getAllSectionSummary(files, archivedCollections), + ); + collectionSummaries.set( + TRASH_SECTION, + getTrashedCollectionSummary(trashedFiles), + ); + collectionSummaries.set(ARCHIVE_SECTION, getArchivedSectionSummary(files)); + + return collectionSummaries; +} + +export function getArchivedSectionSummary( + files: EnteFile[], +): CollectionSummary { + const archivedFiles = getUniqueFiles( + files.filter((file) => isArchivedFile(file)), + ); + return { + id: ARCHIVE_SECTION, + name: t("section_archive"), + type: "archive", + coverFile: null, + latestFile: archivedFiles?.[0], + fileCount: archivedFiles?.length, + updationTime: archivedFiles?.[0]?.updationTime, + }; +} + +function getAllSectionSummary( + files: EnteFile[], + archivedCollections: Set, +): CollectionSummary { + const allSectionFiles = getAllSectionVisibleFiles( + files, + archivedCollections, + ); + return { + id: ALL_SECTION, + name: t("section_all"), + type: "all", + coverFile: allSectionFiles?.[0], + latestFile: allSectionFiles?.[0], + fileCount: allSectionFiles?.length || 0, + updationTime: allSectionFiles?.[0]?.updationTime, + }; +} + +function getCollectionsFileCount(files: EnteFile[]): Map { + const collectionIDToFileMap = groupFilesBasedOnCollectionID(files); + const collectionFilesCount = new Map(); + for (const [id, files] of collectionIDToFileMap) { + collectionFilesCount.set(id, files.length); + } + return collectionFilesCount; +} + +function getAllSectionVisibleFiles( + files: EnteFile[], + archivedCollections: Set, +): EnteFile[] { + const allSectionVisibleFiles = getUniqueFiles( + files.filter((file) => { + if ( + isArchivedFile(file) || + archivedCollections.has(file.collectionID) + ) { + return false; + } + return true; + }), + ); + return allSectionVisibleFiles; +} diff --git a/web/packages/new/photos/components/gallery/reducer.tsx b/web/packages/new/photos/components/gallery/reducer.tsx deleted file mode 100644 index 4348979391..0000000000 --- a/web/packages/new/photos/components/gallery/reducer.tsx +++ /dev/null @@ -1,72 +0,0 @@ -/** - * @file code that really belongs to pages/gallery.tsx itself (or related - * files), but it written here in a separate file so that we can write in this - * package that has TypeScript strict mode enabled. - * - * Once the original gallery.tsx is strict mode, this code can be inlined back - * there. - */ - -import type { EnteFile } from "@/media/file"; -import React, { useReducer } from "react"; -import type { Person } from "../../services/ml/people"; - -/** - * Derived UI state backing the gallery. - * - * This might be different from the actual different from the actual underlying - * state since there might be unsynced data (hidden or deleted that have not yet - * been synced with remote) that should be temporarily taken into account for - * the UI state until the operation completes. - */ -export interface GalleryState { - filteredData: EnteFile[]; - /** - * The currently selected person, if any. - * - * Whenever this is present, it is guaranteed to be one of the items from - * within {@link people}. - */ - activePerson: Person | undefined; - /** - * The list of people to show. - */ - people: Person[] | undefined; -} - -// TODO: dummy actions for gradual migration to reducers -export type GalleryAction = - | { - type: "set"; - filteredData: EnteFile[]; - galleryPeopleState: - | { activePerson: Person | undefined; people: Person[] } - | undefined; - } - | { type: "dummy" }; - -const initialGalleryState: GalleryState = { - filteredData: [], - activePerson: undefined, - people: [], -}; - -const galleryReducer: React.Reducer = ( - state, - action, -) => { - switch (action.type) { - case "dummy": - return state; - case "set": - return { - ...state, - filteredData: action.filteredData, - activePerson: action.galleryPeopleState?.activePerson, - people: action.galleryPeopleState?.people, - }; - } -}; - -export const useGalleryReducer = () => - useReducer(galleryReducer, initialGalleryState); diff --git a/web/packages/new/photos/services/collection/index.ts b/web/packages/new/photos/services/collection/index.ts new file mode 100644 index 0000000000..bf24e4aa21 --- /dev/null +++ b/web/packages/new/photos/services/collection/index.ts @@ -0,0 +1,30 @@ +import { COLLECTION_ROLE, SUB_TYPE, type Collection } from "@/media/collection"; +import type { User } from "@ente/shared/user/types"; + +export const ARCHIVE_SECTION = -1; +export const TRASH_SECTION = -2; +export const DUMMY_UNCATEGORIZED_COLLECTION = -3; +export const HIDDEN_ITEMS_SECTION = -4; +export const ALL_SECTION = 0; + +export const getDefaultHiddenCollectionIDs = (collections: Collection[]) => { + return new Set( + collections + .filter(isDefaultHiddenCollection) + .map((collection) => collection.id), + ); +}; + +export const isDefaultHiddenCollection = (collection: Collection) => + // TODO: Need to audit the types + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + collection.magicMetadata?.data.subType === SUB_TYPE.DEFAULT_HIDDEN; + +export function isIncomingShare(collection: Collection, user: User) { + return collection.owner.id !== user.id; +} + +export function isIncomingCollabShare(collection: Collection, user: User) { + const sharee = collection.sharees?.find((sharee) => sharee.id === user.id); + return sharee?.role === COLLECTION_ROLE.COLLABORATOR; +} diff --git a/web/apps/photos/src/utils/magicMetadata/index.ts b/web/packages/new/photos/services/magic-metadata.ts similarity index 86% rename from web/apps/photos/src/utils/magicMetadata/index.ts rename to web/packages/new/photos/services/magic-metadata.ts index f414f72eb8..3d47b96468 100644 --- a/web/apps/photos/src/utils/magicMetadata/index.ts +++ b/web/packages/new/photos/services/magic-metadata.ts @@ -1,6 +1,9 @@ +// TODO: Review this file +/* eslint-disable @typescript-eslint/prefer-optional-chain */ +/* eslint-disable @typescript-eslint/no-unnecessary-condition */ import { sharedCryptoWorker } from "@/base/crypto"; import type { Collection } from "@/media/collection"; -import { EnteFile, MagicMetadataCore } from "@/media/file"; +import type { EnteFile, MagicMetadataCore } from "@/media/file"; import { ItemVisibility } from "@/media/file-metadata"; export function isArchivedFile(item: EnteFile): boolean { @@ -57,6 +60,7 @@ export async function updateMagicMetadata( originalMagicMetadata.data = await cryptoWorker.decryptMetadataJSON({ encryptedDataB64: originalMagicMetadata.data, decryptionHeaderB64: originalMagicMetadata.header, + // @ts-expect-error TODO: Need to use zod here. keyB64: decryptionKey, }); } @@ -73,6 +77,7 @@ export async function updateMagicMetadata( const magicMetadata = { ...originalMagicMetadata, data: nonEmptyMagicMetadataProps, + // @ts-expect-error TODO review this file count: Object.keys(nonEmptyMagicMetadataProps).length, }; @@ -82,7 +87,9 @@ export async function updateMagicMetadata( export const getNewMagicMetadata = (): MagicMetadataCore => { return { version: 1, + // @ts-expect-error TODO review this file data: null, + // @ts-expect-error TODO review this file header: null, count: 0, }; @@ -90,6 +97,7 @@ export const getNewMagicMetadata = (): MagicMetadataCore => { export const getNonEmptyMagicMetadataProps = (magicMetadataProps: T): T => { return Object.fromEntries( + // @ts-expect-error TODO review this file Object.entries(magicMetadataProps).filter( // eslint-disable-next-line @typescript-eslint/no-unused-vars ([_, v]) => v !== null && v !== undefined,