From 49ec70f5cf8aa15a42a490a2470f5ff5d0d1d783 Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Tue, 23 Jan 2024 13:36:46 +0530 Subject: [PATCH 01/20] Load cities on gallery init --- apps/photos/src/pages/gallery/index.tsx | 2 ++ .../src/services/locationSearchService.ts | 23 +++++++++++++++++++ packages/shared/constants/urls.ts | 2 ++ 3 files changed, 27 insertions(+) create mode 100644 apps/photos/src/services/locationSearchService.ts diff --git a/apps/photos/src/pages/gallery/index.tsx b/apps/photos/src/pages/gallery/index.tsx index e477367a5..82731dfdc 100644 --- a/apps/photos/src/pages/gallery/index.tsx +++ b/apps/photos/src/pages/gallery/index.tsx @@ -131,6 +131,7 @@ import { ClipService } from 'services/clipService'; import isElectron from 'is-electron'; import downloadManager from 'services/download'; import { APPS } from '@ente/shared/apps/constants'; +import locationSearchService from 'services/locationSearchService'; export const DeadCenter = styled('div')` flex: 1; @@ -341,6 +342,7 @@ export default function Gallery() { setIsFirstLoad(false); setJustSignedUp(false); setIsFirstFetch(false); + locationSearchService.loadCities(); syncInterval.current = setInterval(() => { syncWithRemote(false, true); }, SYNC_INTERVAL_IN_MICROSECONDS); diff --git a/apps/photos/src/services/locationSearchService.ts b/apps/photos/src/services/locationSearchService.ts new file mode 100644 index 000000000..377c4efed --- /dev/null +++ b/apps/photos/src/services/locationSearchService.ts @@ -0,0 +1,23 @@ +import { CITIES_URL } from '@ente/shared/constants/urls'; + +interface City { + city: string; + country: string; + lat: number; + lng: number; +} + +class LocationSearchService { + private cities: Array = []; + + loadCities() { + fetch(CITIES_URL).then((response) => { + response.json().then((data) => { + this.cities = data; + console.log(this.cities); + }); + }); + } +} + +export default new LocationSearchService(); diff --git a/packages/shared/constants/urls.ts b/packages/shared/constants/urls.ts index 291704e98..65d961ddd 100644 --- a/packages/shared/constants/urls.ts +++ b/packages/shared/constants/urls.ts @@ -17,3 +17,5 @@ export const WEB_ROADMAP_URL = 'https://github.com/ente-io/photos-web/issues'; export const DESKTOP_ROADMAP_URL = 'https://github.com/ente-io/photos-desktop/issues'; + +export const CITIES_URL = 'https://assets.ente.io/world_cities.json'; From 94d6d3462524f3d5c59572100071ad641de8ee14 Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Tue, 23 Jan 2024 16:22:03 +0530 Subject: [PATCH 02/20] WiP --- .../Search/SearchBar/searchInput/index.tsx | 7 +++ .../searchInput/valueContainerWithIcon.tsx | 1 + apps/photos/src/pages/gallery/index.tsx | 19 ++++++- .../src/services/locationSearchService.ts | 55 +++++++++++++++++-- apps/photos/src/services/searchService.ts | 39 +++++++++++-- apps/photos/src/types/search/index.ts | 4 ++ apps/photos/src/utils/search/index.ts | 17 ------ 7 files changed, 113 insertions(+), 29 deletions(-) diff --git a/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx b/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx index 7db8d94ad..8b46f51ae 100644 --- a/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx +++ b/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx @@ -30,6 +30,7 @@ import { LocationTagData } from 'types/entity'; import { FILE_TYPE } from 'constants/file'; import { InputActionMeta } from 'react-select/src/types'; import { components } from 'react-select'; +import { City } from 'services/locationSearchService'; interface Iprops { isOpen: boolean; @@ -122,6 +123,12 @@ export default function SearchInput(props: Iprops) { }; props.setIsOpen(true); break; + case SuggestionType.CITY: + search = { + city: selectedOption.value as City, + }; + props.setIsOpen(true); + break; case SuggestionType.COLLECTION: search = { collection: selectedOption.value as number }; setValue(null); diff --git a/apps/photos/src/components/Search/SearchBar/searchInput/valueContainerWithIcon.tsx b/apps/photos/src/components/Search/SearchBar/searchInput/valueContainerWithIcon.tsx index 8bf92a310..9ebe3cd58 100644 --- a/apps/photos/src/components/Search/SearchBar/searchInput/valueContainerWithIcon.tsx +++ b/apps/photos/src/components/Search/SearchBar/searchInput/valueContainerWithIcon.tsx @@ -17,6 +17,7 @@ const getIconByType = (type: SuggestionType) => { case SuggestionType.DATE: return ; case SuggestionType.LOCATION: + case SuggestionType.CITY: return ; case SuggestionType.COLLECTION: return ; diff --git a/apps/photos/src/pages/gallery/index.tsx b/apps/photos/src/pages/gallery/index.tsx index 82731dfdc..e38ddc657 100644 --- a/apps/photos/src/pages/gallery/index.tsx +++ b/apps/photos/src/pages/gallery/index.tsx @@ -120,7 +120,7 @@ import GalleryEmptyState from 'components/GalleryEmptyState'; import AuthenticateUserModal from 'components/AuthenticateUserModal'; import useMemoSingleThreaded from '@ente/shared/hooks/useMemoSingleThreaded'; import { isArchivedFile } from 'utils/magicMetadata'; -import { isSameDayAnyYear, isInsideLocationTag } from 'utils/search'; +import { isSameDayAnyYear } from 'utils/search'; import { getSessionExpiredMessage } from 'utils/ui'; import { syncEntities } from 'services/entityService'; import { constructUserIDToEmailMap } from 'services/collectionService'; @@ -131,7 +131,10 @@ import { ClipService } from 'services/clipService'; import isElectron from 'is-electron'; import downloadManager from 'services/download'; import { APPS } from '@ente/shared/apps/constants'; -import locationSearchService from 'services/locationSearchService'; +import locationSearchService, { + isInsideCity, + isInsideLocationTag, +} from 'services/locationSearchService'; export const DeadCenter = styled('div')` flex: 1; @@ -518,6 +521,18 @@ export default function Gallery() { ) { return false; } + if ( + search?.city && + !isInsideCity( + { + latitude: item.metadata.latitude, + longitude: item.metadata.longitude, + }, + search.city + ) + ) { + return false; + } if ( search?.person && search.person.files.indexOf(item.id) === -1 diff --git a/apps/photos/src/services/locationSearchService.ts b/apps/photos/src/services/locationSearchService.ts index 377c4efed..454d07002 100644 --- a/apps/photos/src/services/locationSearchService.ts +++ b/apps/photos/src/services/locationSearchService.ts @@ -1,23 +1,68 @@ import { CITIES_URL } from '@ente/shared/constants/urls'; +import { LocationTagData } from 'types/entity'; +import { Location } from 'types/upload'; -interface City { +export interface City { city: string; country: string; lat: number; lng: number; } +const DEFAULT_CITY_RADIUS = 10; + class LocationSearchService { private cities: Array = []; + private citiesPromise: Promise; loadCities() { - fetch(CITIES_URL).then((response) => { - response.json().then((data) => { - this.cities = data; - console.log(this.cities); + if (this.citiesPromise) { + return; + } + this.citiesPromise = fetch(CITIES_URL).then((response) => { + return response.json().then((data) => { + this.cities = data['data']; }); }); } + + async searchCities(searchTerm: string) { + if (!this.citiesPromise) { + this.loadCities(); + } + await this.citiesPromise; + return this.cities.filter((city) => { + return city.city.toLowerCase().includes(searchTerm.toLowerCase()); + }); + } } export default new LocationSearchService(); + +export function isInsideLocationTag( + location: Location, + locationTag: LocationTagData +) { + const { centerPoint, aSquare, bSquare } = locationTag; + const { latitude, longitude } = location; + const x = Math.abs(centerPoint.latitude - latitude); + const y = Math.abs(centerPoint.longitude - longitude); + if ((x * x) / aSquare + (y * y) / bSquare <= 1) { + return true; + } else { + return false; + } +} + +// TODO: Verify correctness +export function isInsideCity(location: Location, city: City) { + const { lat, lng } = city; + const { latitude, longitude } = location; + const x = Math.abs(lat - latitude); + const y = Math.abs(lng - longitude); + if (x * x + y * y <= DEFAULT_CITY_RADIUS * DEFAULT_CITY_RADIUS) { + return true; + } else { + return false; + } +} diff --git a/apps/photos/src/services/searchService.ts b/apps/photos/src/services/searchService.ts index 90665dd0b..42b91614d 100644 --- a/apps/photos/src/services/searchService.ts +++ b/apps/photos/src/services/searchService.ts @@ -16,11 +16,7 @@ import { ClipSearchScores, } from 'types/search'; import ObjectService from './machineLearning/objectService'; -import { - getFormattedDate, - isInsideLocationTag, - isSameDayAnyYear, -} from 'utils/search'; +import { getFormattedDate, isSameDayAnyYear } from 'utils/search'; import { Person, Thing } from 'types/machineLearning'; import { getUniqueFiles } from 'utils/file'; import { getLatestEntities } from './entityService'; @@ -31,6 +27,11 @@ import { ClipService, computeClipMatchScore } from './clipService'; import { CustomError } from '@ente/shared/error'; import { Model } from 'types/embedding'; import { getLocalEmbeddings } from './embeddingService'; +import locationSearchService, { + City, + isInsideCity, + isInsideLocationTag, +} from './locationSearchService'; const DIGITS = new Set(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']); @@ -61,6 +62,7 @@ export const getAutoCompleteSuggestions = getFileNameSuggestion(searchPhrase, files), getFileCaptionSuggestion(searchPhrase, files), ...(await getLocationTagSuggestions(searchPhrase)), + ...(await getCitySuggestions(searchPhrase)), ...(await getThingSuggestion(searchPhrase)), ].filter((suggestion) => !!suggestion); @@ -279,6 +281,21 @@ async function getLocationTagSuggestions(searchPhrase: string) { ); } +async function getCitySuggestions(searchPhrase: string) { + const searchResults = await locationSearchService.searchCities( + searchPhrase + ); + + return searchResults.map( + (city) => + ({ + type: SuggestionType.CITY, + value: city, + label: city.city, + } as Suggestion) + ); +} + async function getThingSuggestion(searchPhrase: string): Promise { const thingResults = await searchThing(searchPhrase); @@ -425,6 +442,15 @@ function isSearchedFile(file: EnteFile, search: Search) { search.location ); } + if (search?.city) { + return isInsideCity( + { + latitude: file.metadata.latitude, + longitude: file.metadata.longitude, + }, + search.city + ); + } if (search?.files) { return search.files.indexOf(file.id) !== -1; } @@ -460,6 +486,9 @@ function convertSuggestionToSearchQuery(option: Suggestion): Search { location: option.value as LocationTagData, }; + case SuggestionType.CITY: + return { city: option.value as City }; + case SuggestionType.COLLECTION: return { collection: option.value as number }; diff --git a/apps/photos/src/types/search/index.ts b/apps/photos/src/types/search/index.ts index 1e41d53f1..2e6c94f48 100644 --- a/apps/photos/src/types/search/index.ts +++ b/apps/photos/src/types/search/index.ts @@ -3,6 +3,7 @@ import { IndexStatus } from 'types/machineLearning/ui'; import { EnteFile } from 'types/file'; import { LocationTagData } from 'types/entity'; import { FILE_TYPE } from 'constants/file'; +import { City } from 'services/locationSearchService'; export enum SuggestionType { DATE = 'DATE', @@ -16,6 +17,7 @@ export enum SuggestionType { FILE_CAPTION = 'FILE_CAPTION', FILE_TYPE = 'FILE_TYPE', CLIP = 'CLIP', + CITY = 'CITY', } export interface DateValue { @@ -35,6 +37,7 @@ export interface Suggestion { | Thing | WordGroup | LocationTagData + | City | FILE_TYPE | ClipSearchScores; hide?: boolean; @@ -43,6 +46,7 @@ export interface Suggestion { export type Search = { date?: DateValue; location?: LocationTagData; + city?: City; collection?: number; files?: number[]; person?: Person; diff --git a/apps/photos/src/utils/search/index.ts b/apps/photos/src/utils/search/index.ts index 891c99254..6392e4840 100644 --- a/apps/photos/src/utils/search/index.ts +++ b/apps/photos/src/utils/search/index.ts @@ -1,6 +1,4 @@ -import { LocationTagData } from 'types/entity'; import { DateValue } from 'types/search'; -import { Location } from 'types/upload'; export const isSameDayAnyYear = (baseDate: DateValue) => (compareDate: Date) => { @@ -28,18 +26,3 @@ export function getFormattedDate(date: DateValue) { new Date(date.year ?? 1, date.month ?? 1, date.date ?? 1) ); } - -export function isInsideLocationTag( - location: Location, - locationTag: LocationTagData -) { - const { centerPoint, aSquare, bSquare } = locationTag; - const { latitude, longitude } = location; - const x = Math.abs(centerPoint.latitude - latitude); - const y = Math.abs(centerPoint.longitude - longitude); - if ((x * x) / aSquare + (y * y) / bSquare <= 1) { - return true; - } else { - return false; - } -} From b84ad8a5bb0379b034574f2183fbe3b43ecb76d3 Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Tue, 23 Jan 2024 16:22:31 +0530 Subject: [PATCH 03/20] Add todo --- apps/photos/src/services/locationSearchService.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/photos/src/services/locationSearchService.ts b/apps/photos/src/services/locationSearchService.ts index 454d07002..a32ac6f13 100644 --- a/apps/photos/src/services/locationSearchService.ts +++ b/apps/photos/src/services/locationSearchService.ts @@ -19,6 +19,7 @@ class LocationSearchService { if (this.citiesPromise) { return; } + // TODO: Ensure the response is cached on the client this.citiesPromise = fetch(CITIES_URL).then((response) => { return response.json().then((data) => { this.cities = data['data']; From 759d973b3a6673f3dd07a8fac4b90ca22ec73863 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Tue, 23 Jan 2024 17:12:49 +0530 Subject: [PATCH 04/20] only suggest cities starting with query --- apps/photos/src/services/locationSearchService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/photos/src/services/locationSearchService.ts b/apps/photos/src/services/locationSearchService.ts index a32ac6f13..2e91ce884 100644 --- a/apps/photos/src/services/locationSearchService.ts +++ b/apps/photos/src/services/locationSearchService.ts @@ -33,7 +33,7 @@ class LocationSearchService { } await this.citiesPromise; return this.cities.filter((city) => { - return city.city.toLowerCase().includes(searchTerm.toLowerCase()); + return city.city.toLowerCase().startsWith(searchTerm.toLowerCase()); }); } } From c3c2be531876d1471d5965d28a284534a5040d73 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Tue, 23 Jan 2024 17:13:33 +0530 Subject: [PATCH 05/20] move search logic to worker --- apps/photos/src/services/searchService.ts | 88 ++++--------------- .../src/utils/comlink/ComlinkSearchWorker.ts | 30 +++++++ apps/photos/src/worker/search.worker.ts | 69 +++++++++++++++ 3 files changed, 118 insertions(+), 69 deletions(-) create mode 100644 apps/photos/src/utils/comlink/ComlinkSearchWorker.ts create mode 100644 apps/photos/src/worker/search.worker.ts diff --git a/apps/photos/src/services/searchService.ts b/apps/photos/src/services/searchService.ts index 42b91614d..f29a98531 100644 --- a/apps/photos/src/services/searchService.ts +++ b/apps/photos/src/services/searchService.ts @@ -16,7 +16,7 @@ import { ClipSearchScores, } from 'types/search'; import ObjectService from './machineLearning/objectService'; -import { getFormattedDate, isSameDayAnyYear } from 'utils/search'; +import { getFormattedDate } from 'utils/search'; import { Person, Thing } from 'types/machineLearning'; import { getUniqueFiles } from 'utils/file'; import { getLatestEntities } from './entityService'; @@ -27,11 +27,8 @@ import { ClipService, computeClipMatchScore } from './clipService'; import { CustomError } from '@ente/shared/error'; import { Model } from 'types/embedding'; import { getLocalEmbeddings } from './embeddingService'; -import locationSearchService, { - City, - isInsideCity, - isInsideLocationTag, -} from './locationSearchService'; +import locationSearchService, { City } from './locationSearchService'; +import ComlinkSearchWorker from 'utils/comlink/ComlinkSearchWorker'; const DIGITS = new Set(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']); @@ -40,7 +37,10 @@ const CLIP_SCORE_THRESHOLD = 0.23; export const getDefaultOptions = async (files: EnteFile[]) => { return [ await getIndexStatusSuggestion(), - ...convertSuggestionsToOptions(await getAllPeopleSuggestion(), files), + ...(await convertSuggestionsToOptions( + await getAllPeopleSuggestion(), + files + )), ].filter((t) => !!t); }; @@ -73,18 +73,16 @@ export const getAutoCompleteSuggestions = } }; -function convertSuggestionsToOptions( +async function convertSuggestionsToOptions( suggestions: Suggestion[], files: EnteFile[] -) { - const previewImageAppendedOptions: SearchOption[] = suggestions - .map((suggestion) => ({ - suggestion, - searchQuery: convertSuggestionToSearchQuery(suggestion), - })) - .map(({ suggestion, searchQuery }) => { +): Promise { + const searchWorker = await ComlinkSearchWorker.getInstance(); + const previewImageAppendedOptions: SearchOption[] = await Promise.all( + suggestions.map(async (suggestion) => { + const searchQuery = convertSuggestionToSearchQuery(suggestion); const resultFiles = getUniqueFiles( - files.filter((file) => isSearchedFile(file, searchQuery)) + await searchWorker.search(files, searchQuery) ); if (searchQuery?.clip) { @@ -101,9 +99,12 @@ function convertSuggestionsToOptions( previewFiles: resultFiles.slice(0, 3), }; }) - .filter((option) => option.fileCount); + ); + const nonEmptyOptions = previewImageAppendedOptions.filter( + (option) => option.fileCount + ); - return previewImageAppendedOptions; + return nonEmptyOptions; } function getFileTypeSuggestion(searchPhrase: string): Suggestion[] { return [ @@ -423,57 +424,6 @@ async function searchClip(searchPhrase: string): Promise { return clipSearchResult; } -function isSearchedFile(file: EnteFile, search: Search) { - if (search?.collection) { - return search.collection === file.collectionID; - } - - if (search?.date) { - return isSameDayAnyYear(search.date)( - new Date(file.metadata.creationTime / 1000) - ); - } - if (search?.location) { - return isInsideLocationTag( - { - latitude: file.metadata.latitude, - longitude: file.metadata.longitude, - }, - search.location - ); - } - if (search?.city) { - return isInsideCity( - { - latitude: file.metadata.latitude, - longitude: file.metadata.longitude, - }, - search.city - ); - } - if (search?.files) { - return search.files.indexOf(file.id) !== -1; - } - if (search?.person) { - return search.person.files.indexOf(file.id) !== -1; - } - - if (search?.thing) { - return search.thing.files.indexOf(file.id) !== -1; - } - - if (search?.text) { - return search.text.files.indexOf(file.id) !== -1; - } - if (typeof search?.fileType !== 'undefined') { - return search.fileType === file.metadata.fileType; - } - if (typeof search?.clip !== 'undefined') { - return search.clip.has(file.id); - } - return false; -} - function convertSuggestionToSearchQuery(option: Suggestion): Search { switch (option.type) { case SuggestionType.DATE: diff --git a/apps/photos/src/utils/comlink/ComlinkSearchWorker.ts b/apps/photos/src/utils/comlink/ComlinkSearchWorker.ts new file mode 100644 index 000000000..927c2d1bc --- /dev/null +++ b/apps/photos/src/utils/comlink/ComlinkSearchWorker.ts @@ -0,0 +1,30 @@ +import { Remote } from 'comlink'; +import { runningInBrowser } from 'utils/common'; +import { ComlinkWorker } from '@ente/shared/worker/comlinkWorker'; +import { DedicatedSearchWorker } from 'worker/search.worker'; + +class ComlinkSearchWorker { + private comlinkWorkerInstance: Remote; + + async getInstance() { + if (!this.comlinkWorkerInstance) { + this.comlinkWorkerInstance = await getDedicatedSearchWorker() + .remote; + } + return this.comlinkWorkerInstance; + } +} + +export const getDedicatedSearchWorker = () => { + if (runningInBrowser()) { + const cryptoComlinkWorker = new ComlinkWorker< + typeof DedicatedSearchWorker + >( + 'ente-search-worker', + new Worker(new URL('worker/search.worker.ts', import.meta.url)) + ); + return cryptoComlinkWorker; + } +}; + +export default new ComlinkSearchWorker(); diff --git a/apps/photos/src/worker/search.worker.ts b/apps/photos/src/worker/search.worker.ts new file mode 100644 index 000000000..8c21d12de --- /dev/null +++ b/apps/photos/src/worker/search.worker.ts @@ -0,0 +1,69 @@ +import * as Comlink from 'comlink'; +import { + isInsideLocationTag, + isInsideCity, +} from 'services/locationSearchService'; +import { EnteFile } from 'types/file'; +import { isSameDayAnyYear } from 'utils/search'; +import { Search } from 'types/search'; + +export class DedicatedSearchWorker { + async search(files: EnteFile[], search: Search) { + return files.filter((file) => { + return isSearchedFile(file, search); + }); + } +} + +Comlink.expose(DedicatedSearchWorker, self); + +function isSearchedFile(file: EnteFile, search: Search) { + if (search?.collection) { + return search.collection === file.collectionID; + } + + if (search?.date) { + return isSameDayAnyYear(search.date)( + new Date(file.metadata.creationTime / 1000) + ); + } + if (search?.location) { + return isInsideLocationTag( + { + latitude: file.metadata.latitude, + longitude: file.metadata.longitude, + }, + search.location + ); + } + if (search?.city) { + return isInsideCity( + { + latitude: file.metadata.latitude, + longitude: file.metadata.longitude, + }, + search.city + ); + } + if (search?.files) { + return search.files.indexOf(file.id) !== -1; + } + if (search?.person) { + return search.person.files.indexOf(file.id) !== -1; + } + + if (search?.thing) { + return search.thing.files.indexOf(file.id) !== -1; + } + + if (search?.text) { + return search.text.files.indexOf(file.id) !== -1; + } + if (typeof search?.fileType !== 'undefined') { + return search.fileType === file.metadata.fileType; + } + if (typeof search?.clip !== 'undefined') { + return search.clip.has(file.id); + } + return false; +} From 30ed259d1abfc19dcadc80750992ff72365d0270 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Tue, 23 Jan 2024 17:23:23 +0530 Subject: [PATCH 06/20] use worker search logic in gallery --- apps/photos/src/pages/gallery/index.tsx | 171 ++++++++---------------- 1 file changed, 56 insertions(+), 115 deletions(-) diff --git a/apps/photos/src/pages/gallery/index.tsx b/apps/photos/src/pages/gallery/index.tsx index e38ddc657..74f462f3b 100644 --- a/apps/photos/src/pages/gallery/index.tsx +++ b/apps/photos/src/pages/gallery/index.tsx @@ -120,7 +120,6 @@ import GalleryEmptyState from 'components/GalleryEmptyState'; import AuthenticateUserModal from 'components/AuthenticateUserModal'; import useMemoSingleThreaded from '@ente/shared/hooks/useMemoSingleThreaded'; import { isArchivedFile } from 'utils/magicMetadata'; -import { isSameDayAnyYear } from 'utils/search'; import { getSessionExpiredMessage } from 'utils/ui'; import { syncEntities } from 'services/entityService'; import { constructUserIDToEmailMap } from 'services/collectionService'; @@ -131,10 +130,8 @@ import { ClipService } from 'services/clipService'; import isElectron from 'is-electron'; import downloadManager from 'services/download'; import { APPS } from '@ente/shared/apps/constants'; -import locationSearchService, { - isInsideCity, - isInsideLocationTag, -} from 'services/locationSearchService'; +import locationSearchService from 'services/locationSearchService'; +import ComlinkSearchWorker from 'utils/comlink/ComlinkSearchWorker'; export const DeadCenter = styled('div')` flex: 1; @@ -471,7 +468,9 @@ export default function Gallery() { ); }, [collections, activeCollectionID]); - const filteredData = useMemoSingleThreaded((): EnteFile[] => { + const filteredData = useMemoSingleThreaded(async (): Promise< + EnteFile[] + > => { if ( !files || !user || @@ -489,125 +488,67 @@ export default function Gallery() { ]); } - const filteredFiles = getUniqueFiles( - (isInHiddenSection ? hiddenFiles : files).filter((item) => { - if (deletedFileIds?.has(item.id)) { - return false; - } + const searchWorker = await ComlinkSearchWorker.getInstance(); - if (!isInHiddenSection && hiddenFileIds?.has(item.id)) { - return false; - } + let filteredFiles: EnteFile[] = []; + if (isInSearchMode) { + filteredFiles = getUniqueFiles( + await searchWorker.search(files, search) + ); + } else { + filteredFiles = getUniqueFiles( + (isInHiddenSection ? hiddenFiles : files).filter((item) => { + if (deletedFileIds?.has(item.id)) { + return false; + } - // SEARCH MODE - if (isInSearchMode) { - if ( - search?.date && - !isSameDayAnyYear(search.date)( - new Date(item.metadata.creationTime / 1000) - ) - ) { + if (!isInHiddenSection && hiddenFileIds?.has(item.id)) { return false; } - if ( - search?.location && - !isInsideLocationTag( - { - latitude: item.metadata.latitude, - longitude: item.metadata.longitude, - }, - search.location - ) - ) { - return false; - } - if ( - search?.city && - !isInsideCity( - { - latitude: item.metadata.latitude, - longitude: item.metadata.longitude, - }, - search.city - ) - ) { - return false; - } - if ( - search?.person && - search.person.files.indexOf(item.id) === -1 - ) { - return false; - } - if ( - search?.thing && - search.thing.files.indexOf(item.id) === -1 - ) { - return false; - } - if ( - search?.text && - search.text.files.indexOf(item.id) === -1 - ) { - return false; - } - if (search?.files && search.files.indexOf(item.id) === -1) { - return false; - } - if ( - typeof search?.fileType !== 'undefined' && - search.fileType !== item.metadata.fileType - ) { - return false; - } - if (search?.clip && search.clip.has(item.id) === false) { - return false; - } - return true; - } - // archived collections files can only be seen in their respective collection - if (archivedCollections.has(item.collectionID)) { + // archived collections files can only be seen in their respective collection + if (archivedCollections.has(item.collectionID)) { + if (activeCollectionID === item.collectionID) { + return true; + } else { + return false; + } + } + + // HIDDEN ITEMS SECTION - show all individual hidden files + if ( + activeCollectionID === HIDDEN_ITEMS_SECTION && + defaultHiddenCollectionIDs.has(item.collectionID) + ) { + return true; + } + + // Archived files can only be seen in archive section or their respective collection + if (isArchivedFile(item)) { + if ( + activeCollectionID === ARCHIVE_SECTION || + activeCollectionID === item.collectionID + ) { + return true; + } else { + return false; + } + } + + // ALL SECTION - show all files + if (activeCollectionID === ALL_SECTION) { + return true; + } + + // COLLECTION SECTION - show files in the active collection if (activeCollectionID === item.collectionID) { return true; } else { return false; } - } - - // HIDDEN ITEMS SECTION - show all individual hidden files - if ( - activeCollectionID === HIDDEN_ITEMS_SECTION && - defaultHiddenCollectionIDs.has(item.collectionID) - ) { - return true; - } - - // Archived files can only be seen in archive section or their respective collection - if (isArchivedFile(item)) { - if ( - activeCollectionID === ARCHIVE_SECTION || - activeCollectionID === item.collectionID - ) { - return true; - } else { - return false; - } - } - - // ALL SECTION - show all files - if (activeCollectionID === ALL_SECTION) { - return true; - } - - // COLLECTION SECTION - show files in the active collection - if (activeCollectionID === item.collectionID) { - return true; - } else { - return false; - } - }) - ); + }) + ); + } if (search?.clip) { return filteredFiles.sort((a, b) => { return search.clip.get(b.id) - search.clip.get(a.id); From abc237945a1b8f02c3a16c50305fafde37de6c7f Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Tue, 23 Jan 2024 17:28:25 +0530 Subject: [PATCH 07/20] Add location proximity check using elliptical area --- .../src/services/locationSearchService.ts | 53 ++++++++++++------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/apps/photos/src/services/locationSearchService.ts b/apps/photos/src/services/locationSearchService.ts index 2e91ce884..2141c81ef 100644 --- a/apps/photos/src/services/locationSearchService.ts +++ b/apps/photos/src/services/locationSearchService.ts @@ -10,6 +10,7 @@ export interface City { } const DEFAULT_CITY_RADIUS = 10; +const KMS_PER_DEGREE = 111.16; class LocationSearchService { private cities: Array = []; @@ -44,26 +45,40 @@ export function isInsideLocationTag( location: Location, locationTag: LocationTagData ) { - const { centerPoint, aSquare, bSquare } = locationTag; - const { latitude, longitude } = location; - const x = Math.abs(centerPoint.latitude - latitude); - const y = Math.abs(centerPoint.longitude - longitude); - if ((x * x) / aSquare + (y * y) / bSquare <= 1) { - return true; - } else { - return false; - } + return isLocationCloseToPoint( + location, + locationTag.centerPoint, + locationTag.radius + ); } -// TODO: Verify correctness export function isInsideCity(location: Location, city: City) { - const { lat, lng } = city; - const { latitude, longitude } = location; - const x = Math.abs(lat - latitude); - const y = Math.abs(lng - longitude); - if (x * x + y * y <= DEFAULT_CITY_RADIUS * DEFAULT_CITY_RADIUS) { - return true; - } else { - return false; - } + return isLocationCloseToPoint( + { latitude: city.lat, longitude: city.lng }, + location, + DEFAULT_CITY_RADIUS + ); +} + +function isLocationCloseToPoint( + centerPoint: Location, + location: Location, + radius: number +) { + const a = (radius * _scaleFactor(centerPoint.latitude!)) / KMS_PER_DEGREE; + const b = radius / KMS_PER_DEGREE; + const x = centerPoint.latitude! - location.latitude!; + const y = centerPoint.longitude! - location.longitude!; + if ((x * x) / (a * a) + (y * y) / (b * b) <= 1) { + return true; + } + return false; +} + +///The area bounded by the location tag becomes more elliptical with increase +///in the magnitude of the latitude on the caritesian plane. When latitude is +///0 degrees, the ellipse is a circle with a = b = r. When latitude incrases, +///the major axis (a) has to be scaled by the secant of the latitude. +function _scaleFactor(lat: number) { + return 1 / Math.cos(lat * (Math.PI / 180)); } From 985b2912d1e7e58ee6a88eec01f7988b4c0834a4 Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Tue, 23 Jan 2024 17:53:12 +0530 Subject: [PATCH 08/20] Add translation --- apps/auth/public/locales/en/translation.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/auth/public/locales/en/translation.json b/apps/auth/public/locales/en/translation.json index 0b315b9d6..67684d46e 100644 --- a/apps/auth/public/locales/en/translation.json +++ b/apps/auth/public/locales/en/translation.json @@ -206,6 +206,7 @@ "SEARCH_TYPE": { "COLLECTION": "Album", "LOCATION": "Location", + "CITY": "Location", "DATE": "Date", "FILE_NAME": "File name", "THING": "Content", From 883bea8baa8634477f4b3a8b1f1db123bcf965f0 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Tue, 23 Jan 2024 19:11:56 +0530 Subject: [PATCH 09/20] fix debouncer issue --- .../components/Search/SearchBar/searchInput/index.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx b/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx index 8b46f51ae..5cfa4cdc6 100644 --- a/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx +++ b/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx @@ -96,9 +96,12 @@ export default function SearchInput(props: Iprops) { } }; - const getOptions = pDebounce( - getAutoCompleteSuggestions(props.files, props.collections), - 250 + const getOptions = useCallback( + pDebounce( + getAutoCompleteSuggestions(props.files, props.collections), + 250 + ), + [props.files, props.collections] ); const blur = () => { From 142f485a4ad0bd93e546fd6834caefb4417b7ae6 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Tue, 23 Jan 2024 20:03:19 +0530 Subject: [PATCH 10/20] remove unneeded non null assertion --- apps/photos/src/services/locationSearchService.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/photos/src/services/locationSearchService.ts b/apps/photos/src/services/locationSearchService.ts index 2141c81ef..48d28654a 100644 --- a/apps/photos/src/services/locationSearchService.ts +++ b/apps/photos/src/services/locationSearchService.ts @@ -65,10 +65,10 @@ function isLocationCloseToPoint( location: Location, radius: number ) { - const a = (radius * _scaleFactor(centerPoint.latitude!)) / KMS_PER_DEGREE; + const a = (radius * _scaleFactor(centerPoint.latitude)) / KMS_PER_DEGREE; const b = radius / KMS_PER_DEGREE; - const x = centerPoint.latitude! - location.latitude!; - const y = centerPoint.longitude! - location.longitude!; + const x = centerPoint.latitude - location.latitude; + const y = centerPoint.longitude - location.longitude; if ((x * x) / (a * a) + (y * y) / (b * b) <= 1) { return true; } From 42e390586444bd07e8f871a1882673dce97e9929 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Tue, 23 Jan 2024 20:09:59 +0530 Subject: [PATCH 11/20] update DedicatedSearchWorker to keep files list locally --- .../Search/SearchBar/searchInput/index.tsx | 2 +- apps/photos/src/pages/gallery/index.tsx | 12 +++++++++--- apps/photos/src/services/searchService.ts | 14 +++++--------- apps/photos/src/worker/search.worker.ts | 10 ++++++++-- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx b/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx index 5cfa4cdc6..c8e9bb289 100644 --- a/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx +++ b/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx @@ -79,7 +79,7 @@ export default function SearchInput(props: Iprops) { }, []); async function refreshDefaultOptions() { - const defaultOptions = await getDefaultOptions(props.files); + const defaultOptions = await getDefaultOptions(); setDefaultOptions(defaultOptions); } diff --git a/apps/photos/src/pages/gallery/index.tsx b/apps/photos/src/pages/gallery/index.tsx index 74f462f3b..ba5592926 100644 --- a/apps/photos/src/pages/gallery/index.tsx +++ b/apps/photos/src/pages/gallery/index.tsx @@ -363,6 +363,14 @@ export default function Gallery() { }; }, []); + useEffect(() => { + const main = async () => { + const searchWorker = await ComlinkSearchWorker.getInstance(); + searchWorker.setFiles(files); + }; + main(); + }, [files]); + useEffect(() => { if (!user || !files || !collections || !hiddenFiles || !trashedFiles) { return; @@ -492,9 +500,7 @@ export default function Gallery() { let filteredFiles: EnteFile[] = []; if (isInSearchMode) { - filteredFiles = getUniqueFiles( - await searchWorker.search(files, search) - ); + filteredFiles = getUniqueFiles(await searchWorker.search(search)); } else { filteredFiles = getUniqueFiles( (isInHiddenSection ? hiddenFiles : files).filter((item) => { diff --git a/apps/photos/src/services/searchService.ts b/apps/photos/src/services/searchService.ts index f29a98531..dbd80a5c9 100644 --- a/apps/photos/src/services/searchService.ts +++ b/apps/photos/src/services/searchService.ts @@ -34,13 +34,10 @@ const DIGITS = new Set(['0', '1', '2', '3', '4', '5', '6', '7', '8', '9']); const CLIP_SCORE_THRESHOLD = 0.23; -export const getDefaultOptions = async (files: EnteFile[]) => { +export const getDefaultOptions = async () => { return [ await getIndexStatusSuggestion(), - ...(await convertSuggestionsToOptions( - await getAllPeopleSuggestion(), - files - )), + ...(await convertSuggestionsToOptions(await getAllPeopleSuggestion())), ].filter((t) => !!t); }; @@ -66,7 +63,7 @@ export const getAutoCompleteSuggestions = ...(await getThingSuggestion(searchPhrase)), ].filter((suggestion) => !!suggestion); - return convertSuggestionsToOptions(suggestions, files); + return convertSuggestionsToOptions(suggestions); } catch (e) { logError(e, 'getAutoCompleteSuggestions failed'); return []; @@ -74,15 +71,14 @@ export const getAutoCompleteSuggestions = }; async function convertSuggestionsToOptions( - suggestions: Suggestion[], - files: EnteFile[] + suggestions: Suggestion[] ): Promise { const searchWorker = await ComlinkSearchWorker.getInstance(); const previewImageAppendedOptions: SearchOption[] = await Promise.all( suggestions.map(async (suggestion) => { const searchQuery = convertSuggestionToSearchQuery(suggestion); const resultFiles = getUniqueFiles( - await searchWorker.search(files, searchQuery) + await searchWorker.search(searchQuery) ); if (searchQuery?.clip) { diff --git a/apps/photos/src/worker/search.worker.ts b/apps/photos/src/worker/search.worker.ts index 8c21d12de..f64b5031a 100644 --- a/apps/photos/src/worker/search.worker.ts +++ b/apps/photos/src/worker/search.worker.ts @@ -8,8 +8,14 @@ import { isSameDayAnyYear } from 'utils/search'; import { Search } from 'types/search'; export class DedicatedSearchWorker { - async search(files: EnteFile[], search: Search) { - return files.filter((file) => { + private files: EnteFile[] = []; + + setFiles(files: EnteFile[]) { + this.files = files; + } + + search(search: Search) { + return this.files.filter((file) => { return isSearchedFile(file, search); }); } From c7ae9fbe5c3f4c371995e4b1dc212ed69de4a4fb Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Tue, 23 Jan 2024 20:41:56 +0530 Subject: [PATCH 12/20] Add translation for City --- apps/photos/public/locales/en/translation.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/photos/public/locales/en/translation.json b/apps/photos/public/locales/en/translation.json index 884a3d91a..48718e8e2 100644 --- a/apps/photos/public/locales/en/translation.json +++ b/apps/photos/public/locales/en/translation.json @@ -210,6 +210,7 @@ "SEARCH_TYPE": { "COLLECTION": "Album", "LOCATION": "Location", + "CITY": "Location", "DATE": "Date", "FILE_NAME": "File name", "THING": "Content", From dba24803396f63de69afa59f36a748788b0ee88d Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Tue, 23 Jan 2024 20:51:52 +0530 Subject: [PATCH 13/20] Dedupe cities and location tags --- apps/photos/src/services/searchService.ts | 39 ++++++++++++----------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/apps/photos/src/services/searchService.ts b/apps/photos/src/services/searchService.ts index dbd80a5c9..9b280166f 100644 --- a/apps/photos/src/services/searchService.ts +++ b/apps/photos/src/services/searchService.ts @@ -58,8 +58,7 @@ export const getAutoCompleteSuggestions = ...getCollectionSuggestion(searchPhrase, collections), getFileNameSuggestion(searchPhrase, files), getFileCaptionSuggestion(searchPhrase, files), - ...(await getLocationTagSuggestions(searchPhrase)), - ...(await getCitySuggestions(searchPhrase)), + ...(await getLocationSuggestions(searchPhrase)), ...(await getThingSuggestion(searchPhrase)), ].filter((suggestion) => !!suggestion); @@ -265,10 +264,8 @@ function getFileCaptionSuggestion( }; } -async function getLocationTagSuggestions(searchPhrase: string) { - const searchResults = await searchLocationTag(searchPhrase); - - return searchResults.map( +async function getLocationSuggestions(searchPhrase: string) { + const locationTagResults = (await searchLocationTag(searchPhrase)).map( (locationTag) => ({ type: SuggestionType.LOCATION, @@ -276,21 +273,27 @@ async function getLocationTagSuggestions(searchPhrase: string) { label: locationTag.data.name, } as Suggestion) ); -} -async function getCitySuggestions(searchPhrase: string) { - const searchResults = await locationSearchService.searchCities( + const locationTagNames = new Set(); + locationTagResults.forEach((result) => { + locationTagNames.add(result.label); + }); + + const citySearchResults = await locationSearchService.searchCities( searchPhrase ); - - return searchResults.map( - (city) => - ({ - type: SuggestionType.CITY, - value: city, - label: city.city, - } as Suggestion) - ); + return [ + ...locationTagResults, + ...citySearchResults + .filter((city) => !locationTagNames.has(city.city)) + .map(function (city) { + return { + type: SuggestionType.CITY, + value: city, + label: city.city, + } as Suggestion; + }), + ]; } async function getThingSuggestion(searchPhrase: string): Promise { From 645db9dc01fa4a415a60df9edd27892c9fb51f7d Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Tue, 23 Jan 2024 21:00:02 +0530 Subject: [PATCH 14/20] Update path to asset --- apps/photos/src/services/locationSearchService.ts | 1 - packages/shared/constants/urls.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/photos/src/services/locationSearchService.ts b/apps/photos/src/services/locationSearchService.ts index 48d28654a..c3d63db78 100644 --- a/apps/photos/src/services/locationSearchService.ts +++ b/apps/photos/src/services/locationSearchService.ts @@ -20,7 +20,6 @@ class LocationSearchService { if (this.citiesPromise) { return; } - // TODO: Ensure the response is cached on the client this.citiesPromise = fetch(CITIES_URL).then((response) => { return response.json().then((data) => { this.cities = data['data']; diff --git a/packages/shared/constants/urls.ts b/packages/shared/constants/urls.ts index 65d961ddd..122785cf5 100644 --- a/packages/shared/constants/urls.ts +++ b/packages/shared/constants/urls.ts @@ -18,4 +18,4 @@ export const WEB_ROADMAP_URL = 'https://github.com/ente-io/photos-web/issues'; export const DESKTOP_ROADMAP_URL = 'https://github.com/ente-io/photos-desktop/issues'; -export const CITIES_URL = 'https://assets.ente.io/world_cities.json'; +export const CITIES_URL = 'https://static.ente.io/world_cities.json'; From 9b3389d4955145391f66cef51f45c6ddc65417fd Mon Sep 17 00:00:00 2001 From: vishnukvmd Date: Tue, 23 Jan 2024 21:02:44 +0530 Subject: [PATCH 15/20] Add attribution for Simple Maps --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 33bfd5ea5..5b8acb0b1 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,9 @@ An important part of our journey is to build better software by consistently lis
---- +## 🙇 Attributions -Cross-browser testing provided by +- Cross-browser testing provided by [](https://www.browserstack.com/open-source) + +- Location search powered by [Simple Maps](https://simplemaps.com/data/world-cities) From 49c4331f8106023f6d5acfaf559443fabc44d820 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Wed, 24 Jan 2024 11:44:36 +0530 Subject: [PATCH 16/20] make searchWorker setFiles useEffect SingleThreaded --- apps/photos/src/pages/gallery/index.tsx | 13 ++++---- .../shared/hooks/useEffectSingleThreaded.tsx | 30 +++++++++++++++++++ packages/shared/utils/index.ts | 4 +++ 3 files changed, 41 insertions(+), 6 deletions(-) create mode 100644 packages/shared/hooks/useEffectSingleThreaded.tsx diff --git a/apps/photos/src/pages/gallery/index.tsx b/apps/photos/src/pages/gallery/index.tsx index ba5592926..c2dadc0b3 100644 --- a/apps/photos/src/pages/gallery/index.tsx +++ b/apps/photos/src/pages/gallery/index.tsx @@ -132,6 +132,7 @@ import downloadManager from 'services/download'; import { APPS } from '@ente/shared/apps/constants'; import locationSearchService from 'services/locationSearchService'; import ComlinkSearchWorker from 'utils/comlink/ComlinkSearchWorker'; +import useEffectSingleThreaded from '@ente/shared/hooks/useEffectSingleThreaded'; export const DeadCenter = styled('div')` flex: 1; @@ -363,13 +364,13 @@ export default function Gallery() { }; }, []); - useEffect(() => { - const main = async () => { + useEffectSingleThreaded( + async ([files]: [files: EnteFile[]]) => { const searchWorker = await ComlinkSearchWorker.getInstance(); - searchWorker.setFiles(files); - }; - main(); - }, [files]); + await searchWorker.setFiles(files); + }, + [files] + ); useEffect(() => { if (!user || !files || !collections || !hiddenFiles || !trashedFiles) { diff --git a/packages/shared/hooks/useEffectSingleThreaded.tsx b/packages/shared/hooks/useEffectSingleThreaded.tsx new file mode 100644 index 000000000..94f4f1931 --- /dev/null +++ b/packages/shared/hooks/useEffectSingleThreaded.tsx @@ -0,0 +1,30 @@ +import { useEffect, useRef } from 'react'; +import { isPromise } from '../utils'; + +export default function useEffectSingleThreaded( + fn: (deps) => void | Promise, + deps: any[] +): void { + const updateInProgress = useRef(false); + const nextRequestDepsRef = useRef(null); + useEffect(() => { + const main = async (deps) => { + if (updateInProgress.current) { + nextRequestDepsRef.current = deps; + return; + } + updateInProgress.current = true; + const result = fn(deps); + if (isPromise(result)) { + await result; + } + updateInProgress.current = false; + if (nextRequestDepsRef.current) { + const deps = nextRequestDepsRef.current; + nextRequestDepsRef.current = null; + setTimeout(() => main(deps), 0); + } + }; + main(deps); + }, deps); +} diff --git a/packages/shared/utils/index.ts b/packages/shared/utils/index.ts index f83937702..d65ef4380 100644 --- a/packages/shared/utils/index.ts +++ b/packages/shared/utils/index.ts @@ -22,3 +22,7 @@ export function downloadUsingAnchor(link: string, name: string) { URL.revokeObjectURL(link); a.remove(); } + +export function isPromise(obj: T | Promise): obj is Promise { + return obj && typeof (obj as any).then === 'function'; +} From cc480a237635b3ce48e6c0835a7c6433c009d6d3 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Wed, 24 Jan 2024 11:52:58 +0530 Subject: [PATCH 17/20] refactor convertSuggestionsToOptions --- apps/photos/src/services/searchService.ts | 43 ++++++++++------------- 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/apps/photos/src/services/searchService.ts b/apps/photos/src/services/searchService.ts index 9b280166f..80512fe4b 100644 --- a/apps/photos/src/services/searchService.ts +++ b/apps/photos/src/services/searchService.ts @@ -73,33 +73,28 @@ async function convertSuggestionsToOptions( suggestions: Suggestion[] ): Promise { const searchWorker = await ComlinkSearchWorker.getInstance(); - const previewImageAppendedOptions: SearchOption[] = await Promise.all( - suggestions.map(async (suggestion) => { - const searchQuery = convertSuggestionToSearchQuery(suggestion); - const resultFiles = getUniqueFiles( - await searchWorker.search(searchQuery) - ); - - if (searchQuery?.clip) { - resultFiles.sort((a, b) => { - const aScore = searchQuery.clip.get(a.id); - const bScore = searchQuery.clip.get(b.id); - return bScore - aScore; - }); - } - - return { + const previewImageAppendedOptions: SearchOption[] = []; + for (const suggestion of suggestions) { + const searchQuery = convertSuggestionToSearchQuery(suggestion); + const resultFiles = getUniqueFiles( + await searchWorker.search(searchQuery) + ); + if (searchQuery?.clip) { + resultFiles.sort((a, b) => { + const aScore = searchQuery.clip.get(a.id); + const bScore = searchQuery.clip.get(b.id); + return bScore - aScore; + }); + } + if (resultFiles.length) { + previewImageAppendedOptions.push({ ...suggestion, fileCount: resultFiles.length, previewFiles: resultFiles.slice(0, 3), - }; - }) - ); - const nonEmptyOptions = previewImageAppendedOptions.filter( - (option) => option.fileCount - ); - - return nonEmptyOptions; + }); + } + } + return previewImageAppendedOptions; } function getFileTypeSuggestion(searchPhrase: string): Suggestion[] { return [ From 603fde07d14f95730693118e6d07ad0f992827c6 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Wed, 24 Jan 2024 12:02:32 +0530 Subject: [PATCH 18/20] refactor getLocationSuggestions --- apps/photos/src/services/searchService.ts | 38 ++++++++++++----------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/apps/photos/src/services/searchService.ts b/apps/photos/src/services/searchService.ts index 80512fe4b..d257fde4a 100644 --- a/apps/photos/src/services/searchService.ts +++ b/apps/photos/src/services/searchService.ts @@ -260,7 +260,8 @@ function getFileCaptionSuggestion( } async function getLocationSuggestions(searchPhrase: string) { - const locationTagResults = (await searchLocationTag(searchPhrase)).map( + const locationTagResults = await searchLocationTag(searchPhrase); + const locationTagSuggestions = locationTagResults.map( (locationTag) => ({ type: SuggestionType.LOCATION, @@ -268,27 +269,28 @@ async function getLocationSuggestions(searchPhrase: string) { label: locationTag.data.name, } as Suggestion) ); - - const locationTagNames = new Set(); - locationTagResults.forEach((result) => { - locationTagNames.add(result.label); - }); + const locationTagNames = new Set( + locationTagSuggestions.map((result) => result.label) + ); const citySearchResults = await locationSearchService.searchCities( searchPhrase ); - return [ - ...locationTagResults, - ...citySearchResults - .filter((city) => !locationTagNames.has(city.city)) - .map(function (city) { - return { - type: SuggestionType.CITY, - value: city, - label: city.city, - } as Suggestion; - }), - ]; + + const nonConflictingCityResult = citySearchResults.filter( + (city) => !locationTagNames.has(city.city) + ); + + const citySearchSuggestions = nonConflictingCityResult.map( + (city) => + ({ + type: SuggestionType.CITY, + value: city, + label: city.city, + } as Suggestion) + ); + + return [...locationTagSuggestions, ...citySearchSuggestions]; } async function getThingSuggestion(searchPhrase: string): Promise { From 6735075880388e90f25fa98553ca4b0110ada46c Mon Sep 17 00:00:00 2001 From: Abhinav Date: Wed, 24 Jan 2024 13:35:00 +0530 Subject: [PATCH 19/20] add comment --- packages/shared/hooks/useEffectSingleThreaded.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/shared/hooks/useEffectSingleThreaded.tsx b/packages/shared/hooks/useEffectSingleThreaded.tsx index 94f4f1931..3bdebed5a 100644 --- a/packages/shared/hooks/useEffectSingleThreaded.tsx +++ b/packages/shared/hooks/useEffectSingleThreaded.tsx @@ -1,6 +1,9 @@ import { useEffect, useRef } from 'react'; import { isPromise } from '../utils'; +// useEffectSingleThreaded is a useEffect that will only run one at a time, and will +// caches the latest deps of requests that come in while it is running, and will +// run that after the current run is complete. export default function useEffectSingleThreaded( fn: (deps) => void | Promise, deps: any[] From 3d72e58c558d3866c8608c5c58412f9d092210c7 Mon Sep 17 00:00:00 2001 From: Abhinav Date: Wed, 24 Jan 2024 13:49:20 +0530 Subject: [PATCH 20/20] add error logging --- .../src/services/locationSearchService.ts | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/apps/photos/src/services/locationSearchService.ts b/apps/photos/src/services/locationSearchService.ts index c3d63db78..eebe44fac 100644 --- a/apps/photos/src/services/locationSearchService.ts +++ b/apps/photos/src/services/locationSearchService.ts @@ -1,4 +1,5 @@ import { CITIES_URL } from '@ente/shared/constants/urls'; +import { logError } from '@ente/shared/sentry'; import { LocationTagData } from 'types/entity'; import { Location } from 'types/upload'; @@ -16,25 +17,38 @@ class LocationSearchService { private cities: Array = []; private citiesPromise: Promise; - loadCities() { - if (this.citiesPromise) { - return; - } - this.citiesPromise = fetch(CITIES_URL).then((response) => { - return response.json().then((data) => { - this.cities = data['data']; + async loadCities() { + try { + if (this.citiesPromise) { + return; + } + this.citiesPromise = fetch(CITIES_URL).then((response) => { + return response.json().then((data) => { + this.cities = data['data']; + }); }); - }); + await this.citiesPromise; + } catch (e) { + logError(e, 'LocationSearchService loadCities failed'); + this.citiesPromise = null; + } } async searchCities(searchTerm: string) { - if (!this.citiesPromise) { - this.loadCities(); + try { + if (!this.citiesPromise) { + this.loadCities(); + } + await this.citiesPromise; + return this.cities.filter((city) => { + return city.city + .toLowerCase() + .startsWith(searchTerm.toLowerCase()); + }); + } catch (e) { + logError(e, 'LocationSearchService searchCities failed'); + throw e; } - await this.citiesPromise; - return this.cities.filter((city) => { - return city.city.toLowerCase().startsWith(searchTerm.toLowerCase()); - }); } }