From 315529eebfb699064f497082b5a3d04da18b0d91 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 07:31:55 +0530 Subject: [PATCH 01/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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/45] 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 aedb689e4591b6718ce0e5ffb4a513fe7f6ece47 Mon Sep 17 00:00:00 2001 From: kishan-dhankecha Date: Thu, 12 Sep 2024 13:05:38 +0530 Subject: [PATCH 21/45] fixed duplicated code being added bug --- .../view/setup_enter_secret_key_page.dart | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/auth/lib/onboarding/view/setup_enter_secret_key_page.dart b/auth/lib/onboarding/view/setup_enter_secret_key_page.dart index b4ab8bfd0f..d970ae9fd9 100644 --- a/auth/lib/onboarding/view/setup_enter_secret_key_page.dart +++ b/auth/lib/onboarding/view/setup_enter_secret_key_page.dart @@ -31,7 +31,7 @@ class _SetupEnterSecretKeyPageState extends State { late TextEditingController _accountController; late TextEditingController _secretController; late bool _secretKeyObscured; - late List tags = [...?widget.code?.display.tags]; + late List selectedTags = [...?widget.code?.display.tags]; List allTags = []; StreamSubscription? _streamSubscription; @@ -164,14 +164,14 @@ class _SetupEnterSecretKeyPageState extends State { (e) => TagChip( label: e, action: TagChipAction.check, - state: tags.contains(e) + state: selectedTags.contains(e) ? TagChipState.selected : TagChipState.unselected, onTap: () { - if (tags.contains(e)) { - tags.remove(e); + if (selectedTags.contains(e)) { + selectedTags.remove(e); } else { - tags.add(e); + selectedTags.add(e); } setState(() {}); }, @@ -184,12 +184,12 @@ class _SetupEnterSecretKeyPageState extends State { builder: (BuildContext context) { return AddTagDialog( onTap: (tag) { - if (allTags.contains(tag) && - tags.contains(tag)) { - return; + final exist = allTags.contains(tag); + if (exist && selectedTags.contains(tag)) { + return Navigator.pop(context); } - allTags.add(tag); - tags.add(tag); + if (!exist) allTags.add(tag); + selectedTags.add(tag); setState(() {}); Navigator.pop(context); }, @@ -240,7 +240,8 @@ class _SetupEnterSecretKeyPageState extends State { final account = _accountController.text.trim(); final issuer = _issuerController.text.trim(); final secret = _secretController.text.trim().replaceAll(' ', ''); - final isStreamCode = issuer.toLowerCase() == "steam" || issuer.toLowerCase().contains('steampowered.com'); + final isStreamCode = issuer.toLowerCase() == "steam" || + issuer.toLowerCase().contains('steampowered.com'); if (widget.code != null && widget.code!.secret != secret) { ButtonResult? result = await showChoiceActionSheet( context, @@ -256,7 +257,8 @@ class _SetupEnterSecretKeyPageState extends State { } } final CodeDisplay display = - widget.code?.display.copyWith(tags: tags) ?? CodeDisplay(tags: tags); + widget.code?.display.copyWith(tags: selectedTags) ?? + CodeDisplay(tags: selectedTags); final Code newCode = widget.code == null ? Code.fromAccountAndSecret( isStreamCode ? Type.steam : Type.totp, From 060a055d38db396bcfb357e1e9b0b5f32eab4302 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 13:22:29 +0530 Subject: [PATCH 22/45] 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 23/45] 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 24/45] 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 25/45] 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 26/45] 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 27/45] 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 28/45] 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 29/45] 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 30/45] 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 31/45] 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 32/45] 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 33/45] 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 { From d976986473d62479773932dd857e3b6bbd0114cf Mon Sep 17 00:00:00 2001 From: ashilkn Date: Thu, 12 Sep 2024 17:04:58 +0530 Subject: [PATCH 34/45] [mob][photos] Fix exceptions from calling dependOnInheritedWidgetOfExactType in init state --- mobile/lib/ui/home/loading_photos_widget.dart | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/mobile/lib/ui/home/loading_photos_widget.dart b/mobile/lib/ui/home/loading_photos_widget.dart index 2980fd34bb..f5c9dd8443 100644 --- a/mobile/lib/ui/home/loading_photos_widget.dart +++ b/mobile/lib/ui/home/loading_photos_widget.dart @@ -25,7 +25,7 @@ class LoadingPhotosWidget extends StatefulWidget { class _LoadingPhotosWidgetState extends State { late StreamSubscription _firstImportEvent; - late StreamSubscription _importProgressEvent; + StreamSubscription? _importProgressEvent; int _currentPage = 0; late String _loadingMessage; final PageController _pageController = PageController( @@ -38,7 +38,6 @@ class _LoadingPhotosWidgetState extends State { @override void initState() { super.initState(); - _loadingMessage = S.of(context).loadingYourPhotos; Future.delayed(const Duration(seconds: 60), () { oneMinuteOnScreen.value = true; }); @@ -53,19 +52,14 @@ class _LoadingPhotosWidgetState extends State { context, BackupFolderSelectionPage( isOnboarding: true, + //Move this from here? buttonText: S.of(context).startBackup, ), ); } } }); - _importProgressEvent = - Bus.instance.on().listen((event) { - _loadingMessage = S.of(context).processingImport(event.folderName); - if (mounted) { - setState(() {}); - } - }); + _didYouKnowTimer = Timer.periodic(const Duration(seconds: 5), (Timer timer) { if (!mounted) { @@ -85,10 +79,26 @@ class _LoadingPhotosWidgetState extends State { }); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_importProgressEvent != null) { + _importProgressEvent!.cancel(); + } else { + _importProgressEvent = + Bus.instance.on().listen((event) { + _loadingMessage = S.of(context).processingImport(event.folderName); + if (mounted) { + setState(() {}); + } + }); + } + } + @override void dispose() { _firstImportEvent.cancel(); - _importProgressEvent.cancel(); + _importProgressEvent?.cancel(); _didYouKnowTimer.cancel(); oneMinuteOnScreen.dispose(); super.dispose(); @@ -96,6 +106,9 @@ class _LoadingPhotosWidgetState extends State { @override Widget build(BuildContext context) { + if (_importProgressEvent == null) { + _loadingMessage = S.of(context).loadingYourPhotos; + } _setupLoadingMessages(context); final isLightMode = Theme.of(context).brightness == Brightness.light; return Scaffold( From b8f2b850c333d1e1302df6026902567969e60008 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 17:12:02 +0530 Subject: [PATCH 35/45] [web] Continue display of trashed items if mobile search is active ...but empty --- web/apps/photos/src/pages/gallery.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/apps/photos/src/pages/gallery.tsx b/web/apps/photos/src/pages/gallery.tsx index 06a8e8b161..fafaedbb87 100644 --- a/web/apps/photos/src/pages/gallery.tsx +++ b/web/apps/photos/src/pages/gallery.tsx @@ -526,7 +526,7 @@ export default function Gallery() { return; } - if (activeCollectionID === TRASH_SECTION && !isInSearchMode) { + if (activeCollectionID === TRASH_SECTION && !selectedSearchOption) { return getUniqueFiles([ ...trashedFiles, ...files.filter((file) => tempDeletedFileIds?.has(file.id)), @@ -610,7 +610,6 @@ export default function Gallery() { tempDeletedFileIds, tempHiddenFileIds, hiddenFileIds, - isInSearchMode, selectedSearchOption, activeCollectionID, archivedCollections, From 623f2c1985917869041022e7941e7c8663935935 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 17:19:51 +0530 Subject: [PATCH 36/45] Alt --- web/packages/new/photos/components/SearchBar.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/packages/new/photos/components/SearchBar.tsx b/web/packages/new/photos/components/SearchBar.tsx index c9f34f2b59..80ed1fb432 100644 --- a/web/packages/new/photos/components/SearchBar.tsx +++ b/web/packages/new/photos/components/SearchBar.tsx @@ -208,7 +208,7 @@ const SearchInput: React.FC> = ({ /> {isInSearchMode && ( - resetSearch()} sx={{ ml: 1 }}> + )} @@ -221,6 +221,7 @@ const SearchInputWrapper = styled(Box)` width: 100%; align-items: center; justify-content: center; + gap: 8px; background: ${({ theme }) => theme.colors.background.base}; max-width: 484px; margin: auto; From 85fe4b317d4137ae3d469f706c34f1149127c5c9 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 17:31:26 +0530 Subject: [PATCH 37/45] Remove empty files --- web/apps/photos/src/types/image/index.ts | 0 web/apps/photos/src/types/search/index.ts | 0 2 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 web/apps/photos/src/types/image/index.ts delete mode 100644 web/apps/photos/src/types/search/index.ts diff --git a/web/apps/photos/src/types/image/index.ts b/web/apps/photos/src/types/image/index.ts deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/web/apps/photos/src/types/search/index.ts b/web/apps/photos/src/types/search/index.ts deleted file mode 100644 index e69de29bb2..0000000000 From d649cbd9fad46a267f5afff5f92bc4532c537fba Mon Sep 17 00:00:00 2001 From: ashilkn Date: Thu, 12 Sep 2024 17:32:43 +0530 Subject: [PATCH 38/45] [mob][photos] Avoid passing text gotten from inherited widget --- mobile/lib/ui/components/home_header_widget.dart | 4 ++-- mobile/lib/ui/home/loading_photos_widget.dart | 5 ++--- mobile/lib/ui/home/start_backup_hook_widget.dart | 4 ++-- .../backup/backup_folder_selection_page.dart | 14 +++++++++----- .../ui/settings/backup/backup_section_widget.dart | 4 ++-- mobile/lib/ui/viewer/search/tab_empty_state.dart | 4 ++-- 6 files changed, 19 insertions(+), 16 deletions(-) diff --git a/mobile/lib/ui/components/home_header_widget.dart b/mobile/lib/ui/components/home_header_widget.dart index 7f2519a190..88471e251a 100644 --- a/mobile/lib/ui/components/home_header_widget.dart +++ b/mobile/lib/ui/components/home_header_widget.dart @@ -86,8 +86,8 @@ class _HomeHeaderWidgetState extends State { unawaited( routeToPage( context, - BackupFolderSelectionPage( - buttonText: S.of(context).backup, + const BackupFolderSelectionPage( + isFirstBackup: false, ), ), ); diff --git a/mobile/lib/ui/home/loading_photos_widget.dart b/mobile/lib/ui/home/loading_photos_widget.dart index f5c9dd8443..29fe1c7b50 100644 --- a/mobile/lib/ui/home/loading_photos_widget.dart +++ b/mobile/lib/ui/home/loading_photos_widget.dart @@ -50,10 +50,9 @@ class _LoadingPhotosWidgetState extends State { // ignore: unawaited_futures routeToPage( context, - BackupFolderSelectionPage( + const BackupFolderSelectionPage( isOnboarding: true, - //Move this from here? - buttonText: S.of(context).startBackup, + isFirstBackup: true, ), ); } diff --git a/mobile/lib/ui/home/start_backup_hook_widget.dart b/mobile/lib/ui/home/start_backup_hook_widget.dart index f331cd586b..64dfa1fa21 100644 --- a/mobile/lib/ui/home/start_backup_hook_widget.dart +++ b/mobile/lib/ui/home/start_backup_hook_widget.dart @@ -49,8 +49,8 @@ class StartBackupHookWidget extends StatelessWidget { // ignore: unawaited_futures routeToPage( context, - BackupFolderSelectionPage( - buttonText: S.of(context).startBackup, + const BackupFolderSelectionPage( + isFirstBackup: true, ), ); } diff --git a/mobile/lib/ui/settings/backup/backup_folder_selection_page.dart b/mobile/lib/ui/settings/backup/backup_folder_selection_page.dart index 783a509c12..d657c60950 100644 --- a/mobile/lib/ui/settings/backup/backup_folder_selection_page.dart +++ b/mobile/lib/ui/settings/backup/backup_folder_selection_page.dart @@ -19,14 +19,14 @@ import 'package:photos/ui/viewer/file/thumbnail_widget.dart'; import 'package:photos/utils/dialog_util.dart'; class BackupFolderSelectionPage extends StatefulWidget { + final bool isFirstBackup; final bool isOnboarding; - final String buttonText; const BackupFolderSelectionPage({ - required this.buttonText, + required this.isFirstBackup, this.isOnboarding = false, - Key? key, - }) : super(key: key); + super.key, + }); @override State createState() => @@ -173,7 +173,11 @@ class _BackupFolderSelectionPageState extends State { : () async { await updateFolderSettings(); }, - child: Text(widget.buttonText), + child: Text( + widget.isFirstBackup + ? S.of(context).startBackup + : S.of(context).backup, + ), ), ), widget.isOnboarding diff --git a/mobile/lib/ui/settings/backup/backup_section_widget.dart b/mobile/lib/ui/settings/backup/backup_section_widget.dart index 15d885389c..904f899d57 100644 --- a/mobile/lib/ui/settings/backup/backup_section_widget.dart +++ b/mobile/lib/ui/settings/backup/backup_section_widget.dart @@ -42,8 +42,8 @@ class BackupSectionWidgetState extends State { onTap: () async { await routeToPage( context, - BackupFolderSelectionPage( - buttonText: S.of(context).backup, + const BackupFolderSelectionPage( + isFirstBackup: false, ), ); }, diff --git a/mobile/lib/ui/viewer/search/tab_empty_state.dart b/mobile/lib/ui/viewer/search/tab_empty_state.dart index ab072dc6f8..d8ab31c240 100644 --- a/mobile/lib/ui/viewer/search/tab_empty_state.dart +++ b/mobile/lib/ui/viewer/search/tab_empty_state.dart @@ -38,8 +38,8 @@ class SearchTabEmptyState extends StatelessWidget { // ignore: unawaited_futures routeToPage( context, - BackupFolderSelectionPage( - buttonText: S.of(context).backup, + const BackupFolderSelectionPage( + isFirstBackup: false, ), ); }, From 816d74a5e6e8c27131a8d17df863c41d04a0d4c2 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 18:00:56 +0530 Subject: [PATCH 39/45] SearchPerson => Person --- .../new/photos/components/PeopleList.tsx | 8 ++-- .../new/photos/components/SearchBar.tsx | 6 +-- web/packages/new/photos/services/ml/index.ts | 47 ++++++++++++++++--- .../new/photos/services/search/types.ts | 15 +----- 4 files changed, 49 insertions(+), 27 deletions(-) diff --git a/web/packages/new/photos/components/PeopleList.tsx b/web/packages/new/photos/components/PeopleList.tsx index 7c42d157dd..6de6dadd1b 100644 --- a/web/packages/new/photos/components/PeopleList.tsx +++ b/web/packages/new/photos/components/PeopleList.tsx @@ -1,14 +1,14 @@ +import type { Person } from "@/new/photos/services/ml"; import { faceCrop, unidentifiedFaceIDs } from "@/new/photos/services/ml"; import type { EnteFile } from "@/new/photos/types/file"; import { Skeleton, Typography, styled } from "@mui/material"; import { t } from "i18next"; import React, { useEffect, useState } from "react"; -import type { SearchPerson } from "../services/search/types"; export interface PeopleListProps { - people: SearchPerson[]; + people: Person[]; maxRows: number; - onSelect?: (person: SearchPerson, index: number) => void; + onSelect?: (person: Person, index: number) => void; } export const PeopleList: React.FC = ({ @@ -60,7 +60,7 @@ const FaceChip = styled("div")<{ clickable?: boolean }>` export interface PhotoPeopleListProps { file: EnteFile; - onSelect?: (person: SearchPerson, index: number) => void; + onSelect?: (person: Person, index: number) => void; } export function PhotoPeopleList() { diff --git a/web/packages/new/photos/components/SearchBar.tsx b/web/packages/new/photos/components/SearchBar.tsx index 80ed1fb432..cfbd6e15d4 100644 --- a/web/packages/new/photos/components/SearchBar.tsx +++ b/web/packages/new/photos/components/SearchBar.tsx @@ -396,7 +396,7 @@ const EmptyState: React.FC = () => { // const peopleSuggestions = options.filter( // (o) => o.type === SuggestionType.PERSON, // ); - // const people = peopleSuggestions.map((o) => o.value as SearchPerson); + // const people = peopleSuggestions.map((o) => o.value as Person); // return ( // // @@ -457,7 +457,7 @@ export async function getAllPeopleSuggestion(): Promise> { } async function getAllPeople(limit: number = undefined) { - return (await wipSearchPersons()).slice(0, limit); + return (await wipPersons()).slice(0, limit); // TODO-Clustetr // if (done) return []; @@ -468,7 +468,7 @@ async function getAllPeople(limit: number = undefined) { // log.debug(() => ["people", { people }]); // } - // let people: Array = []; // await mlIDbStorage.getAllPeople(); + // let people: Array = []; // await mlIDbStorage.getAllPeople(); // people = await wipCluster(); // // await mlPeopleStore.iterate((person) => { // // people.push(person); diff --git a/web/packages/new/photos/services/ml/index.ts b/web/packages/new/photos/services/ml/index.ts index c5c326120c..673803a6dd 100644 --- a/web/packages/new/photos/services/ml/index.ts +++ b/web/packages/new/photos/services/ml/index.ts @@ -17,7 +17,7 @@ import { throttled } from "@/utils/promise"; import { proxy, transfer } from "comlink"; import { isInternalUser } from "../feature-flags"; import { getRemoteFlag, updateRemoteFlag } from "../remote-store"; -import type { SearchPerson } from "../search/types"; +import type { Person } from "../search/types"; import type { UploadItem } from "../upload/types"; import { type ClusterFace, @@ -329,7 +329,7 @@ export const wipClusterEnable = async (): Promise => // // TODO-Cluster temporary state here let _wip_isClustering = false; -let _wip_searchPersons: SearchPerson[] | undefined; +let _wip_Persons: Person[] | undefined; let _wip_hasSwitchedOnce = false; export const wipHasSwitchedOnceCmpAndSet = () => { @@ -338,9 +338,9 @@ export const wipHasSwitchedOnceCmpAndSet = () => { return false; }; -export const wipSearchPersons = async () => { +export const wipPersons = async () => { if (!(await wipClusterEnable())) return []; - return _wip_searchPersons ?? []; + return _wip_Persons ?? []; }; export interface ClusterPreviewWithFile { @@ -374,7 +374,7 @@ export const wipClusterDebugPageContents = async ( log.info("clustering", opts); _wip_isClustering = true; - _wip_searchPersons = undefined; + _wip_Persons = undefined; triggerStatusUpdate(); const { @@ -407,7 +407,7 @@ export const wipClusterDebugPageContents = async ( const clusterByID = new Map(clusters.map((c) => [c.id, c])); - const searchPersons = cgroups + const Persons = cgroups .map((cgroup) => { const faceID = ensure(cgroup.displayFaceID); const fileID = ensure(fileIDFromFaceID(faceID)); @@ -432,7 +432,7 @@ export const wipClusterDebugPageContents = async ( .sort((a, b) => b.faceIDs.length - a.faceIDs.length); _wip_isClustering = false; - _wip_searchPersons = searchPersons; + _wip_Persons = Persons; triggerStatusUpdate(); return { @@ -581,6 +581,39 @@ const setInterimScheduledStatus = () => { const workerDidProcessFileOrIdle = throttled(updateMLStatusSnapshot, 2000); +/** + * A massaged version of {@link CGroup} suitable for being shown in the UI. + * + * While cgroups are synced with remote, they do not directly correspond to + * "people" (this is ignoring the other issue that the cluster groups may be for + * non-human faces too). CGroups represent both positive and negative feedback, + * and the negations are specifically meant so that they're not shown in the UI. + * + * So while each Person has an underlying cgroups, not all cgroups have a + * corresponding Person. + * + * Beyond this, a {@link Person} object has data converted into a format that + * the UI can use directly and efficiently (as compared to a {@link CGroup}, + * which is tailored for transmission and storage). + */ +export interface Person { + /** Unique ID (nanoid) of the underlying {@link CGroup}. */ + id: string; + /** If this is a named person, then their name. */ + name?: string; + /** The files in which this face occurs. */ + files: number[]; + /** + * The face that should be used as the "cover" face to represent this {@link + * Person} in the UI. + */ + displayFaceID: string; + /** + * The {@link EnteFile} which contains the display face. + */ + displayFaceFile: EnteFile; +} + /** * Use CLIP to perform a natural language search over image embeddings. * diff --git a/web/packages/new/photos/services/search/types.ts b/web/packages/new/photos/services/search/types.ts index c386684d85..c22e462282 100644 --- a/web/packages/new/photos/services/search/types.ts +++ b/web/packages/new/photos/services/search/types.ts @@ -6,6 +6,7 @@ import type { Location } from "@/base/types"; import type { Collection } from "@/media/collection"; import { FileType } from "@/media/file-type"; +import type { Person } from "@/new/photos/services/ml"; import type { EnteFile } from "@/new/photos/types/file"; import type { LocationTag } from "../user-entity"; @@ -24,7 +25,7 @@ export type SearchSuggestion = { label: string } & ( | { type: "location"; locationTag: LocationTag } | { type: "city"; city: City } | { type: "clip"; clipScoreForFileID: Map } - | { type: "cgroup"; cgroup: SearchPerson } + | { type: "person"; person: Person } ); /** @@ -117,18 +118,6 @@ export interface SearchDateComponents { hour?: number; } -/** - * A massaged version of {@link CGroup} suitable for being shown in search - * results. - */ -export interface SearchPerson { - id: string; - name?: string; - files: number[]; - displayFaceID: string; - displayFaceFile: EnteFile; -} - /** * A city as identified by a static dataset. * From bc0d6adfd1bb194177c9287b29e89a67333b03ec Mon Sep 17 00:00:00 2001 From: ashilkn Date: Thu, 12 Sep 2024 18:14:16 +0530 Subject: [PATCH 40/45] [mob][auth] Bump up auth to v3.1.4 --- auth/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auth/pubspec.yaml b/auth/pubspec.yaml index 3b49002a09..9cfad4f7f5 100644 --- a/auth/pubspec.yaml +++ b/auth/pubspec.yaml @@ -1,6 +1,6 @@ name: ente_auth description: ente two-factor authenticator -version: 3.1.3+323 +version: 3.1.4+324 publish_to: none environment: From 44d66da742c58e79c7345899c80fa20eeedb140c Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 18:20:19 +0530 Subject: [PATCH 41/45] More state --- web/packages/new/photos/services/ml/index.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/web/packages/new/photos/services/ml/index.ts b/web/packages/new/photos/services/ml/index.ts index 673803a6dd..e0842fa24d 100644 --- a/web/packages/new/photos/services/ml/index.ts +++ b/web/packages/new/photos/services/ml/index.ts @@ -59,19 +59,31 @@ class MLState { comlinkWorker: Promise> | undefined; /** - * Subscriptions to {@link MLStatus}. + * Subscriptions to {@link MLStatus} updates. * * See {@link mlStatusSubscribe}. */ mlStatusListeners: (() => void)[] = []; /** - * Snapshot of {@link MLStatus}. - * - * See {@link mlStatusSnapshot}. + * Snapshot of the {@link MLStatus} returned by the {@link mlStatusSnapshot} + * function. */ mlStatusSnapshot: MLStatus | undefined; + /** + * Subscriptions to updates to the list of {@link Person}s we know about. + * + * See {@link peopleSubscribe}. + */ + peopleListeners: (() => void)[] = []; + + /** + * Snapshot of the {@link Person}s returned by the {@link peopleSnapshot} + * function. + */ + peopleSnapshot: Person[] | undefined; + /** * In flight face crop regeneration promises indexed by the IDs of the files * whose faces we are regenerating. From 8cd43e9e4b9c8729c78aa08516d924c7a7a0e9d7 Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 18:31:25 +0530 Subject: [PATCH 42/45] Dup --- web/packages/new/photos/services/ml/index.ts | 67 +++++++++++++++++++- 1 file changed, 65 insertions(+), 2 deletions(-) diff --git a/web/packages/new/photos/services/ml/index.ts b/web/packages/new/photos/services/ml/index.ts index e0842fa24d..65df9b12c9 100644 --- a/web/packages/new/photos/services/ml/index.ts +++ b/web/packages/new/photos/services/ml/index.ts @@ -13,7 +13,7 @@ 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 { throttled } from "@/utils/promise"; +import { throttled, wait } from "@/utils/promise"; import { proxy, transfer } from "comlink"; import { isInternalUser } from "../feature-flags"; import { getRemoteFlag, updateRemoteFlag } from "../remote-store"; @@ -526,7 +526,7 @@ export const mlStatusSnapshot = (): MLStatus | undefined => { */ const triggerStatusUpdate = () => void updateMLStatusSnapshot(); -/** Unconditionally update of the {@link MLStatus} snapshot. */ +/** Unconditional update of the {@link MLStatus} snapshot. */ const updateMLStatusSnapshot = async () => setMLStatusSnapshot(await getMLStatus()); @@ -626,6 +626,69 @@ export interface Person { displayFaceFile: EnteFile; } +/** + * A function that can be used to subscribe to updates to {@link Person}s. + * + * This, along with {@link peopleSnapshot}, is meant to be used as arguments to + * React's {@link useSyncExternalStore}. + * + * @param callback A function that will be invoked whenever the result of + * {@link peopleSnapshot} changes. + * + * @returns A function that can be used to clear the subscription. + */ +export const peopleSubscribe = (onChange: () => void): (() => void) => { + _state.peopleListeners.push(onChange); + return () => { + _state.peopleListeners = _state.peopleListeners.filter( + (l) => l != onChange, + ); + }; +}; + +/** + * Return the last known, cached {@link people}. + * + * This, along with {@link peopleSnapshot}, is meant to be used as arguments to + * React's {@link useSyncExternalStore}. + * + * A return value of `undefined` indicates that we're either still loading the + * initial list of people, or that the user has ML disabled and thus doesn't + * have any people (this is distinct from the case where the user has ML enabled + * but doesn't have any named "person" clusters so far). + */ +export const peopleSnapshot = (): Person[] | undefined => { + const result = _state.peopleSnapshot; + // We don't have it yet, trigger an update. + if (!result) triggerPeopleUpdate(); + return result; +}; + +/** + * Trigger an asynchronous and unconditional update of the people snapshot. + */ +const triggerPeopleUpdate = () => void updatePeopleSnapshot(); + +/** Unconditional update of the people snapshot. */ +const updatePeopleSnapshot = async () => setPeopleSnapshot(await getPeople()); + +const setPeopleSnapshot = (snapshot: Person[]) => { + _state.peopleSnapshot = snapshot; + _state.peopleListeners.forEach((l) => l()); +}; + +/** + * Compute the list of people. + * + * TODO-Cluster this is a placeholder function and might not be needed since + * people might be updated in a push based manner. + */ +const getPeople = async (): Promise => { + if (!_state.isMLEnabled) return undefined; + await wait(0); + return _wip_people; +}; + /** * Use CLIP to perform a natural language search over image embeddings. * From 9400f2e134ae9e9f9a2f8c76d28453d3e263132c Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 18:37:47 +0530 Subject: [PATCH 43/45] Integrate --- web/packages/new/photos/services/ml/index.ts | 32 +++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/web/packages/new/photos/services/ml/index.ts b/web/packages/new/photos/services/ml/index.ts index 65df9b12c9..a36bdc02a1 100644 --- a/web/packages/new/photos/services/ml/index.ts +++ b/web/packages/new/photos/services/ml/index.ts @@ -13,11 +13,10 @@ 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 { throttled, wait } from "@/utils/promise"; +import { throttled } from "@/utils/promise"; import { proxy, transfer } from "comlink"; import { isInternalUser } from "../feature-flags"; import { getRemoteFlag, updateRemoteFlag } from "../remote-store"; -import type { Person } from "../search/types"; import type { UploadItem } from "../upload/types"; import { type ClusterFace, @@ -341,7 +340,7 @@ export const wipClusterEnable = async (): Promise => // // TODO-Cluster temporary state here let _wip_isClustering = false; -let _wip_Persons: Person[] | undefined; +let _wip_people: Person[] | undefined; let _wip_hasSwitchedOnce = false; export const wipHasSwitchedOnceCmpAndSet = () => { @@ -350,11 +349,6 @@ export const wipHasSwitchedOnceCmpAndSet = () => { return false; }; -export const wipPersons = async () => { - if (!(await wipClusterEnable())) return []; - return _wip_Persons ?? []; -}; - export interface ClusterPreviewWithFile { clusterSize: number; faces: ClusterPreviewFaceWithFile[]; @@ -386,7 +380,7 @@ export const wipClusterDebugPageContents = async ( log.info("clustering", opts); _wip_isClustering = true; - _wip_Persons = undefined; + _wip_people = undefined; triggerStatusUpdate(); const { @@ -419,7 +413,7 @@ export const wipClusterDebugPageContents = async ( const clusterByID = new Map(clusters.map((c) => [c.id, c])); - const Persons = cgroups + const people = cgroups .map((cgroup) => { const faceID = ensure(cgroup.displayFaceID); const fileID = ensure(fileIDFromFaceID(faceID)); @@ -444,7 +438,7 @@ export const wipClusterDebugPageContents = async ( .sort((a, b) => b.faceIDs.length - a.faceIDs.length); _wip_isClustering = false; - _wip_Persons = Persons; + _wip_people = people; triggerStatusUpdate(); return { @@ -601,8 +595,8 @@ const workerDidProcessFileOrIdle = throttled(updateMLStatusSnapshot, 2000); * non-human faces too). CGroups represent both positive and negative feedback, * and the negations are specifically meant so that they're not shown in the UI. * - * So while each Person has an underlying cgroups, not all cgroups have a - * corresponding Person. + * So while each person has an underlying cgroups, not all cgroups have a + * corresponding person. * * Beyond this, a {@link Person} object has data converted into a format that * the UI can use directly and efficiently (as compared to a {@link CGroup}, @@ -616,8 +610,8 @@ export interface Person { /** The files in which this face occurs. */ files: number[]; /** - * The face that should be used as the "cover" face to represent this {@link - * Person} in the UI. + * The face that should be used as the "cover" face to represent this + * {@link Person} in the UI. */ displayFaceID: string; /** @@ -672,7 +666,7 @@ const triggerPeopleUpdate = () => void updatePeopleSnapshot(); /** Unconditional update of the people snapshot. */ const updatePeopleSnapshot = async () => setPeopleSnapshot(await getPeople()); -const setPeopleSnapshot = (snapshot: Person[]) => { +const setPeopleSnapshot = (snapshot: Person[] | undefined) => { _state.peopleSnapshot = snapshot; _state.peopleListeners.forEach((l) => l()); }; @@ -683,9 +677,11 @@ const setPeopleSnapshot = (snapshot: Person[]) => { * TODO-Cluster this is a placeholder function and might not be needed since * people might be updated in a push based manner. */ -const getPeople = async (): Promise => { +const getPeople = async (): Promise => { if (!_state.isMLEnabled) return undefined; - await wait(0); + // TODO-Cluster additional check for now as it is heavily WIP. + if (!process.env.NEXT_PUBLIC_ENTE_WIP_CL) return undefined; + if (!(await wipClusterEnable())) return []; return _wip_people; }; From c8c3d8f81417b73465a0e2beb9fe2522e436d8fb Mon Sep 17 00:00:00 2001 From: Manav Rathi Date: Thu, 12 Sep 2024 18:48:16 +0530 Subject: [PATCH 44/45] Prep --- web/packages/new/photos/components/SearchBar.tsx | 5 ++++- web/packages/new/photos/services/search/worker.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/web/packages/new/photos/components/SearchBar.tsx b/web/packages/new/photos/components/SearchBar.tsx index cfbd6e15d4..276dff408b 100644 --- a/web/packages/new/photos/components/SearchBar.tsx +++ b/web/packages/new/photos/components/SearchBar.tsx @@ -383,6 +383,9 @@ const EmptyState: React.FC = () => { break; } + // TODO-Cluster this is where it'll go. + // const people = wipPersons(); + return ( @@ -547,7 +550,7 @@ const labelForOption = (option: SearchOption) => { case "clip": return t("magic"); - case "cgroup": + case "person": return t("person"); } }; diff --git a/web/packages/new/photos/services/search/worker.ts b/web/packages/new/photos/services/search/worker.ts index f1b9c61302..24f351e058 100644 --- a/web/packages/new/photos/services/search/worker.ts +++ b/web/packages/new/photos/services/search/worker.ts @@ -385,7 +385,7 @@ const isMatchingFile = (file: EnteFile, suggestion: SearchSuggestion) => { case "clip": return suggestion.clipScoreForFileID.has(file.id); - case "cgroup": + case "person": // return query.person.files.includes(file.id); // TODO-Cluster implement me return false; From 75c3bc1c842848583e744c6124644e4ae453bdc3 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 13 Sep 2024 09:59:15 +0530 Subject: [PATCH 45/45] [auth] Fix: uriEncdoe issuer name --- auth/lib/models/code.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/auth/lib/models/code.dart b/auth/lib/models/code.dart index 5f7cc0f135..9553167daa 100644 --- a/auth/lib/models/code.dart +++ b/auth/lib/models/code.dart @@ -81,6 +81,7 @@ class Code { final Type updatedType = type ?? this.type; final int updatedCounter = counter ?? this.counter; final CodeDisplay updatedDisplay = display ?? this.display; + final String encodedIssuer = Uri.encodeQueryComponent(updateIssuer); return Code( updateAccount, @@ -92,7 +93,7 @@ class Code { updatedType, updatedCounter, "otpauth://${updatedType.name}/$updateIssuer:$updateAccount?algorithm=${updatedAlgo.name}" - "&digits=$updatedDigits&issuer=$updateIssuer" + "&digits=$updatedDigits&issuer=$encodedIssuer" "&period=$updatePeriod&secret=$updatedSecret${updatedType == Type.hotp ? "&counter=$updatedCounter" : ""}", generatedID: generatedID, display: updatedDisplay, @@ -107,6 +108,7 @@ class Code { CodeDisplay? display, int digits, ) { + final String encodedIssuer = Uri.encodeQueryComponent(issuer); return Code( account, issuer, @@ -116,7 +118,7 @@ class Code { Algorithm.sha1, type, 0, - "otpauth://${type.name}/$issuer:$account?algorithm=SHA1&digits=$digits&issuer=$issuer&period=30&secret=$secret", + "otpauth://${type.name}/$issuer:$account?algorithm=SHA1&digits=$digits&issuer=$encodedIssuer&period=30&secret=$secret", display: display ?? CodeDisplay(), ); }