From 315529eebfb699064f497082b5a3d04da18b0d91 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 07:31:55 +0530 Subject: [PATCH 01/32] T --- web/apps/photos/src/components/SearchBar.tsx | 20 ++++++-------- .../new/photos/services/search/index.ts | 26 +++++++++---------- 2 files changed, 21 insertions(+), 25 deletions(-) diff --git a/web/apps/photos/src/components/SearchBar.tsx b/web/apps/photos/src/components/SearchBar.tsx index 37149b00a2..32e727668d 100644 --- a/web/apps/photos/src/components/SearchBar.tsx +++ b/web/apps/photos/src/components/SearchBar.tsx @@ -159,6 +159,7 @@ const SearchInput: React.FC = ({ const theme = useTheme(); const styles = useMemo(() => useSelectStyles(theme), [theme]); + const components = useMemo(() => ({ Option, Control, Input }), []); useEffect(() => { search(value); @@ -176,9 +177,7 @@ const SearchInput: React.FC = ({ }; const handleInputChange = (value: string, actionMeta: InputActionMeta) => { - if (actionMeta.action === "input-change") { - setInputValue(value); - } + if (actionMeta.action == "input-change") setInputValue(value); }; const resetSearch = () => { @@ -187,8 +186,8 @@ const SearchInput: React.FC = ({ setInputValue(""); }; - const getOptions = useCallback( - pDebounce(getAutoCompleteSuggestions(), 250), + const loadOptions = useCallback( + pDebounce(getAutoCompleteSuggestions, 250), [], ); @@ -245,8 +244,6 @@ const SearchInput: React.FC = ({ setValue(value); }; - const components = useMemo(() => ({ Option, Control, Input }), []); - return ( = ({ value={value} components={components} styles={styles} - placeholder={t("search_hint")} - loadOptions={getOptions} + loadOptions={loadOptions} onChange={handleChange} - isMulti={false} - isClearable - escapeClearsValue inputValue={inputValue} onInputChange={handleInputChange} + isClearable + escapeClearsValue + placeholder={t("search_hint")} noOptionsMessage={({ inputValue }) => shouldShowEmptyState(inputValue) ? ( diff --git a/web/packages/new/photos/services/search/index.ts b/web/packages/new/photos/services/search/index.ts index af0dca4a23..29115122cd 100644 --- a/web/packages/new/photos/services/search/index.ts +++ b/web/packages/new/photos/services/search/index.ts @@ -163,19 +163,19 @@ const labelledFileTypes = (): LabelledFileType[] => [ // TODO-Cluster -- AUDIT BELOW THIS // Suggestions shown in the search dropdown when the user has typed something. -export const getAutoCompleteSuggestions = - () => - async (searchPhrase: string): Promise => { - log.debug(() => ["getAutoCompleteSuggestions"]); - try { - const suggestions: Suggestion[] = - await suggestionsForString(searchPhrase); - return convertSuggestionsToOptions(suggestions); - } catch (e) { - log.error("getAutoCompleteSuggestions failed", e); - return []; - } - }; +export const getAutoCompleteSuggestions = async ( + searchPhrase: string, +): Promise => { + log.debug(() => ["getAutoCompleteSuggestions"]); + try { + const suggestions: Suggestion[] = + await suggestionsForString(searchPhrase); + return convertSuggestionsToOptions(suggestions); + } catch (e) { + log.error("getAutoCompleteSuggestions failed", e); + return []; + } +}; async function convertSuggestionsToOptions( suggestions: Suggestion[], From a16830f5ca7f9db8309285292dcdf56adcc2a9ac Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 07:33:55 +0530 Subject: [PATCH 02/32] Prefix match for file types --- .../new/photos/services/search/worker.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/web/packages/new/photos/services/search/worker.ts b/web/packages/new/photos/services/search/worker.ts index aa31c7f16c..0b42418a76 100644 --- a/web/packages/new/photos/services/search/worker.ts +++ b/web/packages/new/photos/services/search/worker.ts @@ -154,6 +154,18 @@ const fileCaptionMatches = (s: string, files: EnteFile[]) => file.pubMagicMetadata?.data?.caption?.toLowerCase().includes(s), ); +const fileTypeSuggestions = ( + s: string, + labelledFileTypes: Searchable[], +) => + labelledFileTypes + .filter(({ lowercasedName }) => lowercasedName.startsWith(s)) + .map(({ fileType, label }) => ({ + type: SuggestionType.FILE_TYPE, + value: fileType, + label, + })); + const dateSuggestions = ( s: string, locale: string, @@ -298,18 +310,6 @@ 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 973eac2b3474f09e636f4f89ef6a328736b4bdfd Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 08:07:49 +0530 Subject: [PATCH 03/32] Add workaround to restore suggestions on focus openMenuOnClick and openMenuOnFocus did not seem to work for AsyncSelect. Workaround source: https://github.com/JedWatson/react-select/issues/5714#issuecomment-1653251587 The underlying problem is perhaps because of an earlier workaround we are using, for editable selects. https://github.com/JedWatson/react-select/issues/4675#issuecomment-944010398 --- web/apps/photos/src/components/SearchBar.tsx | 22 +++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/web/apps/photos/src/components/SearchBar.tsx b/web/apps/photos/src/components/SearchBar.tsx index 32e727668d..2cc273091b 100644 --- a/web/apps/photos/src/components/SearchBar.tsx +++ b/web/apps/photos/src/components/SearchBar.tsx @@ -244,6 +244,18 @@ const SearchInput: React.FC = ({ setValue(value); }; + const handleFocus = () => { + // A workaround to show the suggestions again for the current non-empty + // search string if the user focuses back on the input field after + // moving focus elsewhere. + if (inputValue) { + selectRef?.current.onInputChange(inputValue, { + action: "select-value", + prevInputValue: "", + }); + } + }; + return ( = ({ onInputChange={handleInputChange} isClearable escapeClearsValue + onFocus={handleFocus} placeholder={t("search_hint")} noOptionsMessage={({ inputValue }) => shouldShowEmptyState(inputValue) ? ( @@ -573,9 +586,12 @@ const LabelWithInfo = ({ data }: { data: SearchOption }) => { ); }; -// A custom input for react-select that is always visible. This is a roundabout -// hack the existing code used to display the search string when showing the -// results page; likely there should be a better way. +/** + * A custom input for react-select that is always visible. + * + * This is a workaround to allow the search string to be always displayed, and + * editable, even after the user has moved focus away from it. + */ const Input: React.FC> = (props) => ( ); From feb0dde7061bd73385bbec9cc9cbe3c2df6d7f7c Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 08:17:28 +0530 Subject: [PATCH 04/32] Inline --- web/apps/photos/src/components/SearchBar.tsx | 16 +++++++--------- .../shared/components/CodeBlock/index.tsx | 9 +++++++-- web/packages/shared/components/Container.tsx | 6 ------ 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/web/apps/photos/src/components/SearchBar.tsx b/web/apps/photos/src/components/SearchBar.tsx index 2cc273091b..5559b74ef4 100644 --- a/web/apps/photos/src/components/SearchBar.tsx +++ b/web/apps/photos/src/components/SearchBar.tsx @@ -21,10 +21,7 @@ import { } from "@/new/photos/services/search/types"; import { labelForSuggestionType } from "@/new/photos/services/search/ui"; import type { LocationTag } from "@/new/photos/services/user-entity"; -import { - FreeFlowText, - SpaceBetweenFlex, -} from "@ente/shared/components/Container"; +import { 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"; @@ -559,11 +556,12 @@ const LabelWithInfo = ({ data }: { data: SearchOption }) => { - - - {data.label} - - + + {data.label} + + {t("photos_count", { count: data.fileCount })} diff --git a/web/packages/shared/components/CodeBlock/index.tsx b/web/packages/shared/components/CodeBlock/index.tsx index f0a5ac803e..6771fe1b43 100644 --- a/web/packages/shared/components/CodeBlock/index.tsx +++ b/web/packages/shared/components/CodeBlock/index.tsx @@ -1,6 +1,5 @@ -import { FreeFlowText } from "@ente/shared/components/Container"; import EnteSpinner from "@ente/shared/components/EnteSpinner"; -import type { BoxProps } from "@mui/material"; +import { type BoxProps, styled } from "@mui/material"; import React from "react"; import CopyButton from "./CopyButton"; import { CodeWrapper, CopyButtonWrapper, Wrapper } from "./styledComponents"; @@ -35,3 +34,9 @@ export default function CodeBlock({ ); } + +const FreeFlowText = styled("div")` + word-break: break-word; + min-width: 30%; + text-align: left; +`; diff --git a/web/packages/shared/components/Container.tsx b/web/packages/shared/components/Container.tsx index 517e058b5a..cd0dca4914 100644 --- a/web/packages/shared/components/Container.tsx +++ b/web/packages/shared/components/Container.tsx @@ -31,12 +31,6 @@ export const FlexWrapper = styled(Box)` align-items: center; `; -export const FreeFlowText = styled("div")` - word-break: break-word; - min-width: 30%; - text-align: left; -`; - export const SpaceBetweenFlex = styled(FlexWrapper)` justify-content: space-between; `; From eb2d1f04c47565b48b516088f1dd09345ce3529e Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 08:23:30 +0530 Subject: [PATCH 05/32] Remove unused --- web/packages/shared/components/CodeBlock/index.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/web/packages/shared/components/CodeBlock/index.tsx b/web/packages/shared/components/CodeBlock/index.tsx index 6771fe1b43..b32b8fca66 100644 --- a/web/packages/shared/components/CodeBlock/index.tsx +++ b/web/packages/shared/components/CodeBlock/index.tsx @@ -6,12 +6,10 @@ import { CodeWrapper, CopyButtonWrapper, Wrapper } from "./styledComponents"; type Iprops = React.PropsWithChildren<{ code: string | null; - wordBreak?: "normal" | "break-all" | "keep-all" | "break-word"; }>; export default function CodeBlock({ code, - wordBreak, ...props }: BoxProps<"div", Iprops>) { if (!code) { @@ -24,9 +22,7 @@ export default function CodeBlock({ return ( - - {code} - + {code} From 88a0a2f9fcc93847d0add92dcb2da2f33e567004 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 08:54:42 +0530 Subject: [PATCH 06/32] Style tweaks --- web/apps/photos/src/components/SearchBar.tsx | 72 ++++++++++---------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/web/apps/photos/src/components/SearchBar.tsx b/web/apps/photos/src/components/SearchBar.tsx index 5559b74ef4..17dd66e74f 100644 --- a/web/apps/photos/src/components/SearchBar.tsx +++ b/web/apps/photos/src/components/SearchBar.tsx @@ -21,7 +21,6 @@ import { } from "@/new/photos/services/search/types"; import { labelForSuggestionType } from "@/new/photos/services/search/ui"; import type { LocationTag } from "@/new/photos/services/user-entity"; -import { 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"; @@ -322,7 +321,7 @@ const useSelectStyles = ({ "& :hover": { cursor: "pointer", }, - "& .main": { + "& .option-contents": { backgroundColor: isFocused && colors.background.elevated2, }, "&:last-child .MuiDivider-root": { @@ -543,46 +542,45 @@ async function getAllPeople(limit: number = undefined) { const Option: React.FC> = (props) => ( - + + ); -const LabelWithInfo = ({ data }: { data: SearchOption }) => { - return ( - <> - - - {labelForSuggestionType(data.type)} +const OptionContents = ({ data }: { data: SearchOption }) => ( + + + {labelForSuggestionType(data.type)} + + + + + {data.label} + + + {t("photos_count", { count: data.fileCount })} - - - - {data.label} - - - - {t("photos_count", { count: data.fileCount })} - - - - - {data.previewFiles.map((file) => ( - null} - collectionTile={ResultPreviewTile} - /> - ))} - - - - - ); -}; + + + {data.previewFiles.map((file) => ( + null} + collectionTile={ResultPreviewTile} + /> + ))} + + + +); /** * A custom input for react-select that is always visible. From ea46ac0196e0a0ab2cd32a384c50c8f40f2c65a8 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 09:16:05 +0530 Subject: [PATCH 07/32] Rearrange --- web/apps/photos/src/components/SearchBar.tsx | 22 ++++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/web/apps/photos/src/components/SearchBar.tsx b/web/apps/photos/src/components/SearchBar.tsx index 17dd66e74f..6c4d1928bd 100644 --- a/web/apps/photos/src/components/SearchBar.tsx +++ b/web/apps/photos/src/components/SearchBar.tsx @@ -155,7 +155,7 @@ const SearchInput: React.FC = ({ const theme = useTheme(); const styles = useMemo(() => useSelectStyles(theme), [theme]); - const components = useMemo(() => ({ Option, Control, Input }), []); + const components = useMemo(() => ({ Control, Input, Option }), []); useEffect(() => { search(value); @@ -381,6 +381,16 @@ const iconForOptionType = (type: SuggestionType | undefined) => { } }; +/** + * A custom input for react-select that is always visible. + * + * This is a workaround to allow the search string to be always displayed, and + * editable, even after the user has moved focus away from it. + */ +const Input: React.FC> = (props) => ( + +); + /** * A preflight check for whether or not we should show the EmptyState. * @@ -581,13 +591,3 @@ const OptionContents = ({ data }: { data: SearchOption }) => ( ); - -/** - * A custom input for react-select that is always visible. - * - * This is a workaround to allow the search string to be always displayed, and - * editable, even after the user has moved focus away from it. - */ -const Input: React.FC> = (props) => ( - -); From b5eaa757da9c3121d8ac0f9689be3fe6592a67e4 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 09:44:43 +0530 Subject: [PATCH 08/32] Move --- .../components/Collections/CollectionCard.tsx | 2 +- web/apps/photos/src/components/SearchBar.tsx | 7 ++- .../components/pages/gallery/PreviewCard.tsx | 2 +- .../new/photos/components/ItemCards.tsx | 43 +++++++++++++++++++ .../components/PlaceholderThumbnails.tsx | 0 5 files changed, 48 insertions(+), 6 deletions(-) create mode 100644 web/packages/new/photos/components/ItemCards.tsx rename web/{apps/photos/src => packages/new/photos}/components/PlaceholderThumbnails.tsx (100%) diff --git a/web/apps/photos/src/components/Collections/CollectionCard.tsx b/web/apps/photos/src/components/Collections/CollectionCard.tsx index 7d757561ba..b6d4b36c49 100644 --- a/web/apps/photos/src/components/Collections/CollectionCard.tsx +++ b/web/apps/photos/src/components/Collections/CollectionCard.tsx @@ -3,7 +3,7 @@ import { EnteFile } from "@/new/photos/types/file"; import { LoadingThumbnail, StaticThumbnail, -} from "components/PlaceholderThumbnails"; +} from "@/new/photos/components/PlaceholderThumbnails"; import { useEffect, useState } from "react"; export default function CollectionCard(props: { diff --git a/web/apps/photos/src/components/SearchBar.tsx b/web/apps/photos/src/components/SearchBar.tsx index 6c4d1928bd..541afb524e 100644 --- a/web/apps/photos/src/components/SearchBar.tsx +++ b/web/apps/photos/src/components/SearchBar.tsx @@ -1,6 +1,7 @@ import { assertionFailed } from "@/base/assert"; import { useIsMobileWidth } from "@/base/hooks"; import { FileType } from "@/media/file-type"; +import { ItemCard } from "@/new/photos/components/ItemCards"; import { isMLSupported, mlStatusSnapshot, @@ -37,7 +38,6 @@ import { useTheme, type Theme, } from "@mui/material"; -import CollectionCard from "components/Collections/CollectionCard"; import { ResultPreviewTile } from "components/Collections/styledComponents"; import { t } from "i18next"; import pDebounce from "p-debounce"; @@ -580,11 +580,10 @@ const OptionContents = ({ data }: { data: SearchOption }) => ( {data.previewFiles.map((file) => ( - null} - collectionTile={ResultPreviewTile} + TileComponent={ResultPreviewTile} /> ))} diff --git a/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx b/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx index deb212ef77..61ec39e21d 100644 --- a/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx +++ b/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx @@ -15,7 +15,7 @@ import { import { LoadingThumbnail, StaticThumbnail, -} from "components/PlaceholderThumbnails"; +} from "@/new/photos/components/PlaceholderThumbnails"; import i18n from "i18next"; import { DeduplicateContext } from "pages/deduplicate"; import { GalleryContext } from "pages/gallery"; diff --git a/web/packages/new/photos/components/ItemCards.tsx b/web/packages/new/photos/components/ItemCards.tsx new file mode 100644 index 0000000000..9bfa8afb89 --- /dev/null +++ b/web/packages/new/photos/components/ItemCards.tsx @@ -0,0 +1,43 @@ +import { + LoadingThumbnail, + StaticThumbnail, +} from "@/new/photos/components/PlaceholderThumbnails"; +import downloadManager from "@/new/photos/services/download"; +import { type EnteFile } from "@/new/photos/types/file"; +import React, { useEffect, useState } from "react"; + +interface ItemCardProps { + coverFile: EnteFile; + TileComponent: React.FC; +} + +/** + * A simplified variant of {@link CollectionCard}, meant to be used for + * representing either collections and files. + */ +export const ItemCard: React.FC = ({ + coverFile, + TileComponent, +}) => { + const [coverImageURL, setCoverImageURL] = useState(""); + + useEffect(() => { + const main = async () => { + const url = await downloadManager.getThumbnailForPreview(coverFile); + if (url) setCoverImageURL(url); + }; + void main(); + }, [coverFile]); + + return ( + + {coverFile.metadata.hasStaticThumbnail ? ( + + ) : coverImageURL ? ( + + ) : ( + + )} + + ); +}; diff --git a/web/apps/photos/src/components/PlaceholderThumbnails.tsx b/web/packages/new/photos/components/PlaceholderThumbnails.tsx similarity index 100% rename from web/apps/photos/src/components/PlaceholderThumbnails.tsx rename to web/packages/new/photos/components/PlaceholderThumbnails.tsx From 03b6ed6f1ade0cd6633ad595b38af2ac2538ea0e Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 09:51:10 +0530 Subject: [PATCH 09/32] Tile dup --- .../components/Collections/CollectionCard.tsx | 1 + .../Collections/styledComponents.ts | 6 +---- .../src/components/ExportPendingList.tsx | 2 +- web/apps/photos/src/components/SearchBar.tsx | 4 +-- .../new/photos/components/ItemCards.tsx | 26 +++++++++++++++++++ 5 files changed, 30 insertions(+), 9 deletions(-) diff --git a/web/apps/photos/src/components/Collections/CollectionCard.tsx b/web/apps/photos/src/components/Collections/CollectionCard.tsx index b6d4b36c49..2a60d9ce72 100644 --- a/web/apps/photos/src/components/Collections/CollectionCard.tsx +++ b/web/apps/photos/src/components/Collections/CollectionCard.tsx @@ -6,6 +6,7 @@ import { } from "@/new/photos/components/PlaceholderThumbnails"; import { useEffect, useState } from "react"; +/** See also: {@link ItemCard}. */ export default function CollectionCard(props: { children?: any; coverFile: EnteFile; diff --git a/web/apps/photos/src/components/Collections/styledComponents.ts b/web/apps/photos/src/components/Collections/styledComponents.ts index d7bfcbde94..91dfdc9c58 100644 --- a/web/apps/photos/src/components/Collections/styledComponents.ts +++ b/web/apps/photos/src/components/Collections/styledComponents.ts @@ -35,6 +35,7 @@ export const ScrollContainer = styled("div")` gap: 4px; `; +/** See also: {@link ItemTile}. */ export const CollectionTile = styled("div")` display: flex; position: relative; @@ -67,11 +68,6 @@ export const AllCollectionTile = styled(CollectionTile)` height: 150px; `; -export const ResultPreviewTile = styled(CollectionTile)` - width: 48px; - height: 48px; -`; - export const CollectionBarTileText = styled(Overlay)` padding: 4px; background: linear-gradient( diff --git a/web/apps/photos/src/components/ExportPendingList.tsx b/web/apps/photos/src/components/ExportPendingList.tsx index 88abf66b22..7b1267903a 100644 --- a/web/apps/photos/src/components/ExportPendingList.tsx +++ b/web/apps/photos/src/components/ExportPendingList.tsx @@ -1,3 +1,4 @@ +import { ResultPreviewTile } from "@/new/photos/components/ItemCards"; import { EnteFile } from "@/new/photos/types/file"; import { FlexWrapper } from "@ente/shared/components/Container"; import DialogBoxV2 from "@ente/shared/components/DialogBoxV2"; @@ -5,7 +6,6 @@ import { Box, styled } from "@mui/material"; import ItemList from "components/ItemList"; import { t } from "i18next"; import CollectionCard from "./Collections/CollectionCard"; -import { ResultPreviewTile } from "./Collections/styledComponents"; interface Iprops { isOpen: boolean; diff --git a/web/apps/photos/src/components/SearchBar.tsx b/web/apps/photos/src/components/SearchBar.tsx index 541afb524e..57040915b0 100644 --- a/web/apps/photos/src/components/SearchBar.tsx +++ b/web/apps/photos/src/components/SearchBar.tsx @@ -1,7 +1,7 @@ import { assertionFailed } from "@/base/assert"; import { useIsMobileWidth } from "@/base/hooks"; import { FileType } from "@/media/file-type"; -import { ItemCard } from "@/new/photos/components/ItemCards"; +import { ItemCard, ResultPreviewTile } from "@/new/photos/components/ItemCards"; import { isMLSupported, mlStatusSnapshot, @@ -38,7 +38,6 @@ import { useTheme, type Theme, } from "@mui/material"; -import { ResultPreviewTile } from "components/Collections/styledComponents"; import { t } from "i18next"; import pDebounce from "p-debounce"; import { @@ -547,7 +546,6 @@ async function getAllPeople(limit: number = undefined) { // return result; } - */ const Option: React.FC> = (props) => ( diff --git a/web/packages/new/photos/components/ItemCards.tsx b/web/packages/new/photos/components/ItemCards.tsx index 9bfa8afb89..4487351ac2 100644 --- a/web/packages/new/photos/components/ItemCards.tsx +++ b/web/packages/new/photos/components/ItemCards.tsx @@ -4,10 +4,13 @@ import { } from "@/new/photos/components/PlaceholderThumbnails"; import downloadManager from "@/new/photos/services/download"; import { type EnteFile } from "@/new/photos/types/file"; +import { styled } from "@mui/material"; import React, { useEffect, useState } from "react"; interface ItemCardProps { + /** The file whose thumbnail (if any) should be should be shown. */ coverFile: EnteFile; + /** One of the *Tile components to use as the top level element. */ TileComponent: React.FC; } @@ -41,3 +44,26 @@ export const ItemCard: React.FC = ({ ); }; + +/** + * A verbatim copy of CollectionTile, meant to be used with ItemCards. + */ +export const ItemTile = styled("div")` + display: flex; + position: relative; + border-radius: 4px; + overflow: hidden; + cursor: pointer; + & > img { + object-fit: cover; + width: 100%; + height: 100%; + pointer-events: none; + } + user-select: none; +`; + +export const ResultPreviewTile = styled(ItemTile)` + width: 48px; + height: 48px; +`; From 4c5b59b45372e0e4c52c252cc3731d52b997547e Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 09:51:54 +0530 Subject: [PATCH 10/32] Move to new --- .../photos/src => packages/new/photos}/components/SearchBar.tsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename web/{apps/photos/src => packages/new/photos}/components/SearchBar.tsx (100%) diff --git a/web/apps/photos/src/components/SearchBar.tsx b/web/packages/new/photos/components/SearchBar.tsx similarity index 100% rename from web/apps/photos/src/components/SearchBar.tsx rename to web/packages/new/photos/components/SearchBar.tsx From fbd8346edf22fbec9c1c7d332dadeb6e6d03657b Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 09:55:11 +0530 Subject: [PATCH 11/32] Fix lint 1 --- web/apps/photos/src/pages/gallery.tsx | 5 +++- .../new/photos/components/SearchBar.tsx | 26 +++++++++---------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index 6e2dd613bd..cd0170dbea 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -3,6 +3,10 @@ import { NavbarBase } from "@/base/components/Navbar"; import { useIsMobileWidth } from "@/base/hooks"; import log from "@/base/log"; import type { Collection } from "@/media/collection"; +import { + SearchBar, + type UpdateSearch, +} from "@/new/photos/components/SearchBar"; import { WhatsNew } from "@/new/photos/components/WhatsNew"; import { shouldShowWhatsNew } from "@/new/photos/services/changelog"; import downloadManager from "@/new/photos/services/download"; @@ -74,7 +78,6 @@ import GalleryEmptyState from "components/GalleryEmptyState"; import { LoadingOverlay } from "components/LoadingOverlay"; import PhotoFrame from "components/PhotoFrame"; import { ITEM_TYPE, TimeStampListItem } from "components/PhotoList"; -import { SearchBar, type UpdateSearch } from "components/SearchBar"; import Sidebar from "components/Sidebar"; import { type UploadTypeSelectorIntent } from "components/Upload/UploadTypeSelector"; import Uploader from "components/Upload/Uploader"; diff --git a/web/packages/new/photos/components/SearchBar.tsx b/web/packages/new/photos/components/SearchBar.tsx index 57040915b0..06f7a18f97 100644 --- a/web/packages/new/photos/components/SearchBar.tsx +++ b/web/packages/new/photos/components/SearchBar.tsx @@ -10,16 +10,14 @@ import { import { getAutoCompleteSuggestions } from "@/new/photos/services/search"; import type { City, + ClipSearchScores, SearchDateComponents, + SearchOption, SearchPerson, + SearchQuery, SearchResultSummary, } from "@/new/photos/services/search/types"; -import { - ClipSearchScores, - SearchOption, - SearchQuery, - SuggestionType, -} from "@/new/photos/services/search/types"; +import { SuggestionType } from "@/new/photos/services/search/types"; import { labelForSuggestionType } from "@/new/photos/services/search/ui"; import type { LocationTag } from "@/new/photos/services/user-entity"; import CalendarIcon from "@mui/icons-material/CalendarMonth"; @@ -40,7 +38,7 @@ import { } from "@mui/material"; import { t } from "i18next"; import pDebounce from "p-debounce"; -import { +import React, { useCallback, useEffect, useMemo, @@ -83,8 +81,8 @@ interface SearchBarProps { } export type UpdateSearch = ( - search: SearchQuery, - summary: SearchResultSummary, + search: SearchQuery | null, + summary: SearchResultSummary | null, ) => void; /** @@ -145,15 +143,15 @@ const SearchInput: React.FC = ({ updateSearch, }) => { // A ref to the top level Select. - const selectRef = useRef(null); + const selectRef = useRef(null); // The currently selected option. - const [value, setValue] = useState(); + const [value, setValue] = useState(); // The contents of the input field associated with the select. - const [inputValue, setInputValue] = useState(""); + const [inputValue, setInputValue] = useState(""); const theme = useTheme(); - const styles = useMemo(() => useSelectStyles(theme), [theme]); + const styles = useMemo(() => createSelectStyles(theme), [theme]); const components = useMemo(() => ({ Control, Input, Option }), []); useEffect(() => { @@ -292,7 +290,7 @@ const SearchInputWrapper = styled(Box)` margin: auto; `; -const useSelectStyles = ({ +const createSelectStyles = ({ colors, }: Theme): StylesConfig => ({ container: (style) => ({ ...style, flex: 1 }), From fcd4f360368fcd5323511175d66f16f7b188ea08 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 10:08:01 +0530 Subject: [PATCH 12/32] Lint 2 --- web/packages/new/photos/components/SearchBar.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/web/packages/new/photos/components/SearchBar.tsx b/web/packages/new/photos/components/SearchBar.tsx index 06f7a18f97..f6550272c2 100644 --- a/web/packages/new/photos/components/SearchBar.tsx +++ b/web/packages/new/photos/components/SearchBar.tsx @@ -143,7 +143,7 @@ const SearchInput: React.FC = ({ updateSearch, }) => { // A ref to the top level Select. - const selectRef = useRef(null); + const selectRef = useRef(null); + const selectRef = useRef | null>(null); // The currently selected option. const [value, setValue] = useState(); // The contents of the input field associated with the select. - const [inputValue, setInputValue] = useState(""); + const [inputValue, setInputValue] = useState(""); const theme = useTheme(); @@ -158,7 +159,7 @@ const SearchInput: React.FC = ({ search(value); }, [value]); - const handleChange = (value: SearchOption) => { + const handleChange = (value: SearchOption | null) => { setValue(value); setInputValue(value?.label); // The Select has a blurInputOnSelect prop, but that makes the input @@ -184,7 +185,7 @@ const SearchInput: React.FC = ({ [], ); - const search = (selectedOption: SearchOption) => { + const search = (selectedOption: SearchOption | null | undefined) => { if (!selectedOption) { return; } @@ -242,8 +243,8 @@ const SearchInput: React.FC = ({ // search string if the user focuses back on the input field after // moving focus elsewhere. if (inputValue) { - selectRef?.current.onInputChange(inputValue, { - action: "select-value", + selectRef.current?.onInputChange(inputValue, { + action: "set-value", prevInputValue: "", }); } @@ -258,7 +259,7 @@ const SearchInput: React.FC = ({ styles={styles} loadOptions={loadOptions} onChange={handleChange} - inputValue={inputValue} + inputValue={inputValue ?? ""} onInputChange={handleInputChange} isClearable escapeClearsValue From d06f7a869eef824438a659000aaa4799b1a2b063 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 11:15:32 +0530 Subject: [PATCH 14/32] types wip --- .../new/photos/services/search/types.ts | 104 ++++++------------ 1 file changed, 35 insertions(+), 69 deletions(-) diff --git a/web/packages/new/photos/services/search/types.ts b/web/packages/new/photos/services/search/types.ts index 7c91facf04..ee3f208003 100644 --- a/web/packages/new/photos/services/search/types.ts +++ b/web/packages/new/photos/services/search/types.ts @@ -9,6 +9,39 @@ import { FileType } from "@/media/file-type"; import type { EnteFile } from "@/new/photos/types/file"; import type { LocationTag } from "../user-entity"; +/** + * A search suggestion. + * + * These (wrapped up in {@link SearchOption}s) are shown in the search results + * dropdown, and can also be used to filter the list of files that are shown. + */ +export type SearchSuggestion = { label: string } & ( + | { type: "collection"; collectionID: number } + | { type: "files"; fileIDs: number[] } + | { type: "fileType"; fileType: FileType } + | { type: "date"; dateComponents: SearchDateComponents } + | { type: "location"; locationTag: LocationTag } + | { type: "city"; city: City } + | { type: "clip"; clipScoreForFileID: Map } + | { type: "cgroup"; cgroup: SearchPerson } +); + +/** + * An option shown in the the search bar's select dropdown. + * + * The {@link SearchOption} wraps a {@link SearchSuggestion} with some metadata + * used when showing a corresponding entry in the dropdown, and in the results + * header. + * + * If the user selects the option, then we will re-run the search using the + * {@link suggestion} to filter the list of files shown to the user. + */ +export interface SearchOption { + suggestion: SearchSuggestion; + fileCount: number; + previewFiles: EnteFile[]; +} + /** * The base data over which we should search. */ @@ -17,6 +50,8 @@ export interface SearchableData { files: EnteFile[]; } +// TODO-cgroup: Audit below + export interface DateSearchResult { components: SearchDateComponents; label: string; @@ -105,72 +140,3 @@ export type City = Location & { /** Name of the city. */ name: string; }; - -// TODO-cgroup: Audit below - -export enum SuggestionType { - DATE = "DATE", - LOCATION = "LOCATION", - COLLECTION = "COLLECTION", - FILE_NAME = "FILE_NAME", - PERSON = "PERSON", - FILE_CAPTION = "FILE_CAPTION", - FILE_TYPE = "FILE_TYPE", - CLIP = "CLIP", - CITY = "CITY", -} - -export interface Suggestion { - type: SuggestionType; - label: string; - value: - | SearchDateComponents - | number[] - | SearchPerson - | LocationTag - | City - | FileType - | ClipSearchScores; -} - -export interface SearchQuery { - suggestion?: SearchSuggestion; - date?: SearchDateComponents; - location?: LocationTag; - city?: City; - collection?: number; - files?: number[]; - person?: SearchPerson; - fileType?: FileType; - clip?: ClipSearchScores; -} - -export interface SearchResultSummary { - optionName: string; - fileCount: number; -} - -export type SearchSuggestion = { label: string } & ( - | { type: "collection"; collectionID: number } - | { type: "files"; fileIDs: number[] } - | { type: "fileType"; fileType: FileType } - | { type: "date"; dateComponents: SearchDateComponents } - | { type: "location"; locationTag: LocationTag } - | { type: "city"; city: City } - | { type: "clip"; clipScoreForFileID: Map } - | { type: "cgroup"; cgroup: SearchPerson } -); - -/** - * An option shown in the the search bar's select dropdown. - * - * The option includes essential data that is necessary to show a corresponding - * entry in the dropdown. If the user selects the option, then we will re-run - * the search, using the data to filter the list of files shown to the user. - */ -export interface SearchOption extends Suggestion { - fileCount: number; - previewFiles: EnteFile[]; -} - -export type ClipSearchScores = Map; From ae7134a80fef2d38637aab339b1f1e3f195da170 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 11:29:00 +0530 Subject: [PATCH 15/32] New types wip --- .../new/photos/services/search/types.ts | 3 +- .../new/photos/services/search/worker.ts | 100 ++++++++++-------- 2 files changed, 56 insertions(+), 47 deletions(-) diff --git a/web/packages/new/photos/services/search/types.ts b/web/packages/new/photos/services/search/types.ts index ee3f208003..a15b9288ec 100644 --- a/web/packages/new/photos/services/search/types.ts +++ b/web/packages/new/photos/services/search/types.ts @@ -17,8 +17,9 @@ import type { LocationTag } from "../user-entity"; */ export type SearchSuggestion = { label: string } & ( | { type: "collection"; collectionID: number } - | { type: "files"; fileIDs: number[] } | { type: "fileType"; fileType: FileType } + | { type: "fileName"; fileIDs: number[] } + | { type: "fileCaption"; fileIDs: number[] } | { type: "date"; dateComponents: SearchDateComponents } | { type: "location"; locationTag: LocationTag } | { type: "city"; city: City } diff --git a/web/packages/new/photos/services/search/worker.ts b/web/packages/new/photos/services/search/worker.ts index 0b42418a76..a12d63a312 100644 --- a/web/packages/new/photos/services/search/worker.ts +++ b/web/packages/new/photos/services/search/worker.ts @@ -25,7 +25,6 @@ import type { SearchQuery, Suggestion, } from "./types"; -import { SuggestionType } from "./types"; /** * A web worker that runs the search asynchronously so that the main thread @@ -111,60 +110,69 @@ const suggestionsForString = ( cities: Searchable[], ): Suggestion[] => [ + // <-- caption suggestions will be inserted here by our caller. fileTypeSuggestions(s, labelledFileTypes), dateSuggestions(s, locale, holidays), locationSuggestions(s, locationTags, cities), collectionSuggestions(s, collections), - suggestionForFiles(fileNameMatches(s, files), searchString), - suggestionForFiles(fileCaptionMatches(s, files), searchString), + fileNameSuggestion(s, searchString, files), + fileCaptionSuggestion(s, searchString, files), ].flat(); const collectionSuggestions = (s: string, collections: Collection[]) => collections .filter(({ name }) => name.toLowerCase().includes(s)) .map(({ id, name }) => ({ - type: SuggestionType.COLLECTION, - value: id, + type: "collection", + collectionID: id, label: name, })); -const fileNameMatches = (s: string, files: EnteFile[]) => { - // Convert the search string to a number. This allows searching a file by - // its exact (integral) ID. - const sn = Number(s) || undefined; - - return files.filter( - ({ id, metadata }) => - id === sn || metadata.title.toLowerCase().includes(s), - ); -}; - -const suggestionForFiles = (matchingFiles: EnteFile[], searchString: string) => - matchingFiles.length - ? { - type: SuggestionType.FILE_NAME, - value: matchingFiles.map((f) => f.id), - label: searchString, - } - : []; - -const fileCaptionMatches = (s: string, files: EnteFile[]) => - files.filter((file) => - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - file.pubMagicMetadata?.data?.caption?.toLowerCase().includes(s), - ); - const fileTypeSuggestions = ( s: string, labelledFileTypes: Searchable[], ) => labelledFileTypes .filter(({ lowercasedName }) => lowercasedName.startsWith(s)) - .map(({ fileType, label }) => ({ - type: SuggestionType.FILE_TYPE, - value: fileType, - label, - })); + .map(({ fileType, label }) => ({ type: "fileType", fileType, label })); + +const fileNameSuggestion = ( + s: string, + searchString: string, + files: EnteFile[], +) => { + // Convert the search string to a number. This allows searching a file by + // its exact (integral) ID. + const sn = Number(s) || undefined; + + const fileIDs = files + .filter( + ({ id, metadata }) => + id === sn || metadata.title.toLowerCase().includes(s), + ) + .map((f) => f.id); + + return fileIDs.length + ? { type: "fileName", fileIDs, label: searchString } + : []; +}; + +const fileCaptionSuggestion = ( + s: string, + searchString: string, + files: EnteFile[], +) => { + const fileIDs = files + .filter((file) => + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + file.pubMagicMetadata?.data?.caption?.toLowerCase().includes(s), + ) + .map((f) => f.id); + + return fileIDs.length + ? { type: "fileCaption", fileIDs, label: searchString } + : []; +}; const dateSuggestions = ( s: string, @@ -172,8 +180,8 @@ const dateSuggestions = ( holidays: Searchable[], ) => parseDateComponents(s, locale, holidays).map(({ components, label }) => ({ - type: SuggestionType.DATE, - value: components, + type: "date", + dateComponents: components, label, })); @@ -297,15 +305,15 @@ const locationSuggestions = ( ); return [ - matchingLocationTags.map((t) => ({ - type: SuggestionType.LOCATION, - value: t, - label: t.name, + matchingLocationTags.map((locationTag) => ({ + type: "location", + locationTag, + label: locationTag.name, })), - matchingCities.map((c) => ({ - type: SuggestionType.CITY, - value: c, - label: c.name, + matchingCities.map((city) => ({ + type: "city", + city, + label: city.name, })), ].flat(); }; From 18f622d007dcb16627d731ae18fb23a34146583c Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 11:41:20 +0530 Subject: [PATCH 16/32] wip --- .../new/photos/services/search/worker.ts | 133 +++++++++--------- 1 file changed, 69 insertions(+), 64 deletions(-) diff --git a/web/packages/new/photos/services/search/worker.ts b/web/packages/new/photos/services/search/worker.ts index a12d63a312..ba2627da7b 100644 --- a/web/packages/new/photos/services/search/worker.ts +++ b/web/packages/new/photos/services/search/worker.ts @@ -22,8 +22,7 @@ import type { Searchable, SearchableData, SearchDateComponents, - SearchQuery, - Suggestion, + SearchSuggestion, } from "./types"; /** @@ -88,7 +87,7 @@ export class SearchWorker { /** * Return {@link EnteFile}s that satisfy the given {@link suggestion}. */ - filterSearchableFiles(suggestion: SearchQuery) { + filterSearchableFiles(suggestion: SearchSuggestion) { return this.searchableData.files.filter((f) => isMatchingFile(f, suggestion), ); @@ -108,7 +107,7 @@ const suggestionsForString = ( { locale, holidays, labelledFileTypes }: LocalizedSearchData, locationTags: Searchable[], cities: Searchable[], -): Suggestion[] => +): SearchSuggestion[] => [ // <-- caption suggestions will be inserted here by our caller. fileTypeSuggestions(s, labelledFileTypes), @@ -119,7 +118,10 @@ const suggestionsForString = ( fileCaptionSuggestion(s, searchString, files), ].flat(); -const collectionSuggestions = (s: string, collections: Collection[]) => +const collectionSuggestions = ( + s: string, + collections: Collection[], +): SearchSuggestion[] => collections .filter(({ name }) => name.toLowerCase().includes(s)) .map(({ id, name }) => ({ @@ -131,7 +133,7 @@ const collectionSuggestions = (s: string, collections: Collection[]) => const fileTypeSuggestions = ( s: string, labelledFileTypes: Searchable[], -) => +): SearchSuggestion[] => labelledFileTypes .filter(({ lowercasedName }) => lowercasedName.startsWith(s)) .map(({ fileType, label }) => ({ type: "fileType", fileType, label })); @@ -140,7 +142,7 @@ const fileNameSuggestion = ( s: string, searchString: string, files: EnteFile[], -) => { +): SearchSuggestion[] => { // Convert the search string to a number. This allows searching a file by // its exact (integral) ID. const sn = Number(s) || undefined; @@ -153,7 +155,7 @@ const fileNameSuggestion = ( .map((f) => f.id); return fileIDs.length - ? { type: "fileName", fileIDs, label: searchString } + ? [{ type: "fileName", fileIDs, label: searchString }] : []; }; @@ -161,7 +163,7 @@ const fileCaptionSuggestion = ( s: string, searchString: string, files: EnteFile[], -) => { +): SearchSuggestion[] => { const fileIDs = files .filter((file) => // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition @@ -170,7 +172,7 @@ const fileCaptionSuggestion = ( .map((f) => f.id); return fileIDs.length - ? { type: "fileCaption", fileIDs, label: searchString } + ? [{ type: "fileCaption", fileIDs, label: searchString }] : []; }; @@ -178,7 +180,7 @@ const dateSuggestions = ( s: string, locale: string, holidays: Searchable[], -) => +): SearchSuggestion[] => parseDateComponents(s, locale, holidays).map(({ components, label }) => ({ type: "date", dateComponents: components, @@ -291,7 +293,7 @@ const locationSuggestions = ( s: string, locationTags: Searchable[], cities: Searchable[], -) => { +): SearchSuggestion[] => { const matchingLocationTags = locationTags.filter(searchableIncludes(s)); const matchingLocationTagLNames = new Set( @@ -305,65 +307,68 @@ const locationSuggestions = ( ); return [ - matchingLocationTags.map((locationTag) => ({ - type: "location", - locationTag, - label: locationTag.name, - })), - matchingCities.map((city) => ({ - type: "city", - city, - label: city.name, - })), + matchingLocationTags.map( + (locationTag): SearchSuggestion => ({ + type: "location", + locationTag, + label: locationTag.name, + }), + ), + matchingCities.map( + (city): SearchSuggestion => ({ + type: "city", + city, + label: city.name, + }), + ), ].flat(); }; /** * Return true if file satisfies the given {@link query}. */ -const isMatchingFile = (file: EnteFile, query: SearchQuery) => { - if (query.collection) { - return query.collection === file.collectionID; +const isMatchingFile = (file: EnteFile, suggestion: SearchSuggestion) => { + switch (suggestion.type) { + case "collection": + return suggestion.collectionID === file.collectionID; + + case "fileType": + return suggestion.fileType === file.metadata.fileType; + + case "fileName": + return suggestion.fileIDs.includes(file.id); + + case "fileCaption": + return suggestion.fileIDs.includes(file.id); + + case "date": + return isDateComponentsMatch( + suggestion.dateComponents, + fileCreationPhotoDate(file, getPublicMagicMetadataSync(file)), + ); + + case "location": { + const location = fileLocation(file); + if (!location) return false; + + return isInsideLocationTag(location, suggestion.locationTag); + } + + case "city": { + const location = fileLocation(file); + if (!location) return false; + + return isInsideCity(location, suggestion.city); + } + + case "clip": + return suggestion.clipScoreForFileID.has(file.id); + + case "cgroup": + // return query.person.files.includes(file.id); + // TODO-Cluster implement me + return false; } - - if (query.date) { - return isDateComponentsMatch( - query.date, - fileCreationPhotoDate(file, getPublicMagicMetadataSync(file)), - ); - } - - if (query.location) { - const location = fileLocation(file); - if (!location) return false; - - return isInsideLocationTag(location, query.location); - } - - if (query.city) { - const location = fileLocation(file); - if (!location) return false; - - return isInsideCity(location, query.city); - } - - if (query.files) { - return query.files.includes(file.id); - } - - if (query.person) { - return query.person.files.includes(file.id); - } - - if (typeof query.fileType !== "undefined") { - return query.fileType === file.metadata.fileType; - } - - if (typeof query.clip !== "undefined") { - return query.clip.has(file.id); - } - - return false; }; const isDateComponentsMatch = ( From 45b0dd4887b62f1789b3ecd64576fdc144042d13 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 11:47:01 +0530 Subject: [PATCH 17/32] R --- web/packages/new/photos/services/search/index.ts | 4 ++-- web/packages/new/photos/services/search/types.ts | 6 ++---- web/packages/new/photos/services/search/worker.ts | 15 +++++++++------ 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/web/packages/new/photos/services/search/index.ts b/web/packages/new/photos/services/search/index.ts index 29115122cd..4e00757b79 100644 --- a/web/packages/new/photos/services/search/index.ts +++ b/web/packages/new/photos/services/search/index.ts @@ -9,8 +9,8 @@ import { clipMatches, isMLEnabled } from "../ml"; import type { City, ClipSearchScores, - DateSearchResult, LabelledFileType, + LabelledSearchDateComponents, LocalizedSearchData, SearchableData, SearchDateComponents, @@ -144,7 +144,7 @@ const localizedSearchData = () => /** * A list of holidays - their yearly dates and localized names. */ -const holidays = (): DateSearchResult[] => [ +const holidays = (): LabelledSearchDateComponents[] => [ { components: { month: 12, day: 25 }, label: t("CHRISTMAS") }, { components: { month: 12, day: 24 }, label: t("CHRISTMAS_EVE") }, { components: { month: 1, day: 1 }, label: t("NEW_YEAR") }, diff --git a/web/packages/new/photos/services/search/types.ts b/web/packages/new/photos/services/search/types.ts index a15b9288ec..c386684d85 100644 --- a/web/packages/new/photos/services/search/types.ts +++ b/web/packages/new/photos/services/search/types.ts @@ -51,9 +51,7 @@ export interface SearchableData { files: EnteFile[]; } -// TODO-cgroup: Audit below - -export interface DateSearchResult { +export interface LabelledSearchDateComponents { components: SearchDateComponents; label: string; } @@ -83,7 +81,7 @@ export type Searchable = T & { */ export interface LocalizedSearchData { locale: string; - holidays: Searchable[]; + 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 ba2627da7b..354327e21c 100644 --- a/web/packages/new/photos/services/search/worker.ts +++ b/web/packages/new/photos/services/search/worker.ts @@ -16,8 +16,8 @@ import { } from "../user-entity"; import type { City, - DateSearchResult, LabelledFileType, + LabelledSearchDateComponents, LocalizedSearchData, Searchable, SearchableData, @@ -179,7 +179,7 @@ const fileCaptionSuggestion = ( const dateSuggestions = ( s: string, locale: string, - holidays: Searchable[], + holidays: Searchable[], ): SearchSuggestion[] => parseDateComponents(s, locale, holidays).map(({ components, label }) => ({ type: "date", @@ -204,15 +204,18 @@ const dateSuggestions = ( const parseDateComponents = ( s: string, locale: string, - holidays: Searchable[], -): DateSearchResult[] => + holidays: Searchable[], +): LabelledSearchDateComponents[] => [ parseChrono(s, locale), parseYearComponents(s), holidays.filter(searchableIncludes(s)), ].flat(); -const parseChrono = (s: string, locale: string): DateSearchResult[] => +const parseChrono = ( + s: string, + locale: string, +): LabelledSearchDateComponents[] => chrono .parse(s) .map((result) => { @@ -246,7 +249,7 @@ const parseChrono = (s: string, locale: string): DateSearchResult[] => .filter((x) => x !== undefined); /** chrono does not parse years like "2024", so do it manually. */ -const parseYearComponents = (s: string): DateSearchResult[] => { +const parseYearComponents = (s: string): LabelledSearchDateComponents[] => { // s is already trimmed. if (s.length == 4) { const year = parseInt(s); From 5bc5823ef231414e6b0fa56ca950aa2368d8a188 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 11:53:33 +0530 Subject: [PATCH 18/32] wip --- web/packages/new/photos/services/ml/index.ts | 4 +-- .../new/photos/services/search/index.ts | 27 ++++++++----------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/web/packages/new/photos/services/ml/index.ts b/web/packages/new/photos/services/ml/index.ts index f230460f78..c5c326120c 100644 --- a/web/packages/new/photos/services/ml/index.ts +++ b/web/packages/new/photos/services/ml/index.ts @@ -586,8 +586,8 @@ const workerDidProcessFileOrIdle = throttled(updateMLStatusSnapshot, 2000); * * @param searchPhrase Normalized (trimmed and lowercased) search phrase. * - * It returns file (IDs) that should be shown in the search results, along with - * their scores. + * It returns file (IDs) that should be shown in the search results, each + * annotated with its score. * * The result can also be `undefined`, which indicates that the download for the * ML model is still in progress (trying again later should succeed). diff --git a/web/packages/new/photos/services/search/index.ts b/web/packages/new/photos/services/search/index.ts index 4e00757b79..0da82a3279 100644 --- a/web/packages/new/photos/services/search/index.ts +++ b/web/packages/new/photos/services/search/index.ts @@ -1,14 +1,12 @@ -import { isDesktop } from "@/base/app"; import log from "@/base/log"; import { masterKeyFromSession } from "@/base/session-store"; import { ComlinkWorker } from "@/base/worker/comlink-worker"; import { FileType } from "@/media/file-type"; import type { LocationTag } from "@/new/photos/services/user-entity"; import i18n, { t } from "i18next"; -import { clipMatches, isMLEnabled } from "../ml"; +import { clipMatches, isMLEnabled, isMLSupported } from "../ml"; import type { City, - ClipSearchScores, LabelledFileType, LabelledSearchDateComponents, LocalizedSearchData, @@ -16,10 +14,8 @@ import type { SearchDateComponents, SearchOption, SearchPerson, - SearchQuery, - Suggestion, + SearchSuggestion, } from "./types"; -import { SuggestionType } from "./types"; import type { SearchWorker } from "./worker"; /** @@ -80,7 +76,7 @@ export const suggestionsForString = async (searchString: string) => { // separately, in parallel with the rest of the search query construction in // the search worker, then combine the two. const results = await Promise.all([ - clipSuggestions(s, searchString).then((s) => s ?? []), + clipSuggestion(s, searchString).then((s) => s ?? []), worker().then((w) => w.suggestionsForString(s, searchString, localizedSearchData()), ), @@ -88,24 +84,23 @@ export const suggestionsForString = async (searchString: string) => { return results.flat(); }; -const clipSuggestions = async (s: string, searchString: string) => { - if (!isDesktop) return undefined; +const clipSuggestion = async ( + s: string, + searchString: string, +): Promise => { + if (!isMLSupported) return undefined; if (!isMLEnabled()) return undefined; const matches = await clipMatches(s); if (!matches) return undefined; - return { - type: SuggestionType.CLIP, - value: matches, - label: searchString, - }; + return { type: "clip", clipScoreForFileID: matches, label: searchString }; }; /** * Return the list of {@link EnteFile}s (from amongst the previously set * {@link SearchableData}) that match the given search {@link suggestion}. */ -export const filterSearchableFiles = async (suggestion: SearchQuery) => +export const filterSearchableFiles = async (suggestion: SearchSuggestion) => worker().then((w) => w.filterSearchableFiles(suggestion)); /** @@ -203,7 +198,7 @@ async function convertSuggestionsToOptions( return previewImageAppendedOptions; } -function convertSuggestionToSearchQuery(option: Suggestion): SearchQuery { +function convertSuggestionToSearchQuery(option: Suggestion): SearchSuggestion { switch (option.type) { case SuggestionType.DATE: return { From 74f6e52c748f1f965fe0ffd43972b2acc5e7e110 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 12:16:55 +0530 Subject: [PATCH 19/32] wip 3 --- .../new/photos/services/search/index.ts | 130 ++++++------------ 1 file changed, 45 insertions(+), 85 deletions(-) diff --git a/web/packages/new/photos/services/search/index.ts b/web/packages/new/photos/services/search/index.ts index 0da82a3279..6742edbfe7 100644 --- a/web/packages/new/photos/services/search/index.ts +++ b/web/packages/new/photos/services/search/index.ts @@ -2,18 +2,14 @@ import log from "@/base/log"; import { masterKeyFromSession } from "@/base/session-store"; import { ComlinkWorker } from "@/base/worker/comlink-worker"; import { FileType } from "@/media/file-type"; -import type { LocationTag } from "@/new/photos/services/user-entity"; import i18n, { t } from "i18next"; import { clipMatches, isMLEnabled, isMLSupported } from "../ml"; import type { - City, LabelledFileType, LabelledSearchDateComponents, LocalizedSearchData, SearchableData, - SearchDateComponents, SearchOption, - SearchPerson, SearchSuggestion, } from "./types"; import type { SearchWorker } from "./worker"; @@ -62,12 +58,54 @@ export const setSearchableData = (data: SearchableData) => void worker().then((w) => w.setSearchableData(data)); /** - * Convert a search string into a suggestions that can be shown in the search - * results, and can also be used filter the searchable files. + * Convert a search string into (annotated) suggestions that can be shown in the + * search results dropdown. * * @param searchString The string we want to search for. */ -export const suggestionsForString = async (searchString: string) => { +const searchOptionsForString = async (searchString: string) => { + const t = Date.now(); + const suggestions = await suggestionsForString(searchString); + const options = await suggestionsToOptions(suggestions); + log.debug(() => [ "search", {searchString, options, timeMs: Date.now() - t} ]); + return options; +}; + +const sortMatchesIfNeeded = (files: EnteFile[], suggestion: SearchSuggestion) => { + if (suggestion.type != "clip") return files; + // Sort CLIP matches by their corresponding scores. + const score = (fileID: number)=>ensure(suggestion.clipScoreForFileID.get(fileID)) + return files.sort((a, b) => score(b) - score(a)); +} +const suggestionsToOptions = async (suggestions: SearchSuggestion[]) => + Promise.all(suggestions.map((suggestion) => { + const matchingFiles = await filterSearchableFiles(suggestion); + const files = sortMatchesIfNeeded(matchingFiles); + return { suggestion, fileCount: files.length, previewFiles: files.slice(0, 3)} + })); + for (const suggestion of suggestions) { + const searchQuery = convertSuggestionToSearchQuery(suggestion); + const resultFiles = await filterSearchableFiles(searchQuery); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (searchQuery?.clip) { + resultFiles.sort((a, b) => { + const aScore = searchQuery.clip?.get(a.id) ?? 0; + const bScore = searchQuery.clip?.get(b.id) ?? 0; + return bScore - aScore; + }); + } + if (resultFiles.length) { + previewImageAppendedOptions.push({ + ...suggestion, + fileCount: resultFiles.length, + previewFiles: resultFiles.slice(0, 3), + }); + } + } + return previewImageAppendedOptions; +} + +const suggestionsForString = async (searchString: string) => { // Normalize it by trimming whitespace and converting to lowercase. const s = searchString.trim().toLowerCase(); if (s.length == 0) return []; @@ -154,81 +192,3 @@ const labelledFileTypes = (): LabelledFileType[] => [ { fileType: FileType.video, label: t("VIDEO") }, { fileType: FileType.livePhoto, label: t("LIVE_PHOTO") }, ]; - -// TODO-Cluster -- AUDIT BELOW THIS - -// Suggestions shown in the search dropdown when the user has typed something. -export const getAutoCompleteSuggestions = async ( - searchPhrase: string, -): Promise => { - log.debug(() => ["getAutoCompleteSuggestions"]); - try { - const suggestions: Suggestion[] = - await suggestionsForString(searchPhrase); - return convertSuggestionsToOptions(suggestions); - } catch (e) { - log.error("getAutoCompleteSuggestions failed", e); - return []; - } -}; - -async function convertSuggestionsToOptions( - suggestions: Suggestion[], -): Promise { - const previewImageAppendedOptions: SearchOption[] = []; - for (const suggestion of suggestions) { - const searchQuery = convertSuggestionToSearchQuery(suggestion); - const resultFiles = await filterSearchableFiles(searchQuery); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (searchQuery?.clip) { - resultFiles.sort((a, b) => { - const aScore = searchQuery.clip?.get(a.id) ?? 0; - const bScore = searchQuery.clip?.get(b.id) ?? 0; - return bScore - aScore; - }); - } - if (resultFiles.length) { - previewImageAppendedOptions.push({ - ...suggestion, - fileCount: resultFiles.length, - previewFiles: resultFiles.slice(0, 3), - }); - } - } - return previewImageAppendedOptions; -} - -function convertSuggestionToSearchQuery(option: Suggestion): SearchSuggestion { - switch (option.type) { - case SuggestionType.DATE: - return { - date: option.value as SearchDateComponents, - }; - - case SuggestionType.LOCATION: - return { - location: option.value as LocationTag, - }; - - case SuggestionType.CITY: - return { city: option.value as City }; - - case SuggestionType.COLLECTION: - return { collection: option.value as number }; - - case SuggestionType.FILE_NAME: - return { files: option.value as number[] }; - - case SuggestionType.FILE_CAPTION: - return { files: option.value as number[] }; - - case SuggestionType.PERSON: - return { person: option.value as SearchPerson }; - - case SuggestionType.FILE_TYPE: - return { fileType: option.value as FileType }; - - case SuggestionType.CLIP: - return { clip: option.value as ClipSearchScores }; - } -} From 877c0a7c73ee9aedee312ced3bf1c32e2306defa Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 12:22:05 +0530 Subject: [PATCH 20/32] wip 4 --- .../new/photos/services/search/index.ts | 68 +++++++++---------- 1 file changed, 31 insertions(+), 37 deletions(-) diff --git a/web/packages/new/photos/services/search/index.ts b/web/packages/new/photos/services/search/index.ts index 6742edbfe7..d8448a971b 100644 --- a/web/packages/new/photos/services/search/index.ts +++ b/web/packages/new/photos/services/search/index.ts @@ -2,6 +2,8 @@ import log from "@/base/log"; import { masterKeyFromSession } from "@/base/session-store"; import { ComlinkWorker } from "@/base/worker/comlink-worker"; import { FileType } from "@/media/file-type"; +import type { EnteFile } from "@/new/photos/types/file"; +import { ensure } from "@/utils/ensure"; import i18n, { t } from "i18next"; import { clipMatches, isMLEnabled, isMLSupported } from "../ml"; import type { @@ -9,7 +11,6 @@ import type { LabelledSearchDateComponents, LocalizedSearchData, SearchableData, - SearchOption, SearchSuggestion, } from "./types"; import type { SearchWorker } from "./worker"; @@ -63,48 +64,17 @@ export const setSearchableData = (data: SearchableData) => * * @param searchString The string we want to search for. */ -const searchOptionsForString = async (searchString: string) => { +export const searchOptionsForString = async (searchString: string) => { const t = Date.now(); const suggestions = await suggestionsForString(searchString); const options = await suggestionsToOptions(suggestions); - log.debug(() => [ "search", {searchString, options, timeMs: Date.now() - t} ]); + log.debug(() => [ + "search", + { searchString, options, duration: `${Date.now() - t} ms` }, + ]); return options; }; -const sortMatchesIfNeeded = (files: EnteFile[], suggestion: SearchSuggestion) => { - if (suggestion.type != "clip") return files; - // Sort CLIP matches by their corresponding scores. - const score = (fileID: number)=>ensure(suggestion.clipScoreForFileID.get(fileID)) - return files.sort((a, b) => score(b) - score(a)); -} -const suggestionsToOptions = async (suggestions: SearchSuggestion[]) => - Promise.all(suggestions.map((suggestion) => { - const matchingFiles = await filterSearchableFiles(suggestion); - const files = sortMatchesIfNeeded(matchingFiles); - return { suggestion, fileCount: files.length, previewFiles: files.slice(0, 3)} - })); - for (const suggestion of suggestions) { - const searchQuery = convertSuggestionToSearchQuery(suggestion); - const resultFiles = await filterSearchableFiles(searchQuery); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (searchQuery?.clip) { - resultFiles.sort((a, b) => { - const aScore = searchQuery.clip?.get(a.id) ?? 0; - const bScore = searchQuery.clip?.get(b.id) ?? 0; - return bScore - aScore; - }); - } - if (resultFiles.length) { - previewImageAppendedOptions.push({ - ...suggestion, - fileCount: resultFiles.length, - previewFiles: resultFiles.slice(0, 3), - }); - } - } - return previewImageAppendedOptions; -} - const suggestionsForString = async (searchString: string) => { // Normalize it by trimming whitespace and converting to lowercase. const s = searchString.trim().toLowerCase(); @@ -134,6 +104,30 @@ const clipSuggestion = async ( return { type: "clip", clipScoreForFileID: matches, label: searchString }; }; +const suggestionsToOptions = async (suggestions: SearchSuggestion[]) => + Promise.all( + suggestions.map(async (suggestion) => { + const matchingFiles = await filterSearchableFiles(suggestion); + const files = sortMatchesIfNeeded(matchingFiles, suggestion); + return { + suggestion, + fileCount: files.length, + previewFiles: files.slice(0, 3), + }; + }), + ); + +const sortMatchesIfNeeded = ( + files: EnteFile[], + suggestion: SearchSuggestion, +) => { + if (suggestion.type != "clip") return files; + // Sort CLIP matches by their corresponding scores. + const score = ({ id }: EnteFile) => + ensure(suggestion.clipScoreForFileID.get(id)); + return files.sort((a, b) => score(b) - score(a)); +}; + /** * Return the list of {@link EnteFile}s (from amongst the previously set * {@link SearchableData}) that match the given search {@link suggestion}. From 060a055d38db396bcfb357e1e9b0b5f32eab4302 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 13:22:29 +0530 Subject: [PATCH 21/32] Fix bar --- .../new/photos/components/SearchBar.tsx | 194 +++++++----------- web/packages/new/photos/services/search/ui.ts | 28 --- 2 files changed, 72 insertions(+), 150 deletions(-) delete mode 100644 web/packages/new/photos/services/search/ui.ts diff --git a/web/packages/new/photos/components/SearchBar.tsx b/web/packages/new/photos/components/SearchBar.tsx index c4bb28a477..837210ce20 100644 --- a/web/packages/new/photos/components/SearchBar.tsx +++ b/web/packages/new/photos/components/SearchBar.tsx @@ -1,28 +1,15 @@ import { assertionFailed } from "@/base/assert"; import { useIsMobileWidth } from "@/base/hooks"; -import { FileType } from "@/media/file-type"; import { ItemCard, ResultPreviewTile } from "@/new/photos/components/ItemCards"; import { isMLSupported, mlStatusSnapshot, mlStatusSubscribe, } from "@/new/photos/services/ml"; -import { getAutoCompleteSuggestions } from "@/new/photos/services/search"; -import type { - City, - ClipSearchScores, - SearchDateComponents, - SearchOption, - SearchPerson, - SearchQuery, - SearchResultSummary, -} from "@/new/photos/services/search/types"; -import { SuggestionType } from "@/new/photos/services/search/types"; -import { labelForSuggestionType } from "@/new/photos/services/search/ui"; -import type { LocationTag } from "@/new/photos/services/user-entity"; +import { searchOptionsForString } from "@/new/photos/services/search"; +import type { SearchOption } from "@/new/photos/services/search/types"; 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/Search"; @@ -38,14 +25,7 @@ import { } from "@mui/material"; import { t } from "i18next"; import pDebounce from "p-debounce"; -import React, { - useCallback, - useEffect, - useMemo, - useRef, - useState, - useSyncExternalStore, -} from "react"; +import React, { useMemo, useRef, useState, useSyncExternalStore } from "react"; import { components as SelectComponents, type ControlProps, @@ -70,22 +50,20 @@ interface SearchBarProps { * * When we're in search mode, * - * 1. Other icons from the navbar are hidden + * 1. Other icons from the navbar are hidden. * 2. Next to the search input there is a cancel button to exit search mode. */ isInSearchMode: boolean; /** * Enter or exit "search mode". */ - setIsInSearchMode: (v: boolean) => void; - updateSearch: UpdateSearch; + setIsInSearchMode: (b: boolean) => void; + /** + * Set or clear the selected {@link SearchOption}. + */ + setSelectedSearchOption: (o: SearchOption | undefined) => void; } -export type UpdateSearch = ( - search: SearchQuery | null, - summary: SearchResultSummary | null, -) => void; - /** * The search bar is a styled "select" element that allow the user to type in * the attached input field, and shows a list of matching suggestions in a @@ -104,7 +82,7 @@ export type UpdateSearch = ( export const SearchBar: React.FC = ({ setIsInSearchMode, isInSearchMode, - ...props + setSelectedSearchOption, }) => { const isMobileWidth = useIsMobileWidth(); @@ -115,7 +93,7 @@ export const SearchBar: React.FC = ({ {isMobileWidth && !isInSearchMode ? ( ) : ( - + )} ); @@ -134,39 +112,34 @@ const MobileSearchArea: React.FC = ({ onSearch }) => ( ); -interface SearchInputProps { - isInSearchMode: boolean; - updateSearch: UpdateSearch; -} - -const SearchInput: React.FC = ({ +const SearchInput: React.FC> = ({ isInSearchMode, - updateSearch, + setSelectedSearchOption, }) => { // A ref to the top level Select. const selectRef = useRef | null>(null); // The currently selected option. - const [value, setValue] = useState(); + const [value, setValue] = useState(); // The contents of the input field associated with the select. - const [inputValue, setInputValue] = useState(""); + const [inputValue, setInputValue] = useState(""); const theme = useTheme(); const styles = useMemo(() => createSelectStyles(theme), [theme]); const components = useMemo(() => ({ Control, Input, Option }), []); - useEffect(() => { - search(value); - }, [value]); + const handleChange = (value: SearchOption | null | undefined) => { + setValue(value ?? undefined); + setInputValue(value?.suggestion.label ?? ""); + // TODO-Cluster: + if (value) setSelectedSearchOption(value); - const handleChange = (value: SearchOption | null) => { - setValue(value); - setInputValue(value?.label); // The Select has a blurInputOnSelect prop, but that makes the input // field lose focus, not the entire menu (e.g. when pressing twice). // // We anyways need the ref so that we can blur on selecting a person - // from the default options. + // from the default options. So also use it to blur the entire Select + // (including the menu) when the user selects an option. selectRef.current?.blur(); }; @@ -175,67 +148,16 @@ const SearchInput: React.FC = ({ }; const resetSearch = () => { - updateSearch(null, null); - setValue(null); + setValue(undefined); setInputValue(""); - }; - - const loadOptions = useCallback( - pDebounce(getAutoCompleteSuggestions, 250), - [], - ); - - const search = (selectedOption: SearchOption | null | undefined) => { - if (!selectedOption) { - return; - } - let search: SearchQuery; - switch (selectedOption.type) { - case SuggestionType.DATE: - search = { - date: selectedOption.value as SearchDateComponents, - }; - break; - case SuggestionType.LOCATION: - search = { - location: selectedOption.value as LocationTag, - }; - break; - case SuggestionType.CITY: - search = { - city: selectedOption.value as City, - }; - break; - case SuggestionType.COLLECTION: - search = { collection: selectedOption.value as number }; - setValue(null); - setInputValue(""); - break; - case SuggestionType.FILE_NAME: - search = { files: selectedOption.value as number[] }; - break; - case SuggestionType.FILE_CAPTION: - search = { files: selectedOption.value as number[] }; - break; - case SuggestionType.PERSON: - search = { person: selectedOption.value as SearchPerson }; - break; - case SuggestionType.FILE_TYPE: - search = { fileType: selectedOption.value as FileType }; - break; - case SuggestionType.CLIP: - search = { clip: selectedOption.value as ClipSearchScores }; - } - updateSearch(search, { - optionName: selectedOption.label, - fileCount: selectedOption.fileCount, - }); + setSelectedSearchOption(undefined); }; const handleSelectCGroup = (value: SearchOption) => { // Dismiss the search menu. selectRef.current?.blur(); setValue(value); + setSelectedSearchOption(undefined); }; const handleFocus = () => { @@ -259,7 +181,7 @@ const SearchInput: React.FC = ({ styles={styles} loadOptions={loadOptions} onChange={handleChange} - inputValue={inputValue ?? ""} + inputValue={inputValue} onInputChange={handleInputChange} isClearable escapeClearsValue @@ -291,6 +213,8 @@ const SearchInputWrapper = styled(Box)` margin: auto; `; +const loadOptions = pDebounce(searchOptionsForString, 250); + const createSelectStyles = ({ colors, }: Theme): StylesConfig => ({ @@ -356,24 +280,22 @@ const Control = ({ children, ...props }: ControlProps) => ( color: (theme) => theme.colors.stroke.muted, }} > - {iconForOptionType(props.getValue()[0]?.type)} + {iconForOption(props.getValue()[0])} {children} ); -const iconForOptionType = (type: SuggestionType | undefined) => { - switch (type) { - case SuggestionType.DATE: - return ; - case SuggestionType.LOCATION: - case SuggestionType.CITY: - return ; - case SuggestionType.COLLECTION: - return ; - case SuggestionType.FILE_NAME: +const iconForOption = (option: SearchOption | undefined) => { + switch (option?.suggestion.type) { + case "fileName": return ; + case "date": + return ; + case "location": + case "city": + return ; default: return ; } @@ -554,11 +476,9 @@ const Option: React.FC> = (props) => ( ); -const OptionContents = ({ data }: { data: SearchOption }) => ( +const OptionContents = ({ data: option }: { data: SearchOption }) => ( - - {labelForSuggestionType(data.type)} - + {labelForOption(option)} ( - {data.label} + {option.suggestion.label} - {t("photos_count", { count: data.fileCount })} + {t("photos_count", { count: option.fileCount })} - {data.previewFiles.map((file) => ( + {option.previewFiles.map((file) => ( ( ); + +const labelForOption = (option: SearchOption) => { + switch (option.suggestion.type) { + case "collection": + return t("album"); + + case "fileType": + return t("file_type"); + + case "fileName": + return t("file_name"); + + case "fileCaption": + return t("description"); + + case "date": + return t("date"); + case "location": + return t("location"); + + case "city": + return t("location"); + + case "clip": + return t("magic"); + + case "cgroup": + return t("person"); + } +}; diff --git a/web/packages/new/photos/services/search/ui.ts b/web/packages/new/photos/services/search/ui.ts deleted file mode 100644 index eeeac84d8c..0000000000 --- a/web/packages/new/photos/services/search/ui.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { t } from "i18next"; -import { SuggestionType } from "./types"; - -/** - * Return a localized label for the given suggestion {@link type}. - */ -export const labelForSuggestionType = (type: SuggestionType) => { - switch (type) { - case SuggestionType.DATE: - return t("date"); - case SuggestionType.LOCATION: - return t("location"); - case SuggestionType.CITY: - return t("location"); - case SuggestionType.COLLECTION: - return t("album"); - case SuggestionType.FILE_NAME: - return t("file_name"); - case SuggestionType.PERSON: - return t("person"); - case SuggestionType.FILE_CAPTION: - return t("description"); - case SuggestionType.FILE_TYPE: - return t("file_type"); - case SuggestionType.CLIP: - return t("magic"); - } -}; From 859cfc46d3d8ae2836e118c93c34e34647b40fb3 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 13:28:26 +0530 Subject: [PATCH 22/32] Fix gallery 1 --- web/apps/photos/src/pages/gallery.tsx | 76 ++++++++----------- .../new/photos/components/SearchBar.tsx | 2 +- 2 files changed, 31 insertions(+), 47 deletions(-) diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index cd0170dbea..2b02c1e5f2 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -5,7 +5,7 @@ import log from "@/base/log"; import type { Collection } from "@/media/collection"; import { SearchBar, - type UpdateSearch, + type SearchBarProps, } from "@/new/photos/components/SearchBar"; import { WhatsNew } from "@/new/photos/components/WhatsNew"; import { shouldShowWhatsNew } from "@/new/photos/services/changelog"; @@ -19,10 +19,7 @@ import { filterSearchableFiles, setSearchableData, } from "@/new/photos/services/search"; -import { - SearchQuery, - SearchResultSummary, -} from "@/new/photos/services/search/types"; +import type { SearchOption } from "@/new/photos/services/search/types"; import { EnteFile } from "@/new/photos/types/file"; import { mergeMetadata } from "@/new/photos/utils/file"; import { @@ -253,8 +250,9 @@ export default function Gallery() { }); const [isInSearchMode, setIsInSearchMode] = useState(false); - const [searchResultSummary, setSetSearchResultSummary] = - useState(null); + const [selectedSearchOption, setSelectedSearchOption] = useState< + SearchOption | undefined + >(); const syncInProgress = useRef(true); const syncInterval = useRef(); const resync = useRef<{ force: boolean; silent: boolean }>(); @@ -495,18 +493,18 @@ export default function Gallery() { }, [router.isReady]); useEffect(() => { - if (isInSearchMode && searchResultSummary) { + if (isInSearchMode && selectedSearchOption) { setPhotoListHeader({ height: 104, item: ( - ), itemType: ITEM_TYPE.HEADER, }); } - }, [isInSearchMode, searchResultSummary]); + }, [isInSearchMode, selectedSearchOption]); const activeCollection = useMemo(() => { if (!collections || !hiddenCollections) { @@ -1274,29 +1272,20 @@ const mergeMaps = (map1: Map, map2: Map) => { return mergedMap; }; -interface NormalNavbarContentsProps { +type NormalNavbarContentsProps = SearchBarProps & { openSidebar: () => void; openUploader: () => void; - isInSearchMode: boolean; - setIsInSearchMode: (v: boolean) => void; - updateSearch: UpdateSearch; -} +}; const NormalNavbarContents: React.FC = ({ openSidebar, openUploader, - isInSearchMode, - setIsInSearchMode, - updateSearch, + ...props }) => ( <> - {!isInSearchMode && } - - {!isInSearchMode && } + {!props.isInSearchMode && } + + {!props.isInSearchMode && } ); @@ -1355,25 +1344,20 @@ const HiddenSectionNavbarContents: React.FC< ); -interface SearchResultSummaryHeaderProps { - searchResultSummary: SearchResultSummary; +interface SearchResultsHeaderProps { + selectedOption: SearchOption; } -const SearchResultSummaryHeader: React.FC = ({ - searchResultSummary, -}) => { - if (!searchResultSummary) { - return <>; - } - - const { optionName, fileCount } = searchResultSummary; - - return ( - - - {t("search_results")} - - - - ); -}; +const SearchResultsHeader: React.FC = ({ + selectedOption, +}) => ( + + + {t("search_results")} + + + +); diff --git a/web/packages/new/photos/components/SearchBar.tsx b/web/packages/new/photos/components/SearchBar.tsx index 837210ce20..895bdef2c0 100644 --- a/web/packages/new/photos/components/SearchBar.tsx +++ b/web/packages/new/photos/components/SearchBar.tsx @@ -37,7 +37,7 @@ import { } from "react-select"; import AsyncSelect from "react-select/async"; -interface SearchBarProps { +export interface SearchBarProps { /** * [Note: "Search mode"] * From 5aa9671037786693d92379582a1a18685e27176a Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 13:39:48 +0530 Subject: [PATCH 23/32] Gallery fix 2 --- web/apps/photos/src/pages/gallery.tsx | 17 ++++++------ .../new/photos/services/search/index.ts | 18 ++----------- .../new/photos/services/search/worker.ts | 26 +++++++++++++++++-- 3 files changed, 34 insertions(+), 27 deletions(-) diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index 2b02c1e5f2..2fc5e447a4 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -206,7 +206,6 @@ export default function Gallery() { const [collectionNamerAttributes, setCollectionNamerAttributes] = useState(null); const [collectionNamerView, setCollectionNamerView] = useState(false); - const [searchQuery, setSearchQuery] = useState(null); const [shouldDisableDropzone, setShouldDisableDropzone] = useState(false); const [isPhotoSwipeOpen, setIsPhotoSwipeOpen] = useState(false); // TODO(MR): This is never true currently, this is the WIP ability to show @@ -249,7 +248,9 @@ export default function Gallery() { accept: ".zip", }); + // If we're in "search mode". See: [Note: "search mode"]. const [isInSearchMode, setIsInSearchMode] = useState(false); + // The option selected by the user selected from the search bar dropdown. const [selectedSearchOption, setSelectedSearchOption] = useState< SearchOption | undefined >(); @@ -536,8 +537,10 @@ export default function Gallery() { } let filteredFiles: EnteFile[] = []; - if (isInSearchMode) { - filteredFiles = await filterSearchableFiles(searchQuery); + if (selectedSearchOption) { + filteredFiles = await filterSearchableFiles( + selectedSearchOption.suggestion, + ); } else { filteredFiles = getUniqueFiles( (isInHiddenSection ? hiddenFiles : files).filter((item) => { @@ -597,11 +600,6 @@ export default function Gallery() { }), ); } - if (searchQuery?.clip) { - return filteredFiles.sort((a, b) => { - return searchQuery.clip.get(b.id) - searchQuery.clip.get(a.id); - }); - } const sortAsc = activeCollection?.pubMagicMetadata?.data?.asc ?? false; if (sortAsc) { return sortFiles(filteredFiles, true); @@ -615,7 +613,8 @@ export default function Gallery() { tempDeletedFileIds, tempHiddenFileIds, hiddenFileIds, - searchQuery, + isInSearchMode, + selectedSearchOption, activeCollectionID, archivedCollections, ]); diff --git a/web/packages/new/photos/services/search/index.ts b/web/packages/new/photos/services/search/index.ts index d8448a971b..c87c19127d 100644 --- a/web/packages/new/photos/services/search/index.ts +++ b/web/packages/new/photos/services/search/index.ts @@ -2,8 +2,6 @@ import log from "@/base/log"; import { masterKeyFromSession } from "@/base/session-store"; import { ComlinkWorker } from "@/base/worker/comlink-worker"; import { FileType } from "@/media/file-type"; -import type { EnteFile } from "@/new/photos/types/file"; -import { ensure } from "@/utils/ensure"; import i18n, { t } from "i18next"; import { clipMatches, isMLEnabled, isMLSupported } from "../ml"; import type { @@ -108,26 +106,14 @@ const suggestionsToOptions = async (suggestions: SearchSuggestion[]) => Promise.all( suggestions.map(async (suggestion) => { const matchingFiles = await filterSearchableFiles(suggestion); - const files = sortMatchesIfNeeded(matchingFiles, suggestion); return { suggestion, - fileCount: files.length, - previewFiles: files.slice(0, 3), + fileCount: matchingFiles.length, + previewFiles: matchingFiles.slice(0, 3), }; }), ); -const sortMatchesIfNeeded = ( - files: EnteFile[], - suggestion: SearchSuggestion, -) => { - if (suggestion.type != "clip") return files; - // Sort CLIP matches by their corresponding scores. - const score = ({ id }: EnteFile) => - ensure(suggestion.clipScoreForFileID.get(id)); - return files.sort((a, b) => score(b) - score(a)); -}; - /** * Return the list of {@link EnteFile}s (from amongst the previously set * {@link SearchableData}) that match the given search {@link suggestion}. diff --git a/web/packages/new/photos/services/search/worker.ts b/web/packages/new/photos/services/search/worker.ts index 354327e21c..b32bf46439 100644 --- a/web/packages/new/photos/services/search/worker.ts +++ b/web/packages/new/photos/services/search/worker.ts @@ -3,6 +3,7 @@ import type { Location } from "@/base/types"; import type { Collection } from "@/media/collection"; import { fileCreationPhotoDate, fileLocation } from "@/media/file-metadata"; import type { EnteFile } from "@/new/photos/types/file"; +import { ensure } from "@/utils/ensure"; import { nullToUndefined } from "@/utils/transform"; import { getPublicMagicMetadataSync } from "@ente/shared/file-metadata"; import type { Component } from "chrono-node"; @@ -88,8 +89,11 @@ export class SearchWorker { * Return {@link EnteFile}s that satisfy the given {@link suggestion}. */ filterSearchableFiles(suggestion: SearchSuggestion) { - return this.searchableData.files.filter((f) => - isMatchingFile(f, suggestion), + return sortMatchesIfNeeded( + this.searchableData.files.filter((f) => + isMatchingFile(f, suggestion), + ), + suggestion, ); } } @@ -428,3 +432,21 @@ const isWithinRadius = ( * major axis (a) has to be scaled by the secant of the latitude. */ const radiusScaleFactor = (lat: number) => 1 / Math.cos(lat * (Math.PI / 180)); + +/** + * Sort the files if necessary. + * + * Currently, only the CLIP results are sorted (by their score), in the other + * cases the files are displayed chronologically (when displaying them in search + * results) or arbitrarily (when showing them in the search option preview). + */ +const sortMatchesIfNeeded = ( + files: EnteFile[], + suggestion: SearchSuggestion, +) => { + if (suggestion.type != "clip") return files; + // Sort CLIP matches by their corresponding scores. + const score = ({ id }: EnteFile) => + ensure(suggestion.clipScoreForFileID.get(id)); + return files.sort((a, b) => score(b) - score(a)); +}; From 2cd2aee11c2aeb476c5960721233e50d7b839392 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 13:45:31 +0530 Subject: [PATCH 24/32] Fix gallery 3 --- web/apps/photos/src/pages/gallery.tsx | 18 ++++++++++-------- .../new/photos/components/SearchBar.tsx | 14 +++++++------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index 2fc5e447a4..edfdaa9a47 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -980,16 +980,18 @@ export default function Gallery() { }); }; - const updateSearch: UpdateSearch = (newSearch, summary) => { - if (newSearch?.collection) { - setActiveCollectionID(newSearch?.collection); + const handleSelectSearchOption = ( + searchOption: SearchOption | undefined, + ) => { + if (searchOption?.suggestion.type == "collection") { setIsInSearchMode(false); + setSelectedSearchOption(undefined); + setActiveCollectionID(searchOption.suggestion.collectionID); } else { - setSearchQuery(newSearch); - setIsInSearchMode(!!newSearch); - setSetSearchResultSummary(summary); + setIsInSearchMode(!!searchOption); + setSelectedSearchOption(searchOption); } - setIsClipSearchResult(!!newSearch?.clip); + setIsClipSearchResult(searchOption?.suggestion.type == "clip"); }; const openUploader = (intent?: UploadTypeSelectorIntent) => { @@ -1098,7 +1100,7 @@ export default function Gallery() { openUploader={openUploader} isInSearchMode={isInSearchMode} setIsInSearchMode={setIsInSearchMode} - updateSearch={updateSearch} + onSelectSearchOption={handleSelectSearchOption} /> )} diff --git a/web/packages/new/photos/components/SearchBar.tsx b/web/packages/new/photos/components/SearchBar.tsx index 895bdef2c0..a2adaff281 100644 --- a/web/packages/new/photos/components/SearchBar.tsx +++ b/web/packages/new/photos/components/SearchBar.tsx @@ -61,7 +61,7 @@ export interface SearchBarProps { /** * Set or clear the selected {@link SearchOption}. */ - setSelectedSearchOption: (o: SearchOption | undefined) => void; + onSelectSearchOption: (o: SearchOption | undefined) => void; } /** @@ -82,7 +82,7 @@ export interface SearchBarProps { export const SearchBar: React.FC = ({ setIsInSearchMode, isInSearchMode, - setSelectedSearchOption, + onSelectSearchOption, }) => { const isMobileWidth = useIsMobileWidth(); @@ -93,7 +93,7 @@ export const SearchBar: React.FC = ({ {isMobileWidth && !isInSearchMode ? ( ) : ( - + )} ); @@ -114,7 +114,7 @@ const MobileSearchArea: React.FC = ({ onSearch }) => ( const SearchInput: React.FC> = ({ isInSearchMode, - setSelectedSearchOption, + onSelectSearchOption, }) => { // A ref to the top level Select. const selectRef = useRef | null>(null); @@ -132,7 +132,7 @@ const SearchInput: React.FC> = ({ setValue(value ?? undefined); setInputValue(value?.suggestion.label ?? ""); // TODO-Cluster: - if (value) setSelectedSearchOption(value); + if (value) onSelectSearchOption(value); // The Select has a blurInputOnSelect prop, but that makes the input // field lose focus, not the entire menu (e.g. when pressing twice). @@ -150,14 +150,14 @@ const SearchInput: React.FC> = ({ const resetSearch = () => { setValue(undefined); setInputValue(""); - setSelectedSearchOption(undefined); + onSelectSearchOption(undefined); }; const handleSelectCGroup = (value: SearchOption) => { // Dismiss the search menu. selectRef.current?.blur(); setValue(value); - setSelectedSearchOption(undefined); + onSelectSearchOption(undefined); }; const handleFocus = () => { From a9537e59cfd6aaed043988937878d77798cda711 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 13:47:12 +0530 Subject: [PATCH 25/32] Lint fix --- .../photos/src/components/Collections/CollectionCard.tsx | 4 ++-- .../photos/src/components/pages/gallery/PreviewCard.tsx | 8 ++++---- .../new/photos/components/PlaceholderThumbnails.tsx | 3 ++- web/packages/shared/components/CodeBlock/index.tsx | 5 +---- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/web/apps/photos/src/components/Collections/CollectionCard.tsx b/web/apps/photos/src/components/Collections/CollectionCard.tsx index 2a60d9ce72..44f6fac84a 100644 --- a/web/apps/photos/src/components/Collections/CollectionCard.tsx +++ b/web/apps/photos/src/components/Collections/CollectionCard.tsx @@ -1,9 +1,9 @@ -import downloadManager from "@/new/photos/services/download"; -import { EnteFile } from "@/new/photos/types/file"; import { LoadingThumbnail, StaticThumbnail, } from "@/new/photos/components/PlaceholderThumbnails"; +import downloadManager from "@/new/photos/services/download"; +import { EnteFile } from "@/new/photos/types/file"; import { useEffect, useState } from "react"; /** See also: {@link ItemCard}. */ diff --git a/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx b/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx index 61ec39e21d..718fb628d1 100644 --- a/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx +++ b/web/apps/photos/src/components/pages/gallery/PreviewCard.tsx @@ -1,5 +1,9 @@ import log from "@/base/log"; import { FileType } from "@/media/file-type"; +import { + LoadingThumbnail, + StaticThumbnail, +} from "@/new/photos/components/PlaceholderThumbnails"; import DownloadManager from "@/new/photos/services/download"; import { EnteFile } from "@/new/photos/types/file"; import { Overlay } from "@ente/shared/components/Container"; @@ -12,10 +16,6 @@ import { GAP_BTW_TILES, IMAGE_CONTAINER_MAX_WIDTH, } from "components/PhotoList/constants"; -import { - LoadingThumbnail, - StaticThumbnail, -} from "@/new/photos/components/PlaceholderThumbnails"; import i18n from "i18next"; import { DeduplicateContext } from "pages/deduplicate"; import { GalleryContext } from "pages/gallery"; diff --git a/web/packages/new/photos/components/PlaceholderThumbnails.tsx b/web/packages/new/photos/components/PlaceholderThumbnails.tsx index 3ea247a15d..0fcab06569 100644 --- a/web/packages/new/photos/components/PlaceholderThumbnails.tsx +++ b/web/packages/new/photos/components/PlaceholderThumbnails.tsx @@ -3,6 +3,7 @@ import { Overlay } from "@ente/shared/components/Container"; import PhotoOutlined from "@mui/icons-material/PhotoOutlined"; import PlayCircleOutlineOutlined from "@mui/icons-material/PlayCircleOutlineOutlined"; import { styled } from "@mui/material"; +import React from "react"; interface Iprops { fileType: FileType; @@ -14,7 +15,7 @@ const CenteredOverlay = styled(Overlay)` align-items: center; `; -export const StaticThumbnail = (props: Iprops) => { +export const StaticThumbnail: React.FC = (props) => { return ( ({ diff --git a/web/packages/shared/components/CodeBlock/index.tsx b/web/packages/shared/components/CodeBlock/index.tsx index b32b8fca66..e556638702 100644 --- a/web/packages/shared/components/CodeBlock/index.tsx +++ b/web/packages/shared/components/CodeBlock/index.tsx @@ -8,10 +8,7 @@ type Iprops = React.PropsWithChildren<{ code: string | null; }>; -export default function CodeBlock({ - code, - ...props -}: BoxProps<"div", Iprops>) { +export default function CodeBlock({ code, ...props }: BoxProps<"div", Iprops>) { if (!code) { return ( From 32315b1149c4ed8c6ab225e3880c3f069fbe0ef0 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 14:00:47 +0530 Subject: [PATCH 26/32] Fix --- web/packages/new/photos/services/search/index.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/web/packages/new/photos/services/search/index.ts b/web/packages/new/photos/services/search/index.ts index c87c19127d..c98416bc53 100644 --- a/web/packages/new/photos/services/search/index.ts +++ b/web/packages/new/photos/services/search/index.ts @@ -106,13 +106,15 @@ const suggestionsToOptions = async (suggestions: SearchSuggestion[]) => Promise.all( suggestions.map(async (suggestion) => { const matchingFiles = await filterSearchableFiles(suggestion); - return { - suggestion, - fileCount: matchingFiles.length, - previewFiles: matchingFiles.slice(0, 3), - }; + return matchingFiles.length + ? { + suggestion, + fileCount: matchingFiles.length, + previewFiles: matchingFiles.slice(0, 3), + } + : undefined; }), - ); + ).then((r) => r.filter((o) => !!o)); /** * Return the list of {@link EnteFile}s (from amongst the previously set From 117c884b3ed77acdc733aa21726d3e49f383dfcf Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 14:22:26 +0530 Subject: [PATCH 27/32] Reset to placeholder on reset --- web/packages/new/photos/components/SearchBar.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/packages/new/photos/components/SearchBar.tsx b/web/packages/new/photos/components/SearchBar.tsx index a2adaff281..b4ac2d5da4 100644 --- a/web/packages/new/photos/components/SearchBar.tsx +++ b/web/packages/new/photos/components/SearchBar.tsx @@ -183,6 +183,8 @@ const SearchInput: React.FC> = ({ onChange={handleChange} inputValue={inputValue} onInputChange={handleInputChange} + // Needed to get the placeholder to reset on `resetSearch`. + controlShouldRenderValue={false} isClearable escapeClearsValue onFocus={handleFocus} From d65597c44f8dd4c9cda2073ee39a77e713c461b1 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 14:30:13 +0530 Subject: [PATCH 28/32] Handle album selection --- web/packages/new/photos/components/SearchBar.tsx | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/web/packages/new/photos/components/SearchBar.tsx b/web/packages/new/photos/components/SearchBar.tsx index b4ac2d5da4..add9780c77 100644 --- a/web/packages/new/photos/components/SearchBar.tsx +++ b/web/packages/new/photos/components/SearchBar.tsx @@ -129,10 +129,18 @@ const SearchInput: React.FC> = ({ const components = useMemo(() => ({ Control, Input, Option }), []); const handleChange = (value: SearchOption | null | undefined) => { - setValue(value ?? undefined); - setInputValue(value?.suggestion.label ?? ""); - // TODO-Cluster: - if (value) onSelectSearchOption(value); + // Collection suggestions are handled differently - our caller will + // switch to the collection view, dismissing search. + if (value?.suggestion.type == "collection") { + setValue(undefined); + setInputValue(""); + } else { + setValue(value ?? undefined); + setInputValue(value?.suggestion.label ?? ""); + } + + // Let our parent know the selection was changed. + onSelectSearchOption(value ?? undefined); // The Select has a blurInputOnSelect prop, but that makes the input // field lose focus, not the entire menu (e.g. when pressing twice). From 38b3e0471890031449c6a631e2ec83269131c0ab Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 15:18:22 +0530 Subject: [PATCH 29/32] Reset placeholder - Part Deux Fixes 117c884b3ed77acdc733aa21726d3e49f383dfcf to also reset the icon Underlying reason: https://github.com/JedWatson/react-select/issues/5219 Nb: React itself does not recommend null either > The value you pass to controlled components should not be `undefined` or `null`. > > https://react.dev/reference/react-dom/components/input --- .../new/photos/components/SearchBar.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/web/packages/new/photos/components/SearchBar.tsx b/web/packages/new/photos/components/SearchBar.tsx index add9780c77..c9f34f2b59 100644 --- a/web/packages/new/photos/components/SearchBar.tsx +++ b/web/packages/new/photos/components/SearchBar.tsx @@ -8,6 +8,7 @@ import { } from "@/new/photos/services/ml"; import { searchOptionsForString } from "@/new/photos/services/search"; import type { SearchOption } from "@/new/photos/services/search/types"; +import { nullToUndefined } from "@/utils/transform"; import CalendarIcon from "@mui/icons-material/CalendarMonth"; import CloseIcon from "@mui/icons-material/Close"; import ImageIcon from "@mui/icons-material/Image"; @@ -119,7 +120,11 @@ const SearchInput: React.FC> = ({ // A ref to the top level Select. const selectRef = useRef | null>(null); // The currently selected option. - const [value, setValue] = useState(); + // + // We need to use `null` instead of `undefined` to indicate missing values, + // because using `undefined` instead moves the Select from being a controlled + // component to an uncontrolled component. + const [value, setValue] = useState(null); // The contents of the input field associated with the select. const [inputValue, setInputValue] = useState(""); @@ -128,19 +133,19 @@ const SearchInput: React.FC> = ({ const styles = useMemo(() => createSelectStyles(theme), [theme]); const components = useMemo(() => ({ Control, Input, Option }), []); - const handleChange = (value: SearchOption | null | undefined) => { + const handleChange = (value: SearchOption | null) => { // Collection suggestions are handled differently - our caller will // switch to the collection view, dismissing search. if (value?.suggestion.type == "collection") { - setValue(undefined); + setValue(null); setInputValue(""); } else { - setValue(value ?? undefined); + setValue(value); setInputValue(value?.suggestion.label ?? ""); } // Let our parent know the selection was changed. - onSelectSearchOption(value ?? undefined); + onSelectSearchOption(nullToUndefined(value)); // The Select has a blurInputOnSelect prop, but that makes the input // field lose focus, not the entire menu (e.g. when pressing twice). @@ -156,7 +161,7 @@ const SearchInput: React.FC> = ({ }; const resetSearch = () => { - setValue(undefined); + setValue(null); setInputValue(""); onSelectSearchOption(undefined); }; @@ -191,8 +196,6 @@ const SearchInput: React.FC> = ({ onChange={handleChange} inputValue={inputValue} onInputChange={handleInputChange} - // Needed to get the placeholder to reset on `resetSearch`. - controlShouldRenderValue={false} isClearable escapeClearsValue onFocus={handleFocus} From 2990ba855f98f2dfd1d37ea3e0dcae1ef8c27927 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 16:03:56 +0530 Subject: [PATCH 30/32] Speed --- .../new/photos/services/search/index.ts | 41 +++++++++++++------ .../new/photos/services/search/worker.ts | 11 +++++ 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/web/packages/new/photos/services/search/index.ts b/web/packages/new/photos/services/search/index.ts index c98416bc53..89d99eef8d 100644 --- a/web/packages/new/photos/services/search/index.ts +++ b/web/packages/new/photos/services/search/index.ts @@ -102,19 +102,31 @@ const clipSuggestion = async ( return { type: "clip", clipScoreForFileID: matches, label: searchString }; }; -const suggestionsToOptions = async (suggestions: SearchSuggestion[]) => - Promise.all( - suggestions.map(async (suggestion) => { - const matchingFiles = await filterSearchableFiles(suggestion); - return matchingFiles.length - ? { - suggestion, - fileCount: matchingFiles.length, - previewFiles: matchingFiles.slice(0, 3), - } - : undefined; - }), - ).then((r) => r.filter((o) => !!o)); +// const suggestionsToOptions = async (suggestions: SearchSuggestion[]) => +// Promise.all( +// suggestions.map(async (suggestion) => { +// const matchingFiles = await filterSearchableFiles(suggestion); +// return matchingFiles.length +// ? { +// suggestion, +// fileCount: matchingFiles.length, +// previewFiles: matchingFiles.slice(0, 3), +// } +// : undefined; +// }), +// ).then((r) => r.filter((o) => !!o)); + const suggestionsToOptions = async (suggestions: SearchSuggestion[]) => { + const filess = await filterSearchableFiles2(suggestions); + const f2 = filess.map((f, i) => [f, suggestions[i]!] as const); + return f2 + .filter(([fs]) => fs.length) + .map(([f, s]) => ({ + // suggestion: suggestions[i], + suggestion: s, + fileCount: f.length, + previewFiles: f.slice(0, 3), + })); + }; /** * Return the list of {@link EnteFile}s (from amongst the previously set @@ -123,6 +135,9 @@ const suggestionsToOptions = async (suggestions: SearchSuggestion[]) => export const filterSearchableFiles = async (suggestion: SearchSuggestion) => worker().then((w) => w.filterSearchableFiles(suggestion)); +export const filterSearchableFiles2 = async (suggestion: SearchSuggestion[]) => + worker().then((w) => w.filterSearchableFiles2(suggestion)); + /** * Cached value of {@link localizedSearchData}. */ diff --git a/web/packages/new/photos/services/search/worker.ts b/web/packages/new/photos/services/search/worker.ts index b32bf46439..ffbc118bc6 100644 --- a/web/packages/new/photos/services/search/worker.ts +++ b/web/packages/new/photos/services/search/worker.ts @@ -96,6 +96,17 @@ export class SearchWorker { suggestion, ); } + + filterSearchableFiles2(suggestions: SearchSuggestion[]) { + return suggestions.map((suggestion) => { + return sortMatchesIfNeeded( + this.searchableData.files.filter((f) => + isMatchingFile(f, suggestion), + ), + suggestion, + ); + }); + } } expose(SearchWorker); From 016761be9ae48e9520ba79e93f101ffbab909c2e Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 16:20:37 +0530 Subject: [PATCH 31/32] Cleanup --- .../new/photos/services/search/index.ts | 44 +++++++------------ .../new/photos/services/search/worker.ts | 33 +++++++------- 2 files changed, 35 insertions(+), 42 deletions(-) diff --git a/web/packages/new/photos/services/search/index.ts b/web/packages/new/photos/services/search/index.ts index 89d99eef8d..21322c6648 100644 --- a/web/packages/new/photos/services/search/index.ts +++ b/web/packages/new/photos/services/search/index.ts @@ -102,31 +102,14 @@ const clipSuggestion = async ( return { type: "clip", clipScoreForFileID: matches, label: searchString }; }; -// const suggestionsToOptions = async (suggestions: SearchSuggestion[]) => -// Promise.all( -// suggestions.map(async (suggestion) => { -// const matchingFiles = await filterSearchableFiles(suggestion); -// return matchingFiles.length -// ? { -// suggestion, -// fileCount: matchingFiles.length, -// previewFiles: matchingFiles.slice(0, 3), -// } -// : undefined; -// }), -// ).then((r) => r.filter((o) => !!o)); - const suggestionsToOptions = async (suggestions: SearchSuggestion[]) => { - const filess = await filterSearchableFiles2(suggestions); - const f2 = filess.map((f, i) => [f, suggestions[i]!] as const); - return f2 - .filter(([fs]) => fs.length) - .map(([f, s]) => ({ - // suggestion: suggestions[i], - suggestion: s, - fileCount: f.length, - previewFiles: f.slice(0, 3), - })); - }; +const suggestionsToOptions = (suggestions: SearchSuggestion[]) => + filterSearchableFilesMulti(suggestions).then((res) => + res.map(([files, suggestion]) => ({ + suggestion, + fileCount: files.length, + previewFiles: files.slice(0, 3), + })), + ); /** * Return the list of {@link EnteFile}s (from amongst the previously set @@ -135,8 +118,15 @@ const clipSuggestion = async ( export const filterSearchableFiles = async (suggestion: SearchSuggestion) => worker().then((w) => w.filterSearchableFiles(suggestion)); -export const filterSearchableFiles2 = async (suggestion: SearchSuggestion[]) => - worker().then((w) => w.filterSearchableFiles2(suggestion)); +/** + * A batched variant of {@link filterSearchableFiles}. + * + * This has drastically (10x) better performance when filtering files for a + * large number of suggestions (e.g. single letter searches that lead to a large + * number of city prefix matches), likely because of reduced worker IPC. + */ +const filterSearchableFilesMulti = async (suggestions: SearchSuggestion[]) => + worker().then((w) => w.filterSearchableFilesMulti(suggestions)); /** * Cached value of {@link localizedSearchData}. diff --git a/web/packages/new/photos/services/search/worker.ts b/web/packages/new/photos/services/search/worker.ts index ffbc118bc6..f1b9c61302 100644 --- a/web/packages/new/photos/services/search/worker.ts +++ b/web/packages/new/photos/services/search/worker.ts @@ -89,23 +89,17 @@ export class SearchWorker { * Return {@link EnteFile}s that satisfy the given {@link suggestion}. */ filterSearchableFiles(suggestion: SearchSuggestion) { - return sortMatchesIfNeeded( - this.searchableData.files.filter((f) => - isMatchingFile(f, suggestion), - ), - suggestion, - ); + return filterSearchableFiles(this.searchableData.files, suggestion); } - filterSearchableFiles2(suggestions: SearchSuggestion[]) { - return suggestions.map((suggestion) => { - return sortMatchesIfNeeded( - this.searchableData.files.filter((f) => - isMatchingFile(f, suggestion), - ), - suggestion, - ); - }); + /** + * Batched variant of {@link filterSearchableFiles}. + */ + filterSearchableFilesMulti(suggestions: SearchSuggestion[]) { + const files = this.searchableData.files; + return suggestions + .map((sg) => [filterSearchableFiles(files, sg), sg] as const) + .filter(([files]) => files.length); } } @@ -342,6 +336,15 @@ const locationSuggestions = ( ].flat(); }; +const filterSearchableFiles = ( + files: EnteFile[], + suggestion: SearchSuggestion, +) => + sortMatchesIfNeeded( + files.filter((f) => isMatchingFile(f, suggestion)), + suggestion, + ); + /** * Return true if file satisfies the given {@link query}. */ From 897dd78ffd9b1de7cd92096d5f29cf975d3b3ebc Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 16:26:18 +0530 Subject: [PATCH 32/32] Remove stale TODO --- web/apps/photos/src/pages/gallery.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index edfdaa9a47..06a8e8b161 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -208,9 +208,6 @@ export default function Gallery() { const [collectionNamerView, setCollectionNamerView] = useState(false); const [shouldDisableDropzone, setShouldDisableDropzone] = useState(false); const [isPhotoSwipeOpen, setIsPhotoSwipeOpen] = useState(false); - // TODO(MR): This is never true currently, this is the WIP ability to show - // what's new dialog on desktop app updates. The UI is done, need to hook - // this up to logic to trigger it. const [openWhatsNew, setOpenWhatsNew] = useState(false); const {