diff --git a/web/apps/auth/src/pages/auth.tsx b/web/apps/auth/src/pages/auth.tsx index 06a4f51175..9c0502d46a 100644 --- a/web/apps/auth/src/pages/auth.tsx +++ b/web/apps/auth/src/pages/auth.tsx @@ -16,7 +16,6 @@ import { } from "@ente/shared/components/OverflowMenu"; import { AUTH_PAGES as PAGES } from "@ente/shared/constants/pages"; import LogoutOutlined from "@mui/icons-material/LogoutOutlined"; -import MoreHoriz from "@mui/icons-material/MoreHoriz"; import { Button, ButtonBase, @@ -155,10 +154,7 @@ const AuthNavbar: React.FC = () => { - } - > + } diff --git a/web/apps/photos/src/components/Collections/CollectionHeader.tsx b/web/apps/photos/src/components/Collections/CollectionHeader.tsx index ee5787a8de..3077cf341c 100644 --- a/web/apps/photos/src/components/Collections/CollectionHeader.tsx +++ b/web/apps/photos/src/components/Collections/CollectionHeader.tsx @@ -330,7 +330,7 @@ const CollectionOptions: React.FC = ({ /> } > {collectionSummaryType == "trash" ? ( diff --git a/web/apps/photos/src/components/Export.tsx b/web/apps/photos/src/components/Export.tsx index 43418b73e6..2f6f4b71d9 100644 --- a/web/apps/photos/src/components/Export.tsx +++ b/web/apps/photos/src/components/Export.tsx @@ -18,7 +18,6 @@ import { } from "@ente/shared/components/OverflowMenu"; import { CustomError } from "@ente/shared/error"; import FolderIcon from "@mui/icons-material/Folder"; -import MoreHoriz from "@mui/icons-material/MoreHoriz"; import { Box, Button, @@ -269,11 +268,7 @@ const DirectoryPathContainer = styled(LinkButton)( ); const ChangeDirectoryOption: React.FC = ({ onClick }) => ( - } - > + }> {t("CHANGE_FOLDER")} diff --git a/web/apps/photos/src/components/PhotoList/index.tsx b/web/apps/photos/src/components/PhotoList/index.tsx index a6dfaa3a5a..f6a3aeee1f 100644 --- a/web/apps/photos/src/components/PhotoList/index.tsx +++ b/web/apps/photos/src/components/PhotoList/index.tsx @@ -325,7 +325,7 @@ export function PhotoList({ timeStampList.push(getEmptyListItem()); } timeStampList.push(getVacuumItem(timeStampList)); - if (publicCollectionGalleryContext.accessedThroughSharedURL) { + if (publicCollectionGalleryContext.credentials) { if (publicCollectionGalleryContext.photoListFooter) { timeStampList.push( getPhotoListFooter( @@ -396,7 +396,7 @@ export function PhotoList({ if (hasFooter) { return timeStampList; } - if (publicCollectionGalleryContext.accessedThroughSharedURL) { + if (publicCollectionGalleryContext.credentials) { if (publicCollectionGalleryContext.photoListFooter) { return [ ...timeStampList, @@ -413,7 +413,7 @@ export function PhotoList({ } }); }, [ - publicCollectionGalleryContext.accessedThroughSharedURL, + publicCollectionGalleryContext.credentials, showAppDownloadBanner, publicCollectionGalleryContext.photoListFooter, ]); @@ -521,7 +521,7 @@ export function PhotoList({ const getVacuumItem = (timeStampList) => { let footerHeight; - if (publicCollectionGalleryContext.accessedThroughSharedURL) { + if (publicCollectionGalleryContext.credentials) { footerHeight = publicCollectionGalleryContext.referralCode ? ALBUM_FOOTER_HEIGHT_WITH_REFERRAL : ALBUM_FOOTER_HEIGHT; diff --git a/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx b/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx index df4a4ab0a1..f140a3b05c 100644 --- a/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx +++ b/web/apps/photos/src/components/PhotoViewer/FileInfo/index.tsx @@ -213,7 +213,7 @@ export const FileInfo: React.FC = ({ title={t("location")} caption={ !mapEnabled || - publicCollectionGalleryContext.accessedThroughSharedURL ? ( + publicCollectionGalleryContext.credentials ? ( = ({ /> } /> - {!publicCollectionGalleryContext.accessedThroughSharedURL && ( + {!publicCollectionGalleryContext.credentials && ( = ({ if ( open && directlyShowUploadFiles && - publicCollectionGalleryContext.accessedThroughSharedURL + publicCollectionGalleryContext.credentials ) { uploadFiles(); onClose(); diff --git a/web/apps/photos/src/components/Upload/Uploader.tsx b/web/apps/photos/src/components/Upload/Uploader.tsx index 505fd992ae..1c3c17e108 100644 --- a/web/apps/photos/src/components/Upload/Uploader.tsx +++ b/web/apps/photos/src/components/Upload/Uploader.tsx @@ -249,7 +249,7 @@ export default function Uploader({ setUploadProgressView, }, onUploadFile, - publicCollectionGalleryContext, + publicCollectionGalleryContext.credentials, ); if (uploadManager.isUploadRunning()) { @@ -288,11 +288,7 @@ export default function Uploader({ setDesktopZipItems(zipItems); }); } - }, [ - publicCollectionGalleryContext.accessedThroughSharedURL, - publicCollectionGalleryContext.token, - publicCollectionGalleryContext.passwordToken, - ]); + }, [publicCollectionGalleryContext.credentials]); // Handle selected files when user selects files for upload through the open // file / open folder selection dialog, or drag-and-drops them. @@ -417,10 +413,10 @@ export default function Uploader({ props.setLoading(false); (async () => { - if (publicCollectionGalleryContext.accessedThroughSharedURL) { + if (publicCollectionGalleryContext.credentials) { const uploaderName = await getPublicCollectionUploaderName( getPublicCollectionUID( - publicCollectionGalleryContext.token, + publicCollectionGalleryContext.credentials.accessToken, ), ); uploaderNameRef.current = uploaderName; @@ -727,7 +723,7 @@ export default function Uploader({ if (!skipSave) { savePublicCollectionUploaderName( getPublicCollectionUID( - publicCollectionGalleryContext.token, + publicCollectionGalleryContext.credentials.accessToken, ), uploaderName, ); diff --git a/web/apps/photos/src/components/WatchFolder.tsx b/web/apps/photos/src/components/WatchFolder.tsx index eec2309a24..c80d336412 100644 --- a/web/apps/photos/src/components/WatchFolder.tsx +++ b/web/apps/photos/src/components/WatchFolder.tsx @@ -23,7 +23,6 @@ import CheckIcon from "@mui/icons-material/Check"; import DoNotDisturbOutlinedIcon from "@mui/icons-material/DoNotDisturbOutlined"; import FolderCopyOutlinedIcon from "@mui/icons-material/FolderCopyOutlined"; import FolderOpenIcon from "@mui/icons-material/FolderOpen"; -import MoreHorizIcon from "@mui/icons-material/MoreHoriz"; import { Box, Button, @@ -311,14 +310,13 @@ interface EntryOptionsProps { const EntryOptions: React.FC = ({ confirmStopWatching }) => { return ( theme.colors.background.elevated2, }, }} - ariaControls={"watch-mapping-option"} - triggerButtonIcon={} > setShowNavBar(show); - const onGenericError = useCallback((e: unknown) => { log.error(e); // The generic error handler is sometimes called in the context of @@ -172,21 +170,30 @@ export default function App({ Component, pageProps }: AppProps) { const logout = useCallback(() => void photosLogout(), []); - const appContext = { - showNavBar, - showLoadingBar, - hideLoadingBar, - watchFolderView, - setWatchFolderView, - watchFolderFiles, - setWatchFolderFiles, - setNotificationAttributes, - themeColor, - setThemeColor, - showMiniDialog, - onGenericError, - logout, - }; + const appContext = useMemo( + () => ({ + showNavBar: (show: boolean) => setShowNavBar(show), + showLoadingBar, + hideLoadingBar, + watchFolderView, + setWatchFolderView, + watchFolderFiles, + setWatchFolderFiles, + setNotificationAttributes, + themeColor, + setThemeColor, + showMiniDialog, + onGenericError, + logout, + }), + [ + showLoadingBar, + hideLoadingBar, + showMiniDialog, + onGenericError, + logout, + ], + ); const title = isI18nReady ? t("title_photos") : staticAppTitle; diff --git a/web/apps/photos/src/pages/shared-albums.tsx b/web/apps/photos/src/pages/shared-albums.tsx index 0a612bac4a..241f31fe15 100644 --- a/web/apps/photos/src/pages/shared-albums.tsx +++ b/web/apps/photos/src/pages/shared-albums.tsx @@ -8,7 +8,7 @@ import { useIsTouchscreen, } from "@/base/components/utils/hooks"; import { sharedCryptoWorker } from "@/base/crypto"; -import { isHTTP401Error } from "@/base/http"; +import { isHTTP401Error, PublicAlbumsCredentials } from "@/base/http"; import log from "@/base/log"; import { downloadManager } from "@/gallery/services/download"; import { updateShouldDisableCFUploadProxy } from "@/gallery/services/upload"; @@ -44,7 +44,6 @@ import AddPhotoAlternateOutlined from "@mui/icons-material/AddPhotoAlternateOutl import CloseIcon from "@mui/icons-material/Close"; import DownloadIcon from "@mui/icons-material/Download"; import FileDownloadOutlinedIcon from "@mui/icons-material/FileDownloadOutlined"; -import MoreHoriz from "@mui/icons-material/MoreHoriz"; import type { ButtonProps, IconButtonProps } from "@mui/material"; import { Box, Button, IconButton, Stack, styled, Tooltip } from "@mui/material"; import Typography from "@mui/material/Typography"; @@ -86,9 +85,7 @@ import { downloadSelectedFiles, getSelectedFiles } from "utils/file"; import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery"; export default function PublicCollectionGallery() { - const token = useRef(null); - // passwordJWTToken refers to the jwt token which is used for album protected by password. - const passwordJWTToken = useRef(null); + const credentials = useRef(); const collectionKey = useRef(null); const url = useRef(null); const referralCode = useRef(""); @@ -242,16 +239,13 @@ export default function PublicCollectionGallery() { ck.length < 50 ? await cryptoWorker.toB64(bs58.decode(ck)) : await cryptoWorker.fromHex(ck); - token.current = t; - downloadManager.setPublicAlbumsCredentials({ - accessToken: token.current, - }); - await updateShouldDisableCFUploadProxy(); collectionKey.current = dck; url.current = window.location.href; const localCollection = await getLocalPublicCollection( collectionKey.current, ); + const accessToken = t; + let accessTokenJWT: string | undefined; if (localCollection) { referralCode.current = await getReferralCode(); const sortAsc: boolean = @@ -260,20 +254,20 @@ export default function PublicCollectionGallery() { const isPasswordProtected = localCollection?.publicURLs?.[0]?.passwordEnabled; setIsPasswordProtected(isPasswordProtected); - const collectionUID = getPublicCollectionUID(token.current); + const collectionUID = getPublicCollectionUID(accessToken); const localFiles = await getLocalPublicFiles(collectionUID); const localPublicFiles = sortFiles( mergeMetadata(localFiles), sortAsc, ); setPublicFiles(localPublicFiles); - passwordJWTToken.current = + accessTokenJWT = await getLocalPublicCollectionPassword(collectionUID); - downloadManager.setPublicAlbumsCredentials({ - accessToken: token.current, - accessTokenJWT: passwordJWTToken.current, - }); } + credentials.current = { accessToken, accessTokenJWT }; + downloadManager.setPublicAlbumsCredentials(credentials.current); + // Update the CF proxy flag, but we don't need to block on it. + void updateShouldDisableCFUploadProxy(); await syncWithRemote(); } finally { if (!redirectingToWebsite) { @@ -322,12 +316,14 @@ export default function PublicCollectionGallery() { }, [onAddPhotos]); const syncWithRemote = async () => { - const collectionUID = getPublicCollectionUID(token.current); + const collectionUID = getPublicCollectionUID( + credentials.current.accessToken, + ); try { showLoadingBar(); setLoading(true); const [collection, userReferralCode] = await getPublicCollection( - token.current, + credentials.current.accessToken, collectionKey.current, ); referralCode.current = userReferralCode; @@ -338,19 +334,22 @@ export default function PublicCollectionGallery() { setIsPasswordProtected(isPasswordProtected); setErrorMessage(null); - // remove outdated password, sharer has disabled the password - if (!isPasswordProtected && passwordJWTToken.current) { - passwordJWTToken.current = null; + // Remove the locally saved outdated password token if the sharer + // has disabled password protection on the link. + if (!isPasswordProtected && credentials.current.accessTokenJWT) { + credentials.current.accessTokenJWT = undefined; + downloadManager.setPublicAlbumsCredentials(credentials.current); savePublicCollectionPassword(collectionUID, null); } + if ( !isPasswordProtected || - (isPasswordProtected && passwordJWTToken.current) + (isPasswordProtected && credentials.current.accessTokenJWT) ) { try { await syncPublicFiles( - token.current, - passwordJWTToken.current, + credentials.current.accessToken, + credentials.current.accessTokenJWT, collection, setPublicFiles, ); @@ -359,11 +358,15 @@ export default function PublicCollectionGallery() { if (parsedError.message === CustomError.TOKEN_EXPIRED) { // passwordToken has expired, sharer has changed the password, // so,clearing local cache token value to prompt user to re-enter password - passwordJWTToken.current = null; + credentials.current.accessTokenJWT = undefined; + downloadManager.setPublicAlbumsCredentials( + credentials.current, + ); } } } - if (isPasswordProtected && !passwordJWTToken.current) { + + if (isPasswordProtected && !credentials.current.accessTokenJWT) { await removePublicFiles(collectionUID); } } catch (e) { @@ -399,18 +402,17 @@ export default function PublicCollectionGallery() { setFieldError, ) => { try { - const jwtToken = await verifyPublicAlbumPassword( + const accessTokenJWT = await verifyPublicAlbumPassword( publicCollection.publicURLs[0]!, password, - token.current, + credentials.current.accessToken, ); - passwordJWTToken.current = jwtToken; - downloadManager.setPublicAlbumsCredentials({ - accessToken: token.current, - accessTokenJWT: passwordJWTToken.current, - }); - const collectionUID = getPublicCollectionUID(token.current); - await savePublicCollectionPassword(collectionUID, jwtToken); + credentials.current.accessTokenJWT = accessTokenJWT; + downloadManager.setPublicAlbumsCredentials(credentials.current); + const collectionUID = getPublicCollectionUID( + credentials.current.accessToken, + ); + await savePublicCollectionPassword(collectionUID, accessTokenJWT); } catch (e) { log.error("Failed to verifyLinkPassword", e); if (isHTTP401Error(e)) { @@ -424,41 +426,6 @@ export default function PublicCollectionGallery() { await syncWithRemote(); }; - if (loading) { - if (!publicFiles) { - return ( - - - - ); - } - } else { - if (errorMessage) { - return {errorMessage}; - } - if (isPasswordProtected && !passwordJWTToken.current) { - return ( - - - {t("password")} - - {t("link_password_description")} - - - - - ); - } - if (!publicFiles) { - return {t("NOT_FOUND")}; - } - } - const clearSelection = () => { if (!selected?.count) { return; @@ -488,17 +455,45 @@ export default function PublicCollectionGallery() { } }; + if (loading && (!publicFiles || !credentials.current)) { + return ( + + + + ); + } else if (errorMessage) { + return {errorMessage}; + } else if (isPasswordProtected && !credentials.current.accessTokenJWT) { + return ( + + + {t("password")} + + {t("link_password_description")} + + + + + ); + } else if (!publicFiles || !credentials.current) { + return {t("NOT_FOUND")}; + } + + // TODO: memo this (after the dependencies are traceable). + const context = { + credentials: credentials.current, + referralCode: referralCode.current, + photoListHeader, + photoListFooter, + }; + return ( - + = ({ fileCount={publicFiles.length} /> {downloadEnabled && ( - } - > + } onClick={downloadAllFiles} diff --git a/web/apps/photos/src/services/upload/upload-service.ts b/web/apps/photos/src/services/upload/upload-service.ts index 7bf38ee3fc..6b0e020243 100644 --- a/web/apps/photos/src/services/upload/upload-service.ts +++ b/web/apps/photos/src/services/upload/upload-service.ts @@ -3,6 +3,7 @@ import type { BytesOrB64 } from "@/base/crypto/types"; import { type CryptoWorker } from "@/base/crypto/worker"; import { ensureElectron } from "@/base/electron"; import { basename, nameAndExtension } from "@/base/file-name"; +import type { PublicAlbumsCredentials } from "@/base/http"; import log from "@/base/log"; import { CustomErrorMessage } from "@/base/types/ipc"; import { extractVideoMetadata } from "@/gallery/services/ffmpeg"; @@ -42,10 +43,7 @@ import { mergeUint8Arrays } from "@/utils/array"; import { ensureInteger, ensureNumber } from "@/utils/ensure"; import { CustomError, handleUploadError } from "@ente/shared/error"; import { addToCollection } from "services/collectionService"; -import { - PublicUploadProps, - type LivePhotoAssets, -} from "services/upload/uploadManager"; +import { type LivePhotoAssets } from "services/upload/uploadManager"; import * as convert from "xml-js"; import { tryParseEpochMicrosecondsFromFileName } from "./date"; import publicUploadHttpClient from "./publicUploadHttpClient"; @@ -110,17 +108,17 @@ const multipartChunksPerPart = 5; class UploadService { private uploadURLs: UploadURL[] = []; private pendingUploadCount: number = 0; - private publicUploadProps: PublicUploadProps = undefined; + private publicAlbumsCredentials: PublicAlbumsCredentials | undefined; private activeUploadURLRefill: Promise | undefined; - init(publicUploadProps: PublicUploadProps) { - this.publicUploadProps = publicUploadProps; + init(publicAlbumsCredentials: PublicAlbumsCredentials | undefined) { + this.publicAlbumsCredentials = publicAlbumsCredentials; } logout() { this.uploadURLs = []; this.pendingUploadCount = 0; - this.publicUploadProps = undefined; + this.publicAlbumsCredentials = undefined; this.activeUploadURLRefill = undefined; } @@ -153,11 +151,12 @@ class UploadService { } async uploadFile(uploadFile: UploadFile) { - if (this.publicUploadProps.accessedThroughSharedURL) { + if (this.publicAlbumsCredentials) { return publicUploadHttpClient.uploadFile( uploadFile, - this.publicUploadProps.token, - this.publicUploadProps.passwordToken, + // TODO: publicAlbumsCredentials + this.publicAlbumsCredentials.accessToken, + this.publicAlbumsCredentials.accessTokenJWT, ); } else { return UploadHttpClient.uploadFile(uploadFile); @@ -188,16 +187,10 @@ class UploadService { private async _refillUploadURLs() { let urls: UploadURL[]; - if (this.publicUploadProps.accessedThroughSharedURL) { - if (!this.publicUploadProps.token) { - throw Error(CustomError.TOKEN_MISSING); - } + if (this.publicAlbumsCredentials) { urls = await publicUploadHttpClient.fetchUploadURLs( this.pendingUploadCount, - { - accessToken: this.publicUploadProps.token, - accessTokenJWT: this.publicUploadProps.passwordToken, - }, + this.publicAlbumsCredentials, ); } else { urls = await UploadHttpClient.fetchUploadURLs( @@ -208,11 +201,12 @@ class UploadService { } async fetchMultipartUploadURLs(count: number) { - if (this.publicUploadProps.accessedThroughSharedURL) { + if (this.publicAlbumsCredentials) { + // TODO: publicAlbumsCredentials return await publicUploadHttpClient.fetchMultipartUploadURLs( count, - this.publicUploadProps.token, - this.publicUploadProps.passwordToken, + this.publicAlbumsCredentials.accessToken, + this.publicAlbumsCredentials.accessTokenJWT, ); } else { return await UploadHttpClient.fetchMultipartUploadURLs(count); diff --git a/web/apps/photos/src/services/upload/uploadManager.ts b/web/apps/photos/src/services/upload/uploadManager.ts index f49925210e..474a4645f2 100644 --- a/web/apps/photos/src/services/upload/uploadManager.ts +++ b/web/apps/photos/src/services/upload/uploadManager.ts @@ -2,6 +2,7 @@ import { isDesktop } from "@/base/app"; import { createComlinkCryptoWorker } from "@/base/crypto"; import { type CryptoWorker } from "@/base/crypto/worker"; import { lowercaseExtension, nameAndExtension } from "@/base/file-name"; +import type { PublicAlbumsCredentials } from "@/base/http"; import log from "@/base/log"; import type { Electron } from "@/base/types/ipc"; import { ComlinkWorker } from "@/base/worker/comlink-worker"; @@ -96,12 +97,6 @@ export interface LivePhotoAssets { video: UploadItem; } -export interface PublicUploadProps { - token: string; - passwordToken: string; - accessedThroughSharedURL: boolean; -} - interface UploadCancelStatus { value: boolean; } @@ -325,7 +320,7 @@ class UploadManager { private onUploadFile: (file: EnteFile) => void; private collections: Map; private uploadInProgress: boolean; - private publicUploadProps: PublicUploadProps; + private publicAlbumsCredentials: PublicAlbumsCredentials | undefined; private uploaderName: string; private uiService: UIService; @@ -336,12 +331,12 @@ class UploadManager { public async init( progressUpdater: ProgressUpdater, onUploadFile: (file: EnteFile) => void, - publicCollectProps: PublicUploadProps, + publicAlbumsCredentials: PublicAlbumsCredentials | undefined, ) { this.uiService.init(progressUpdater); - UploadService.init(publicCollectProps); + UploadService.init(publicAlbumsCredentials); this.onUploadFile = onUploadFile; - this.publicUploadProps = publicCollectProps; + this.publicAlbumsCredentials = publicAlbumsCredentials; } logout() { @@ -497,9 +492,11 @@ class UploadManager { }; private async updateExistingFilesAndCollections(collections: Collection[]) { - if (this.publicUploadProps.accessedThroughSharedURL) { + if (this.publicAlbumsCredentials) { this.existingFiles = await getLocalPublicFiles( - getPublicCollectionUID(this.publicUploadProps.token), + getPublicCollectionUID( + this.publicAlbumsCredentials.accessToken, + ), ); } else { this.existingFiles = getUserOwnedFiles(await getLocalFiles()); diff --git a/web/apps/photos/src/utils/publicCollectionGallery/index.ts b/web/apps/photos/src/utils/publicCollectionGallery/index.ts index c0e4cc9ff6..d5334d7f3f 100644 --- a/web/apps/photos/src/utils/publicCollectionGallery/index.ts +++ b/web/apps/photos/src/utils/publicCollectionGallery/index.ts @@ -1,21 +1,23 @@ +import type { PublicAlbumsCredentials } from "@/base/http"; import { TimeStampListItem } from "components/PhotoList"; import { createContext } from "react"; export interface PublicCollectionGalleryContextType { - token: string; - passwordToken: string; + /** + * The {@link PublicAlbumsCredentials} to use. These are guaranteed to be + * set if we are in the context of the public albums app, and will be + * undefined when we're in the default photos app context. + */ + credentials: PublicAlbumsCredentials | undefined; referralCode: string | null; - accessedThroughSharedURL: boolean; photoListHeader: TimeStampListItem; photoListFooter: TimeStampListItem; } export const PublicCollectionGalleryContext = createContext({ - token: null, - passwordToken: null, + credentials: undefined, referralCode: null, - accessedThroughSharedURL: false, photoListHeader: null, photoListFooter: null, }); diff --git a/web/packages/new/photos/components/CollectionsSortOptions.tsx b/web/packages/new/photos/components/CollectionsSortOptions.tsx index 59e42fb1a7..d33bce1ed1 100644 --- a/web/packages/new/photos/components/CollectionsSortOptions.tsx +++ b/web/packages/new/photos/components/CollectionsSortOptions.tsx @@ -24,10 +24,10 @@ interface CollectionsSortOptionsProps { */ nestedInDialog?: boolean; /** - * Set this to true to disable the background in the button that triggers - * the menu. + * Set this to true to disable the background for the icon button that + * triggers the menu. */ - disableTriggerButtonBackground?: boolean; + transparentTriggerButtonBackground?: boolean; } /** @@ -37,11 +37,11 @@ interface CollectionsSortOptionsProps { */ export const CollectionsSortOptions: React.FC = ({ nestedInDialog, - disableTriggerButtonBackground, + transparentTriggerButtonBackground, ...optProps }) => ( } menuPaperProps={{ sx: { @@ -54,7 +54,7 @@ export const CollectionsSortOptions: React.FC = ({ triggerButtonProps={{ sx: { backgroundColor: (theme) => - disableTriggerButtonBackground + transparentTriggerButtonBackground ? undefined : theme.colors.fill.faint, }, diff --git a/web/packages/new/photos/components/gallery/BarImpl.tsx b/web/packages/new/photos/components/gallery/BarImpl.tsx index 6cdaf4b6ea..43ca6853ef 100644 --- a/web/packages/new/photos/components/gallery/BarImpl.tsx +++ b/web/packages/new/photos/components/gallery/BarImpl.tsx @@ -223,7 +223,7 @@ export const GalleryBarImpl: React.FC = ({ diff --git a/web/packages/new/photos/components/gallery/PeopleHeader.tsx b/web/packages/new/photos/components/gallery/PeopleHeader.tsx index ca8a1bca2b..73a2e680f6 100644 --- a/web/packages/new/photos/components/gallery/PeopleHeader.tsx +++ b/web/packages/new/photos/components/gallery/PeopleHeader.tsx @@ -40,7 +40,6 @@ import ClearIcon from "@mui/icons-material/Clear"; import EditIcon from "@mui/icons-material/Edit"; import HideImageOutlinedIcon from "@mui/icons-material/HideImageOutlined"; import ListAltOutlinedIcon from "@mui/icons-material/ListAltOutlined"; -import MoreHorizIcon from "@mui/icons-material/MoreHoriz"; import RestoreIcon from "@mui/icons-material/Restore"; import VisibilityOutlinedIcon from "@mui/icons-material/VisibilityOutlined"; import { @@ -146,10 +145,7 @@ const CGroupPersonHeader: React.FC = ({ person }) => { name={name} fileCount={person.fileIDs.length} /> - } - > + } centerAlign @@ -210,10 +206,7 @@ const IgnoredPersonHeader: React.FC = ({ nameProps={{ color: "text.muted" }} fileCount={person.fileIDs.length} /> - } - > + } centerAlign @@ -271,10 +264,7 @@ const ClusterPersonHeader: React.FC = ({ - } - > + } centerAlign diff --git a/web/packages/shared/components/OverflowMenu.tsx b/web/packages/shared/components/OverflowMenu.tsx index ab4b71fb95..2469094358 100644 --- a/web/packages/shared/components/OverflowMenu.tsx +++ b/web/packages/shared/components/OverflowMenu.tsx @@ -1,4 +1,5 @@ import { FluidContainer } from "@ente/shared/components/Container"; +import MoreHorizIcon from "@mui/icons-material/MoreHoriz"; import { Box, IconButton, @@ -10,7 +11,7 @@ import { type PaperProps, } from "@mui/material"; import Menu, { type MenuProps } from "@mui/material/Menu"; -import React, { createContext, useContext, useState } from "react"; +import React, { createContext, useContext, useMemo, useState } from "react"; const OverflowMenuContext = createContext({ // eslint-disable-next-line @typescript-eslint/no-empty-function @@ -18,13 +19,30 @@ const OverflowMenuContext = createContext({ }); interface OverflowMenuProps { - triggerButtonIcon: React.ReactNode; + /** + * An ARIA identifier for the overflow menu when it is displayed. + */ + ariaID: string; + /** + * The icon for the trigger button. + * + * If not provided, then by default the MoreHoriz icon from MUI is used. + */ + triggerButtonIcon?: React.ReactNode; + /** + * Optional additional properties for the trigger icon button. + */ triggerButtonProps?: Partial; - children?: React.ReactNode; - ariaControls: string; + /** + * Optional additional properties for the MUI {@link Paper} that underlies + * the {@link Menu}. + */ menuPaperProps?: Partial; } +/** + * A custom MUI {@link Menu} with some Ente specific styling applied to it. + */ export const StyledMenu = styled(Menu)` & .MuiPaper-root { margin: 16px auto; @@ -38,38 +56,47 @@ export const StyledMenu = styled(Menu)` } `; -export const OverflowMenu: React.FC = ({ - children, - ariaControls, +/** + * An overflow menu showing {@link OverflowMenuOptions}, alongwith a button to + * trigger the visibility of the menu. + */ +export const OverflowMenu: React.FC< + React.PropsWithChildren +> = ({ + ariaID, triggerButtonIcon, triggerButtonProps, menuPaperProps, + children, }) => { - const [sortByEl, setSortByEl] = useState( - null, + const [anchorEl, setAnchorEl] = useState(); + const context = useMemo( + () => ({ close: () => setAnchorEl(undefined) }), + [], ); - const handleClose = () => setSortByEl(null); return ( - + setSortByEl(event.currentTarget)} - aria-controls={sortByEl ? ariaControls : undefined} + onClick={(event) => setAnchorEl(event.currentTarget)} + aria-controls={anchorEl ? ariaID : undefined} aria-haspopup="true" - aria-expanded={sortByEl ? "true" : undefined} + aria-expanded={anchorEl ? "true" : undefined} {...triggerButtonProps} > - {triggerButtonIcon} + {triggerButtonIcon ?? } setAnchorEl(undefined)} MenuListProps={{ disablePadding: true, - "aria-labelledby": ariaControls, + "aria-labelledby": ariaID, + }} + slotProps={{ + paper: menuPaperProps, }} - PaperProps={menuPaperProps} anchorOrigin={{ vertical: "bottom", horizontal: "right", @@ -90,7 +117,6 @@ interface OverflowMenuOptionProps { color?: ButtonProps["color"]; startIcon?: React.ReactNode; endIcon?: React.ReactNode; - keepOpenAfterClick?: boolean; children?: any; // To avoid changing old places without an audit, new code should use this // option explicitly to fix/tweak the alignment of the button label and @@ -103,7 +129,6 @@ export const OverflowMenuOption: React.FC = ({ color = "primary", startIcon, endIcon, - keepOpenAfterClick, centerAlign, children, }) => { @@ -111,10 +136,9 @@ export const OverflowMenuOption: React.FC = ({ const handleClick = () => { onClick(); - if (!keepOpenAfterClick) { - menuContext.close(); - } + menuContext.close(); }; + return (