From 34211dafef960507594a784834725fd02a1c0b72 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sat, 7 Sep 2024 14:31:48 +0530 Subject: [PATCH 01/18] Nomenclature --- .../new/photos/services/ml/cgroups.ts | 10 ++++++++-- .../new/photos/services/search/worker.ts | 4 ++-- .../new/photos/services/user-entity.ts | 20 +++++++++---------- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/web/packages/new/photos/services/ml/cgroups.ts b/web/packages/new/photos/services/ml/cgroups.ts index 49388eb990..5a2d24957c 100644 --- a/web/packages/new/photos/services/ml/cgroups.ts +++ b/web/packages/new/photos/services/ml/cgroups.ts @@ -1,3 +1,6 @@ +import { masterKeyFromSession } from "@/base/session-store"; +import { pullCGroups } from "../user-entity"; + /** * A cgroup ("cluster group") is a group of clusters (possibly containing a * single cluster) that the user has interacted with. @@ -109,7 +112,7 @@ export interface CGroup { * - They can hide a cluster. This creates an unnamed cgroup so that the * user's other clients know not to show it. */ -export const syncCGroups = () => { +export const syncCGroups = async () => { // 1. Fetch existing cgroups for the user from remote. // 2. Save them to DB. // 3. Prune stale faceIDs from the clusters in the DB. @@ -118,7 +121,10 @@ export const syncCGroups = () => { // // The user can see both the cgroups and clusters in the UI, but only the // cgroups are synced. - // const syncCGroupsWithRemote() + + const masterKey = await masterKeyFromSession(); + await pullCGroups(masterKey); + /* * After clustering, we also do some routine cleanup. Faces belonging to files * that have been deleted (including those in Trash) should be pruned off. diff --git a/web/packages/new/photos/services/search/worker.ts b/web/packages/new/photos/services/search/worker.ts index 714453b419..0492863db0 100644 --- a/web/packages/new/photos/services/search/worker.ts +++ b/web/packages/new/photos/services/search/worker.ts @@ -9,8 +9,8 @@ import * as chrono from "chrono-node"; import { expose } from "comlink"; import { z } from "zod"; import { + pullLocationTags, savedLocationTags, - syncLocationTags, type LocationTag, } from "../user-entity"; import type { @@ -54,7 +54,7 @@ export class SearchWorker { */ async sync(masterKey: Uint8Array) { return Promise.all([ - syncLocationTags(masterKey) + pullLocationTags(masterKey) .then(() => savedLocationTags()) .then((ts) => { this.locationTags = ts.map((t) => ({ diff --git a/web/packages/new/photos/services/user-entity.ts b/web/packages/new/photos/services/user-entity.ts index 7064d21d82..5c12ddaebb 100644 --- a/web/packages/new/photos/services/user-entity.ts +++ b/web/packages/new/photos/services/user-entity.ts @@ -35,16 +35,16 @@ export type EntityType = | "cgroup"; /** - * Sync our local location tags with those on remote. + * Update our local location tags with changes from remote. * * This function fetches all the location tag user entities from remote and - * updates our local database. It uses local state to remember the last time it - * synced, so each subsequent sync is a lightweight diff. + * updates our local database. It uses local state to remember the latest entry + * the last time it did a pull, so each subsequent pull is a lightweight diff. * * @param masterKey The user's master key. This is used to encrypt and decrypt * the location tags specific entity key. */ -export const syncLocationTags = async (masterKey: Uint8Array) => { +export const pullLocationTags = async (masterKey: Uint8Array) => { const decoder = new TextDecoder(); const parse = (id: string, data: Uint8Array): LocationTag => ({ id, @@ -63,7 +63,7 @@ export const syncLocationTags = async (masterKey: Uint8Array) => { return saveLocationTags([...existingTagsByID.values()]); }; - return syncUserEntity("location", masterKey, processBatch); + return pullUserEntities("location", masterKey, processBatch); }; /** Zod schema for the tag that we get from or put to remote. */ @@ -89,7 +89,7 @@ const saveLocationTags = (tags: LocationTag[]) => /** * Return all the location tags that are present locally. * - * Use {@link syncLocationTags} to sync this list with remote. + * Use {@link pullLocationTags} to synchronize this list with remote. */ export const savedLocationTags = async () => LocalLocationTag.array().parse( @@ -97,7 +97,7 @@ export const savedLocationTags = async () => ); /** - * Sync the {@link CGroup} entities that we have locally with remote. + * Update our local cgroups with changes from remote. * * This fetches all the user entities corresponding to the "cgroup" entity type * from remote that have been created, updated or deleted since the last time we @@ -108,7 +108,7 @@ export const savedLocationTags = async () => * @param masterKey The user's master key. This is used to encrypt and decrypt * the cgroup specific entity key. */ -export const syncCGroups = (masterKey: Uint8Array) => { +export const pullCGroups = (masterKey: Uint8Array) => { const parse = async (id: string, data: Uint8Array): Promise => { const rp = RemoteCGroup.parse(JSON.parse(await gunzip(data))); return { @@ -130,7 +130,7 @@ export const syncCGroups = (masterKey: Uint8Array) => { ), ); - return syncUserEntity("cgroup", masterKey, processBatch); + return pullUserEntities("cgroup", masterKey, processBatch); }; const RemoteCGroup = z.object({ @@ -209,7 +209,7 @@ interface UserEntityChange { * The user's {@link masterKey} is used to decrypt (or encrypt, when generating * a new one) the entity key. */ -const syncUserEntity = async ( +const pullUserEntities = async ( type: EntityType, masterKey: Uint8Array, processBatch: (entities: UserEntityChange[]) => Promise, From d6344093b6e5f5084d2dffa0a16b4eb281a614a4 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sat, 7 Sep 2024 14:51:54 +0530 Subject: [PATCH 02/18] OnnA --- .../new/photos/services/ml/cgroups.ts | 20 ++++++++++---- web/packages/new/photos/services/ml/db.ts | 27 +++---------------- .../new/photos/services/user-entity.ts | 13 ++++----- 3 files changed, 25 insertions(+), 35 deletions(-) diff --git a/web/packages/new/photos/services/ml/cgroups.ts b/web/packages/new/photos/services/ml/cgroups.ts index 5a2d24957c..77cdd0ee8f 100644 --- a/web/packages/new/photos/services/ml/cgroups.ts +++ b/web/packages/new/photos/services/ml/cgroups.ts @@ -21,11 +21,14 @@ import { pullCGroups } from "../user-entity"; * cluster, or they may hide an named {@link CGroup}. In both cases, we promote * the cluster to a CGroup if needed so that their request to hide gets synced. * + * cgroups are synced with remote. + * * While in our local representation we separately maintain clusters and link to * them from within CGroups by their clusterID, in the remote representation - * clusters themselves don't get synced. Instead, the "cgroup" entities synced - * with remote contain the clusters within themselves. So a group that gets - * synced with remote looks something like: + * clusters themselves don't get synced. Instead, the cgroup entities synced + * with remote contain the clusters within themselves. + * + * That is, a cgroup that gets synced with remote looks something like: * * { id, name, clusters: [{ clusterID, faceIDs }] } * @@ -66,7 +69,7 @@ export interface CGroup { isHidden: boolean; /** * The ID of the face that should be used as the cover photo for this - * cluster group (if the user has set one). + * cluster group. Optional. * * This is similar to the [@link displayFaceID}, the difference being: * @@ -76,6 +79,13 @@ export interface CGroup { * into effect if the user has not explicitly selected a face. */ avatarFaceID: string | undefined; +} + +/** + * A {@link CGroup} annotated with various in-memory state to make it easier for + * the upper layers of our code to directly use it. + */ +export type AnnotatedCGroup = CGroup & { /** * Locally determined ID of the "best" face that should be used as the * display face, to represent this cluster group in the UI. @@ -84,7 +94,7 @@ export interface CGroup { * {@link avatarFaceID}. */ displayFaceID: string | undefined; -} +}; /** * Syncronize the user's cluster groups with remote, running local clustering if diff --git a/web/packages/new/photos/services/ml/db.ts b/web/packages/new/photos/services/ml/db.ts index 86cc0afd03..833bcffbd3 100644 --- a/web/packages/new/photos/services/ml/db.ts +++ b/web/packages/new/photos/services/ml/db.ts @@ -32,17 +32,17 @@ import type { LocalFaceIndex } from "./face"; * In tandem, these serve as the underlying storage for the indexes maintained * in the ML database. * - * The cluster related object stores are the following: + * The face clustering related object stores are the following: * * - "face-cluster": Contains {@link FaceCluster} objects, one for each * cluster of faces that either the clustering algorithm produced locally or - * were synced from remote. It is indexed by the (cluster) ID. + * were synced from remote. It is indexed by the cluster ID. * * - "cluster-group": Contains {@link CGroup} objects, one for each group of * clusters that were synced from remote. The client can also locally * generate cluster groups on certain user interactions, but these too will * eventually get synced with remote. This object store is indexed by the - * (cgroup) ID. + * cgroup ID. */ interface MLDBSchema extends DBSchema { "file-status": { @@ -441,24 +441,3 @@ export const applyCGroupDiff = async (diff: (string | CGroup)[]) => { ); return tx.done; }; - -/** - * Add or overwrite the entry for the given {@link cgroup}, as identified by - * their {@link id}. - */ -// TODO-Cluster: Remove me -export const saveClusterGroup = async (cgroup: CGroup) => { - const db = await mlDB(); - const tx = db.transaction("cluster-group", "readwrite"); - await Promise.all([tx.store.put(cgroup), tx.done]); -}; - -/** - * Delete the entry (if any) for the cluster group with the given {@link id}. - */ -// TODO-Cluster: Remove me -export const deleteClusterGroup = async (id: string) => { - const db = await mlDB(); - const tx = db.transaction("cluster-group", "readwrite"); - await Promise.all([tx.store.delete(id), tx.done]); -}; diff --git a/web/packages/new/photos/services/user-entity.ts b/web/packages/new/photos/services/user-entity.ts index 5c12ddaebb..731a39d565 100644 --- a/web/packages/new/photos/services/user-entity.ts +++ b/web/packages/new/photos/services/user-entity.ts @@ -110,14 +110,13 @@ export const savedLocationTags = async () => */ export const pullCGroups = (masterKey: Uint8Array) => { const parse = async (id: string, data: Uint8Array): Promise => { - const rp = RemoteCGroup.parse(JSON.parse(await gunzip(data))); + const r = RemoteCGroup.parse(JSON.parse(await gunzip(data))); return { id, - name: rp.name, - clusterIDs: rp.assigned.map(({ id }) => id), - isHidden: rp.isHidden, - avatarFaceID: rp.avatarFaceID, - displayFaceID: undefined, + name: r.name, + clusterIDs: r.assigned.map(({ id }) => id), + isHidden: r.isHidden, + avatarFaceID: r.avatarFaceID, }; }; @@ -141,6 +140,8 @@ const RemoteCGroup = z.object({ faces: z.string().array(), }), ), + // The remote cgroup also has a "rejected" property, but that is not + // currently used by any of the clients. isHidden: z.boolean(), avatarFaceID: z.string().nullish().transform(nullToUndefined), }); From 8e7a3a9347d2f7897d7429ba8e87e661335da532 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sat, 7 Sep 2024 16:07:00 +0530 Subject: [PATCH 03/18] Pass lft --- web/apps/photos/src/services/logout.ts | 13 ++++- .../new/photos/services/search/index.ts | 57 +++++++++++++++++-- .../new/photos/services/search/types.ts | 15 +++++ .../new/photos/services/search/worker.ts | 9 ++- 4 files changed, 80 insertions(+), 14 deletions(-) diff --git a/web/apps/photos/src/services/logout.ts b/web/apps/photos/src/services/logout.ts index 8ebf30aec9..fa36e4f8fa 100644 --- a/web/apps/photos/src/services/logout.ts +++ b/web/apps/photos/src/services/logout.ts @@ -3,6 +3,7 @@ import log from "@/base/log"; import DownloadManager from "@/new/photos/services/download"; import { clearFeatureFlagSessionState } from "@/new/photos/services/feature-flags"; import { logoutML, terminateMLWorker } from "@/new/photos/services/ml"; +import { logoutSearch } from "@/new/photos/services/search"; import exportService from "./export"; /** @@ -18,13 +19,13 @@ export const photosLogout = async () => { // - Workers - // Terminate any workers before clearing persistent state. - // See: [Note: Caching IDB instances in separate execution contexts]. + // Terminate any workers that might access the DB before clearing persistent + // state. See: [Note: Caching IDB instances in separate execution contexts]. try { await terminateMLWorker(); } catch (e) { - ignoreError("face", e); + ignoreError("ml/worker", e); } // - Remote logout and clear state @@ -47,6 +48,12 @@ export const photosLogout = async () => { ignoreError("download", e); } + try { + logoutSearch(); + } catch (e) { + ignoreError("search", e); + } + // - Desktop const electron = globalThis.electron; diff --git a/web/packages/new/photos/services/search/index.ts b/web/packages/new/photos/services/search/index.ts index abd920c9e1..7a8dd9e749 100644 --- a/web/packages/new/photos/services/search/index.ts +++ b/web/packages/new/photos/services/search/index.ts @@ -1,12 +1,15 @@ import { isDesktop } from "@/base/app"; import { masterKeyFromSession } from "@/base/session-store"; import { ComlinkWorker } from "@/base/worker/comlink-worker"; +import { FileType } from "@/media/file-type"; import i18n, { t } from "i18next"; import type { EnteFile } from "../../types/file"; import { clipMatches, isMLEnabled } from "../ml"; import { SuggestionType, type DateSearchResult, + type LabelledFileType, + type LocalizedSearchData, type SearchQuery, } from "./types"; import type { SearchWorker } from "./worker"; @@ -31,6 +34,17 @@ const createComlinkWorker = () => new Worker(new URL("worker.ts", import.meta.url)), ); +/** + * Perform any logout specific cleanup for the search subsystem. + */ +export const logoutSearch = () => { + if (_comlinkWorker) { + _comlinkWorker.terminate(); + _comlinkWorker = undefined; + } + _localizedSearchData = undefined; +}; + /** * Fetch any data that would be needed if the user were to search. */ @@ -59,7 +73,7 @@ export const createSearchQuery = async (searchString: string) => { // the search worker, then combine the two. const results = await Promise.all([ clipSuggestions(s, searchString).then((s) => s ?? []), - worker().then((w) => w.createSearchQuery(s, i18n.language, holidays())), + worker().then((w) => w.createSearchQuery(s, localizedSearchData())), ]); return results.flat(); }; @@ -85,12 +99,34 @@ export const search = async (search: SearchQuery) => worker().then((w) => w.search(search)); /** - * A list of holidays - their yearly dates and localized names. + * Cached value of {@link localizedSearchData}. + */ +let _localizedSearchData: LocalizedSearchData | undefined; + +/* + * For searching, the web worker needs a bunch of otherwise static data that has + * names and labels formed by localized strings. * - * We need to keep this on the main thread since it uses the t() function for - * localization (although I haven't tried that in a web worker, it might work - * there too). Also, it cannot be a const since it needs to be evaluated lazily - * for the t() to work. + * Since it would be tricky to get the t() function to work in a web worker, we + * instead pass this from the main thread (lazily initialized and cached). + * + * Note that these need to be evaluated at runtime, and cannot be static + * constants since t() depends on the user's locale. + * + * We currently clear the cached data on logout, but this is not necessary. The + * only point we necessarily need to clear this data is if the user changes their + * preferred locale, but currently we reload the page in such cases so any in + * memory state would be reset that way. + */ +const localizedSearchData = () => + (_localizedSearchData ??= { + locale: i18n.language, + holidays: holidays(), + labelledFileTypes: labelledFileTypes(), + }); + +/** + * A list of holidays - their yearly dates and localized names. */ const holidays = (): DateSearchResult[] => [ { components: { month: 12, day: 25 }, label: t("CHRISTMAS") }, @@ -98,3 +134,12 @@ const holidays = (): DateSearchResult[] => [ { components: { month: 1, day: 1 }, label: t("NEW_YEAR") }, { components: { month: 12, day: 31 }, label: t("NEW_YEAR_EVE") }, ]; + +/** + * A list of file types with their localized names. + */ +const labelledFileTypes = (): LabelledFileType[] => [ + { fileType: FileType.image, label: t("IMAGE") }, + { fileType: FileType.video, label: t("VIDEO") }, + { fileType: FileType.livePhoto, label: t("LIVE_PHOTO") }, +]; diff --git a/web/packages/new/photos/services/search/types.ts b/web/packages/new/photos/services/search/types.ts index 080803956a..9066f12791 100644 --- a/web/packages/new/photos/services/search/types.ts +++ b/web/packages/new/photos/services/search/types.ts @@ -14,6 +14,21 @@ export interface DateSearchResult { label: string; } +export interface LabelledFileType { + fileType: FileType; + label: string; +} + +/** + * Various bits of static but locale specific data that the search worker needs + * during searching. + */ +export interface LocalizedSearchData { + locale: string; + holidays: DateSearchResult[]; + labelledFileTypes: LabelledFileType[]; +} + /** * A parsed version of a potential natural language date time string. * diff --git a/web/packages/new/photos/services/search/worker.ts b/web/packages/new/photos/services/search/worker.ts index 0492863db0..62e21af4bf 100644 --- a/web/packages/new/photos/services/search/worker.ts +++ b/web/packages/new/photos/services/search/worker.ts @@ -16,6 +16,7 @@ import { import type { City, DateSearchResult, + LocalizedSearchData, SearchDateComponents, SearchQuery, Suggestion, @@ -81,11 +82,10 @@ export class SearchWorker { /** * Convert a search string into a reusable query. */ - createSearchQuery(s: string, locale: string, holidays: DateSearchResult[]) { + createSearchQuery(s: string, localizedSearchData: LocalizedSearchData) { return createSearchQuery( s, - locale, - holidays, + localizedSearchData, this.locationTags, this.cities, ); @@ -103,8 +103,7 @@ expose(SearchWorker); const createSearchQuery = ( s: string, - locale: string, - holidays: DateSearchResult[], + { locale, holidays }: LocalizedSearchData, locationTags: SearchableLocationTag[], cities: SearchableCity[], ): Suggestion[] => From e9a6b55ba42a1488647e30d17c8f936081938da9 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sat, 7 Sep 2024 16:31:27 +0530 Subject: [PATCH 04/18] Lowercased + searchable --- web/apps/photos/src/services/searchService.ts | 22 ------- .../new/photos/services/search/index.ts | 10 ++- .../new/photos/services/search/types.ts | 18 +++++- .../new/photos/services/search/worker.ts | 63 ++++++++++--------- 4 files changed, 57 insertions(+), 56 deletions(-) diff --git a/web/apps/photos/src/services/searchService.ts b/web/apps/photos/src/services/searchService.ts index cd19fb0c9f..42f2054ced 100644 --- a/web/apps/photos/src/services/searchService.ts +++ b/web/apps/photos/src/services/searchService.ts @@ -48,7 +48,6 @@ export const getAutoCompleteSuggestions = // - getDateSuggestion(searchPhrase), // - getLocationSuggestion(searchPhrase), ...(await createSearchQuery(searchPhrase)), - ...getFileTypeSuggestion(searchPhrase2), ...getCollectionSuggestion(searchPhrase2, collections), getFileNameSuggestion(searchPhrase2, files), getFileCaptionSuggestion(searchPhrase2, files), @@ -85,27 +84,6 @@ async function convertSuggestionsToOptions( } return previewImageAppendedOptions; } -function getFileTypeSuggestion(searchPhrase: string): Suggestion[] { - return [ - { - label: t("IMAGE"), - value: FileType.image, - type: SuggestionType.FILE_TYPE, - }, - { - label: t("VIDEO"), - value: FileType.video, - type: SuggestionType.FILE_TYPE, - }, - { - label: t("LIVE_PHOTO"), - value: FileType.livePhoto, - type: SuggestionType.FILE_TYPE, - }, - ].filter((suggestion) => - suggestion.label.toLowerCase().includes(searchPhrase), - ); -} export async function getAllPeopleSuggestion(): Promise> { try { diff --git a/web/packages/new/photos/services/search/index.ts b/web/packages/new/photos/services/search/index.ts index 7a8dd9e749..cfd7a0ca21 100644 --- a/web/packages/new/photos/services/search/index.ts +++ b/web/packages/new/photos/services/search/index.ts @@ -121,8 +121,14 @@ let _localizedSearchData: LocalizedSearchData | undefined; const localizedSearchData = () => (_localizedSearchData ??= { locale: i18n.language, - holidays: holidays(), - labelledFileTypes: labelledFileTypes(), + holidays: holidays().map((h) => ({ + ...h, + lowercasedName: h.label.toLowerCase(), + })), + labelledFileTypes: labelledFileTypes().map((t) => ({ + ...t, + lowercasedName: t.label.toLowerCase(), + })), }); /** diff --git a/web/packages/new/photos/services/search/types.ts b/web/packages/new/photos/services/search/types.ts index 9066f12791..b2130e4bbc 100644 --- a/web/packages/new/photos/services/search/types.ts +++ b/web/packages/new/photos/services/search/types.ts @@ -19,14 +19,28 @@ export interface LabelledFileType { label: string; } +/** + * An annotated version of {@link T} that includes its searchable "lowercased" + * label or name. + * + * Precomputing these lowercased values saves us from doing the lowercasing + * during the search itself. + */ +export type Searchable = T & { + /** + * The name or label of T, lowercased. + */ + lowercasedName: string; +}; + /** * Various bits of static but locale specific data that the search worker needs * during searching. */ export interface LocalizedSearchData { locale: string; - holidays: DateSearchResult[]; - labelledFileTypes: LabelledFileType[]; + holidays: Searchable[]; + labelledFileTypes: Searchable[]; } /** diff --git a/web/packages/new/photos/services/search/worker.ts b/web/packages/new/photos/services/search/worker.ts index 62e21af4bf..ca842d40d6 100644 --- a/web/packages/new/photos/services/search/worker.ts +++ b/web/packages/new/photos/services/search/worker.ts @@ -16,36 +16,23 @@ import { import type { City, DateSearchResult, + LabelledFileType, LocalizedSearchData, + Searchable, SearchDateComponents, SearchQuery, Suggestion, } from "./types"; import { SuggestionType } from "./types"; -type SearchableCity = City & { - /** - * Name of the city, lowercased. Precomputed to save an op during search. - */ - lowercasedName: string; -}; - -type SearchableLocationTag = LocationTag & { - /** - * Name of the location tag, lowercased. Precomputed to save an op during - * search. - */ - lowercasedName: string; -}; - /** * A web worker that runs the search asynchronously so that the main thread * remains responsive. */ export class SearchWorker { private enteFiles: EnteFile[] = []; - private locationTags: SearchableLocationTag[] = []; - private cities: SearchableCity[] = []; + private locationTags: Searchable[] = []; + private cities: Searchable[] = []; /** * Fetch any state we might need when the actual search happens. @@ -103,19 +90,20 @@ expose(SearchWorker); const createSearchQuery = ( s: string, - { locale, holidays }: LocalizedSearchData, - locationTags: SearchableLocationTag[], - cities: SearchableCity[], + { locale, holidays, labelledFileTypes }: LocalizedSearchData, + locationTags: Searchable[], + cities: Searchable[], ): Suggestion[] => [ dateSuggestions(s, locale, holidays), locationSuggestions(s, locationTags, cities), + fileTypeSuggestions(s, labelledFileTypes), ].flat(); const dateSuggestions = ( s: string, locale: string, - holidays: DateSearchResult[], + holidays: Searchable[], ) => parseDateComponents(s, locale, holidays).map(({ components, label }) => ({ type: SuggestionType.DATE, @@ -140,12 +128,12 @@ const dateSuggestions = ( const parseDateComponents = ( s: string, locale: string, - holidays: DateSearchResult[], + holidays: Searchable[], ): DateSearchResult[] => [ parseChrono(s, locale), parseYearComponents(s), - parseHolidayComponents(s, holidays), + holidays.filter(searchableIncludes(s)), ].flat(); const parseChrono = (s: string, locale: string): DateSearchResult[] => @@ -194,8 +182,13 @@ const parseYearComponents = (s: string): DateSearchResult[] => { return []; }; -const parseHolidayComponents = (s: string, holidays: DateSearchResult[]) => - holidays.filter(({ label }) => label.toLowerCase().includes(s)); +/** + * A helper function to directly pass to filters on Searchable[]. + */ +const searchableIncludes = + (s: string) => + ({ lowercasedName }: { lowercasedName: string }) => + lowercasedName.includes(s); /** * Zod schema describing world_cities.json. @@ -222,12 +215,10 @@ const fetchCities = async () => { const locationSuggestions = ( s: string, - locationTags: SearchableLocationTag[], - cities: SearchableCity[], + locationTags: Searchable[], + cities: Searchable[], ) => { - const matchingLocationTags = locationTags.filter((t) => - t.lowercasedName.includes(s), - ); + const matchingLocationTags = locationTags.filter(searchableIncludes(s)); const matchingLocationTagLNames = new Set( matchingLocationTags.map((t) => t.lowercasedName), @@ -253,6 +244,18 @@ const locationSuggestions = ( ].flat(); }; +const fileTypeSuggestions = ( + s: string, + labelledFileTypes: Searchable[], +) => + labelledFileTypes + .filter(searchableIncludes(s)) + .map(({ fileType, label }) => ({ + label, + value: fileType, + type: SuggestionType.FILE_TYPE, + })); + /** * Return true if file satisfies the given {@link query}. */ From 7a85fa5c6154d4ee90c4997d556ace98bea6f7b8 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sat, 7 Sep 2024 16:56:43 +0530 Subject: [PATCH 05/18] Inline --- .../Search/SearchBar/searchInput/index.tsx | 46 ++++++++++++++++- .../searchInput/valueContainerWithIcon.tsx | 49 ------------------- web/apps/photos/src/services/searchService.ts | 1 + 3 files changed, 45 insertions(+), 51 deletions(-) delete mode 100644 web/apps/photos/src/components/Search/SearchBar/searchInput/valueContainerWithIcon.tsx diff --git a/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx b/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx index 7ce05576f5..a48d557f63 100644 --- a/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx +++ b/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx @@ -14,8 +14,14 @@ import { } from "@/new/photos/services/search/types"; import type { LocationTag } from "@/new/photos/services/user-entity"; import { EnteFile } from "@/new/photos/types/file"; +import { FlexWrapper } from "@ente/shared/components/Container"; +import CalendarIcon from "@mui/icons-material/CalendarMonth"; import CloseIcon from "@mui/icons-material/Close"; -import { IconButton } from "@mui/material"; +import FolderIcon from "@mui/icons-material/Folder"; +import ImageIcon from "@mui/icons-material/Image"; +import LocationIcon from "@mui/icons-material/LocationOn"; +import SearchIcon from "@mui/icons-material/SearchOutlined"; +import { Box, IconButton } from "@mui/material"; import { t } from "i18next"; import memoize from "memoize-one"; import pDebounce from "p-debounce"; @@ -23,6 +29,7 @@ import { AppContext } from "pages/_app"; import { useCallback, useContext, useEffect, useRef, useState } from "react"; import { components } from "react-select"; import AsyncSelect from "react-select/async"; +import { SelectComponents } from "react-select/src/components"; import { InputActionMeta } from "react-select/src/types"; import { getAutoCompleteSuggestions, @@ -33,7 +40,8 @@ import { SelectStyles } from "../../../../styles/search"; import { SearchInputWrapper } from "../styledComponents"; import MenuWithPeople from "./MenuWithPeople"; import { OptionWithInfo } from "./optionWithInfo"; -import { ValueContainerWithIcon } from "./valueContainerWithIcon"; + +const { ValueContainer } = components; interface Iprops { isOpen: boolean; @@ -215,3 +223,37 @@ export default function SearchInput(props: Iprops) { ); } + +const getIconByType = (type: SuggestionType) => { + switch (type) { + case SuggestionType.DATE: + return ; + case SuggestionType.LOCATION: + case SuggestionType.CITY: + return ; + case SuggestionType.COLLECTION: + return ; + case SuggestionType.FILE_NAME: + return ; + default: + return ; + } +}; + +const ValueContainerWithIcon: SelectComponents< + SearchOption, + false +>["ValueContainer"] = (props) => ( + + + theme.colors.stroke.muted} + > + {getIconByType(props.getValue()[0]?.type)} + + {props.children} + + +); diff --git a/web/apps/photos/src/components/Search/SearchBar/searchInput/valueContainerWithIcon.tsx b/web/apps/photos/src/components/Search/SearchBar/searchInput/valueContainerWithIcon.tsx deleted file mode 100644 index 75529a925f..0000000000 --- a/web/apps/photos/src/components/Search/SearchBar/searchInput/valueContainerWithIcon.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { - SearchOption, - SuggestionType, -} from "@/new/photos/services/search/types"; -import { FlexWrapper } from "@ente/shared/components/Container"; -import CalendarIcon from "@mui/icons-material/CalendarMonth"; -import FolderIcon from "@mui/icons-material/Folder"; -import ImageIcon from "@mui/icons-material/Image"; -import LocationIcon from "@mui/icons-material/LocationOn"; -import SearchIcon from "@mui/icons-material/SearchOutlined"; -import { Box } from "@mui/material"; -import { components } from "react-select"; -import { SelectComponents } from "react-select/src/components"; - -const { ValueContainer } = components; - -const getIconByType = (type: SuggestionType) => { - switch (type) { - case SuggestionType.DATE: - return ; - case SuggestionType.LOCATION: - case SuggestionType.CITY: - return ; - case SuggestionType.COLLECTION: - return ; - case SuggestionType.FILE_NAME: - return ; - default: - return ; - } -}; - -export const ValueContainerWithIcon: SelectComponents< - SearchOption, - false ->["ValueContainer"] = (props) => ( - - - theme.colors.stroke.muted} - > - {getIconByType(props.getValue()[0]?.type)} - - {props.children} - - -); diff --git a/web/apps/photos/src/services/searchService.ts b/web/apps/photos/src/services/searchService.ts index 42f2054ced..037af63839 100644 --- a/web/apps/photos/src/services/searchService.ts +++ b/web/apps/photos/src/services/searchService.ts @@ -47,6 +47,7 @@ export const getAutoCompleteSuggestions = // - getClipSuggestion(searchPhrase) // - getDateSuggestion(searchPhrase), // - getLocationSuggestion(searchPhrase), + // - getFileTypeSuggestion(searchPhrase), ...(await createSearchQuery(searchPhrase)), ...getCollectionSuggestion(searchPhrase2, collections), getFileNameSuggestion(searchPhrase2, files), From c4c9f71b0181574206da3542b3d77fcfbe4e529b Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sat, 7 Sep 2024 16:58:32 +0530 Subject: [PATCH 06/18] Inline --- .../Search/SearchBar/searchInput/index.tsx | 86 +++++++++++++++---- .../SearchBar/searchInput/optionWithInfo.tsx | 58 ------------- 2 files changed, 68 insertions(+), 76 deletions(-) delete mode 100644 web/apps/photos/src/components/Search/SearchBar/searchInput/optionWithInfo.tsx diff --git a/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx b/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx index a48d557f63..cf819121ab 100644 --- a/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx +++ b/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx @@ -12,16 +12,23 @@ import { SuggestionType, UpdateSearch, } from "@/new/photos/services/search/types"; +import { labelForSuggestionType } from "@/new/photos/services/search/ui"; import type { LocationTag } from "@/new/photos/services/user-entity"; import { EnteFile } from "@/new/photos/types/file"; -import { FlexWrapper } from "@ente/shared/components/Container"; +import { + FlexWrapper, + FreeFlowText, + SpaceBetweenFlex, +} from "@ente/shared/components/Container"; import CalendarIcon from "@mui/icons-material/CalendarMonth"; import CloseIcon from "@mui/icons-material/Close"; import FolderIcon from "@mui/icons-material/Folder"; import ImageIcon from "@mui/icons-material/Image"; import LocationIcon from "@mui/icons-material/LocationOn"; import SearchIcon from "@mui/icons-material/SearchOutlined"; -import { Box, IconButton } from "@mui/material"; +import { Box, Divider, IconButton, Stack, Typography } from "@mui/material"; +import CollectionCard from "components/Collections/CollectionCard"; +import { ResultPreviewTile } from "components/Collections/styledComponents"; import { t } from "i18next"; import memoize from "memoize-one"; import pDebounce from "p-debounce"; @@ -39,9 +46,8 @@ import { Collection } from "types/collection"; import { SelectStyles } from "../../../../styles/search"; import { SearchInputWrapper } from "../styledComponents"; import MenuWithPeople from "./MenuWithPeople"; -import { OptionWithInfo } from "./optionWithInfo"; -const { ValueContainer } = components; +const { Option, ValueContainer } = components; interface Iprops { isOpen: boolean; @@ -224,20 +230,48 @@ export default function SearchInput(props: Iprops) { ); } -const getIconByType = (type: SuggestionType) => { - switch (type) { - case SuggestionType.DATE: - return ; - case SuggestionType.LOCATION: - case SuggestionType.CITY: - return ; - case SuggestionType.COLLECTION: - return ; - case SuggestionType.FILE_NAME: - return ; - default: - return ; - } +const OptionWithInfo = (props) => ( + +); + +const LabelWithInfo = ({ data }: { data: SearchOption }) => { + return ( + !data.hide && ( + <> + + + {labelForSuggestionType(data.type)} + + + + + + {data.label} + + + + {t("photos_count", { count: data.fileCount })} + + + + + {data.previewFiles.map((file) => ( + null} + collectionTile={ResultPreviewTile} + /> + ))} + + + + + + ) + ); }; const ValueContainerWithIcon: SelectComponents< @@ -257,3 +291,19 @@ const ValueContainerWithIcon: SelectComponents< ); + +const getIconByType = (type: SuggestionType) => { + switch (type) { + case SuggestionType.DATE: + return ; + case SuggestionType.LOCATION: + case SuggestionType.CITY: + return ; + case SuggestionType.COLLECTION: + return ; + case SuggestionType.FILE_NAME: + return ; + default: + return ; + } +}; diff --git a/web/apps/photos/src/components/Search/SearchBar/searchInput/optionWithInfo.tsx b/web/apps/photos/src/components/Search/SearchBar/searchInput/optionWithInfo.tsx deleted file mode 100644 index 2957e77318..0000000000 --- a/web/apps/photos/src/components/Search/SearchBar/searchInput/optionWithInfo.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { SearchOption } from "@/new/photos/services/search/types"; -import { labelForSuggestionType } from "@/new/photos/services/search/ui"; -import { - FreeFlowText, - SpaceBetweenFlex, -} from "@ente/shared/components/Container"; -import { Box, Divider, Stack, Typography } from "@mui/material"; -import CollectionCard from "components/Collections/CollectionCard"; -import { ResultPreviewTile } from "components/Collections/styledComponents"; -import { t } from "i18next"; - -import { components } from "react-select"; - -const { Option } = components; - -export const OptionWithInfo = (props) => ( - -); - -const LabelWithInfo = ({ data }: { data: SearchOption }) => { - return ( - !data.hide && ( - <> - - - {labelForSuggestionType(data.type)} - - - - - - {data.label} - - - - {t("photos_count", { count: data.fileCount })} - - - - - {data.previewFiles.map((file) => ( - null} - collectionTile={ResultPreviewTile} - /> - ))} - - - - - - ) - ); -}; From 752ae51f462ee7428aea6fc4c1c91a44f2509608 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sat, 7 Sep 2024 16:59:10 +0530 Subject: [PATCH 07/18] ns --- .../src/components/Search/SearchBar/searchInput/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx b/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx index cf819121ab..ca999aec69 100644 --- a/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx +++ b/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx @@ -49,7 +49,7 @@ import MenuWithPeople from "./MenuWithPeople"; const { Option, ValueContainer } = components; -interface Iprops { +interface SearchInputProps { isOpen: boolean; updateSearch: UpdateSearch; setIsOpen: (value: boolean) => void; @@ -68,7 +68,7 @@ const VisibleInput = (props) => ( ); -export default function SearchInput(props: Iprops) { +export default function SearchInput(props: SearchInputProps) { const selectRef = useRef(null); const [value, setValue] = useState(null); const appContext = useContext(AppContext); From 3cb0c1b325a269d47d95d88de5424ca9de83101b Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sat, 7 Sep 2024 17:01:12 +0530 Subject: [PATCH 08/18] Inline --- .../SearchBar/searchInput/MenuWithPeople.tsx | 72 ---------------- .../Search/SearchBar/searchInput/index.tsx | 84 +++++++++++++++++-- 2 files changed, 77 insertions(+), 79 deletions(-) delete mode 100644 web/apps/photos/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx diff --git a/web/apps/photos/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx b/web/apps/photos/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx deleted file mode 100644 index 72ed38de99..0000000000 --- a/web/apps/photos/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { PeopleList } from "@/new/photos/components/PeopleList"; -import { isMLEnabled } from "@/new/photos/services/ml"; -import { Suggestion, SuggestionType } from "@/new/photos/services/search/types"; -import { Row } from "@ente/shared/components/Container"; -import { Box, styled } from "@mui/material"; -import { t } from "i18next"; -import { components } from "react-select"; - -const { Menu } = components; - -const Legend = styled("span")` - font-size: 20px; - color: #ddd; - display: inline; - padding: 0px 12px; -`; - -const Caption = styled("span")` - font-size: 12px; - display: inline; - padding: 0px 12px; -`; - -const MenuWithPeople = (props) => { - // log.info("props.selectProps.options: ", selectRef); - const peopleSuggestions = props.selectProps.options.filter( - (o) => o.type === SuggestionType.PERSON, - ); - const people = peopleSuggestions.map((o) => o.value); - - const indexStatusSuggestion = props.selectProps.options.filter( - (o) => o.type === SuggestionType.INDEX_STATUS, - )[0] as Suggestion; - - const indexStatus = indexStatusSuggestion?.value; - return ( - - - {isMLEnabled() && - indexStatus && - (people && people.length > 0 ? ( - - {t("people")} - - ) : ( - - ))} - - {isMLEnabled() && indexStatus && ( - - {indexStatusSuggestion.label} - - )} - {people && people.length > 0 && ( - - { - props.selectRef.current.blur(); - props.setValue(peopleSuggestions[index]); - }} - /> - - )} - - {props.children} - - ); -}; - -export default MenuWithPeople; diff --git a/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx b/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx index ca999aec69..a8be46a46d 100644 --- a/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx +++ b/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx @@ -1,4 +1,5 @@ import { FileType } from "@/media/file-type"; +import { PeopleList } from "@/new/photos/components/PeopleList"; import { isMLEnabled } from "@/new/photos/services/ml"; import type { City, @@ -9,6 +10,7 @@ import { ClipSearchScores, SearchOption, SearchQuery, + Suggestion, SuggestionType, UpdateSearch, } from "@/new/photos/services/search/types"; @@ -18,6 +20,7 @@ import { EnteFile } from "@/new/photos/types/file"; import { FlexWrapper, FreeFlowText, + Row, SpaceBetweenFlex, } from "@ente/shared/components/Container"; import CalendarIcon from "@mui/icons-material/CalendarMonth"; @@ -26,7 +29,14 @@ import FolderIcon from "@mui/icons-material/Folder"; import ImageIcon from "@mui/icons-material/Image"; import LocationIcon from "@mui/icons-material/LocationOn"; import SearchIcon from "@mui/icons-material/SearchOutlined"; -import { Box, Divider, IconButton, Stack, Typography } from "@mui/material"; +import { + Box, + Divider, + IconButton, + Stack, + styled, + Typography, +} from "@mui/material"; import CollectionCard from "components/Collections/CollectionCard"; import { ResultPreviewTile } from "components/Collections/styledComponents"; import { t } from "i18next"; @@ -45,9 +55,8 @@ import { import { Collection } from "types/collection"; import { SelectStyles } from "../../../../styles/search"; import { SearchInputWrapper } from "../styledComponents"; -import MenuWithPeople from "./MenuWithPeople"; -const { Option, ValueContainer } = components; +const { Option, ValueContainer, Menu } = components; interface SearchInputProps { isOpen: boolean; @@ -64,10 +73,6 @@ const createComponents = memoize((Option, ValueContainer, Menu, Input) => ({ Input, })); -const VisibleInput = (props) => ( - -); - export default function SearchInput(props: SearchInputProps) { const selectRef = useRef(null); const [value, setValue] = useState(null); @@ -307,3 +312,68 @@ const getIconByType = (type: SuggestionType) => { return ; } }; + +export const MenuWithPeople = (props) => { + // log.info("props.selectProps.options: ", selectRef); + const peopleSuggestions = props.selectProps.options.filter( + (o) => o.type === SuggestionType.PERSON, + ); + const people = peopleSuggestions.map((o) => o.value); + + const indexStatusSuggestion = props.selectProps.options.filter( + (o) => o.type === SuggestionType.INDEX_STATUS, + )[0] as Suggestion; + + const indexStatus = indexStatusSuggestion?.value; + return ( + + + {isMLEnabled() && + indexStatus && + (people && people.length > 0 ? ( + + {t("people")} + + ) : ( + + ))} + + {isMLEnabled() && indexStatus && ( + + {indexStatusSuggestion.label} + + )} + {people && people.length > 0 && ( + + { + props.selectRef.current.blur(); + props.setValue(peopleSuggestions[index]); + }} + /> + + )} + + {props.children} + + ); +}; + +const Legend = styled("span")` + font-size: 20px; + color: #ddd; + display: inline; + padding: 0px 12px; +`; + +const Caption = styled("span")` + font-size: 12px; + display: inline; + padding: 0px 12px; +`; + +const VisibleInput = (props) => ( + +); From 980a1f4c5a1bb28e9173bf46c1956291ef5bf4ac Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Sat, 7 Sep 2024 17:02:37 +0530 Subject: [PATCH 09/18] Inline --- .../SearchBar/{searchInput/index.tsx => SearchInput.tsx} | 8 ++++---- web/apps/photos/src/components/Search/SearchBar/index.tsx | 7 +++---- 2 files changed, 7 insertions(+), 8 deletions(-) rename web/apps/photos/src/components/Search/SearchBar/{searchInput/index.tsx => SearchInput.tsx} (98%) diff --git a/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx b/web/apps/photos/src/components/Search/SearchBar/SearchInput.tsx similarity index 98% rename from web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx rename to web/apps/photos/src/components/Search/SearchBar/SearchInput.tsx index a8be46a46d..3fd3834a4d 100644 --- a/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx +++ b/web/apps/photos/src/components/Search/SearchBar/SearchInput.tsx @@ -53,8 +53,8 @@ import { getDefaultOptions, } from "services/searchService"; import { Collection } from "types/collection"; -import { SelectStyles } from "../../../../styles/search"; -import { SearchInputWrapper } from "../styledComponents"; +import { SelectStyles } from "../../../styles/search"; +import { SearchInputWrapper } from "./styledComponents"; const { Option, ValueContainer, Menu } = components; @@ -73,7 +73,7 @@ const createComponents = memoize((Option, ValueContainer, Menu, Input) => ({ Input, })); -export default function SearchInput(props: SearchInputProps) { +export const SearchInput: React.FC = (props) => { const selectRef = useRef(null); const [value, setValue] = useState(null); const appContext = useContext(AppContext); @@ -233,7 +233,7 @@ export default function SearchInput(props: SearchInputProps) { )} ); -} +}; const OptionWithInfo = (props) => (