From 34621dd00fb77484bdcfd7d87c177a3570cb083b Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 19 May 2025 17:58:40 +0530 Subject: [PATCH 1/6] notes --- .../Collections/CollectionHeader.tsx | 4 +- web/packages/media/collection.ts | 54 ++++++++++++++++++- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/web/apps/photos/src/components/Collections/CollectionHeader.tsx b/web/apps/photos/src/components/Collections/CollectionHeader.tsx index 456ed16038..aa85534a57 100644 --- a/web/apps/photos/src/components/Collections/CollectionHeader.tsx +++ b/web/apps/photos/src/components/Collections/CollectionHeader.tsx @@ -582,9 +582,9 @@ const EmptyTrashQuickOption: React.FC = ({ onClick }) => ( ); const showDownloadQuickOption = (type: CollectionSummaryType) => + type == "album" || type == "folder" || type == "favorites" || - type == "album" || type == "uncategorized" || type == "hiddenItems" || type == "incomingShareViewer" || @@ -620,10 +620,10 @@ const DownloadQuickOption: React.FC = ({ ); const showShareQuickOption = (type: CollectionSummaryType) => + type == "album" || type == "folder" || (type == "favorites" && /* TODO(FAV): */ settingsSnapshot().isInternalUser) || - type == "album" || type == "outgoingShare" || type == "sharedOnlyViaLink" || type == "archived" || diff --git a/web/packages/media/collection.ts b/web/packages/media/collection.ts index cf078880db..03469265e1 100644 --- a/web/packages/media/collection.ts +++ b/web/packages/media/collection.ts @@ -6,10 +6,55 @@ import { ItemVisibility } from "ente-media/file-metadata"; // TODO: Audit this file -export type CollectionType = "folder" | "favorites" | "album" | "uncategorized"; +/** + * The type of a collection. + * + * - "album" - A regular "Ente Album" that the user sees in their library. + * + * - "folder" - An Ente Album that is also associated with an OS album on the + * user's mobile device. + * + * A collection of type "folder" is created by the mobile app if there is an + * associated on-device album for the new Ente album being created. + * + * The web/desktop app does not create collections of type "folder", and + * otherwise treats them as aliases for "album". + * + * - "favorites" - A special collection consisting of the items that the user + * has marked as their favorites. + * + * The user can have at most one collection of type "favorites" (enforced at + * remote). This collection is created on demand by the client where the user + * first marks an item as a favorite. The user can choose to share their + * "favorites" with other users, so it is possible for there to be multiple + * collections of type "favorites" present in our local database, however only + * one of those will belong to the logged in user (cf `owner.id`). + * + * - "uncategorized" - A special collection consisting of items that do not + * belong to any other collection. + * + * In the remote schema, each item ({@link EnteFile}) is always associated + * with a collection. The same item may belong to multiple collections (See: + * [Note: Collection File]), but it must belong to at least one collection. + * + * In some scenarios, e.g. when deleting the last collection to which a file + * belongs, the file would thus get orphaned and violate the schema + * invariants. So in such cases, the client which is performing the + * corresponding operation moves the file to the user's special + * "uncategorized" collection, creating it if needed. + * + * Similar to "favorites", the user can have only one "uncategorized" + * collection. However, unlike "favorites", the "uncategorized" collection + * cannot be shared. + */ +export type CollectionType = "album" | "folder" | "favorites" | "uncategorized"; export type CollectionRole = "VIEWER" | "OWNER" | "COLLABORATOR" | "UNKNOWN"; +/** + * Information about the user associated with a collection, either as an owner, + * or as someone with whom the collection has been shared with. + */ export interface CollectionUser { id: number; email: string; @@ -25,6 +70,13 @@ export interface EncryptedCollection { * all collections on an Ente instance (i.e., it is not scoped to a user). */ id: number; + /** + * Information about the user who owns the collection. + * + * Each collection is owned by exactly one user. The owner may optionally + * choose to share it with additional users, granting them varying level of + * privileges. + */ owner: CollectionUser; // collection name was unencrypted in the past, so we need to keep it as optional name?: string; From 191db47d796f1b65ac7c5a4c98b7e481d80a0d34 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 19 May 2025 18:22:27 +0530 Subject: [PATCH 2/6] type --- web/packages/media/collection.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/web/packages/media/collection.ts b/web/packages/media/collection.ts index 03469265e1..8d6d0f1bcf 100644 --- a/web/packages/media/collection.ts +++ b/web/packages/media/collection.ts @@ -49,7 +49,11 @@ import { ItemVisibility } from "ente-media/file-metadata"; */ export type CollectionType = "album" | "folder" | "favorites" | "uncategorized"; -export type CollectionRole = "VIEWER" | "OWNER" | "COLLABORATOR" | "UNKNOWN"; +export type CollectionUserRole = + | "VIEWER" + | "OWNER" + | "COLLABORATOR" + | "UNKNOWN"; /** * Information about the user associated with a collection, either as an owner, @@ -58,7 +62,7 @@ export type CollectionRole = "VIEWER" | "OWNER" | "COLLABORATOR" | "UNKNOWN"; export interface CollectionUser { id: number; email: string; - role: CollectionRole; + role: CollectionUserRole; } export interface EncryptedCollection { From 1f7e74131ba9639c546d92513954b18f5d0a4ee1 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 19 May 2025 19:05:37 +0530 Subject: [PATCH 3/6] + --- web/packages/media/collection.ts | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/web/packages/media/collection.ts b/web/packages/media/collection.ts index 8d6d0f1bcf..63552bb6a5 100644 --- a/web/packages/media/collection.ts +++ b/web/packages/media/collection.ts @@ -49,10 +49,24 @@ import { ItemVisibility } from "ente-media/file-metadata"; */ export type CollectionType = "album" | "folder" | "favorites" | "uncategorized"; +/** + * The privilege level of a participant associated with a collection. + * + * - "VIEWER" - Has read-only access to files in the collection. + * + * - "COLLABORATOR" - Can additionally add files from the collection, and remove + * files that they added from the collection. + * + * - "OWNER" - The owner of the collection. Can remove any file, including those + * added by other users, from the collection. + * + * It is guaranteed that a there will be exactly one participant of type OWNER, + * and their user ID will be the same as the collection `owner.id`. + */ export type CollectionUserRole = | "VIEWER" - | "OWNER" | "COLLABORATOR" + | "OWNER" | "UNKNOWN"; /** From 8bad8b87b11c9f474a578f6b626259c176e0559d Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 19 May 2025 19:23:37 +0530 Subject: [PATCH 4/6] more context from chat --- web/packages/media/collection.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/packages/media/collection.ts b/web/packages/media/collection.ts index 63552bb6a5..745cb63ce7 100644 --- a/web/packages/media/collection.ts +++ b/web/packages/media/collection.ts @@ -17,6 +17,11 @@ import { ItemVisibility } from "ente-media/file-metadata"; * A collection of type "folder" is created by the mobile app if there is an * associated on-device album for the new Ente album being created. * + * This separation between "album" and "folder" allows different mobile + * clients to push to the same Folder ("Camera", "Screenshots"), not allowing + * for duplicate folders with the same name, while still allowing users to + * create different albums with the same name. + * * The web/desktop app does not create collections of type "folder", and * otherwise treats them as aliases for "album". * @@ -55,7 +60,7 @@ export type CollectionType = "album" | "folder" | "favorites" | "uncategorized"; * - "VIEWER" - Has read-only access to files in the collection. * * - "COLLABORATOR" - Can additionally add files from the collection, and remove - * files that they added from the collection. + * files that they added from the collection (i.e., files they "own"). * * - "OWNER" - The owner of the collection. Can remove any file, including those * added by other users, from the collection. From aadab316f620785ffefc5daac1e5b09fc37458c0 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 19 May 2025 19:26:06 +0530 Subject: [PATCH 5/6] + --- web/packages/media/collection.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/web/packages/media/collection.ts b/web/packages/media/collection.ts index 745cb63ce7..b2021fa92c 100644 --- a/web/packages/media/collection.ts +++ b/web/packages/media/collection.ts @@ -79,8 +79,18 @@ export type CollectionUserRole = * or as someone with whom the collection has been shared with. */ export interface CollectionUser { + /** + * The ID of the underlying {@link User} that this {@link CollectionUser} + * stands for. + */ id: number; + /** + * The email of the user. + */ email: string; + /** + * The association / privilege level of the user with the collection. + */ role: CollectionUserRole; } From b7cd55aec3e329a09972e43f041ba660a5b9c95f Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Mon, 19 May 2025 19:44:29 +0530 Subject: [PATCH 6/6] ctx "OwnerEmail currently is always present for shared collection but missing for owned collection." --- web/packages/media/collection.ts | 13 +++++++++++-- .../new/photos/components/gallery/helpers.ts | 7 ++++--- .../new/photos/components/gallery/reducer.ts | 2 +- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/web/packages/media/collection.ts b/web/packages/media/collection.ts index b2021fa92c..5581cac124 100644 --- a/web/packages/media/collection.ts +++ b/web/packages/media/collection.ts @@ -86,12 +86,21 @@ export interface CollectionUser { id: number; /** * The email of the user. + * + * - The email is present for the {@link owner} only for shared collections. + * - The email is present for all {@link sharees}. + * - Remote uses a blank string to indicate absent values. */ - email: string; + email?: string; /** * The association / privilege level of the user with the collection. + * + * - The role is not present blank for the {@link owner}. + * - The role is present, and one of "VIEWER" and "COLLABORATOR" for the + * {@link sharees}. + * - Remote uses a blank string to indicate absent values. */ - role: CollectionUserRole; + role?: CollectionUserRole; } export interface EncryptedCollection { diff --git a/web/packages/new/photos/components/gallery/helpers.ts b/web/packages/new/photos/components/gallery/helpers.ts index fde1b89526..afce9688d9 100644 --- a/web/packages/new/photos/components/gallery/helpers.ts +++ b/web/packages/new/photos/components/gallery/helpers.ts @@ -52,7 +52,7 @@ export const constructUserIDToEmailMap = ( // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (sharees) { sharees.forEach((item) => { - if (item.id !== user.id) + if (item.id !== user.id && item.email) userIDToEmailMap.set(item.id, item.email); }); } @@ -78,10 +78,11 @@ export const createShareeSuggestionEmails = ( // type for Collection. // // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - return (sharees ?? []).map((sharee) => sharee.email); + return (sharees ?? []).map(({ email }) => email); } }) - .flat(); + .flat() + .filter((e) => e !== undefined); // Add family members. if (familyData) { diff --git a/web/packages/new/photos/components/gallery/reducer.ts b/web/packages/new/photos/components/gallery/reducer.ts index 6881274c0d..891b972cfa 100644 --- a/web/packages/new/photos/components/gallery/reducer.ts +++ b/web/packages/new/photos/components/gallery/reducer.ts @@ -1442,7 +1442,7 @@ const createCollectionSummaries = ( // particular favorite as a prefix to disambiguate this collection // from the user's own favorites. // TODO(FAV): localize - const initial = collection.owner.email.at(0)?.toUpperCase(); + const initial = collection.owner.email?.at(0)?.toUpperCase(); if (initial) { // TODO(FAV): // name = `${initial}'s ${t("favorites")}`;