[web] General refactoring, focus on public albums (#4254)

This commit is contained in:
Manav Rathi
2024-11-29 17:24:31 +05:30
committed by GitHub
18 changed files with 211 additions and 220 deletions

View File

@@ -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 = () => {
<EnteLogo />
</HorizontalFlex>
<HorizontalFlex position={"absolute"} right="24px">
<OverflowMenu
ariaControls={"auth-options"}
triggerButtonIcon={<MoreHoriz />}
>
<OverflowMenu ariaID={"auth-options"}>
<OverflowMenuOption
color="critical"
startIcon={<LogoutOutlined />}

View File

@@ -330,7 +330,7 @@ const CollectionOptions: React.FC<CollectionOptionsProps> = ({
/>
<OverflowMenu
ariaControls={"collection-options"}
ariaID={"collection-options"}
triggerButtonIcon={<MoreHoriz ref={overFlowMenuIconRef} />}
>
{collectionSummaryType == "trash" ? (

View File

@@ -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<ButtonishProps> = ({ onClick }) => (
<OverflowMenu
triggerButtonProps={{ sx: { ml: 1 } }}
ariaControls={"export-option"}
triggerButtonIcon={<MoreHoriz />}
>
<OverflowMenu ariaID="export-option" triggerButtonProps={{ sx: { ml: 1 } }}>
<OverflowMenuOption onClick={onClick} startIcon={<FolderIcon />}>
{t("CHANGE_FOLDER")}
</OverflowMenuOption>

View File

@@ -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;

View File

@@ -213,7 +213,7 @@ export const FileInfo: React.FC<FileInfoProps> = ({
title={t("location")}
caption={
!mapEnabled ||
publicCollectionGalleryContext.accessedThroughSharedURL ? (
publicCollectionGalleryContext.credentials ? (
<Link
href={openStreetMapLink(location)}
target="_blank"
@@ -245,7 +245,7 @@ export const FileInfo: React.FC<FileInfoProps> = ({
/>
}
/>
{!publicCollectionGalleryContext.accessedThroughSharedURL && (
{!publicCollectionGalleryContext.credentials && (
<MapBox
location={location}
mapEnabled={mapEnabled}

View File

@@ -345,7 +345,7 @@ function PhotoViewer(props: PhotoViewerProps) {
function updateIsOwnFile(file: EnteFile) {
const isOwnFile =
!publicCollectionGalleryContext.accessedThroughSharedURL &&
!publicCollectionGalleryContext.credentials &&
galleryContext.user?.id === file.ownerID;
setIsOwnFile(isOwnFile);
}

View File

@@ -62,7 +62,7 @@ export const UploadTypeSelector: React.FC<UploadTypeSelectorProps> = ({
if (
open &&
directlyShowUploadFiles &&
publicCollectionGalleryContext.accessedThroughSharedURL
publicCollectionGalleryContext.credentials
) {
uploadFiles();
onClose();

View File

@@ -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,
);

View File

@@ -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<EntryOptionsProps> = ({ confirmStopWatching }) => {
return (
<OverflowMenu
ariaID={"watch-mapping-option"}
menuPaperProps={{
sx: {
backgroundColor: (theme) =>
theme.colors.background.elevated2,
},
}}
ariaControls={"watch-mapping-option"}
triggerButtonIcon={<MoreHorizIcon />}
>
<OverflowMenuOption
color="critical"

View File

@@ -44,7 +44,7 @@ import { t } from "i18next";
import type { AppProps } from "next/app";
import { useRouter } from "next/router";
import "photoswipe/dist/photoswipe.css";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useMemo, useState } from "react";
import LoadingBar from "react-top-loading-bar";
import { resumeExportsIfNeeded } from "services/export";
import { photosLogout } from "services/logout";
@@ -156,8 +156,6 @@ export default function App({ Component, pageProps }: AppProps) {
setNotificationView(true);
}, [notificationAttributes]);
const showNavBar = (show: boolean) => 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;

View File

@@ -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<string>(null);
// passwordJWTToken refers to the jwt token which is used for album protected by password.
const passwordJWTToken = useRef<string>(null);
const credentials = useRef<PublicAlbumsCredentials | undefined>();
const collectionKey = useRef<string>(null);
const url = useRef<string>(null);
const referralCode = useRef<string>("");
@@ -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 (
<VerticallyCentered>
<ActivityIndicator />
</VerticallyCentered>
);
}
} else {
if (errorMessage) {
return <VerticallyCentered>{errorMessage}</VerticallyCentered>;
}
if (isPasswordProtected && !passwordJWTToken.current) {
return (
<VerticallyCentered>
<FormPaper>
<FormPaperTitle>{t("password")}</FormPaperTitle>
<Typography color={"text.muted"} mb={2} variant="small">
{t("link_password_description")}
</Typography>
<SingleInputForm
callback={verifyLinkPassword}
placeholder={t("password")}
buttonText={t("unlock")}
fieldType="password"
/>
</FormPaper>
</VerticallyCentered>
);
}
if (!publicFiles) {
return <VerticallyCentered>{t("NOT_FOUND")}</VerticallyCentered>;
}
}
const clearSelection = () => {
if (!selected?.count) {
return;
@@ -488,17 +455,45 @@ export default function PublicCollectionGallery() {
}
};
if (loading && (!publicFiles || !credentials.current)) {
return (
<VerticallyCentered>
<ActivityIndicator />
</VerticallyCentered>
);
} else if (errorMessage) {
return <VerticallyCentered>{errorMessage}</VerticallyCentered>;
} else if (isPasswordProtected && !credentials.current.accessTokenJWT) {
return (
<VerticallyCentered>
<FormPaper>
<FormPaperTitle>{t("password")}</FormPaperTitle>
<Typography color={"text.muted"} mb={2} variant="small">
{t("link_password_description")}
</Typography>
<SingleInputForm
callback={verifyLinkPassword}
placeholder={t("password")}
buttonText={t("unlock")}
fieldType="password"
/>
</FormPaper>
</VerticallyCentered>
);
} else if (!publicFiles || !credentials.current) {
return <VerticallyCentered>{t("NOT_FOUND")}</VerticallyCentered>;
}
// TODO: memo this (after the dependencies are traceable).
const context = {
credentials: credentials.current,
referralCode: referralCode.current,
photoListHeader,
photoListFooter,
};
return (
<PublicCollectionGalleryContext.Provider
value={{
token: token.current,
referralCode: referralCode.current,
passwordToken: passwordJWTToken.current,
accessedThroughSharedURL: true,
photoListHeader,
photoListFooter,
}}
>
<PublicCollectionGalleryContext.Provider value={context}>
<FullScreenDropZone {...{ getDragAndDropRootProps }}>
<UploadSelectorInputs
{...{
@@ -715,10 +710,7 @@ const ListHeader: React.FC<ListHeaderProps> = ({
fileCount={publicFiles.length}
/>
{downloadEnabled && (
<OverflowMenu
ariaControls={"collection-options"}
triggerButtonIcon={<MoreHoriz />}
>
<OverflowMenu ariaID={"collection-options"}>
<OverflowMenuOption
startIcon={<FileDownloadOutlinedIcon />}
onClick={downloadAllFiles}

View File

@@ -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<void> | 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);

View File

@@ -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<number, Collection>;
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());

View File

@@ -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<PublicCollectionGalleryContextType>({
token: null,
passwordToken: null,
credentials: undefined,
referralCode: null,
accessedThroughSharedURL: false,
photoListHeader: null,
photoListFooter: null,
});

View File

@@ -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<CollectionsSortOptionsProps> = ({
nestedInDialog,
disableTriggerButtonBackground,
transparentTriggerButtonBackground,
...optProps
}) => (
<OverflowMenu
ariaControls="collection-sort"
ariaID="collection-sort"
triggerButtonIcon={<SortIcon />}
menuPaperProps={{
sx: {
@@ -54,7 +54,7 @@ export const CollectionsSortOptions: React.FC<CollectionsSortOptionsProps> = ({
triggerButtonProps={{
sx: {
backgroundColor: (theme) =>
disableTriggerButtonBackground
transparentTriggerButtonBackground
? undefined
: theme.colors.fill.faint,
},

View File

@@ -223,7 +223,7 @@ export const GalleryBarImpl: React.FC<GalleryBarImplProps> = ({
<CollectionsSortOptions
activeSortBy={collectionsSortBy}
onChangeSortBy={onChangeCollectionsSortBy}
disableTriggerButtonBackground
transparentTriggerButtonBackground
/>
<IconButton onClick={onShowAllCollections}>
<ExpandMore />

View File

@@ -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<CGroupPersonHeaderProps> = ({ person }) => {
name={name}
fileCount={person.fileIDs.length}
/>
<OverflowMenu
ariaControls={"person-options"}
triggerButtonIcon={<MoreHorizIcon />}
>
<OverflowMenu ariaID={"person-options"}>
<OverflowMenuOption
startIcon={<ListAltOutlinedIcon />}
centerAlign
@@ -210,10 +206,7 @@ const IgnoredPersonHeader: React.FC<IgnoredPersonHeaderProps> = ({
nameProps={{ color: "text.muted" }}
fileCount={person.fileIDs.length}
/>
<OverflowMenu
ariaControls={"person-options"}
triggerButtonIcon={<MoreHorizIcon />}
>
<OverflowMenu ariaID={"person-options"}>
<OverflowMenuOption
startIcon={<VisibilityOutlinedIcon />}
centerAlign
@@ -271,10 +264,7 @@ const ClusterPersonHeader: React.FC<ClusterPersonHeaderProps> = ({
</IconButton>
</Tooltip>
<OverflowMenu
ariaControls={"person-options"}
triggerButtonIcon={<MoreHorizIcon />}
>
<OverflowMenu ariaID={"person-options"}>
<OverflowMenuOption
startIcon={<AddIcon />}
centerAlign

View File

@@ -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<IconButtonProps>;
children?: React.ReactNode;
ariaControls: string;
/**
* Optional additional properties for the MUI {@link Paper} that underlies
* the {@link Menu}.
*/
menuPaperProps?: Partial<PaperProps>;
}
/**
* 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<OverflowMenuProps> = ({
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<OverflowMenuProps>
> = ({
ariaID,
triggerButtonIcon,
triggerButtonProps,
menuPaperProps,
children,
}) => {
const [sortByEl, setSortByEl] = useState<MenuProps["anchorEl"] | null>(
null,
const [anchorEl, setAnchorEl] = useState<MenuProps["anchorEl"]>();
const context = useMemo(
() => ({ close: () => setAnchorEl(undefined) }),
[],
);
const handleClose = () => setSortByEl(null);
return (
<OverflowMenuContext.Provider value={{ close: handleClose }}>
<OverflowMenuContext.Provider value={context}>
<IconButton
onClick={(event) => 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 ?? <MoreHorizIcon />}
</IconButton>
<StyledMenu
id={ariaControls}
anchorEl={sortByEl}
open={Boolean(sortByEl)}
onClose={handleClose}
id={ariaID}
{...(anchorEl ? { anchorEl } : {})}
open={!!anchorEl}
onClose={() => 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<OverflowMenuOptionProps> = ({
color = "primary",
startIcon,
endIcon,
keepOpenAfterClick,
centerAlign,
children,
}) => {
@@ -111,10 +136,9 @@ export const OverflowMenuOption: React.FC<OverflowMenuOptionProps> = ({
const handleClick = () => {
onClick();
if (!keepOpenAfterClick) {
menuContext.close();
}
menuContext.close();
};
return (
<MenuItem
onClick={handleClick}