[web] Search refactoring - Part x/x (#3233)

This commit is contained in:
Manav Rathi
2024-09-11 21:23:37 +05:30
committed by GitHub
50 changed files with 422 additions and 381 deletions

View File

@@ -1,3 +1,4 @@
import type { Collection } from "@/media/collection";
import { SpaceBetweenFlex } from "@ente/shared/components/Container";
import ArchiveOutlined from "@mui/icons-material/ArchiveOutlined";
import Favorite from "@mui/icons-material/FavoriteRounded";
@@ -6,9 +7,9 @@ import PeopleIcon from "@mui/icons-material/People";
import { SetCollectionNamerAttributes } from "components/Collections/CollectionNamer";
import CollectionOptions from "components/Collections/CollectionOptions";
import type { Dispatch, SetStateAction } from "react";
import { Collection, CollectionSummary } from "types/collection";
import { CollectionSummary, CollectionSummaryType } from "types/collection";
import { SetFilesDownloadProgressAttributesCreator } from "types/gallery";
import { CollectionSummaryType, shouldShowOptions } from "utils/collection";
import { shouldShowOptions } from "utils/collection";
import { CollectionInfo } from "./CollectionInfo";
import { CollectionInfoBarWrapper } from "./styledComponents";

View File

@@ -5,8 +5,7 @@ import PeopleIcon from "@mui/icons-material/People";
import PushPin from "@mui/icons-material/PushPin";
import { Box, Typography, styled } from "@mui/material";
import Tooltip from "@mui/material/Tooltip";
import { CollectionSummary } from "types/collection";
import { CollectionSummaryType } from "utils/collection";
import { CollectionSummary, CollectionSummaryType } from "types/collection";
import CollectionCard from "../CollectionCard";
import {
ActiveIndicator,

View File

@@ -1,5 +1,6 @@
import { boxSeal } from "@/base/crypto/libsodium";
import log from "@/base/log";
import type { Collection } from "@/media/collection";
import { VerticallyCentered } from "@ente/shared/components/Container";
import DialogBoxV2 from "@ente/shared/components/DialogBoxV2";
import EnteButton from "@ente/shared/components/EnteButton";
@@ -12,7 +13,6 @@ import { Link, Typography } from "@mui/material";
import { t } from "i18next";
import { useEffect, useState } from "react";
import { Trans } from "react-i18next";
import { Collection } from "types/collection";
import { v4 as uuidv4 } from "uuid";
import { loadSender } from "../../../utils/useCastSender";

View File

@@ -1,7 +1,7 @@
import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined";
import { IconButton, Tooltip } from "@mui/material";
import { t } from "i18next";
import { CollectionSummaryType } from "utils/collection";
import { CollectionSummaryType } from "types/collection";
import { CollectionActions } from "..";
interface Iprops {
handleCollectionAction: (

View File

@@ -1,7 +1,7 @@
import PeopleIcon from "@mui/icons-material/People";
import { IconButton, Tooltip } from "@mui/material";
import { t } from "i18next";
import { CollectionSummaryType } from "utils/collection";
import { CollectionSummaryType } from "types/collection";
import { CollectionActions } from "..";
interface Iprops {

View File

@@ -1,7 +1,7 @@
import { FlexWrapper } from "@ente/shared/components/Container";
import EnteSpinner from "@ente/shared/components/EnteSpinner";
import { CollectionSummaryType } from "types/collection";
import {
CollectionSummaryType,
showDownloadQuickOption,
showEmptyTrashQuickOption,
showShareQuickOption,

View File

@@ -1,4 +1,5 @@
import log from "@/base/log";
import type { Collection } from "@/media/collection";
import { ItemVisibility } from "@/media/file-metadata";
import { HorizontalFlex } from "@ente/shared/components/Container";
import OverflowMenu from "@ente/shared/components/OverflowMenu/menu";
@@ -12,14 +13,13 @@ import { useContext, useRef, useState } from "react";
import { Trans } from "react-i18next";
import * as CollectionAPI from "services/collectionService";
import * as TrashService from "services/trashService";
import { Collection } from "types/collection";
import { CollectionSummaryType } from "types/collection";
import { SetFilesDownloadProgressAttributesCreator } from "types/gallery";
import {
ALL_SECTION,
changeCollectionOrder,
changeCollectionSortOrder,
changeCollectionVisibility,
CollectionSummaryType,
downloadCollectionHelper,
downloadDefaultHiddenCollectionHelper,
HIDDEN_ITEMS_SECTION,

View File

@@ -1,3 +1,4 @@
import type { Collection } from "@/media/collection";
import { FlexWrapper } from "@ente/shared/components/Container";
import DialogTitleWithCloseButton from "@ente/shared/components/DialogBox/TitleWithCloseButton";
import { DialogContent, useMediaQuery } from "@mui/material";
@@ -6,14 +7,13 @@ import { t } from "i18next";
import { useEffect, useState } from "react";
import { createUnCategorizedCollection } from "services/collectionService";
import {
Collection,
CollectionSummaries,
CollectionSummary,
CollectionSummaryType,
} from "types/collection";
import { CollectionSelectorIntent } from "types/gallery";
import {
COLLECTION_SORT_ORDER,
CollectionSummaryType,
DUMMY_UNCATEGORIZED_COLLECTION,
isAddToAllowedCollection,
isMoveToAllowedCollection,

View File

@@ -1,8 +1,8 @@
import { EnteDrawer } from "@/base/components/EnteDrawer";
import { Titlebar } from "@/base/components/Titlebar";
import { COLLECTION_ROLE, type Collection } from "@/media/collection";
import { DialogProps, Stack } from "@mui/material";
import { t } from "i18next";
import { COLLECTION_ROLE, Collection } from "types/collection";
import { GalleryContext } from "pages/gallery";
import { useContext, useMemo } from "react";

View File

@@ -5,6 +5,11 @@ import {
MenuSectionTitle,
} from "@/base/components/Menu";
import { Titlebar } from "@/base/components/Titlebar";
import {
COLLECTION_ROLE,
type Collection,
type CollectionUser,
} from "@/media/collection";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import Add from "@mui/icons-material/Add";
import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings";
@@ -18,7 +23,6 @@ import { AppContext } from "pages/_app";
import { GalleryContext } from "pages/gallery";
import { useContext, useRef, useState } from "react";
import { unshareCollection } from "services/collectionService";
import { COLLECTION_ROLE, Collection, CollectionUser } from "types/collection";
import AddParticipant from "./AddParticipant";
import ManageParticipant from "./ManageParticipant";

View File

@@ -2,6 +2,7 @@ import { EnteDrawer } from "@/base/components/EnteDrawer";
import { MenuItemDivider, MenuItemGroup } from "@/base/components/Menu";
import { Titlebar } from "@/base/components/Titlebar";
import log from "@/base/log";
import type { Collection, CollectionUser } from "@/media/collection";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import BlockIcon from "@mui/icons-material/Block";
import DoneIcon from "@mui/icons-material/Done";
@@ -14,7 +15,6 @@ import { GalleryContext } from "pages/gallery";
import { useContext } from "react";
import { Trans } from "react-i18next";
import { shareCollection } from "services/collectionService";
import { Collection, CollectionUser } from "types/collection";
import { handleSharingErrors } from "utils/error/ui";
interface Iprops {

View File

@@ -1,5 +1,5 @@
import { COLLECTION_ROLE, type Collection } from "@/media/collection";
import { useRef, useState } from "react";
import { COLLECTION_ROLE, Collection } from "types/collection";
import {
MenuItemDivider,

View File

@@ -1,9 +1,9 @@
import { EnteDrawer } from "@/base/components/EnteDrawer";
import { Titlebar } from "@/base/components/Titlebar";
import type { Collection } from "@/media/collection";
import { DialogProps, Stack } from "@mui/material";
import { t } from "i18next";
import { Collection, CollectionSummary } from "types/collection";
import { CollectionSummaryType } from "utils/collection";
import { CollectionSummary, CollectionSummaryType } from "types/collection";
import EmailShare from "./emailShare";
import PublicShare from "./publicShare";
import SharingDetails from "./sharingDetails";

View File

@@ -3,6 +3,7 @@ import {
MenuItemGroup,
MenuSectionTitle,
} from "@/base/components/Menu";
import type { Collection, PublicURL } from "@/media/collection";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import DownloadSharp from "@mui/icons-material/DownloadSharp";
import LinkIcon from "@mui/icons-material/Link";
@@ -15,7 +16,6 @@ import {
createShareableURL,
updateShareableURL,
} from "services/collectionService";
import { Collection, PublicURL } from "types/collection";
import { handleSharingErrors } from "utils/error/ui";
interface Iprops {
collection: Collection;

View File

@@ -1,5 +1,5 @@
import type { Collection, PublicURL } from "@/media/collection";
import { useEffect, useState } from "react";
import { Collection, PublicURL } from "types/collection";
import { appendCollectionKeyToShareURL } from "utils/collection";
import EnablePublicShareOptions from "./EnablePublicShareOptions";
import CopyLinkModal from "./copyLinkModal";

View File

@@ -1,12 +1,16 @@
import { EnteDrawer } from "@/base/components/EnteDrawer";
import { MenuItemDivider, MenuItemGroup } from "@/base/components/Menu";
import { Titlebar } from "@/base/components/Titlebar";
import type {
Collection,
PublicURL,
UpdatePublicURL,
} from "@/media/collection";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import ChevronRight from "@mui/icons-material/ChevronRight";
import { DialogProps, Stack } from "@mui/material";
import { t } from "i18next";
import { useMemo, useState } from "react";
import { Collection, PublicURL, UpdatePublicURL } from "types/collection";
import { getDeviceLimitOptions } from "utils/collection";
interface Iprops {

View File

@@ -1,9 +1,13 @@
import type {
Collection,
PublicURL,
UpdatePublicURL,
} from "@/media/collection";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import { t } from "i18next";
import { AppContext } from "pages/_app";
import { useContext } from "react";
import { Trans } from "react-i18next";
import { Collection, PublicURL, UpdatePublicURL } from "types/collection";
interface Iprops {
publicShareProp: PublicURL;
collection: Collection;

View File

@@ -1,6 +1,11 @@
import { EnteDrawer } from "@/base/components/EnteDrawer";
import { MenuItemDivider, MenuItemGroup } from "@/base/components/Menu";
import { Titlebar } from "@/base/components/Titlebar";
import type {
Collection,
PublicURL,
UpdatePublicURL,
} from "@/media/collection";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import ContentCopyIcon from "@mui/icons-material/ContentCopy";
import RemoveCircleOutline from "@mui/icons-material/RemoveCircleOutline";
@@ -12,7 +17,6 @@ import {
deleteShareableURL,
updateShareableURL,
} from "services/collectionService";
import { Collection, PublicURL, UpdatePublicURL } from "types/collection";
import { SetPublicShareProp } from "types/publicCollection";
import { handleSharingErrors } from "utils/error/ui";
import { ManageDeviceLimit } from "./deviceLimit";

View File

@@ -1,13 +1,17 @@
import { EnteDrawer } from "@/base/components/EnteDrawer";
import { MenuItemDivider, MenuItemGroup } from "@/base/components/Menu";
import { Titlebar } from "@/base/components/Titlebar";
import type {
Collection,
PublicURL,
UpdatePublicURL,
} from "@/media/collection";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import { formatDateTime } from "@ente/shared/time/format";
import ChevronRight from "@mui/icons-material/ChevronRight";
import { DialogProps, Stack } from "@mui/material";
import { t } from "i18next";
import { useMemo, useState } from "react";
import { Collection, PublicURL, UpdatePublicURL } from "types/collection";
import { shareExpiryOptions } from "utils/collection";
import { isLinkExpired } from "../managePublicShare";

View File

@@ -1,8 +1,12 @@
import type {
Collection,
PublicURL,
UpdatePublicURL,
} from "@/media/collection";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import { t } from "i18next";
import { AppContext } from "pages/_app";
import { useContext, useState } from "react";
import { Collection, PublicURL, UpdatePublicURL } from "types/collection";
import { PublicLinkSetPassword } from "./setPassword";
interface Iprops {

View File

@@ -1,8 +1,12 @@
import { MenuItemGroup, MenuSectionTitle } from "@/base/components/Menu";
import type {
Collection,
PublicURL,
UpdatePublicURL,
} from "@/media/collection";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import { Stack } from "@mui/material";
import { t } from "i18next";
import { Collection, PublicURL, UpdatePublicURL } from "types/collection";
interface Iprops {
publicShareProp: PublicURL;

View File

@@ -1,4 +1,5 @@
import { MenuItemDivider, MenuItemGroup } from "@/base/components/Menu";
import type { Collection, PublicURL } from "@/media/collection";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import ContentCopyIcon from "@mui/icons-material/ContentCopyOutlined";
@@ -8,7 +9,6 @@ import PublicIcon from "@mui/icons-material/Public";
import { Stack, Typography } from "@mui/material";
import { t } from "i18next";
import { useState } from "react";
import { Collection, PublicURL } from "types/collection";
import { SetPublicShareProp } from "types/publicCollection";
import ManagePublicShareOptions from "./manage";

View File

@@ -3,6 +3,7 @@ import {
MenuItemGroup,
MenuSectionTitle,
} from "@/base/components/Menu";
import { COLLECTION_ROLE } from "@/media/collection";
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
import AdminPanelSettingsIcon from "@mui/icons-material/AdminPanelSettings";
import ModeEditIcon from "@mui/icons-material/ModeEdit";
@@ -12,8 +13,7 @@ import Avatar from "components/pages/gallery/Avatar";
import { t } from "i18next";
import { GalleryContext } from "pages/gallery";
import { useContext } from "react";
import { COLLECTION_ROLE } from "types/collection";
import { CollectionSummaryType } from "utils/collection";
import { CollectionSummaryType } from "types/collection";
export default function SharingDetails({ collection, type }) {
const galleryContext = useContext(GalleryContext);

View File

@@ -1,3 +1,4 @@
import type { Collection } from "@/media/collection";
import { useLocalState } from "@ente/shared/hooks/useLocalState";
import { LS_KEYS } from "@ente/shared/storage/localStorage";
import AllCollections from "components/Collections/AllCollections";
@@ -8,7 +9,7 @@ import CollectionShare from "components/Collections/CollectionShare";
import { ITEM_TYPE, TimeStampListItem } from "components/PhotoList";
import { useCallback, useEffect, useMemo, useState } from "react";
import { sortCollectionSummaries } from "services/collectionService";
import { Collection, CollectionSummaries } from "types/collection";
import { CollectionSummaries } from "types/collection";
import { SetFilesDownloadProgressAttributesCreator } from "types/gallery";
import {
ALL_SECTION,

View File

@@ -6,6 +6,7 @@ import {
mlStatusSnapshot,
mlStatusSubscribe,
} from "@/new/photos/services/ml";
import { getAutoCompleteSuggestions } from "@/new/photos/services/search";
import type {
City,
SearchDateComponents,
@@ -20,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 { EnteFile } from "@/new/photos/types/file";
import {
FreeFlowText,
SpaceBetweenFlex,
@@ -38,15 +38,15 @@ import {
Stack,
styled,
Typography,
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";
import { AppContext } from "pages/_app";
import {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
@@ -62,15 +62,29 @@ import {
type StylesConfig,
} from "react-select";
import AsyncSelect from "react-select/async";
import { getAutoCompleteSuggestions } from "services/searchService";
import { Collection } from "types/collection";
interface SearchBarProps {
/**
* [Note: "Search mode"]
*
* On mobile sized screens, normally the search input areas is not
* displayed. Clicking the search icon enters the "search mode", where we
* show the search input area.
*
* On other screens, the search input is always shown even if we are not in
* search mode.
*
* When we're in search mode,
*
* 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;
collections: Collection[];
files: EnteFile[];
}
export type UpdateSearch = (
@@ -78,6 +92,21 @@ export type UpdateSearch = (
summary: SearchResultSummary,
) => 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
* dropdown.
*
* When the search input is empty, it shows some general information in the
* dropdown instead (e.g. the ML indexing status).
*
* When the search input is not empty, it shows these {@link SearchSuggestion}s.
* Alongside each suggestion is shows a count of matching files, and some
* previews.
*
* Selecting one of the these suggestions causes the gallery to shows a filtered
* list of files that match that suggestion.
*/
export const SearchBar: React.FC<SearchBarProps> = ({
setIsInSearchMode,
isInSearchMode,
@@ -92,11 +121,7 @@ export const SearchBar: React.FC<SearchBarProps> = ({
{isMobileWidth && !isInSearchMode ? (
<MobileSearchArea onSearch={showSearchInput} />
) : (
<SearchInput
{...props}
isOpen={isInSearchMode}
setIsOpen={setIsInSearchMode}
/>
<SearchInput {...props} isInSearchMode={isInSearchMode} />
)}
</Box>
);
@@ -116,22 +141,14 @@ const MobileSearchArea: React.FC<MobileSearchAreaProps> = ({ onSearch }) => (
);
interface SearchInputProps {
isOpen: boolean;
setIsOpen: (value: boolean) => void;
isInSearchMode: boolean;
updateSearch: UpdateSearch;
files: EnteFile[];
collections: Collection[];
}
const SearchInput: React.FC<SearchInputProps> = ({
isOpen,
setIsOpen,
isInSearchMode,
updateSearch,
files,
collections,
}) => {
const appContext = useContext(AppContext);
// A ref to the top level Select.
const selectRef = useRef(null);
// The currently selected option.
@@ -139,6 +156,10 @@ const SearchInput: React.FC<SearchInputProps> = ({
// The contents of the input field associated with the select.
const [inputValue, setInputValue] = useState("");
const theme = useTheme();
const styles = useMemo(() => useSelectStyles(theme), [theme]);
useEffect(() => {
search(value);
}, [value]);
@@ -161,21 +182,14 @@ const SearchInput: React.FC<SearchInputProps> = ({
};
const resetSearch = () => {
if (isOpen) {
appContext.startLoading();
updateSearch(null, null);
setTimeout(() => {
appContext.finishLoading();
}, 10);
setIsOpen(false);
setValue(null);
setInputValue("");
}
updateSearch(null, null);
setValue(null);
setInputValue("");
};
const getOptions = useCallback(
pDebounce(getAutoCompleteSuggestions(files, collections), 250),
[files, collections],
pDebounce(getAutoCompleteSuggestions(), 250),
[],
);
const search = (selectedOption: SearchOption) => {
@@ -188,19 +202,16 @@ const SearchInput: React.FC<SearchInputProps> = ({
search = {
date: selectedOption.value as SearchDateComponents,
};
setIsOpen(true);
break;
case SuggestionType.LOCATION:
search = {
location: selectedOption.value as LocationTag,
};
setIsOpen(true);
break;
case SuggestionType.CITY:
search = {
city: selectedOption.value as City,
};
setIsOpen(true);
break;
case SuggestionType.COLLECTION:
search = { collection: selectedOption.value as number };
@@ -242,6 +253,7 @@ const SearchInput: React.FC<SearchInputProps> = ({
ref={selectRef}
value={value}
components={components}
styles={styles}
placeholder={t("search_hint")}
loadOptions={getOptions}
onChange={handleChange}
@@ -250,7 +262,6 @@ const SearchInput: React.FC<SearchInputProps> = ({
escapeClearsValue
inputValue={inputValue}
onInputChange={handleInputChange}
styles={SelectStyles}
noOptionsMessage={({ inputValue }) =>
shouldShowEmptyState(inputValue) ? (
<EmptyState onSelectCGroup={handleSelectCGroup} />
@@ -258,7 +269,7 @@ const SearchInput: React.FC<SearchInputProps> = ({
}
/>
{isOpen && (
{isInSearchMode && (
<IconButton onClick={() => resetSearch()} sx={{ ml: 1 }}>
<CloseIcon />
</IconButton>
@@ -277,27 +288,26 @@ const SearchInputWrapper = styled(Box)`
margin: auto;
`;
const SelectStyles: StylesConfig<SearchOption, false> = {
const useSelectStyles = ({
colors,
}: Theme): StylesConfig<SearchOption, false> => ({
container: (style) => ({ ...style, flex: 1 }),
control: (style, { isFocused }) => ({
...style,
// Give a solid background color.
backgroundColor: "rgb(26, 26, 26)",
borderColor: isFocused ? "#1DB954" : "transparent",
backgroundColor: colors.background.elevated,
borderColor: isFocused ? colors.accent.A500 : "transparent",
boxShadow: "none",
":hover": {
borderColor: "#01DE4D",
borderColor: colors.accent.A300,
cursor: "text",
},
}),
input: (styles) => ({ ...styles, color: "#fff" }),
input: (styles) => ({ ...styles, color: colors.text.base }),
menu: (style) => ({
...style,
// Suppress the default margin at the top.
marginTop: "1px",
// Same background color as the control (must be solid, otherwise the
// content behind the menu shows through).
backgroundColor: "rgb(26, 26, 26)",
backgroundColor: colors.background.elevated,
}),
option: (style, { isFocused }) => ({
...style,
@@ -307,7 +317,7 @@ const SelectStyles: StylesConfig<SearchOption, false> = {
cursor: "pointer",
},
"& .main": {
backgroundColor: isFocused && "#202020",
backgroundColor: isFocused && colors.background.elevated2,
},
"&:last-child .MuiDivider-root": {
display: "none",
@@ -315,14 +325,14 @@ const SelectStyles: StylesConfig<SearchOption, false> = {
}),
placeholder: (style) => ({
...style,
color: "rgba(255, 255, 255, 0.7)",
color: colors.text.muted,
whiteSpace: "nowrap",
}),
// Hide some things we don't need.
dropdownIndicator: (style) => ({ ...style, display: "none" }),
indicatorSeparator: (style) => ({ ...style, display: "none" }),
clearIndicator: (style) => ({ ...style, display: "none" }),
};
});
const Control = ({ children, ...props }: ControlProps<SearchOption, false>) => (
<SelectComponents.Control {...props}>

View File

@@ -1,6 +1,7 @@
import { basename } from "@/base/file";
import log from "@/base/log";
import type { CollectionMapping, Electron, ZipItem } from "@/base/types/ipc";
import type { Collection } from "@/media/collection";
import { exportMetadataDirectoryName } from "@/new/photos/services/export";
import type {
FileAndPath,
@@ -33,8 +34,6 @@ import type {
} from "services/upload/uploadManager";
import uploadManager from "services/upload/uploadManager";
import watcher from "services/watch";
import { NotificationAttributes } from "types/Notification";
import { Collection } from "types/collection";
import {
CollectionSelectorIntent,
SetCollectionSelectorAttributes,
@@ -42,6 +41,7 @@ import {
SetFiles,
SetLoading,
} from "types/gallery";
import { NotificationAttributes } from "types/Notification";
import { getOrCreateAlbum } from "utils/collection";
import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery";
import {

View File

@@ -1,7 +1,6 @@
import log from "@/base/log";
import { EnteFile } from "@/new/photos/types/file";
import { styled } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import { styled, useTheme } from "@mui/material";
import { GalleryContext } from "pages/gallery";
import React, { useContext, useLayoutEffect, useState } from "react";

View File

@@ -1,6 +1,6 @@
import type { Collection } from "@/media/collection";
import { styled } from "@mui/material";
import NumberAvatar from "@mui/material/Avatar";
import { Collection } from "types/collection";
import Avatar from "./Avatar";
const AvatarContainer = styled("div")({

View File

@@ -1,4 +1,5 @@
import { SelectionBar } from "@/base/components/Navbar";
import type { Collection } from "@/media/collection";
import { FluidContainer } from "@ente/shared/components/Container";
import ClockIcon from "@mui/icons-material/AccessTime";
import AddIcon from "@mui/icons-material/Add";
@@ -16,7 +17,6 @@ import { Box, IconButton, Stack, Tooltip } from "@mui/material";
import { t } from "i18next";
import { AppContext } from "pages/_app";
import { useContext } from "react";
import { Collection } from "types/collection";
import {
CollectionSelectorIntent,
SetCollectionSelectorAttributes,

View File

@@ -2,6 +2,7 @@ import { stashRedirect } from "@/accounts/services/redirect";
import { NavbarBase } from "@/base/components/Navbar";
import { useIsMobileWidth } from "@/base/hooks";
import log from "@/base/log";
import type { Collection } from "@/media/collection";
import { WhatsNew } from "@/new/photos/components/WhatsNew";
import { shouldShowWhatsNew } from "@/new/photos/services/changelog";
import downloadManager from "@/new/photos/services/download";
@@ -10,7 +11,10 @@ import {
getLocalTrashedFiles,
} from "@/new/photos/services/files";
import { wipHasSwitchedOnceCmpAndSet } from "@/new/photos/services/ml";
import { search, setSearchableFiles } from "@/new/photos/services/search";
import {
filterSearchableFiles,
setSearchableData,
} from "@/new/photos/services/search";
import {
SearchQuery,
SearchResultSummary,
@@ -105,7 +109,7 @@ import { sync, triggerPreFileInfoSync } from "services/sync";
import { syncTrash } from "services/trashService";
import uploadManager from "services/upload/uploadManager";
import { isTokenValid } from "services/userService";
import { Collection, CollectionSummaries } from "types/collection";
import { CollectionSummaries, CollectionSummaryType } from "types/collection";
import {
GalleryContextType,
SelectedState,
@@ -118,7 +122,6 @@ import {
ALL_SECTION,
ARCHIVE_SECTION,
COLLECTION_OPS_TYPE,
CollectionSummaryType,
HIDDEN_ITEMS_SECTION,
TRASH_SECTION,
constructCollectionNameMap,
@@ -407,7 +410,14 @@ export default function Gallery() {
};
}, []);
useEffect(() => setSearchableFiles(files), [files]);
useEffect(
() =>
setSearchableData({
collections: collections ?? [],
files: getUniqueFiles(files ?? []),
}),
[collections, files],
);
useEffect(() => {
if (!user || !files || !collections || !hiddenFiles || !trashedFiles) {
@@ -526,7 +536,7 @@ export default function Gallery() {
let filteredFiles: EnteFile[] = [];
if (isInSearchMode) {
filteredFiles = getUniqueFiles(await search(searchQuery));
filteredFiles = await filterSearchableFiles(searchQuery);
} else {
filteredFiles = getUniqueFiles(
(isInHiddenSection ? hiddenFiles : files).filter((item) => {
@@ -1089,8 +1099,6 @@ export default function Gallery() {
isInSearchMode={isInSearchMode}
setIsInSearchMode={setIsInSearchMode}
updateSearch={updateSearch}
collections={collections}
files={files}
/>
)}
</NavbarBase>
@@ -1269,8 +1277,6 @@ interface NormalNavbarContentsProps {
isInSearchMode: boolean;
setIsInSearchMode: (v: boolean) => void;
updateSearch: UpdateSearch;
collections: Collection[];
files: EnteFile[];
}
const NormalNavbarContents: React.FC<NormalNavbarContentsProps> = ({
@@ -1279,8 +1285,6 @@ const NormalNavbarContents: React.FC<NormalNavbarContentsProps> = ({
isInSearchMode,
setIsInSearchMode,
updateSearch,
collections,
files,
}) => (
<>
{!isInSearchMode && <SidebarButton onClick={openSidebar} />}
@@ -1288,8 +1292,6 @@ const NormalNavbarContents: React.FC<NormalNavbarContentsProps> = ({
isInSearchMode={isInSearchMode}
setIsInSearchMode={setIsInSearchMode}
updateSearch={updateSearch}
collections={collections}
files={files}
/>
{!isInSearchMode && <UploadButton onClick={openUploader} />}
</>

View File

@@ -2,6 +2,7 @@ import { NavbarBase, SelectionBar } from "@/base/components/Navbar";
import { sharedCryptoWorker } from "@/base/crypto";
import { useIsMobileWidth, useIsTouchscreen } from "@/base/hooks";
import log from "@/base/log";
import type { Collection } from "@/media/collection";
import downloadManager from "@/new/photos/services/download";
import { EnteFile } from "@/new/photos/types/file";
import { mergeMetadata } from "@/new/photos/utils/file";
@@ -63,7 +64,6 @@ import {
verifyPublicCollectionPassword,
} from "services/publicCollectionService";
import uploadManager from "services/upload/uploadManager";
import { Collection } from "types/collection";
import {
SelectedState,
SetFilesDownloadProgressAttributes,

View File

@@ -1,6 +1,23 @@
import { encryptMetadataJSON, sharedCryptoWorker } from "@/base/crypto";
import log from "@/base/log";
import { apiURL } from "@/base/origins";
import {
AddToCollectionRequest,
Collection,
CollectionMagicMetadata,
CollectionMagicMetadataProps,
CollectionPublicMagicMetadata,
CollectionShareeMagicMetadata,
CollectionToFileMap,
CollectionType,
CreatePublicAccessTokenRequest,
EncryptedCollection,
EncryptedFileKey,
MoveToCollectionRequest,
PublicURL,
RemoveFromCollectionRequest,
UpdatePublicURL,
} from "@/media/collection";
import { ItemVisibility } from "@/media/file-metadata";
import { getLocalFiles } from "@/new/photos/services/files";
import { EnteFile } from "@/new/photos/types/file";
@@ -19,23 +36,10 @@ import { getActualKey } from "@ente/shared/user";
import type { User } from "@ente/shared/user/types";
import { t } from "i18next";
import {
AddToCollectionRequest,
Collection,
CollectionFilesCount,
CollectionMagicMetadata,
CollectionMagicMetadataProps,
CollectionPublicMagicMetadata,
CollectionShareeMagicMetadata,
CollectionSummaries,
CollectionSummary,
CollectionToFileMap,
CreatePublicAccessTokenRequest,
EncryptedCollection,
EncryptedFileKey,
MoveToCollectionRequest,
PublicURL,
RemoveFromCollectionRequest,
UpdatePublicURL,
CollectionSummaryType,
} from "types/collection";
import { FamilyData } from "types/user";
import {
@@ -43,8 +47,6 @@ import {
ARCHIVE_SECTION,
COLLECTION_LIST_SORT_BY,
COLLECTION_SORT_ORDER,
CollectionSummaryType,
CollectionType,
DUMMY_UNCATEGORIZED_COLLECTION,
HIDDEN_ITEMS_SECTION,
TRASH_SECTION,

View File

@@ -1,5 +1,6 @@
import { ensureElectron } from "@/base/electron";
import log from "@/base/log";
import type { Collection } from "@/media/collection";
import {
fileCreationPhotoDate,
fileLocation,
@@ -27,7 +28,6 @@ import QueueProcessor, {
RequestCanceller,
} from "@ente/shared/utils/queueProcessor";
import i18n from "i18next";
import { Collection } from "types/collection";
import {
CollectionExportNames,
ExportProgress,

View File

@@ -1,6 +1,7 @@
import { ensureElectron } from "@/base/electron";
import { nameAndExtension } from "@/base/file";
import log from "@/base/log";
import type { Collection } from "@/media/collection";
import { FileType } from "@/media/file-type";
import { decodeLivePhoto } from "@/media/live-photo";
import downloadManager from "@/new/photos/services/download";
@@ -17,7 +18,6 @@ import { wait } from "@/utils/promise";
import { LS_KEYS, getData } from "@ente/shared/storage/localStorage";
import type { User } from "@ente/shared/user/types";
import { getLocalCollections } from "services/collectionService";
import { Collection } from "types/collection";
import {
CollectionExportNames,
ExportProgress,

View File

@@ -1,6 +1,7 @@
import { encryptMetadataJSON } from "@/base/crypto";
import log from "@/base/log";
import { apiURL } from "@/base/origins";
import type { Collection } from "@/media/collection";
import {
clearCachedThumbnailsIfChanged,
getLocalFiles,
@@ -19,7 +20,6 @@ import { batch } from "@/utils/array";
import HTTPService from "@ente/shared/network/HTTPService";
import { getToken } from "@ente/shared/storage/localStorage/helpers";
import exportService from "services/export";
import { Collection } from "types/collection";
import { SetFiles } from "types/gallery";
import { decryptFile, getLatestVersionFiles, sortFiles } from "utils/file";
import {

View File

@@ -1,12 +1,15 @@
import { sharedCryptoWorker } from "@/base/crypto";
import log from "@/base/log";
import { apiURL } from "@/base/origins";
import type {
Collection,
CollectionPublicMagicMetadata,
} from "@/media/collection";
import { EncryptedEnteFile, EnteFile } from "@/new/photos/types/file";
import { mergeMetadata } from "@/new/photos/utils/file";
import { CustomError, parseSharingErrorCodes } from "@ente/shared/error";
import HTTPService from "@ente/shared/network/HTTPService";
import localForage from "@ente/shared/storage/localForage";
import { Collection, CollectionPublicMagicMetadata } from "types/collection";
import { LocalSavedPublicCollectionFiles } from "types/publicCollection";
import { decryptFile, sortFiles } from "utils/file";

View File

@@ -1,175 +0,0 @@
import log from "@/base/log";
import { FileType } from "@/media/file-type";
import { createSearchQuery, search } from "@/new/photos/services/search";
import type {
SearchDateComponents,
SearchPerson,
} from "@/new/photos/services/search/types";
import {
City,
ClipSearchScores,
SearchOption,
SearchQuery,
Suggestion,
SuggestionType,
} from "@/new/photos/services/search/types";
import type { LocationTag } from "@/new/photos/services/user-entity";
import { EnteFile } from "@/new/photos/types/file";
import { Collection } from "types/collection";
import { getUniqueFiles } from "utils/file";
// Suggestions shown in the search dropdown when the user has typed something.
export const getAutoCompleteSuggestions =
(files: EnteFile[], collections: Collection[]) =>
async (searchPhrase: string): Promise<SearchOption[]> => {
log.debug(() => ["getAutoCompleteSuggestions", { searchPhrase }]);
try {
const searchPhrase2 = searchPhrase.trim().toLowerCase();
if (!searchPhrase2?.length) {
return [];
}
const suggestions: Suggestion[] = [
// The following functionality has moved to createSearchQuery
// - getClipSuggestion(searchPhrase)
// - getDateSuggestion(searchPhrase),
// - getLocationSuggestion(searchPhrase),
// - getFileTypeSuggestion(searchPhrase),
...(await createSearchQuery(searchPhrase)),
...getCollectionSuggestion(searchPhrase2, collections),
getFileNameSuggestion(searchPhrase2, files),
getFileCaptionSuggestion(searchPhrase2, files),
].filter((suggestion) => !!suggestion);
return convertSuggestionsToOptions(suggestions);
} catch (e) {
log.error("getAutoCompleteSuggestions failed", e);
return [];
}
};
async function convertSuggestionsToOptions(
suggestions: Suggestion[],
): Promise<SearchOption[]> {
const previewImageAppendedOptions: SearchOption[] = [];
for (const suggestion of suggestions) {
const searchQuery = convertSuggestionToSearchQuery(suggestion);
const resultFiles = getUniqueFiles(await search(searchQuery));
if (searchQuery?.clip) {
resultFiles.sort((a, b) => {
const aScore = searchQuery.clip.get(a.id);
const bScore = searchQuery.clip.get(b.id);
return bScore - aScore;
});
}
if (resultFiles.length) {
previewImageAppendedOptions.push({
...suggestion,
fileCount: resultFiles.length,
previewFiles: resultFiles.slice(0, 3),
});
}
}
return previewImageAppendedOptions;
}
function getCollectionSuggestion(
searchPhrase: string,
collections: Collection[],
): Suggestion[] {
const collectionResults = searchCollection(searchPhrase, collections);
return collectionResults.map(
(searchResult) =>
({
type: SuggestionType.COLLECTION,
value: searchResult.id,
label: searchResult.name,
}) as Suggestion,
);
}
function getFileNameSuggestion(
searchPhrase: string,
files: EnteFile[],
): Suggestion {
const matchedFiles = searchFilesByName(searchPhrase, files);
return {
type: SuggestionType.FILE_NAME,
value: matchedFiles.map((file) => file.id),
label: searchPhrase,
};
}
function getFileCaptionSuggestion(
searchPhrase: string,
files: EnteFile[],
): Suggestion {
const matchedFiles = searchFilesByCaption(searchPhrase, files);
return {
type: SuggestionType.FILE_CAPTION,
value: matchedFiles.map((file) => file.id),
label: searchPhrase,
};
}
function searchCollection(
searchPhrase: string,
collections: Collection[],
): Collection[] {
return collections.filter((collection) =>
collection.name.toLowerCase().includes(searchPhrase),
);
}
function searchFilesByName(searchPhrase: string, files: EnteFile[]) {
return files.filter(
(file) =>
file.id.toString().includes(searchPhrase) ||
file.metadata.title.toLowerCase().includes(searchPhrase),
);
}
function searchFilesByCaption(searchPhrase: string, files: EnteFile[]) {
return files.filter(
(file) =>
file.pubMagicMetadata &&
file.pubMagicMetadata.data.caption
?.toLowerCase()
.includes(searchPhrase),
);
}
function convertSuggestionToSearchQuery(option: Suggestion): SearchQuery {
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 };
}
}

View File

@@ -1,5 +1,6 @@
import log from "@/base/log";
import { apiURL } from "@/base/origins";
import type { Collection } from "@/media/collection";
import {
getLocalTrash,
getTrashedFiles,
@@ -9,7 +10,6 @@ import { EncryptedTrashItem, Trash } from "@/new/photos/types/file";
import HTTPService from "@ente/shared/network/HTTPService";
import localForage from "@ente/shared/storage/localForage";
import { getToken } from "@ente/shared/storage/localStorage/helpers";
import { Collection } from "types/collection";
import { SetFiles } from "types/gallery";
import { decryptFile } from "utils/file";
import { getCollection } from "./collectionService";

View File

@@ -4,6 +4,7 @@ import { lowercaseExtension, nameAndExtension } from "@/base/file";
import log from "@/base/log";
import type { Electron } from "@/base/types/ipc";
import { ComlinkWorker } from "@/base/worker/comlink-worker";
import type { Collection } from "@/media/collection";
import { FileType } from "@/media/file-type";
import { potentialFileTypeFromExtension } from "@/media/live-photo";
import { getLocalFiles } from "@/new/photos/services/files";
@@ -26,7 +27,6 @@ import {
} from "services/publicCollectionService";
import { getDisableCFUploadProxyFlag } from "services/userService";
import watcher from "services/watch";
import { Collection } from "types/collection";
import { SetFiles } from "types/gallery";
import { decryptFile, getUserOwnedFiles, sortFiles } from "utils/file";
import {

View File

@@ -11,6 +11,7 @@ import type {
FolderWatch,
FolderWatchSyncedFile,
} from "@/base/types/ipc";
import type { Collection } from "@/media/collection";
import { getLocalFiles } from "@/new/photos/services/files";
import { UPLOAD_RESULT } from "@/new/photos/services/upload/types";
import { EncryptedEnteFile } from "@/new/photos/types/file";
@@ -19,7 +20,6 @@ import debounce from "debounce";
import uploadManager, {
type UploadItemWithCollection,
} from "services/upload/uploadManager";
import { Collection } from "types/collection";
import { groupFilesBasedOnCollectionID } from "utils/file";
import { removeFromCollection } from "./collectionService";

View File

@@ -0,0 +1,33 @@
import type { EnteFile } from "@/new/photos/types/file";
export enum CollectionSummaryType {
folder = "folder",
favorites = "favorites",
album = "album",
archive = "archive",
trash = "trash",
uncategorized = "uncategorized",
all = "all",
outgoingShare = "outgoingShare",
incomingShareViewer = "incomingShareViewer",
incomingShareCollaborator = "incomingShareCollaborator",
sharedOnlyViaLink = "sharedOnlyViaLink",
archived = "archived",
defaultHidden = "defaultHidden",
hiddenItems = "hiddenItems",
pinned = "pinned",
}
export interface CollectionSummary {
id: number;
name: string;
type: CollectionSummaryType;
coverFile: EnteFile;
latestFile: EnteFile;
fileCount: number;
updationTime: number;
order?: number;
}
export type CollectionSummaries = Map<number, CollectionSummary>;
export type CollectionFilesCount = Map<number, number>;

View File

@@ -1,9 +1,9 @@
import type { Collection } from "@/media/collection";
import { EnteFile } from "@/new/photos/types/file";
import type { User } from "@ente/shared/user/types";
import { CollectionSelectorAttributes } from "components/Collections/CollectionSelector";
import { FilesDownloadProgressAttributes } from "components/FilesDownloadProgress";
import { TimeStampListItem } from "components/PhotoList";
import { Collection } from "types/collection";
export type SelectedState = {
[k: number]: boolean;

View File

@@ -1,6 +1,6 @@
import type { PublicURL } from "@/media/collection";
import { EnteFile } from "@/new/photos/types/file";
import { TimeStampListItem } from "components/PhotoList";
import { PublicURL } from "types/collection";
export interface PublicCollectionGalleryContextType {
token: string;

View File

@@ -1,5 +1,12 @@
import { ensureElectron } from "@/base/electron";
import log from "@/base/log";
import {
COLLECTION_ROLE,
type Collection,
CollectionMagicMetadataProps,
CollectionPublicMagicMetadataProps,
CollectionType,
} from "@/media/collection";
import { ItemVisibility } from "@/media/file-metadata";
import { getAllLocalFiles, getLocalFiles } from "@/new/photos/services/files";
import { EnteFile } from "@/new/photos/types/file";
@@ -25,13 +32,7 @@ import {
updatePublicCollectionMagicMetadata,
updateSharedCollectionMagicMetadata,
} from "services/collectionService";
import {
COLLECTION_ROLE,
Collection,
CollectionMagicMetadataProps,
CollectionPublicMagicMetadataProps,
CollectionSummaries,
} from "types/collection";
import { CollectionSummaries, CollectionSummaryType } from "types/collection";
import { SetFilesDownloadProgressAttributes } from "types/gallery";
import { downloadFilesWithProgress } from "utils/file";
import { isArchivedCollection, updateMagicMetadata } from "utils/magicMetadata";
@@ -42,30 +43,6 @@ export const DUMMY_UNCATEGORIZED_COLLECTION = -3;
export const HIDDEN_ITEMS_SECTION = -4;
export const ALL_SECTION = 0;
export enum CollectionType {
folder = "folder",
favorites = "favorites",
album = "album",
uncategorized = "uncategorized",
}
export enum CollectionSummaryType {
folder = "folder",
favorites = "favorites",
album = "album",
archive = "archive",
trash = "trash",
uncategorized = "uncategorized",
all = "all",
outgoingShare = "outgoingShare",
incomingShareViewer = "incomingShareViewer",
incomingShareCollaborator = "incomingShareCollaborator",
sharedOnlyViaLink = "sharedOnlyViaLink",
archived = "archived",
defaultHidden = "defaultHidden",
hiddenItems = "hiddenItems",
pinned = "pinned",
}
export enum COLLECTION_LIST_SORT_BY {
NAME,
CREATION_TIME_ASCENDING,

View File

@@ -1,8 +1,8 @@
import { sharedCryptoWorker } from "@/base/crypto";
import type { Collection } from "@/media/collection";
import { ItemVisibility } from "@/media/file-metadata";
import { EnteFile } from "@/new/photos/types/file";
import { MagicMetadataCore } from "@/new/photos/types/magicMetadata";
import { Collection } from "types/collection";
export function isArchivedFile(item: EnteFile): boolean {
if (!item || !item.magicMetadata || !item.magicMetadata.data) {

View File

@@ -1,11 +1,19 @@
import { ItemVisibility } from "@/media/file-metadata";
import { EnteFile } from "@/new/photos/types/file";
import type { EnteFile } from "@/new/photos/types/file";
import {
EncryptedMagicMetadata,
MagicMetadataCore,
type EncryptedMagicMetadata,
type MagicMetadataCore,
SUB_TYPE,
} from "@/new/photos/types/magicMetadata";
import { CollectionSummaryType, CollectionType } from "utils/collection";
// TODO: Audit this file
export enum CollectionType {
folder = "folder",
favorites = "favorites",
album = "album",
uncategorized = "uncategorized",
}
export enum COLLECTION_ROLE {
VIEWER = "VIEWER",
@@ -140,17 +148,3 @@ export interface CollectionPublicMagicMetadataProps {
export type CollectionPublicMagicMetadata =
MagicMetadataCore<CollectionPublicMagicMetadataProps>;
export interface CollectionSummary {
id: number;
name: string;
type: CollectionSummaryType;
coverFile: EnteFile;
latestFile: EnteFile;
fileCount: number;
updationTime: number;
order?: number;
}
export type CollectionSummaries = Map<number, CollectionSummary>;
export type CollectionFilesCount = Map<number, number>;

View File

@@ -1,17 +1,25 @@
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 type { EnteFile } from "../../types/file";
import { clipMatches, isMLEnabled } from "../ml";
import {
SuggestionType,
type DateSearchResult,
type LabelledFileType,
type LocalizedSearchData,
type SearchQuery,
import type {
City,
ClipSearchScores,
DateSearchResult,
LabelledFileType,
LocalizedSearchData,
SearchableData,
SearchDateComponents,
SearchOption,
SearchPerson,
SearchQuery,
Suggestion,
} from "./types";
import { SuggestionType } from "./types";
import type { SearchWorker } from "./worker";
/**
@@ -52,18 +60,18 @@ export const triggerSearchDataSync = () =>
void worker().then((w) => masterKeyFromSession().then((k) => w.sync(k)));
/**
* Set the files over which we will search.
* Set the collections and files over which we should search.
*/
export const setSearchableFiles = (enteFiles: EnteFile[]) =>
void worker().then((w) => w.setEnteFiles(enteFiles));
export const setSearchableData = (data: SearchableData) =>
void worker().then((w) => w.setSearchableData(data));
/**
* Convert a search string into a reusable "search query" that can be passed on
* to the {@link search} function.
* Convert a search string into a suggestions that can be shown in the search
* results, and can also be used filter the searchable files.
*
* @param searchString The string we want to search for.
*/
export const createSearchQuery = async (searchString: string) => {
export const suggestionsForString = async (searchString: string) => {
// Normalize it by trimming whitespace and converting to lowercase.
const s = searchString.trim().toLowerCase();
if (s.length == 0) return [];
@@ -73,7 +81,9 @@ 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, localizedSearchData())),
worker().then((w) =>
w.suggestionsForString(s, searchString, localizedSearchData()),
),
]);
return results.flat();
};
@@ -92,11 +102,11 @@ const clipSuggestions = async (s: string, searchString: string) => {
};
/**
* Search for and return the list of {@link EnteFile}s that match the given
* {@link search} query.
* Return the list of {@link EnteFile}s (from amongst the previously set
* {@link SearchableData}) that match the given search {@link suggestion}.
*/
export const search = async (search: SearchQuery) =>
worker().then((w) => w.search(search));
export const filterSearchableFiles = async (suggestion: SearchQuery) =>
worker().then((w) => w.filterSearchableFiles(suggestion));
/**
* Cached value of {@link localizedSearchData}.
@@ -149,3 +159,81 @@ 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<SearchOption[]> => {
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<SearchOption[]> {
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): SearchQuery {
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 };
}
}

View File

@@ -4,10 +4,19 @@
*/
import type { Location } from "@/base/types";
import type { Collection } from "@/media/collection";
import { FileType } from "@/media/file-type";
import type { EnteFile } from "@/new/photos/types/file";
import type { LocationTag } from "../user-entity";
/**
* The base data over which we should search.
*/
export interface SearchableData {
collections: Collection[];
files: EnteFile[];
}
export interface DateSearchResult {
components: SearchDateComponents;
label: string;
@@ -125,6 +134,7 @@ export interface Suggestion {
}
export interface SearchQuery {
suggestion?: SearchSuggestion;
date?: SearchDateComponents;
location?: LocationTag;
city?: City;
@@ -140,8 +150,23 @@ export interface SearchResultSummary {
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<number, number> }
| { 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;

View File

@@ -1,5 +1,6 @@
import { HTTPError } from "@/base/http";
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 { nullToUndefined } from "@/utils/transform";
@@ -19,6 +20,7 @@ import type {
LabelledFileType,
LocalizedSearchData,
Searchable,
SearchableData,
SearchDateComponents,
SearchQuery,
Suggestion,
@@ -30,7 +32,7 @@ import { SuggestionType } from "./types";
* remains responsive.
*/
export class SearchWorker {
private enteFiles: EnteFile[] = [];
private searchableData: SearchableData = { collections: [], files: [] };
private locationTags: Searchable<LocationTag>[] = [];
private cities: Searchable<City>[] = [];
@@ -60,18 +62,24 @@ export class SearchWorker {
}
/**
* Set the files that we should search across.
* Set the data that we should search across.
*/
setEnteFiles(enteFiles: EnteFile[]) {
this.enteFiles = enteFiles;
setSearchableData(data: SearchableData) {
this.searchableData = data;
}
/**
* Convert a search string into a reusable query.
* Convert a search string into a list of {@link SearchSuggestion}s.
*/
createSearchQuery(s: string, localizedSearchData: LocalizedSearchData) {
return createSearchQuery(
suggestionsForString(
s: string,
searchString: string,
localizedSearchData: LocalizedSearchData,
) {
return suggestionsForString(
s,
searchString,
this.searchableData,
localizedSearchData,
this.locationTags,
this.cities,
@@ -79,27 +87,73 @@ export class SearchWorker {
}
/**
* Return {@link EnteFile}s that satisfy the given {@link searchQuery}.
* Return {@link EnteFile}s that satisfy the given {@link suggestion}.
*/
search(searchQuery: SearchQuery) {
return this.enteFiles.filter((f) => isMatchingFile(f, searchQuery));
filterSearchableFiles(suggestion: SearchQuery) {
return this.searchableData.files.filter((f) =>
isMatchingFile(f, suggestion),
);
}
}
expose(SearchWorker);
const createSearchQuery = (
/**
* @param s The normalized form of {@link searchString}.
* @param searchString The original search string.
*/
const suggestionsForString = (
s: string,
searchString: string,
{ collections, files }: SearchableData,
{ locale, holidays, labelledFileTypes }: LocalizedSearchData,
locationTags: Searchable<LocationTag>[],
cities: Searchable<City>[],
): Suggestion[] =>
[
fileTypeSuggestions(s, labelledFileTypes),
dateSuggestions(s, locale, holidays),
locationSuggestions(s, locationTags, cities),
fileTypeSuggestions(s, labelledFileTypes),
collectionSuggestions(s, collections),
suggestionForFiles(fileNameMatches(s, files), searchString),
suggestionForFiles(fileCaptionMatches(s, files), searchString),
].flat();
const collectionSuggestions = (s: string, collections: Collection[]) =>
collections
.filter(({ name }) => name.toLowerCase().includes(s))
.map(({ id, name }) => ({
type: SuggestionType.COLLECTION,
value: 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 dateSuggestions = (
s: string,
locale: string,