wip: checkpoint
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<number>();
|
||||
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<number>(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;
|
||||
|
||||
@@ -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<number, number> {
|
||||
const collectionIDToFileMap = groupFilesBasedOnCollectionID(files);
|
||||
const collectionFilesCount = new Map<number, number>();
|
||||
for (const [id, files] of collectionIDToFileMap) {
|
||||
collectionFilesCount.set(id, files.length);
|
||||
}
|
||||
return collectionFilesCount;
|
||||
}
|
||||
|
||||
export function getSectionSummaries(
|
||||
files: EnteFile[],
|
||||
trashedFiles: EnteFile[],
|
||||
archivedCollections: Set<number>,
|
||||
): 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<number>,
|
||||
): 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<number>,
|
||||
): 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[],
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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<number>(
|
||||
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,
|
||||
|
||||
@@ -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<number>();
|
||||
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,
|
||||
|
||||
370
web/packages/new/photos/components/gallery/reducer.ts
Normal file
370
web/packages/new/photos/components/gallery/reducer.ts
Normal file
@@ -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<GalleryState, GalleryAction> = (
|
||||
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<number>();
|
||||
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<number>(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<number>();
|
||||
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<number>(
|
||||
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<number>,
|
||||
): 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<number>,
|
||||
): 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<number, number> {
|
||||
const collectionIDToFileMap = groupFilesBasedOnCollectionID(files);
|
||||
const collectionFilesCount = new Map<number, number>();
|
||||
for (const [id, files] of collectionIDToFileMap) {
|
||||
collectionFilesCount.set(id, files.length);
|
||||
}
|
||||
return collectionFilesCount;
|
||||
}
|
||||
|
||||
function getAllSectionVisibleFiles(
|
||||
files: EnteFile[],
|
||||
archivedCollections: Set<number>,
|
||||
): EnteFile[] {
|
||||
const allSectionVisibleFiles = getUniqueFiles(
|
||||
files.filter((file) => {
|
||||
if (
|
||||
isArchivedFile(file) ||
|
||||
archivedCollections.has(file.collectionID)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
);
|
||||
return allSectionVisibleFiles;
|
||||
}
|
||||
@@ -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<GalleryState, GalleryAction> = (
|
||||
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);
|
||||
30
web/packages/new/photos/services/collection/index.ts
Normal file
30
web/packages/new/photos/services/collection/index.ts
Normal file
@@ -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<number>(
|
||||
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;
|
||||
}
|
||||
@@ -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<T>(
|
||||
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<T>(
|
||||
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<T>(
|
||||
export const getNewMagicMetadata = <T>(): MagicMetadataCore<T> => {
|
||||
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 = <T>(): MagicMetadataCore<T> => {
|
||||
|
||||
export const getNonEmptyMagicMetadataProps = <T>(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,
|
||||
Reference in New Issue
Block a user