[web] General refactoring, focus on public albums (#4254)
This commit is contained in:
@@ -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 />}
|
||||
|
||||
@@ -330,7 +330,7 @@ const CollectionOptions: React.FC<CollectionOptionsProps> = ({
|
||||
/>
|
||||
|
||||
<OverflowMenu
|
||||
ariaControls={"collection-options"}
|
||||
ariaID={"collection-options"}
|
||||
triggerButtonIcon={<MoreHoriz ref={overFlowMenuIconRef} />}
|
||||
>
|
||||
{collectionSummaryType == "trash" ? (
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ export const UploadTypeSelector: React.FC<UploadTypeSelectorProps> = ({
|
||||
if (
|
||||
open &&
|
||||
directlyShowUploadFiles &&
|
||||
publicCollectionGalleryContext.accessedThroughSharedURL
|
||||
publicCollectionGalleryContext.credentials
|
||||
) {
|
||||
uploadFiles();
|
||||
onClose();
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -223,7 +223,7 @@ export const GalleryBarImpl: React.FC<GalleryBarImplProps> = ({
|
||||
<CollectionsSortOptions
|
||||
activeSortBy={collectionsSortBy}
|
||||
onChangeSortBy={onChangeCollectionsSortBy}
|
||||
disableTriggerButtonBackground
|
||||
transparentTriggerButtonBackground
|
||||
/>
|
||||
<IconButton onClick={onShowAllCollections}>
|
||||
<ExpandMore />
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user