diff --git a/web/apps/photos/src/components/Search/SearchBar/index.tsx b/web/apps/photos/src/components/Search/SearchBar/index.tsx
deleted file mode 100644
index fa8929e4e6..0000000000
--- a/web/apps/photos/src/components/Search/SearchBar/index.tsx
+++ /dev/null
@@ -1,37 +0,0 @@
-import { Collection } from "types/collection";
-import { SearchBarMobile } from "./searchBarMobile";
-
-import { UpdateSearch } from "@/new/photos/services/search/types";
-import { EnteFile } from "@/new/photos/types/file";
-import SearchInput from "./searchInput";
-import { SearchBarWrapper } from "./styledComponents";
-
-interface Props {
- updateSearch: UpdateSearch;
- collections: Collection[];
- files: EnteFile[];
- isInSearchMode: boolean;
- setIsInSearchMode: (v: boolean) => void;
-}
-
-export default function SearchBar({
- setIsInSearchMode,
- isInSearchMode,
- ...props
-}: Props) {
- const showSearchInput = () => setIsInSearchMode(true);
-
- return (
-
-
-
-
- );
-}
diff --git a/web/apps/photos/src/components/Search/SearchBar/searchBarMobile.tsx b/web/apps/photos/src/components/Search/SearchBar/searchBarMobile.tsx
deleted file mode 100644
index 466a5ef79e..0000000000
--- a/web/apps/photos/src/components/Search/SearchBar/searchBarMobile.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { FluidContainer } from "@ente/shared/components/Container";
-import SearchIcon from "@mui/icons-material/Search";
-import { IconButton } from "@mui/material";
-import { SearchMobileBox } from "./styledComponents";
-
-export function SearchBarMobile({ show, showSearchInput }) {
- if (!show) {
- return <>>;
- }
- return (
-
-
-
-
-
-
-
- );
-}
diff --git a/web/apps/photos/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx b/web/apps/photos/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx
deleted file mode 100644
index 72ed38de99..0000000000
--- a/web/apps/photos/src/components/Search/SearchBar/searchInput/MenuWithPeople.tsx
+++ /dev/null
@@ -1,72 +0,0 @@
-import { PeopleList } from "@/new/photos/components/PeopleList";
-import { isMLEnabled } from "@/new/photos/services/ml";
-import { Suggestion, SuggestionType } from "@/new/photos/services/search/types";
-import { Row } from "@ente/shared/components/Container";
-import { Box, styled } from "@mui/material";
-import { t } from "i18next";
-import { components } from "react-select";
-
-const { Menu } = components;
-
-const Legend = styled("span")`
- font-size: 20px;
- color: #ddd;
- display: inline;
- padding: 0px 12px;
-`;
-
-const Caption = styled("span")`
- font-size: 12px;
- display: inline;
- padding: 0px 12px;
-`;
-
-const MenuWithPeople = (props) => {
- // log.info("props.selectProps.options: ", selectRef);
- const peopleSuggestions = props.selectProps.options.filter(
- (o) => o.type === SuggestionType.PERSON,
- );
- const people = peopleSuggestions.map((o) => o.value);
-
- const indexStatusSuggestion = props.selectProps.options.filter(
- (o) => o.type === SuggestionType.INDEX_STATUS,
- )[0] as Suggestion;
-
- const indexStatus = indexStatusSuggestion?.value;
- return (
-
- );
-};
-
-export default MenuWithPeople;
diff --git a/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx b/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx
deleted file mode 100644
index 7ce05576f5..0000000000
--- a/web/apps/photos/src/components/Search/SearchBar/searchInput/index.tsx
+++ /dev/null
@@ -1,217 +0,0 @@
-import { FileType } from "@/media/file-type";
-import { isMLEnabled } from "@/new/photos/services/ml";
-import type {
- City,
- SearchDateComponents,
- SearchPerson,
-} from "@/new/photos/services/search/types";
-import {
- ClipSearchScores,
- SearchOption,
- SearchQuery,
- SuggestionType,
- UpdateSearch,
-} from "@/new/photos/services/search/types";
-import type { LocationTag } from "@/new/photos/services/user-entity";
-import { EnteFile } from "@/new/photos/types/file";
-import CloseIcon from "@mui/icons-material/Close";
-import { IconButton } from "@mui/material";
-import { t } from "i18next";
-import memoize from "memoize-one";
-import pDebounce from "p-debounce";
-import { AppContext } from "pages/_app";
-import { useCallback, useContext, useEffect, useRef, useState } from "react";
-import { components } from "react-select";
-import AsyncSelect from "react-select/async";
-import { InputActionMeta } from "react-select/src/types";
-import {
- getAutoCompleteSuggestions,
- getDefaultOptions,
-} from "services/searchService";
-import { Collection } from "types/collection";
-import { SelectStyles } from "../../../../styles/search";
-import { SearchInputWrapper } from "../styledComponents";
-import MenuWithPeople from "./MenuWithPeople";
-import { OptionWithInfo } from "./optionWithInfo";
-import { ValueContainerWithIcon } from "./valueContainerWithIcon";
-
-interface Iprops {
- isOpen: boolean;
- updateSearch: UpdateSearch;
- setIsOpen: (value: boolean) => void;
- files: EnteFile[];
- collections: Collection[];
-}
-
-const createComponents = memoize((Option, ValueContainer, Menu, Input) => ({
- Option,
- ValueContainer,
- Menu,
- Input,
-}));
-
-const VisibleInput = (props) => (
-
-);
-
-export default function SearchInput(props: Iprops) {
- const selectRef = useRef(null);
- const [value, setValue] = useState(null);
- const appContext = useContext(AppContext);
- const handleChange = (value: SearchOption) => {
- setValue(value);
- setQuery(value?.label);
-
- blur();
- };
- const handleInputChange = (value: string, actionMeta: InputActionMeta) => {
- if (actionMeta.action === "input-change") {
- setQuery(value);
- }
- };
- const [defaultOptions, setDefaultOptions] = useState([]);
- const [query, setQuery] = useState("");
-
- useEffect(() => {
- search(value);
- }, [value]);
-
- useEffect(() => {
- refreshDefaultOptions();
- const t = setInterval(() => refreshDefaultOptions(), 2000);
- return () => clearInterval(t);
- }, []);
-
- async function refreshDefaultOptions() {
- const defaultOptions = await getDefaultOptions();
- setDefaultOptions(defaultOptions);
- }
-
- const resetSearch = () => {
- if (props.isOpen) {
- appContext.startLoading();
- props.updateSearch(null, null);
- setTimeout(() => {
- appContext.finishLoading();
- }, 10);
- props.setIsOpen(false);
- setValue(null);
- setQuery("");
- }
- };
-
- const getOptions = useCallback(
- pDebounce(
- getAutoCompleteSuggestions(props.files, props.collections),
- 250,
- ),
- [props.files, props.collections],
- );
-
- const blur = () => {
- selectRef.current?.blur();
- };
-
- const search = (selectedOption: SearchOption) => {
- if (!selectedOption) {
- return;
- }
- let search: SearchQuery;
- switch (selectedOption.type) {
- case SuggestionType.DATE:
- search = {
- date: selectedOption.value as SearchDateComponents,
- };
- props.setIsOpen(true);
- break;
- case SuggestionType.LOCATION:
- search = {
- location: selectedOption.value as LocationTag,
- };
- props.setIsOpen(true);
- break;
- case SuggestionType.CITY:
- search = {
- city: selectedOption.value as City,
- };
- props.setIsOpen(true);
- break;
- case SuggestionType.COLLECTION:
- search = { collection: selectedOption.value as number };
- setValue(null);
- setQuery("");
- 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 };
- }
- props.updateSearch(search, {
- optionName: selectedOption.label,
- fileCount: selectedOption.fileCount,
- });
- };
-
- // TODO: HACK as AsyncSelect does not support default options reloading on focus/click
- // unwanted side effect: placeholder is not shown on focus/click
- // https://github.com/JedWatson/react-select/issues/1879
- // for correct fix AsyncSelect can be extended to support default options reloading on focus/click
- const handleOnFocus = () => {
- refreshDefaultOptions();
- };
-
- const MemoizedMenuWithPeople = useCallback(
- (props) => (
-
- ),
- [setValue, selectRef],
- );
-
- const components = createComponents(
- OptionWithInfo,
- ValueContainerWithIcon,
- MemoizedMenuWithPeople,
- VisibleInput,
- );
-
- return (
-
- {t("search_hint")}}
- loadOptions={getOptions}
- onChange={handleChange}
- onFocus={handleOnFocus}
- isClearable
- inputValue={query}
- onInputChange={handleInputChange}
- escapeClearsValue
- styles={SelectStyles}
- defaultOptions={isMLEnabled() ? defaultOptions : null}
- noOptionsMessage={() => null}
- />
-
- {props.isOpen && (
- resetSearch()} sx={{ ml: 1 }}>
-
-
- )}
-
- );
-}
diff --git a/web/apps/photos/src/components/Search/SearchBar/searchInput/optionWithInfo.tsx b/web/apps/photos/src/components/Search/SearchBar/searchInput/optionWithInfo.tsx
deleted file mode 100644
index 2957e77318..0000000000
--- a/web/apps/photos/src/components/Search/SearchBar/searchInput/optionWithInfo.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { SearchOption } from "@/new/photos/services/search/types";
-import { labelForSuggestionType } from "@/new/photos/services/search/ui";
-import {
- FreeFlowText,
- SpaceBetweenFlex,
-} from "@ente/shared/components/Container";
-import { Box, Divider, Stack, Typography } from "@mui/material";
-import CollectionCard from "components/Collections/CollectionCard";
-import { ResultPreviewTile } from "components/Collections/styledComponents";
-import { t } from "i18next";
-
-import { components } from "react-select";
-
-const { Option } = components;
-
-export const OptionWithInfo = (props) => (
-
-);
-
-const LabelWithInfo = ({ data }: { data: SearchOption }) => {
- return (
- !data.hide && (
- <>
-
-
- {labelForSuggestionType(data.type)}
-
-
-
-
-
- {data.label}
-
-
-
- {t("photos_count", { count: data.fileCount })}
-
-
-
-
- {data.previewFiles.map((file) => (
- null}
- collectionTile={ResultPreviewTile}
- />
- ))}
-
-
-
-
- >
- )
- );
-};
diff --git a/web/apps/photos/src/components/Search/SearchBar/searchInput/valueContainerWithIcon.tsx b/web/apps/photos/src/components/Search/SearchBar/searchInput/valueContainerWithIcon.tsx
deleted file mode 100644
index 75529a925f..0000000000
--- a/web/apps/photos/src/components/Search/SearchBar/searchInput/valueContainerWithIcon.tsx
+++ /dev/null
@@ -1,49 +0,0 @@
-import {
- SearchOption,
- SuggestionType,
-} from "@/new/photos/services/search/types";
-import { FlexWrapper } from "@ente/shared/components/Container";
-import CalendarIcon from "@mui/icons-material/CalendarMonth";
-import FolderIcon from "@mui/icons-material/Folder";
-import ImageIcon from "@mui/icons-material/Image";
-import LocationIcon from "@mui/icons-material/LocationOn";
-import SearchIcon from "@mui/icons-material/SearchOutlined";
-import { Box } from "@mui/material";
-import { components } from "react-select";
-import { SelectComponents } from "react-select/src/components";
-
-const { ValueContainer } = components;
-
-const getIconByType = (type: SuggestionType) => {
- switch (type) {
- case SuggestionType.DATE:
- return ;
- case SuggestionType.LOCATION:
- case SuggestionType.CITY:
- return ;
- case SuggestionType.COLLECTION:
- return ;
- case SuggestionType.FILE_NAME:
- return ;
- default:
- return ;
- }
-};
-
-export const ValueContainerWithIcon: SelectComponents<
- SearchOption,
- false
->["ValueContainer"] = (props) => (
-
-
- theme.colors.stroke.muted}
- >
- {getIconByType(props.getValue()[0]?.type)}
-
- {props.children}
-
-
-);
diff --git a/web/apps/photos/src/components/Search/SearchBar/styledComponents.tsx b/web/apps/photos/src/components/Search/SearchBar/styledComponents.tsx
deleted file mode 100644
index d33c7c9490..0000000000
--- a/web/apps/photos/src/components/Search/SearchBar/styledComponents.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import {
- CenteredFlex,
- FlexWrapper,
- FluidContainer,
-} from "@ente/shared/components/Container";
-import { css, styled } from "@mui/material";
-import { IMAGE_CONTAINER_MAX_WIDTH, MIN_COLUMNS } from "constants/gallery";
-
-export const SearchBarWrapper = styled(FlexWrapper)`
- padding: 0 24px;
- @media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * MIN_COLUMNS}px) {
- padding: 0 4px;
- }
-`;
-
-export const SearchMobileBox = styled(FluidContainer)`
- display: flex;
- cursor: pointer;
- align-items: center;
- justify-content: flex-end;
- @media (min-width: 625px) {
- display: none;
- }
-`;
-
-export const SearchInputWrapper = styled(CenteredFlex, {
- shouldForwardProp: (propName) => propName != "isOpen",
-})<{ isOpen: boolean }>`
- background: ${({ theme }) => theme.colors.background.base};
- max-width: 484px;
- margin: auto;
- ${(props) =>
- !props.isOpen &&
- css`
- @media (max-width: 624px) {
- display: none;
- }
- `}
-`;
diff --git a/web/apps/photos/src/components/Search/SearchResultInfo.tsx b/web/apps/photos/src/components/Search/SearchResultInfo.tsx
deleted file mode 100644
index f087bf1456..0000000000
--- a/web/apps/photos/src/components/Search/SearchResultInfo.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-import { SearchResultSummary } from "@/new/photos/services/search/types";
-import { Typography } from "@mui/material";
-import { CollectionInfo } from "components/Collections/CollectionInfo";
-import { CollectionInfoBarWrapper } from "components/Collections/styledComponents";
-import { t } from "i18next";
-
-interface Iprops {
- searchResultSummary: SearchResultSummary;
-}
-export default function SearchResultInfo({ searchResultSummary }: Iprops) {
- if (!searchResultSummary) {
- return <>>;
- }
-
- const { optionName, fileCount } = searchResultSummary;
-
- return (
-
-
- {t("search_results")}
-
-
-
- );
-}
diff --git a/web/apps/photos/src/components/Search/SearchStatsContainer.tsx b/web/apps/photos/src/components/Search/SearchStatsContainer.tsx
deleted file mode 100644
index 1e088b58f0..0000000000
--- a/web/apps/photos/src/components/Search/SearchStatsContainer.tsx
+++ /dev/null
@@ -1,12 +0,0 @@
-import { styled } from "@mui/material";
-const SearchStatsContainer = styled("div")(
- ({ theme }) => `
- display: flex;
- justify-content: center;
- align-items: center;
- color: #979797;
- margin: ${theme.spacing(1, 0)};
-`,
-);
-
-export default SearchStatsContainer;
diff --git a/web/apps/photos/src/components/SearchBar.tsx b/web/apps/photos/src/components/SearchBar.tsx
new file mode 100644
index 0000000000..218375cac8
--- /dev/null
+++ b/web/apps/photos/src/components/SearchBar.tsx
@@ -0,0 +1,459 @@
+import { FileType } from "@/media/file-type";
+import { PeopleList } from "@/new/photos/components/PeopleList";
+import { isMLEnabled } from "@/new/photos/services/ml";
+import type {
+ City,
+ SearchDateComponents,
+ SearchPerson,
+} from "@/new/photos/services/search/types";
+import {
+ ClipSearchScores,
+ SearchOption,
+ SearchQuery,
+ Suggestion,
+ SuggestionType,
+ UpdateSearch,
+} from "@/new/photos/services/search/types";
+import { labelForSuggestionType } from "@/new/photos/services/search/ui";
+import type { LocationTag } from "@/new/photos/services/user-entity";
+import { EnteFile } from "@/new/photos/types/file";
+import {
+ CenteredFlex,
+ FlexWrapper,
+ FluidContainer,
+ FreeFlowText,
+ Row,
+ SpaceBetweenFlex,
+} from "@ente/shared/components/Container";
+import CalendarIcon from "@mui/icons-material/CalendarMonth";
+import CloseIcon from "@mui/icons-material/Close";
+import FolderIcon from "@mui/icons-material/Folder";
+import ImageIcon from "@mui/icons-material/Image";
+import LocationIcon from "@mui/icons-material/LocationOn";
+import SearchIcon from "@mui/icons-material/Search";
+import {
+ Box,
+ css,
+ Divider,
+ IconButton,
+ Stack,
+ styled,
+ Typography,
+} from "@mui/material";
+import CollectionCard from "components/Collections/CollectionCard";
+import { ResultPreviewTile } from "components/Collections/styledComponents";
+import { IMAGE_CONTAINER_MAX_WIDTH, MIN_COLUMNS } from "constants/gallery";
+import { t } from "i18next";
+import memoize from "memoize-one";
+import pDebounce from "p-debounce";
+import { AppContext } from "pages/_app";
+import { useCallback, useContext, useEffect, useRef, useState } from "react";
+import { components } from "react-select";
+import AsyncSelect from "react-select/async";
+import { SelectComponents } from "react-select/src/components";
+import { InputActionMeta } from "react-select/src/types";
+import {
+ getAutoCompleteSuggestions,
+ getDefaultOptions,
+} from "services/searchService";
+import { Collection } from "types/collection";
+import { SelectStyles } from "../styles/search";
+
+const { Option, ValueContainer, Menu } = components;
+
+interface SearchBarProps {
+ updateSearch: UpdateSearch;
+ collections: Collection[];
+ files: EnteFile[];
+ isInSearchMode: boolean;
+ setIsInSearchMode: (v: boolean) => void;
+}
+
+export default function SearchBar({
+ setIsInSearchMode,
+ isInSearchMode,
+ ...props
+}: SearchBarProps) {
+ const showSearchInput = () => setIsInSearchMode(true);
+
+ return (
+
+
+
+
+ );
+}
+
+const SearchBarWrapper = styled(FlexWrapper)`
+ padding: 0 24px;
+ @media (max-width: ${IMAGE_CONTAINER_MAX_WIDTH * MIN_COLUMNS}px) {
+ padding: 0 4px;
+ }
+`;
+
+interface SearchInputProps {
+ isOpen: boolean;
+ updateSearch: UpdateSearch;
+ setIsOpen: (value: boolean) => void;
+ files: EnteFile[];
+ collections: Collection[];
+}
+
+const createComponents = memoize((Option, ValueContainer, Menu, Input) => ({
+ Option,
+ ValueContainer,
+ Menu,
+ Input,
+}));
+
+export const SearchInput: React.FC = (props) => {
+ const selectRef = useRef(null);
+ const [value, setValue] = useState(null);
+ const appContext = useContext(AppContext);
+ const handleChange = (value: SearchOption) => {
+ setValue(value);
+ setQuery(value?.label);
+
+ blur();
+ };
+ const handleInputChange = (value: string, actionMeta: InputActionMeta) => {
+ if (actionMeta.action === "input-change") {
+ setQuery(value);
+ }
+ };
+ const [defaultOptions, setDefaultOptions] = useState([]);
+ const [query, setQuery] = useState("");
+
+ useEffect(() => {
+ search(value);
+ }, [value]);
+
+ useEffect(() => {
+ refreshDefaultOptions();
+ const t = setInterval(() => refreshDefaultOptions(), 2000);
+ return () => clearInterval(t);
+ }, []);
+
+ async function refreshDefaultOptions() {
+ const defaultOptions = await getDefaultOptions();
+ setDefaultOptions(defaultOptions);
+ }
+
+ const resetSearch = () => {
+ if (props.isOpen) {
+ appContext.startLoading();
+ props.updateSearch(null, null);
+ setTimeout(() => {
+ appContext.finishLoading();
+ }, 10);
+ props.setIsOpen(false);
+ setValue(null);
+ setQuery("");
+ }
+ };
+
+ const getOptions = useCallback(
+ pDebounce(
+ getAutoCompleteSuggestions(props.files, props.collections),
+ 250,
+ ),
+ [props.files, props.collections],
+ );
+
+ const blur = () => {
+ selectRef.current?.blur();
+ };
+
+ const search = (selectedOption: SearchOption) => {
+ if (!selectedOption) {
+ return;
+ }
+ let search: SearchQuery;
+ switch (selectedOption.type) {
+ case SuggestionType.DATE:
+ search = {
+ date: selectedOption.value as SearchDateComponents,
+ };
+ props.setIsOpen(true);
+ break;
+ case SuggestionType.LOCATION:
+ search = {
+ location: selectedOption.value as LocationTag,
+ };
+ props.setIsOpen(true);
+ break;
+ case SuggestionType.CITY:
+ search = {
+ city: selectedOption.value as City,
+ };
+ props.setIsOpen(true);
+ break;
+ case SuggestionType.COLLECTION:
+ search = { collection: selectedOption.value as number };
+ setValue(null);
+ setQuery("");
+ 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 };
+ }
+ props.updateSearch(search, {
+ optionName: selectedOption.label,
+ fileCount: selectedOption.fileCount,
+ });
+ };
+
+ // TODO: HACK as AsyncSelect does not support default options reloading on focus/click
+ // unwanted side effect: placeholder is not shown on focus/click
+ // https://github.com/JedWatson/react-select/issues/1879
+ // for correct fix AsyncSelect can be extended to support default options reloading on focus/click
+ const handleOnFocus = () => {
+ refreshDefaultOptions();
+ };
+
+ const MemoizedMenuWithPeople = useCallback(
+ (props) => (
+
+ ),
+ [setValue, selectRef],
+ );
+
+ const components = createComponents(
+ OptionWithInfo,
+ ValueContainerWithIcon,
+ MemoizedMenuWithPeople,
+ VisibleInput,
+ );
+
+ return (
+
+ {t("search_hint")}}
+ loadOptions={getOptions}
+ onChange={handleChange}
+ onFocus={handleOnFocus}
+ isClearable
+ inputValue={query}
+ onInputChange={handleInputChange}
+ escapeClearsValue
+ styles={SelectStyles}
+ defaultOptions={isMLEnabled() ? defaultOptions : null}
+ noOptionsMessage={() => null}
+ />
+
+ {props.isOpen && (
+ resetSearch()} sx={{ ml: 1 }}>
+
+
+ )}
+
+ );
+};
+
+const SearchInputWrapper = styled(CenteredFlex, {
+ shouldForwardProp: (propName) => propName != "isOpen",
+})<{ isOpen: boolean }>`
+ background: ${({ theme }) => theme.colors.background.base};
+ max-width: 484px;
+ margin: auto;
+ ${(props) =>
+ !props.isOpen &&
+ css`
+ @media (max-width: 624px) {
+ display: none;
+ }
+ `}
+`;
+
+const OptionWithInfo = (props) => (
+
+);
+
+const LabelWithInfo = ({ data }: { data: SearchOption }) => {
+ return (
+ !data.hide && (
+ <>
+
+
+ {labelForSuggestionType(data.type)}
+
+
+
+
+
+ {data.label}
+
+
+
+ {t("photos_count", { count: data.fileCount })}
+
+
+
+
+ {data.previewFiles.map((file) => (
+ null}
+ collectionTile={ResultPreviewTile}
+ />
+ ))}
+
+
+
+
+ >
+ )
+ );
+};
+
+const ValueContainerWithIcon: SelectComponents<
+ SearchOption,
+ false
+>["ValueContainer"] = (props) => (
+
+
+ theme.colors.stroke.muted}
+ >
+ {getIconByType(props.getValue()[0]?.type)}
+
+ {props.children}
+
+
+);
+
+const getIconByType = (type: SuggestionType) => {
+ switch (type) {
+ case SuggestionType.DATE:
+ return ;
+ case SuggestionType.LOCATION:
+ case SuggestionType.CITY:
+ return ;
+ case SuggestionType.COLLECTION:
+ return ;
+ case SuggestionType.FILE_NAME:
+ return ;
+ default:
+ return ;
+ }
+};
+
+export const MenuWithPeople = (props) => {
+ // log.info("props.selectProps.options: ", selectRef);
+ const peopleSuggestions = props.selectProps.options.filter(
+ (o) => o.type === SuggestionType.PERSON,
+ );
+ const people = peopleSuggestions.map((o) => o.value);
+
+ const indexStatusSuggestion = props.selectProps.options.filter(
+ (o) => o.type === SuggestionType.INDEX_STATUS,
+ )[0] as Suggestion;
+
+ const indexStatus = indexStatusSuggestion?.value;
+ return (
+
+ );
+};
+
+const Legend = styled("span")`
+ font-size: 20px;
+ color: #ddd;
+ display: inline;
+ padding: 0px 12px;
+`;
+
+const Caption = styled("span")`
+ font-size: 12px;
+ display: inline;
+ padding: 0px 12px;
+`;
+
+const VisibleInput = (props) => (
+
+);
+
+function SearchBarMobile({ show, showSearchInput }) {
+ if (!show) {
+ return <>>;
+ }
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+const SearchMobileBox = styled(FluidContainer)`
+ display: flex;
+ cursor: pointer;
+ align-items: center;
+ justify-content: flex-end;
+ @media (min-width: 625px) {
+ display: none;
+ }
+`;
diff --git a/web/apps/photos/src/components/pages/gallery/Navbar.tsx b/web/apps/photos/src/components/pages/gallery/Navbar.tsx
index dfb8a6339c..729a536351 100644
--- a/web/apps/photos/src/components/pages/gallery/Navbar.tsx
+++ b/web/apps/photos/src/components/pages/gallery/Navbar.tsx
@@ -5,7 +5,7 @@ import { FlexWrapper, HorizontalFlex } from "@ente/shared/components/Container";
import ArrowBack from "@mui/icons-material/ArrowBack";
import MenuIcon from "@mui/icons-material/Menu";
import { IconButton, Typography } from "@mui/material";
-import SearchBar from "components/Search/SearchBar";
+import SearchBar from "components/SearchBar";
import UploadButton from "components/Upload/UploadButton";
import { t } from "i18next";
import { Collection } from "types/collection";
diff --git a/web/apps/photos/src/pages/deduplicate/index.tsx b/web/apps/photos/src/pages/deduplicate.tsx
similarity index 100%
rename from web/apps/photos/src/pages/deduplicate/index.tsx
rename to web/apps/photos/src/pages/deduplicate.tsx
diff --git a/web/apps/photos/src/pages/gallery/index.tsx b/web/apps/photos/src/pages/gallery.tsx
similarity index 98%
rename from web/apps/photos/src/pages/gallery/index.tsx
rename to web/apps/photos/src/pages/gallery.tsx
index 8ab0eee90f..1c3b51671a 100644
--- a/web/apps/photos/src/pages/gallery/index.tsx
+++ b/web/apps/photos/src/pages/gallery.tsx
@@ -40,12 +40,14 @@ import type { User } from "@ente/shared/user/types";
import { Typography, styled } from "@mui/material";
import AuthenticateUserModal from "components/AuthenticateUserModal";
import Collections from "components/Collections";
+import { CollectionInfo } from "components/Collections/CollectionInfo";
import CollectionNamer, {
CollectionNamerAttributes,
} from "components/Collections/CollectionNamer";
import CollectionSelector, {
CollectionSelectorAttributes,
} from "components/Collections/CollectionSelector";
+import { CollectionInfoBarWrapper } from "components/Collections/styledComponents";
import ExportModal from "components/ExportModal";
import {
FilesDownloadProgress,
@@ -59,7 +61,6 @@ import GalleryEmptyState from "components/GalleryEmptyState";
import { LoadingOverlay } from "components/LoadingOverlay";
import PhotoFrame from "components/PhotoFrame";
import { ITEM_TYPE, TimeStampListItem } from "components/PhotoList";
-import SearchResultInfo from "components/Search/SearchResultInfo";
import Sidebar from "components/Sidebar";
import type { UploadTypeSelectorIntent } from "components/Upload/UploadTypeSelector";
import Uploader from "components/Upload/Uploader";
@@ -480,7 +481,7 @@ export default function Gallery() {
setPhotoListHeader({
height: 104,
item: (
-
),
@@ -1252,3 +1253,26 @@ const mergeMaps = (map1: Map, map2: Map) => {
});
return mergedMap;
};
+
+interface SearchResultSummaryHeaderProps {
+ searchResultSummary: SearchResultSummary;
+}
+
+const SearchResultSummaryHeader: React.FC = ({
+ searchResultSummary,
+}) => {
+ if (!searchResultSummary) {
+ return <>>;
+ }
+
+ const { optionName, fileCount } = searchResultSummary;
+
+ return (
+
+
+ {t("search_results")}
+
+
+
+ );
+};
diff --git a/web/apps/photos/src/pages/shared-albums/index.tsx b/web/apps/photos/src/pages/shared-albums.tsx
similarity index 100%
rename from web/apps/photos/src/pages/shared-albums/index.tsx
rename to web/apps/photos/src/pages/shared-albums.tsx
diff --git a/web/apps/photos/src/services/logout.ts b/web/apps/photos/src/services/logout.ts
index 8ebf30aec9..fa36e4f8fa 100644
--- a/web/apps/photos/src/services/logout.ts
+++ b/web/apps/photos/src/services/logout.ts
@@ -3,6 +3,7 @@ import log from "@/base/log";
import DownloadManager from "@/new/photos/services/download";
import { clearFeatureFlagSessionState } from "@/new/photos/services/feature-flags";
import { logoutML, terminateMLWorker } from "@/new/photos/services/ml";
+import { logoutSearch } from "@/new/photos/services/search";
import exportService from "./export";
/**
@@ -18,13 +19,13 @@ export const photosLogout = async () => {
// - Workers
- // Terminate any workers before clearing persistent state.
- // See: [Note: Caching IDB instances in separate execution contexts].
+ // Terminate any workers that might access the DB before clearing persistent
+ // state. See: [Note: Caching IDB instances in separate execution contexts].
try {
await terminateMLWorker();
} catch (e) {
- ignoreError("face", e);
+ ignoreError("ml/worker", e);
}
// - Remote logout and clear state
@@ -47,6 +48,12 @@ export const photosLogout = async () => {
ignoreError("download", e);
}
+ try {
+ logoutSearch();
+ } catch (e) {
+ ignoreError("search", e);
+ }
+
// - Desktop
const electron = globalThis.electron;
diff --git a/web/apps/photos/src/services/searchService.ts b/web/apps/photos/src/services/searchService.ts
index cd19fb0c9f..037af63839 100644
--- a/web/apps/photos/src/services/searchService.ts
+++ b/web/apps/photos/src/services/searchService.ts
@@ -47,8 +47,8 @@ export const getAutoCompleteSuggestions =
// - getClipSuggestion(searchPhrase)
// - getDateSuggestion(searchPhrase),
// - getLocationSuggestion(searchPhrase),
+ // - getFileTypeSuggestion(searchPhrase),
...(await createSearchQuery(searchPhrase)),
- ...getFileTypeSuggestion(searchPhrase2),
...getCollectionSuggestion(searchPhrase2, collections),
getFileNameSuggestion(searchPhrase2, files),
getFileCaptionSuggestion(searchPhrase2, files),
@@ -85,27 +85,6 @@ async function convertSuggestionsToOptions(
}
return previewImageAppendedOptions;
}
-function getFileTypeSuggestion(searchPhrase: string): Suggestion[] {
- return [
- {
- label: t("IMAGE"),
- value: FileType.image,
- type: SuggestionType.FILE_TYPE,
- },
- {
- label: t("VIDEO"),
- value: FileType.video,
- type: SuggestionType.FILE_TYPE,
- },
- {
- label: t("LIVE_PHOTO"),
- value: FileType.livePhoto,
- type: SuggestionType.FILE_TYPE,
- },
- ].filter((suggestion) =>
- suggestion.label.toLowerCase().includes(searchPhrase),
- );
-}
export async function getAllPeopleSuggestion(): Promise> {
try {
diff --git a/web/packages/new/photos/services/ml/cgroups.ts b/web/packages/new/photos/services/ml/cgroups.ts
index 49388eb990..77cdd0ee8f 100644
--- a/web/packages/new/photos/services/ml/cgroups.ts
+++ b/web/packages/new/photos/services/ml/cgroups.ts
@@ -1,3 +1,6 @@
+import { masterKeyFromSession } from "@/base/session-store";
+import { pullCGroups } from "../user-entity";
+
/**
* A cgroup ("cluster group") is a group of clusters (possibly containing a
* single cluster) that the user has interacted with.
@@ -18,11 +21,14 @@
* cluster, or they may hide an named {@link CGroup}. In both cases, we promote
* the cluster to a CGroup if needed so that their request to hide gets synced.
*
+ * cgroups are synced with remote.
+ *
* While in our local representation we separately maintain clusters and link to
* them from within CGroups by their clusterID, in the remote representation
- * clusters themselves don't get synced. Instead, the "cgroup" entities synced
- * with remote contain the clusters within themselves. So a group that gets
- * synced with remote looks something like:
+ * clusters themselves don't get synced. Instead, the cgroup entities synced
+ * with remote contain the clusters within themselves.
+ *
+ * That is, a cgroup that gets synced with remote looks something like:
*
* { id, name, clusters: [{ clusterID, faceIDs }] }
*
@@ -63,7 +69,7 @@ export interface CGroup {
isHidden: boolean;
/**
* The ID of the face that should be used as the cover photo for this
- * cluster group (if the user has set one).
+ * cluster group. Optional.
*
* This is similar to the [@link displayFaceID}, the difference being:
*
@@ -73,6 +79,13 @@ export interface CGroup {
* into effect if the user has not explicitly selected a face.
*/
avatarFaceID: string | undefined;
+}
+
+/**
+ * A {@link CGroup} annotated with various in-memory state to make it easier for
+ * the upper layers of our code to directly use it.
+ */
+export type AnnotatedCGroup = CGroup & {
/**
* Locally determined ID of the "best" face that should be used as the
* display face, to represent this cluster group in the UI.
@@ -81,7 +94,7 @@ export interface CGroup {
* {@link avatarFaceID}.
*/
displayFaceID: string | undefined;
-}
+};
/**
* Syncronize the user's cluster groups with remote, running local clustering if
@@ -109,7 +122,7 @@ export interface CGroup {
* - They can hide a cluster. This creates an unnamed cgroup so that the
* user's other clients know not to show it.
*/
-export const syncCGroups = () => {
+export const syncCGroups = async () => {
// 1. Fetch existing cgroups for the user from remote.
// 2. Save them to DB.
// 3. Prune stale faceIDs from the clusters in the DB.
@@ -118,7 +131,10 @@ export const syncCGroups = () => {
//
// The user can see both the cgroups and clusters in the UI, but only the
// cgroups are synced.
- // const syncCGroupsWithRemote()
+
+ const masterKey = await masterKeyFromSession();
+ await pullCGroups(masterKey);
+
/*
* After clustering, we also do some routine cleanup. Faces belonging to files
* that have been deleted (including those in Trash) should be pruned off.
diff --git a/web/packages/new/photos/services/ml/cluster.ts b/web/packages/new/photos/services/ml/cluster.ts
index e00c084fef..919e602aee 100644
--- a/web/packages/new/photos/services/ml/cluster.ts
+++ b/web/packages/new/photos/services/ml/cluster.ts
@@ -3,7 +3,7 @@ import { newNonSecureID } from "@/base/id-worker";
import log from "@/base/log";
import { ensure } from "@/utils/ensure";
import type { EnteFile } from "../../types/file";
-import type { CGroup } from "./cgroups";
+import type { AnnotatedCGroup } from "./cgroups";
import { faceDirection, type Face, type FaceIndex } from "./face";
import { dotProduct } from "./math";
@@ -199,7 +199,7 @@ export const clusterFaces = (
// locally, so cgroups will be empty. Create a temporary (unsaved, unsynced)
// cgroup, one per cluster.
- const cgroups: CGroup[] = [];
+ const cgroups: AnnotatedCGroup[] = [];
for (const cluster of sortedClusters) {
const faces = cluster.faceIDs.map((id) =>
ensure(faceForFaceID.get(id)),
diff --git a/web/packages/new/photos/services/ml/db.ts b/web/packages/new/photos/services/ml/db.ts
index 86cc0afd03..833bcffbd3 100644
--- a/web/packages/new/photos/services/ml/db.ts
+++ b/web/packages/new/photos/services/ml/db.ts
@@ -32,17 +32,17 @@ import type { LocalFaceIndex } from "./face";
* In tandem, these serve as the underlying storage for the indexes maintained
* in the ML database.
*
- * The cluster related object stores are the following:
+ * The face clustering related object stores are the following:
*
* - "face-cluster": Contains {@link FaceCluster} objects, one for each
* cluster of faces that either the clustering algorithm produced locally or
- * were synced from remote. It is indexed by the (cluster) ID.
+ * were synced from remote. It is indexed by the cluster ID.
*
* - "cluster-group": Contains {@link CGroup} objects, one for each group of
* clusters that were synced from remote. The client can also locally
* generate cluster groups on certain user interactions, but these too will
* eventually get synced with remote. This object store is indexed by the
- * (cgroup) ID.
+ * cgroup ID.
*/
interface MLDBSchema extends DBSchema {
"file-status": {
@@ -441,24 +441,3 @@ export const applyCGroupDiff = async (diff: (string | CGroup)[]) => {
);
return tx.done;
};
-
-/**
- * Add or overwrite the entry for the given {@link cgroup}, as identified by
- * their {@link id}.
- */
-// TODO-Cluster: Remove me
-export const saveClusterGroup = async (cgroup: CGroup) => {
- const db = await mlDB();
- const tx = db.transaction("cluster-group", "readwrite");
- await Promise.all([tx.store.put(cgroup), tx.done]);
-};
-
-/**
- * Delete the entry (if any) for the cluster group with the given {@link id}.
- */
-// TODO-Cluster: Remove me
-export const deleteClusterGroup = async (id: string) => {
- const db = await mlDB();
- const tx = db.transaction("cluster-group", "readwrite");
- await Promise.all([tx.store.delete(id), tx.done]);
-};
diff --git a/web/packages/new/photos/services/search/index.ts b/web/packages/new/photos/services/search/index.ts
index abd920c9e1..cfd7a0ca21 100644
--- a/web/packages/new/photos/services/search/index.ts
+++ b/web/packages/new/photos/services/search/index.ts
@@ -1,12 +1,15 @@
import { isDesktop } from "@/base/app";
import { masterKeyFromSession } from "@/base/session-store";
import { ComlinkWorker } from "@/base/worker/comlink-worker";
+import { FileType } from "@/media/file-type";
import i18n, { t } from "i18next";
import type { EnteFile } from "../../types/file";
import { clipMatches, isMLEnabled } from "../ml";
import {
SuggestionType,
type DateSearchResult,
+ type LabelledFileType,
+ type LocalizedSearchData,
type SearchQuery,
} from "./types";
import type { SearchWorker } from "./worker";
@@ -31,6 +34,17 @@ const createComlinkWorker = () =>
new Worker(new URL("worker.ts", import.meta.url)),
);
+/**
+ * Perform any logout specific cleanup for the search subsystem.
+ */
+export const logoutSearch = () => {
+ if (_comlinkWorker) {
+ _comlinkWorker.terminate();
+ _comlinkWorker = undefined;
+ }
+ _localizedSearchData = undefined;
+};
+
/**
* Fetch any data that would be needed if the user were to search.
*/
@@ -59,7 +73,7 @@ export const createSearchQuery = async (searchString: string) => {
// the search worker, then combine the two.
const results = await Promise.all([
clipSuggestions(s, searchString).then((s) => s ?? []),
- worker().then((w) => w.createSearchQuery(s, i18n.language, holidays())),
+ worker().then((w) => w.createSearchQuery(s, localizedSearchData())),
]);
return results.flat();
};
@@ -85,12 +99,40 @@ export const search = async (search: SearchQuery) =>
worker().then((w) => w.search(search));
/**
- * A list of holidays - their yearly dates and localized names.
+ * Cached value of {@link localizedSearchData}.
+ */
+let _localizedSearchData: LocalizedSearchData | undefined;
+
+/*
+ * For searching, the web worker needs a bunch of otherwise static data that has
+ * names and labels formed by localized strings.
*
- * We need to keep this on the main thread since it uses the t() function for
- * localization (although I haven't tried that in a web worker, it might work
- * there too). Also, it cannot be a const since it needs to be evaluated lazily
- * for the t() to work.
+ * Since it would be tricky to get the t() function to work in a web worker, we
+ * instead pass this from the main thread (lazily initialized and cached).
+ *
+ * Note that these need to be evaluated at runtime, and cannot be static
+ * constants since t() depends on the user's locale.
+ *
+ * We currently clear the cached data on logout, but this is not necessary. The
+ * only point we necessarily need to clear this data is if the user changes their
+ * preferred locale, but currently we reload the page in such cases so any in
+ * memory state would be reset that way.
+ */
+const localizedSearchData = () =>
+ (_localizedSearchData ??= {
+ locale: i18n.language,
+ holidays: holidays().map((h) => ({
+ ...h,
+ lowercasedName: h.label.toLowerCase(),
+ })),
+ labelledFileTypes: labelledFileTypes().map((t) => ({
+ ...t,
+ lowercasedName: t.label.toLowerCase(),
+ })),
+ });
+
+/**
+ * A list of holidays - their yearly dates and localized names.
*/
const holidays = (): DateSearchResult[] => [
{ components: { month: 12, day: 25 }, label: t("CHRISTMAS") },
@@ -98,3 +140,12 @@ const holidays = (): DateSearchResult[] => [
{ components: { month: 1, day: 1 }, label: t("NEW_YEAR") },
{ components: { month: 12, day: 31 }, label: t("NEW_YEAR_EVE") },
];
+
+/**
+ * A list of file types with their localized names.
+ */
+const labelledFileTypes = (): LabelledFileType[] => [
+ { fileType: FileType.image, label: t("IMAGE") },
+ { fileType: FileType.video, label: t("VIDEO") },
+ { fileType: FileType.livePhoto, label: t("LIVE_PHOTO") },
+];
diff --git a/web/packages/new/photos/services/search/types.ts b/web/packages/new/photos/services/search/types.ts
index 080803956a..b2130e4bbc 100644
--- a/web/packages/new/photos/services/search/types.ts
+++ b/web/packages/new/photos/services/search/types.ts
@@ -14,6 +14,35 @@ export interface DateSearchResult {
label: string;
}
+export interface LabelledFileType {
+ fileType: FileType;
+ label: string;
+}
+
+/**
+ * An annotated version of {@link T} that includes its searchable "lowercased"
+ * label or name.
+ *
+ * Precomputing these lowercased values saves us from doing the lowercasing
+ * during the search itself.
+ */
+export type Searchable = T & {
+ /**
+ * The name or label of T, lowercased.
+ */
+ lowercasedName: string;
+};
+
+/**
+ * Various bits of static but locale specific data that the search worker needs
+ * during searching.
+ */
+export interface LocalizedSearchData {
+ locale: string;
+ holidays: Searchable[];
+ labelledFileTypes: Searchable[];
+}
+
/**
* A parsed version of a potential natural language date time string.
*
diff --git a/web/packages/new/photos/services/search/worker.ts b/web/packages/new/photos/services/search/worker.ts
index 714453b419..ca842d40d6 100644
--- a/web/packages/new/photos/services/search/worker.ts
+++ b/web/packages/new/photos/services/search/worker.ts
@@ -9,42 +9,30 @@ import * as chrono from "chrono-node";
import { expose } from "comlink";
import { z } from "zod";
import {
+ pullLocationTags,
savedLocationTags,
- syncLocationTags,
type LocationTag,
} from "../user-entity";
import type {
City,
DateSearchResult,
+ LabelledFileType,
+ LocalizedSearchData,
+ Searchable,
SearchDateComponents,
SearchQuery,
Suggestion,
} from "./types";
import { SuggestionType } from "./types";
-type SearchableCity = City & {
- /**
- * Name of the city, lowercased. Precomputed to save an op during search.
- */
- lowercasedName: string;
-};
-
-type SearchableLocationTag = LocationTag & {
- /**
- * Name of the location tag, lowercased. Precomputed to save an op during
- * search.
- */
- lowercasedName: string;
-};
-
/**
* A web worker that runs the search asynchronously so that the main thread
* remains responsive.
*/
export class SearchWorker {
private enteFiles: EnteFile[] = [];
- private locationTags: SearchableLocationTag[] = [];
- private cities: SearchableCity[] = [];
+ private locationTags: Searchable[] = [];
+ private cities: Searchable[] = [];
/**
* Fetch any state we might need when the actual search happens.
@@ -54,7 +42,7 @@ export class SearchWorker {
*/
async sync(masterKey: Uint8Array) {
return Promise.all([
- syncLocationTags(masterKey)
+ pullLocationTags(masterKey)
.then(() => savedLocationTags())
.then((ts) => {
this.locationTags = ts.map((t) => ({
@@ -81,11 +69,10 @@ export class SearchWorker {
/**
* Convert a search string into a reusable query.
*/
- createSearchQuery(s: string, locale: string, holidays: DateSearchResult[]) {
+ createSearchQuery(s: string, localizedSearchData: LocalizedSearchData) {
return createSearchQuery(
s,
- locale,
- holidays,
+ localizedSearchData,
this.locationTags,
this.cities,
);
@@ -103,20 +90,20 @@ expose(SearchWorker);
const createSearchQuery = (
s: string,
- locale: string,
- holidays: DateSearchResult[],
- locationTags: SearchableLocationTag[],
- cities: SearchableCity[],
+ { locale, holidays, labelledFileTypes }: LocalizedSearchData,
+ locationTags: Searchable[],
+ cities: Searchable[],
): Suggestion[] =>
[
dateSuggestions(s, locale, holidays),
locationSuggestions(s, locationTags, cities),
+ fileTypeSuggestions(s, labelledFileTypes),
].flat();
const dateSuggestions = (
s: string,
locale: string,
- holidays: DateSearchResult[],
+ holidays: Searchable[],
) =>
parseDateComponents(s, locale, holidays).map(({ components, label }) => ({
type: SuggestionType.DATE,
@@ -141,12 +128,12 @@ const dateSuggestions = (
const parseDateComponents = (
s: string,
locale: string,
- holidays: DateSearchResult[],
+ holidays: Searchable[],
): DateSearchResult[] =>
[
parseChrono(s, locale),
parseYearComponents(s),
- parseHolidayComponents(s, holidays),
+ holidays.filter(searchableIncludes(s)),
].flat();
const parseChrono = (s: string, locale: string): DateSearchResult[] =>
@@ -195,8 +182,13 @@ const parseYearComponents = (s: string): DateSearchResult[] => {
return [];
};
-const parseHolidayComponents = (s: string, holidays: DateSearchResult[]) =>
- holidays.filter(({ label }) => label.toLowerCase().includes(s));
+/**
+ * A helper function to directly pass to filters on Searchable[].
+ */
+const searchableIncludes =
+ (s: string) =>
+ ({ lowercasedName }: { lowercasedName: string }) =>
+ lowercasedName.includes(s);
/**
* Zod schema describing world_cities.json.
@@ -223,12 +215,10 @@ const fetchCities = async () => {
const locationSuggestions = (
s: string,
- locationTags: SearchableLocationTag[],
- cities: SearchableCity[],
+ locationTags: Searchable[],
+ cities: Searchable[],
) => {
- const matchingLocationTags = locationTags.filter((t) =>
- t.lowercasedName.includes(s),
- );
+ const matchingLocationTags = locationTags.filter(searchableIncludes(s));
const matchingLocationTagLNames = new Set(
matchingLocationTags.map((t) => t.lowercasedName),
@@ -254,6 +244,18 @@ const locationSuggestions = (
].flat();
};
+const fileTypeSuggestions = (
+ s: string,
+ labelledFileTypes: Searchable[],
+) =>
+ labelledFileTypes
+ .filter(searchableIncludes(s))
+ .map(({ fileType, label }) => ({
+ label,
+ value: fileType,
+ type: SuggestionType.FILE_TYPE,
+ }));
+
/**
* Return true if file satisfies the given {@link query}.
*/
diff --git a/web/packages/new/photos/services/user-entity.ts b/web/packages/new/photos/services/user-entity.ts
index 7064d21d82..731a39d565 100644
--- a/web/packages/new/photos/services/user-entity.ts
+++ b/web/packages/new/photos/services/user-entity.ts
@@ -35,16 +35,16 @@ export type EntityType =
| "cgroup";
/**
- * Sync our local location tags with those on remote.
+ * Update our local location tags with changes from remote.
*
* This function fetches all the location tag user entities from remote and
- * updates our local database. It uses local state to remember the last time it
- * synced, so each subsequent sync is a lightweight diff.
+ * updates our local database. It uses local state to remember the latest entry
+ * the last time it did a pull, so each subsequent pull is a lightweight diff.
*
* @param masterKey The user's master key. This is used to encrypt and decrypt
* the location tags specific entity key.
*/
-export const syncLocationTags = async (masterKey: Uint8Array) => {
+export const pullLocationTags = async (masterKey: Uint8Array) => {
const decoder = new TextDecoder();
const parse = (id: string, data: Uint8Array): LocationTag => ({
id,
@@ -63,7 +63,7 @@ export const syncLocationTags = async (masterKey: Uint8Array) => {
return saveLocationTags([...existingTagsByID.values()]);
};
- return syncUserEntity("location", masterKey, processBatch);
+ return pullUserEntities("location", masterKey, processBatch);
};
/** Zod schema for the tag that we get from or put to remote. */
@@ -89,7 +89,7 @@ const saveLocationTags = (tags: LocationTag[]) =>
/**
* Return all the location tags that are present locally.
*
- * Use {@link syncLocationTags} to sync this list with remote.
+ * Use {@link pullLocationTags} to synchronize this list with remote.
*/
export const savedLocationTags = async () =>
LocalLocationTag.array().parse(
@@ -97,7 +97,7 @@ export const savedLocationTags = async () =>
);
/**
- * Sync the {@link CGroup} entities that we have locally with remote.
+ * Update our local cgroups with changes from remote.
*
* This fetches all the user entities corresponding to the "cgroup" entity type
* from remote that have been created, updated or deleted since the last time we
@@ -108,16 +108,15 @@ export const savedLocationTags = async () =>
* @param masterKey The user's master key. This is used to encrypt and decrypt
* the cgroup specific entity key.
*/
-export const syncCGroups = (masterKey: Uint8Array) => {
+export const pullCGroups = (masterKey: Uint8Array) => {
const parse = async (id: string, data: Uint8Array): Promise => {
- const rp = RemoteCGroup.parse(JSON.parse(await gunzip(data)));
+ const r = RemoteCGroup.parse(JSON.parse(await gunzip(data)));
return {
id,
- name: rp.name,
- clusterIDs: rp.assigned.map(({ id }) => id),
- isHidden: rp.isHidden,
- avatarFaceID: rp.avatarFaceID,
- displayFaceID: undefined,
+ name: r.name,
+ clusterIDs: r.assigned.map(({ id }) => id),
+ isHidden: r.isHidden,
+ avatarFaceID: r.avatarFaceID,
};
};
@@ -130,7 +129,7 @@ export const syncCGroups = (masterKey: Uint8Array) => {
),
);
- return syncUserEntity("cgroup", masterKey, processBatch);
+ return pullUserEntities("cgroup", masterKey, processBatch);
};
const RemoteCGroup = z.object({
@@ -141,6 +140,8 @@ const RemoteCGroup = z.object({
faces: z.string().array(),
}),
),
+ // The remote cgroup also has a "rejected" property, but that is not
+ // currently used by any of the clients.
isHidden: z.boolean(),
avatarFaceID: z.string().nullish().transform(nullToUndefined),
});
@@ -209,7 +210,7 @@ interface UserEntityChange {
* The user's {@link masterKey} is used to decrypt (or encrypt, when generating
* a new one) the entity key.
*/
-const syncUserEntity = async (
+const pullUserEntities = async (
type: EntityType,
masterKey: Uint8Array,
processBatch: (entities: UserEntityChange[]) => Promise,