Merge branch 'main' into generic_group_by
This commit is contained in:
commit
ce6160a06a
|
@ -1,69 +0,0 @@
|
||||||
import log from "@/next/log";
|
|
||||||
import { savedLogs } from "@/next/log-web";
|
|
||||||
import { downloadAsFile } from "@ente/shared/utils";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import { EnteMenuItem } from "components/Menu/EnteMenuItem";
|
|
||||||
import { t } from "i18next";
|
|
||||||
import { AppContext } from "pages/_app";
|
|
||||||
import { useContext, useEffect, useState } from "react";
|
|
||||||
import { Trans } from "react-i18next";
|
|
||||||
import { isInternalUser } from "utils/user";
|
|
||||||
import { testUpload } from "../../../tests/upload.test";
|
|
||||||
|
|
||||||
export default function DebugSection() {
|
|
||||||
const appContext = useContext(AppContext);
|
|
||||||
const [appVersion, setAppVersion] = useState<string | undefined>();
|
|
||||||
|
|
||||||
const electron = globalThis.electron;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
electron?.appVersion().then((v) => setAppVersion(v));
|
|
||||||
});
|
|
||||||
|
|
||||||
const confirmLogDownload = () =>
|
|
||||||
appContext.setDialogMessage({
|
|
||||||
title: t("DOWNLOAD_LOGS"),
|
|
||||||
content: <Trans i18nKey={"DOWNLOAD_LOGS_MESSAGE"} />,
|
|
||||||
proceed: {
|
|
||||||
text: t("DOWNLOAD"),
|
|
||||||
variant: "accent",
|
|
||||||
action: downloadLogs,
|
|
||||||
},
|
|
||||||
close: {
|
|
||||||
text: t("CANCEL"),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const downloadLogs = () => {
|
|
||||||
log.info("Downloading logs");
|
|
||||||
if (electron) electron.openLogDirectory();
|
|
||||||
else downloadAsFile(`debug_logs_${Date.now()}.txt`, savedLogs());
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<EnteMenuItem
|
|
||||||
onClick={confirmLogDownload}
|
|
||||||
variant="mini"
|
|
||||||
label={t("DOWNLOAD_UPLOAD_LOGS")}
|
|
||||||
/>
|
|
||||||
{appVersion && (
|
|
||||||
<Typography
|
|
||||||
py={"14px"}
|
|
||||||
px={"16px"}
|
|
||||||
color="text.muted"
|
|
||||||
variant="mini"
|
|
||||||
>
|
|
||||||
{appVersion}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
{isInternalUser() && (
|
|
||||||
<EnteMenuItem
|
|
||||||
variant="secondary"
|
|
||||||
onClick={testUpload}
|
|
||||||
label={"Test Upload"}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,35 +0,0 @@
|
||||||
import { Box, Button, Stack, Typography } from "@mui/material";
|
|
||||||
import Titlebar from "components/Titlebar";
|
|
||||||
import { t } from "i18next";
|
|
||||||
import { Trans } from "react-i18next";
|
|
||||||
|
|
||||||
export default function EnableMap({ onClose, disableMap, onRootClose }) {
|
|
||||||
return (
|
|
||||||
<Stack spacing={"4px"} py={"12px"}>
|
|
||||||
<Titlebar
|
|
||||||
onClose={onClose}
|
|
||||||
title={t("DISABLE_MAPS")}
|
|
||||||
onRootClose={onRootClose}
|
|
||||||
/>
|
|
||||||
<Stack py={"20px"} px={"8px"} spacing={"32px"}>
|
|
||||||
<Box px={"8px"}>
|
|
||||||
<Typography color="text.muted">
|
|
||||||
<Trans i18nKey={"DISABLE_MAP_DESCRIPTION"} />
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Stack px={"8px"} spacing={"8px"}>
|
|
||||||
<Button
|
|
||||||
color={"critical"}
|
|
||||||
size="large"
|
|
||||||
onClick={disableMap}
|
|
||||||
>
|
|
||||||
{t("DISABLE")}
|
|
||||||
</Button>
|
|
||||||
<Button color={"secondary"} size="large" onClick={onClose}>
|
|
||||||
{t("CANCEL")}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
import { Box, Button, Link, Stack, Typography } from "@mui/material";
|
|
||||||
import Titlebar from "components/Titlebar";
|
|
||||||
import { t } from "i18next";
|
|
||||||
import { Trans } from "react-i18next";
|
|
||||||
|
|
||||||
export const OPEN_STREET_MAP_LINK = "https://www.openstreetmap.org/";
|
|
||||||
export default function EnableMap({ onClose, enableMap, onRootClose }) {
|
|
||||||
return (
|
|
||||||
<Stack spacing={"4px"} py={"12px"}>
|
|
||||||
<Titlebar
|
|
||||||
onClose={onClose}
|
|
||||||
title={t("ENABLE_MAPS")}
|
|
||||||
onRootClose={onRootClose}
|
|
||||||
/>
|
|
||||||
<Stack py={"20px"} px={"8px"} spacing={"32px"}>
|
|
||||||
<Box px={"8px"}>
|
|
||||||
{" "}
|
|
||||||
<Typography color="text.muted">
|
|
||||||
<Trans
|
|
||||||
i18nKey={"ENABLE_MAP_DESCRIPTION"}
|
|
||||||
components={{
|
|
||||||
a: (
|
|
||||||
<Link
|
|
||||||
target="_blank"
|
|
||||||
href={OPEN_STREET_MAP_LINK}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Stack px={"8px"} spacing={"8px"}>
|
|
||||||
<Button color={"accent"} size="large" onClick={enableMap}>
|
|
||||||
{t("ENABLE")}
|
|
||||||
</Button>
|
|
||||||
<Button color={"secondary"} size="large" onClick={onClose}>
|
|
||||||
{t("CANCEL")}
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,47 +0,0 @@
|
||||||
import DeleteAccountModal from "components/DeleteAccountModal";
|
|
||||||
import { EnteMenuItem } from "components/Menu/EnteMenuItem";
|
|
||||||
import { t } from "i18next";
|
|
||||||
import { AppContext } from "pages/_app";
|
|
||||||
import { useContext, useState } from "react";
|
|
||||||
|
|
||||||
export default function ExitSection() {
|
|
||||||
const { setDialogMessage, logout } = useContext(AppContext);
|
|
||||||
|
|
||||||
const [deleteAccountModalView, setDeleteAccountModalView] = useState(false);
|
|
||||||
|
|
||||||
const closeDeleteAccountModal = () => setDeleteAccountModalView(false);
|
|
||||||
const openDeleteAccountModal = () => setDeleteAccountModalView(true);
|
|
||||||
|
|
||||||
const confirmLogout = () => {
|
|
||||||
setDialogMessage({
|
|
||||||
title: t("LOGOUT_MESSAGE"),
|
|
||||||
proceed: {
|
|
||||||
text: t("LOGOUT"),
|
|
||||||
action: logout,
|
|
||||||
variant: "critical",
|
|
||||||
},
|
|
||||||
close: { text: t("CANCEL") },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<EnteMenuItem
|
|
||||||
onClick={confirmLogout}
|
|
||||||
color="critical"
|
|
||||||
label={t("LOGOUT")}
|
|
||||||
variant="secondary"
|
|
||||||
/>
|
|
||||||
<EnteMenuItem
|
|
||||||
onClick={openDeleteAccountModal}
|
|
||||||
color="critical"
|
|
||||||
variant="secondary"
|
|
||||||
label={t("DELETE_ACCOUNT")}
|
|
||||||
/>
|
|
||||||
<DeleteAccountModal
|
|
||||||
open={deleteAccountModalView}
|
|
||||||
onClose={closeDeleteAccountModal}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
import { SpaceBetweenFlex } from "@ente/shared/components/Container";
|
|
||||||
import { EnteLogo } from "@ente/shared/components/EnteLogo";
|
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
|
||||||
import { IconButton } from "@mui/material";
|
|
||||||
|
|
||||||
interface IProps {
|
|
||||||
closeSidebar: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function HeaderSection({ closeSidebar }: IProps) {
|
|
||||||
return (
|
|
||||||
<SpaceBetweenFlex mt={0.5} mb={1} pl={1.5}>
|
|
||||||
<EnteLogo />
|
|
||||||
<IconButton
|
|
||||||
aria-label="close"
|
|
||||||
onClick={closeSidebar}
|
|
||||||
color="secondary"
|
|
||||||
>
|
|
||||||
<CloseIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</SpaceBetweenFlex>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,62 +0,0 @@
|
||||||
import { t } from "i18next";
|
|
||||||
import { useContext } from "react";
|
|
||||||
|
|
||||||
import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
|
||||||
import { Typography } from "@mui/material";
|
|
||||||
import { EnteMenuItem } from "components/Menu/EnteMenuItem";
|
|
||||||
import { NoStyleAnchor } from "components/pages/sharedAlbum/GoToEnte";
|
|
||||||
import isElectron from "is-electron";
|
|
||||||
import { AppContext } from "pages/_app";
|
|
||||||
import { GalleryContext } from "pages/gallery";
|
|
||||||
import exportService from "services/export";
|
|
||||||
import { openLink } from "utils/common";
|
|
||||||
import { getDownloadAppMessage } from "utils/ui";
|
|
||||||
|
|
||||||
export default function HelpSection() {
|
|
||||||
const { setDialogMessage } = useContext(AppContext);
|
|
||||||
const { openExportModal } = useContext(GalleryContext);
|
|
||||||
|
|
||||||
const openRoadmap = () =>
|
|
||||||
openLink("https://github.com/ente-io/ente/discussions", true);
|
|
||||||
|
|
||||||
const contactSupport = () => openLink("mailto:support@ente.io", true);
|
|
||||||
|
|
||||||
function openExport() {
|
|
||||||
if (isElectron()) {
|
|
||||||
openExportModal();
|
|
||||||
} else {
|
|
||||||
setDialogMessage(getDownloadAppMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<EnteMenuItem
|
|
||||||
onClick={openRoadmap}
|
|
||||||
label={t("REQUEST_FEATURE")}
|
|
||||||
variant="secondary"
|
|
||||||
/>
|
|
||||||
<EnteMenuItem
|
|
||||||
onClick={contactSupport}
|
|
||||||
labelComponent={
|
|
||||||
<NoStyleAnchor href="mailto:support@ente.io">
|
|
||||||
<Typography fontWeight={"bold"}>
|
|
||||||
{t("SUPPORT")}
|
|
||||||
</Typography>
|
|
||||||
</NoStyleAnchor>
|
|
||||||
}
|
|
||||||
variant="secondary"
|
|
||||||
/>
|
|
||||||
<EnteMenuItem
|
|
||||||
onClick={openExport}
|
|
||||||
label={t("EXPORT")}
|
|
||||||
endIcon={
|
|
||||||
exportService.isExportInProgress() && (
|
|
||||||
<EnteSpinner size="20px" />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
variant="secondary"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
226
web/apps/photos/src/components/Sidebar/MapSetting.tsx
Normal file
226
web/apps/photos/src/components/Sidebar/MapSetting.tsx
Normal file
|
@ -0,0 +1,226 @@
|
||||||
|
import log from "@/next/log";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
DialogProps,
|
||||||
|
Link,
|
||||||
|
Stack,
|
||||||
|
Typography,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { EnteDrawer } from "components/EnteDrawer";
|
||||||
|
import { EnteMenuItem } from "components/Menu/EnteMenuItem";
|
||||||
|
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
|
||||||
|
import Titlebar from "components/Titlebar";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import { AppContext } from "pages/_app";
|
||||||
|
import { useContext, useEffect, useState } from "react";
|
||||||
|
import { Trans } from "react-i18next";
|
||||||
|
import { getMapEnabledStatus } from "services/userService";
|
||||||
|
|
||||||
|
export default function MapSettings({ open, onClose, onRootClose }) {
|
||||||
|
const { mapEnabled, updateMapEnabled } = useContext(AppContext);
|
||||||
|
const [modifyMapEnabledView, setModifyMapEnabledView] = useState(false);
|
||||||
|
|
||||||
|
const openModifyMapEnabled = () => setModifyMapEnabledView(true);
|
||||||
|
const closeModifyMapEnabled = () => setModifyMapEnabledView(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const main = async () => {
|
||||||
|
const remoteMapValue = await getMapEnabledStatus();
|
||||||
|
updateMapEnabled(remoteMapValue);
|
||||||
|
};
|
||||||
|
main();
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const handleRootClose = () => {
|
||||||
|
onClose();
|
||||||
|
onRootClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrawerClose: DialogProps["onClose"] = (_, reason) => {
|
||||||
|
if (reason === "backdropClick") {
|
||||||
|
handleRootClose();
|
||||||
|
} else {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EnteDrawer
|
||||||
|
transitionDuration={0}
|
||||||
|
open={open}
|
||||||
|
onClose={handleDrawerClose}
|
||||||
|
BackdropProps={{
|
||||||
|
sx: { "&&&": { backgroundColor: "transparent" } },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack spacing={"4px"} py={"12px"}>
|
||||||
|
<Titlebar
|
||||||
|
onClose={onClose}
|
||||||
|
title={t("MAP")}
|
||||||
|
onRootClose={handleRootClose}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box px={"8px"}>
|
||||||
|
<Stack py="20px" spacing="24px">
|
||||||
|
<Box>
|
||||||
|
<MenuItemGroup>
|
||||||
|
<EnteMenuItem
|
||||||
|
onClick={openModifyMapEnabled}
|
||||||
|
variant="toggle"
|
||||||
|
checked={mapEnabled}
|
||||||
|
label={t("MAP_SETTINGS")}
|
||||||
|
/>
|
||||||
|
</MenuItemGroup>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
<ModifyMapEnabled
|
||||||
|
open={modifyMapEnabledView}
|
||||||
|
mapEnabled={mapEnabled}
|
||||||
|
onClose={closeModifyMapEnabled}
|
||||||
|
onRootClose={handleRootClose}
|
||||||
|
/>
|
||||||
|
</EnteDrawer>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModifyMapEnabled = ({ open, onClose, onRootClose, mapEnabled }) => {
|
||||||
|
const { somethingWentWrong, updateMapEnabled } = useContext(AppContext);
|
||||||
|
|
||||||
|
const disableMap = async () => {
|
||||||
|
try {
|
||||||
|
await updateMapEnabled(false);
|
||||||
|
onClose();
|
||||||
|
} catch (e) {
|
||||||
|
log.error("Disable Map failed", e);
|
||||||
|
somethingWentWrong();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const enableMap = async () => {
|
||||||
|
try {
|
||||||
|
await updateMapEnabled(true);
|
||||||
|
onClose();
|
||||||
|
} catch (e) {
|
||||||
|
log.error("Enable Map failed", e);
|
||||||
|
somethingWentWrong();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRootClose = () => {
|
||||||
|
onClose();
|
||||||
|
onRootClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDrawerClose: DialogProps["onClose"] = (_, reason) => {
|
||||||
|
if (reason === "backdropClick") {
|
||||||
|
handleRootClose();
|
||||||
|
} else {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<EnteDrawer
|
||||||
|
anchor="left"
|
||||||
|
transitionDuration={0}
|
||||||
|
open={open}
|
||||||
|
onClose={handleDrawerClose}
|
||||||
|
slotProps={{
|
||||||
|
backdrop: {
|
||||||
|
sx: { "&&&": { backgroundColor: "transparent" } },
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{mapEnabled ? (
|
||||||
|
<DisableMap
|
||||||
|
onClose={onClose}
|
||||||
|
disableMap={disableMap}
|
||||||
|
onRootClose={handleRootClose}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<EnableMap
|
||||||
|
onClose={onClose}
|
||||||
|
enableMap={enableMap}
|
||||||
|
onRootClose={handleRootClose}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</EnteDrawer>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function EnableMap({ onClose, enableMap, onRootClose }) {
|
||||||
|
return (
|
||||||
|
<Stack spacing={"4px"} py={"12px"}>
|
||||||
|
<Titlebar
|
||||||
|
onClose={onClose}
|
||||||
|
title={t("ENABLE_MAPS")}
|
||||||
|
onRootClose={onRootClose}
|
||||||
|
/>
|
||||||
|
<Stack py={"20px"} px={"8px"} spacing={"32px"}>
|
||||||
|
<Box px={"8px"}>
|
||||||
|
{" "}
|
||||||
|
<Typography color="text.muted">
|
||||||
|
<Trans
|
||||||
|
i18nKey={"ENABLE_MAP_DESCRIPTION"}
|
||||||
|
components={{
|
||||||
|
a: (
|
||||||
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
href="https://www.openstreetmap.org/"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Stack px={"8px"} spacing={"8px"}>
|
||||||
|
<Button color={"accent"} size="large" onClick={enableMap}>
|
||||||
|
{t("ENABLE")}
|
||||||
|
</Button>
|
||||||
|
<Button color={"secondary"} size="large" onClick={onClose}>
|
||||||
|
{t("CANCEL")}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DisableMap({ onClose, disableMap, onRootClose }) {
|
||||||
|
return (
|
||||||
|
<Stack spacing={"4px"} py={"12px"}>
|
||||||
|
<Titlebar
|
||||||
|
onClose={onClose}
|
||||||
|
title={t("DISABLE_MAPS")}
|
||||||
|
onRootClose={onRootClose}
|
||||||
|
/>
|
||||||
|
<Stack py={"20px"} px={"8px"} spacing={"32px"}>
|
||||||
|
<Box px={"8px"}>
|
||||||
|
<Typography color="text.muted">
|
||||||
|
<Trans i18nKey={"DISABLE_MAP_DESCRIPTION"} />
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Stack px={"8px"} spacing={"8px"}>
|
||||||
|
<Button
|
||||||
|
color={"critical"}
|
||||||
|
size="large"
|
||||||
|
onClick={disableMap}
|
||||||
|
>
|
||||||
|
{t("DISABLE")}
|
||||||
|
</Button>
|
||||||
|
<Button color={"secondary"} size="large" onClick={onClose}>
|
||||||
|
{t("CANCEL")}
|
||||||
|
</Button>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,76 +0,0 @@
|
||||||
import log from "@/next/log";
|
|
||||||
import { Box, DialogProps } from "@mui/material";
|
|
||||||
import { EnteDrawer } from "components/EnteDrawer";
|
|
||||||
import { AppContext } from "pages/_app";
|
|
||||||
import { useContext } from "react";
|
|
||||||
import DisableMap from "../DisableMap";
|
|
||||||
import EnableMap from "../EnableMap";
|
|
||||||
|
|
||||||
const ModifyMapEnabled = ({ open, onClose, onRootClose, mapEnabled }) => {
|
|
||||||
const { somethingWentWrong, updateMapEnabled } = useContext(AppContext);
|
|
||||||
|
|
||||||
const disableMap = async () => {
|
|
||||||
try {
|
|
||||||
await updateMapEnabled(false);
|
|
||||||
onClose();
|
|
||||||
} catch (e) {
|
|
||||||
log.error("Disable Map failed", e);
|
|
||||||
somethingWentWrong();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const enableMap = async () => {
|
|
||||||
try {
|
|
||||||
await updateMapEnabled(true);
|
|
||||||
onClose();
|
|
||||||
} catch (e) {
|
|
||||||
log.error("Enable Map failed", e);
|
|
||||||
somethingWentWrong();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRootClose = () => {
|
|
||||||
onClose();
|
|
||||||
onRootClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrawerClose: DialogProps["onClose"] = (_, reason) => {
|
|
||||||
if (reason === "backdropClick") {
|
|
||||||
handleRootClose();
|
|
||||||
} else {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<EnteDrawer
|
|
||||||
anchor="left"
|
|
||||||
transitionDuration={0}
|
|
||||||
open={open}
|
|
||||||
onClose={handleDrawerClose}
|
|
||||||
slotProps={{
|
|
||||||
backdrop: {
|
|
||||||
sx: { "&&&": { backgroundColor: "transparent" } },
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{mapEnabled ? (
|
|
||||||
<DisableMap
|
|
||||||
onClose={onClose}
|
|
||||||
disableMap={disableMap}
|
|
||||||
onRootClose={handleRootClose}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<EnableMap
|
|
||||||
onClose={onClose}
|
|
||||||
enableMap={enableMap}
|
|
||||||
onRootClose={handleRootClose}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</EnteDrawer>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ModifyMapEnabled;
|
|
|
@ -1,82 +0,0 @@
|
||||||
import { Box, DialogProps, Stack } from "@mui/material";
|
|
||||||
import { EnteDrawer } from "components/EnteDrawer";
|
|
||||||
import { EnteMenuItem } from "components/Menu/EnteMenuItem";
|
|
||||||
import { MenuItemGroup } from "components/Menu/MenuItemGroup";
|
|
||||||
import Titlebar from "components/Titlebar";
|
|
||||||
import { t } from "i18next";
|
|
||||||
import { AppContext } from "pages/_app";
|
|
||||||
import { useContext, useEffect, useState } from "react";
|
|
||||||
import { getMapEnabledStatus } from "services/userService";
|
|
||||||
import ModifyMapEnabled from "./ModifyMapEnabled";
|
|
||||||
|
|
||||||
export default function MapSettings({ open, onClose, onRootClose }) {
|
|
||||||
const { mapEnabled, updateMapEnabled } = useContext(AppContext);
|
|
||||||
const [modifyMapEnabledView, setModifyMapEnabledView] = useState(false);
|
|
||||||
|
|
||||||
const openModifyMapEnabled = () => setModifyMapEnabledView(true);
|
|
||||||
const closeModifyMapEnabled = () => setModifyMapEnabledView(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const main = async () => {
|
|
||||||
const remoteMapValue = await getMapEnabledStatus();
|
|
||||||
updateMapEnabled(remoteMapValue);
|
|
||||||
};
|
|
||||||
main();
|
|
||||||
}, [open]);
|
|
||||||
|
|
||||||
const handleRootClose = () => {
|
|
||||||
onClose();
|
|
||||||
onRootClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrawerClose: DialogProps["onClose"] = (_, reason) => {
|
|
||||||
if (reason === "backdropClick") {
|
|
||||||
handleRootClose();
|
|
||||||
} else {
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<EnteDrawer
|
|
||||||
transitionDuration={0}
|
|
||||||
open={open}
|
|
||||||
onClose={handleDrawerClose}
|
|
||||||
BackdropProps={{
|
|
||||||
sx: { "&&&": { backgroundColor: "transparent" } },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack spacing={"4px"} py={"12px"}>
|
|
||||||
<Titlebar
|
|
||||||
onClose={onClose}
|
|
||||||
title={t("MAP")}
|
|
||||||
onRootClose={handleRootClose}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Box px={"8px"}>
|
|
||||||
<Stack py="20px" spacing="24px">
|
|
||||||
<Box>
|
|
||||||
<MenuItemGroup>
|
|
||||||
<EnteMenuItem
|
|
||||||
onClick={openModifyMapEnabled}
|
|
||||||
variant="toggle"
|
|
||||||
checked={mapEnabled}
|
|
||||||
label={t("MAP_SETTINGS")}
|
|
||||||
/>
|
|
||||||
</MenuItemGroup>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
</Box>
|
|
||||||
</Stack>
|
|
||||||
<ModifyMapEnabled
|
|
||||||
open={modifyMapEnabledView}
|
|
||||||
mapEnabled={mapEnabled}
|
|
||||||
onClose={closeModifyMapEnabled}
|
|
||||||
onRootClose={handleRootClose}
|
|
||||||
/>
|
|
||||||
</EnteDrawer>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,13 +1,20 @@
|
||||||
|
import {
|
||||||
|
getLocaleInUse,
|
||||||
|
setLocaleInUse,
|
||||||
|
supportedLocales,
|
||||||
|
type SupportedLocale,
|
||||||
|
} from "@/next/i18n";
|
||||||
import ChevronRight from "@mui/icons-material/ChevronRight";
|
import ChevronRight from "@mui/icons-material/ChevronRight";
|
||||||
import { Box, DialogProps, Stack } from "@mui/material";
|
import { Box, DialogProps, Stack } from "@mui/material";
|
||||||
|
import DropdownInput from "components/DropdownInput";
|
||||||
import { EnteDrawer } from "components/EnteDrawer";
|
import { EnteDrawer } from "components/EnteDrawer";
|
||||||
import { EnteMenuItem } from "components/Menu/EnteMenuItem";
|
import { EnteMenuItem } from "components/Menu/EnteMenuItem";
|
||||||
import Titlebar from "components/Titlebar";
|
import Titlebar from "components/Titlebar";
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import AdvancedSettings from "../AdvancedSettings";
|
import AdvancedSettings from "./AdvancedSettings";
|
||||||
import MapSettings from "../MapSetting";
|
import MapSettings from "./MapSetting";
|
||||||
import { LanguageSelector } from "./LanguageSelector";
|
|
||||||
|
|
||||||
export default function Preferences({ open, onClose, onRootClose }) {
|
export default function Preferences({ open, onClose, onRootClose }) {
|
||||||
const [advancedSettingsView, setAdvancedSettingsView] = useState(false);
|
const [advancedSettingsView, setAdvancedSettingsView] = useState(false);
|
||||||
|
@ -76,3 +83,53 @@ export default function Preferences({ open, onClose, onRootClose }) {
|
||||||
</EnteDrawer>
|
</EnteDrawer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LanguageSelector = () => {
|
||||||
|
const locale = getLocaleInUse();
|
||||||
|
// Enhancement: Is this full reload needed?
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const updateCurrentLocale = (newLocale: SupportedLocale) => {
|
||||||
|
setLocaleInUse(newLocale);
|
||||||
|
router.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
const options = supportedLocales.map((locale) => ({
|
||||||
|
label: localeName(locale),
|
||||||
|
value: locale,
|
||||||
|
}));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownInput
|
||||||
|
options={options}
|
||||||
|
label={t("LANGUAGE")}
|
||||||
|
labelProps={{ color: "text.muted" }}
|
||||||
|
selected={locale}
|
||||||
|
setSelected={updateCurrentLocale}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Human readable name for each supported locale.
|
||||||
|
*/
|
||||||
|
const localeName = (locale: SupportedLocale) => {
|
||||||
|
switch (locale) {
|
||||||
|
case "en-US":
|
||||||
|
return "English";
|
||||||
|
case "fr-FR":
|
||||||
|
return "Français";
|
||||||
|
case "de-DE":
|
||||||
|
return "Deutsch";
|
||||||
|
case "zh-CN":
|
||||||
|
return "中文";
|
||||||
|
case "nl-NL":
|
||||||
|
return "Nederlands";
|
||||||
|
case "es-ES":
|
||||||
|
return "Español";
|
||||||
|
case "pt-BR":
|
||||||
|
return "Brazilian Portuguese";
|
||||||
|
case "ru-RU":
|
||||||
|
return "Russian";
|
||||||
|
}
|
||||||
|
};
|
|
@ -1,61 +0,0 @@
|
||||||
import {
|
|
||||||
getLocaleInUse,
|
|
||||||
setLocaleInUse,
|
|
||||||
supportedLocales,
|
|
||||||
type SupportedLocale,
|
|
||||||
} from "@/next/i18n";
|
|
||||||
import DropdownInput, { DropdownOption } from "components/DropdownInput";
|
|
||||||
import { t } from "i18next";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Human readable name for each supported locale.
|
|
||||||
*/
|
|
||||||
export const localeName = (locale: SupportedLocale) => {
|
|
||||||
switch (locale) {
|
|
||||||
case "en-US":
|
|
||||||
return "English";
|
|
||||||
case "fr-FR":
|
|
||||||
return "Français";
|
|
||||||
case "de-DE":
|
|
||||||
return "Deutsch";
|
|
||||||
case "zh-CN":
|
|
||||||
return "中文";
|
|
||||||
case "nl-NL":
|
|
||||||
return "Nederlands";
|
|
||||||
case "es-ES":
|
|
||||||
return "Español";
|
|
||||||
case "pt-BR":
|
|
||||||
return "Brazilian Portuguese";
|
|
||||||
case "ru-RU":
|
|
||||||
return "Russian";
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getLanguageOptions = (): DropdownOption<SupportedLocale>[] => {
|
|
||||||
return supportedLocales.map((locale) => ({
|
|
||||||
label: localeName(locale),
|
|
||||||
value: locale,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
export const LanguageSelector = () => {
|
|
||||||
const locale = getLocaleInUse();
|
|
||||||
// Enhancement: Is this full reload needed?
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const updateCurrentLocale = (newLocale: SupportedLocale) => {
|
|
||||||
setLocaleInUse(newLocale);
|
|
||||||
router.reload();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownInput
|
|
||||||
options={getLanguageOptions()}
|
|
||||||
label={t("LANGUAGE")}
|
|
||||||
labelProps={{ color: "text.muted" }}
|
|
||||||
selected={locale}
|
|
||||||
setSelected={updateCurrentLocale}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
|
@ -1,102 +0,0 @@
|
||||||
import { t } from "i18next";
|
|
||||||
import { useContext, useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import ArchiveOutlined from "@mui/icons-material/ArchiveOutlined";
|
|
||||||
import CategoryIcon from "@mui/icons-material/Category";
|
|
||||||
import DeleteOutline from "@mui/icons-material/DeleteOutline";
|
|
||||||
import LockOutlined from "@mui/icons-material/LockOutlined";
|
|
||||||
import VisibilityOff from "@mui/icons-material/VisibilityOff";
|
|
||||||
import { EnteMenuItem } from "components/Menu/EnteMenuItem";
|
|
||||||
import {
|
|
||||||
ARCHIVE_SECTION,
|
|
||||||
DUMMY_UNCATEGORIZED_COLLECTION,
|
|
||||||
TRASH_SECTION,
|
|
||||||
} from "constants/collection";
|
|
||||||
import { GalleryContext } from "pages/gallery";
|
|
||||||
import { getUncategorizedCollection } from "services/collectionService";
|
|
||||||
import { CollectionSummaries } from "types/collection";
|
|
||||||
interface Iprops {
|
|
||||||
closeSidebar: () => void;
|
|
||||||
collectionSummaries: CollectionSummaries;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function ShortcutSection({
|
|
||||||
closeSidebar,
|
|
||||||
collectionSummaries,
|
|
||||||
}: Iprops) {
|
|
||||||
const galleryContext = useContext(GalleryContext);
|
|
||||||
const [uncategorizedCollectionId, setUncategorizedCollectionID] =
|
|
||||||
useState<number>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const main = async () => {
|
|
||||||
const unCategorizedCollection = await getUncategorizedCollection();
|
|
||||||
if (unCategorizedCollection) {
|
|
||||||
setUncategorizedCollectionID(unCategorizedCollection.id);
|
|
||||||
} else {
|
|
||||||
setUncategorizedCollectionID(DUMMY_UNCATEGORIZED_COLLECTION);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
main();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const openUncategorizedSection = () => {
|
|
||||||
galleryContext.setActiveCollectionID(uncategorizedCollectionId);
|
|
||||||
closeSidebar();
|
|
||||||
};
|
|
||||||
|
|
||||||
const openTrashSection = () => {
|
|
||||||
galleryContext.setActiveCollectionID(TRASH_SECTION);
|
|
||||||
closeSidebar();
|
|
||||||
};
|
|
||||||
|
|
||||||
const openArchiveSection = () => {
|
|
||||||
galleryContext.setActiveCollectionID(ARCHIVE_SECTION);
|
|
||||||
closeSidebar();
|
|
||||||
};
|
|
||||||
|
|
||||||
const openHiddenSection = () => {
|
|
||||||
galleryContext.openHiddenSection(() => {
|
|
||||||
closeSidebar();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<EnteMenuItem
|
|
||||||
startIcon={<CategoryIcon />}
|
|
||||||
onClick={openUncategorizedSection}
|
|
||||||
variant="captioned"
|
|
||||||
label={t("UNCATEGORIZED")}
|
|
||||||
subText={collectionSummaries
|
|
||||||
.get(uncategorizedCollectionId)
|
|
||||||
?.fileCount.toString()}
|
|
||||||
/>
|
|
||||||
<EnteMenuItem
|
|
||||||
startIcon={<ArchiveOutlined />}
|
|
||||||
onClick={openArchiveSection}
|
|
||||||
variant="captioned"
|
|
||||||
label={t("ARCHIVE_SECTION_NAME")}
|
|
||||||
subText={collectionSummaries
|
|
||||||
.get(ARCHIVE_SECTION)
|
|
||||||
?.fileCount.toString()}
|
|
||||||
/>
|
|
||||||
<EnteMenuItem
|
|
||||||
startIcon={<VisibilityOff />}
|
|
||||||
onClick={openHiddenSection}
|
|
||||||
variant="captioned"
|
|
||||||
label={t("HIDDEN")}
|
|
||||||
subIcon={<LockOutlined />}
|
|
||||||
/>
|
|
||||||
<EnteMenuItem
|
|
||||||
startIcon={<DeleteOutline />}
|
|
||||||
onClick={openTrashSection}
|
|
||||||
variant="captioned"
|
|
||||||
label={t("TRASH")}
|
|
||||||
subText={collectionSummaries
|
|
||||||
.get(TRASH_SECTION)
|
|
||||||
?.fileCount.toString()}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
export function BackgroundOverlay() {
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
style={{ aspectRatio: "2/1" }}
|
|
||||||
width="100%"
|
|
||||||
src="/images/subscription-card-background/1x.png"
|
|
||||||
srcSet="/images/subscription-card-background/2x.png 2x,
|
|
||||||
/images/subscription-card-background/3x.png 3x"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,15 +0,0 @@
|
||||||
import { FlexWrapper, Overlay } from "@ente/shared/components/Container";
|
|
||||||
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
|
||||||
export function ClickOverlay({ onClick }) {
|
|
||||||
return (
|
|
||||||
<Overlay display="flex">
|
|
||||||
<FlexWrapper
|
|
||||||
onClick={onClick}
|
|
||||||
justifyContent={"flex-end"}
|
|
||||||
sx={{ cursor: "pointer" }}
|
|
||||||
>
|
|
||||||
<ChevronRightIcon />
|
|
||||||
</FlexWrapper>
|
|
||||||
</Overlay>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,8 +1,7 @@
|
||||||
|
import { FlexWrapper, Overlay } from "@ente/shared/components/Container";
|
||||||
|
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
|
||||||
import { Box, Skeleton } from "@mui/material";
|
import { Box, Skeleton } from "@mui/material";
|
||||||
import { UserDetails } from "types/user";
|
import { UserDetails } from "types/user";
|
||||||
import { BackgroundOverlay } from "./backgroundOverlay";
|
|
||||||
import { ClickOverlay } from "./clickOverlay";
|
|
||||||
|
|
||||||
import { SubscriptionCardContentOverlay } from "./contentOverlay";
|
import { SubscriptionCardContentOverlay } from "./contentOverlay";
|
||||||
|
|
||||||
const SUBSCRIPTION_CARD_SIZE = 152;
|
const SUBSCRIPTION_CARD_SIZE = 152;
|
||||||
|
@ -32,3 +31,29 @@ export default function SubscriptionCard({ userDetails, onClick }: Iprops) {
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function BackgroundOverlay() {
|
||||||
|
return (
|
||||||
|
<img
|
||||||
|
style={{ aspectRatio: "2/1" }}
|
||||||
|
width="100%"
|
||||||
|
src="/images/subscription-card-background/1x.png"
|
||||||
|
srcSet="/images/subscription-card-background/2x.png 2x,
|
||||||
|
/images/subscription-card-background/3x.png 3x"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ClickOverlay({ onClick }) {
|
||||||
|
return (
|
||||||
|
<Overlay display="flex">
|
||||||
|
<FlexWrapper
|
||||||
|
onClick={onClick}
|
||||||
|
justifyContent={"flex-end"}
|
||||||
|
sx={{ cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
<ChevronRightIcon />
|
||||||
|
</FlexWrapper>
|
||||||
|
</Overlay>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
|
import CircleIcon from "@mui/icons-material/Circle";
|
||||||
import { LinearProgress, styled } from "@mui/material";
|
import { LinearProgress, styled } from "@mui/material";
|
||||||
import { DotSeparator } from "../styledComponents";
|
|
||||||
|
|
||||||
export const Progressbar = styled(LinearProgress)(() => ({
|
export const Progressbar = styled(LinearProgress)(() => ({
|
||||||
".MuiLinearProgress-bar": {
|
".MuiLinearProgress-bar": {
|
||||||
|
@ -13,6 +13,12 @@ Progressbar.defaultProps = {
|
||||||
variant: "determinate",
|
variant: "determinate",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const DotSeparator = styled(CircleIcon)`
|
||||||
|
font-size: 4px;
|
||||||
|
margin: 0 ${({ theme }) => theme.spacing(1)};
|
||||||
|
color: inherit;
|
||||||
|
`;
|
||||||
|
|
||||||
export const LegendIndicator = styled(DotSeparator)`
|
export const LegendIndicator = styled(DotSeparator)`
|
||||||
font-size: 8.71px;
|
font-size: 8.71px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
|
@ -1,130 +0,0 @@
|
||||||
import Box from "@mui/material/Box";
|
|
||||||
import { t } from "i18next";
|
|
||||||
import { GalleryContext } from "pages/gallery";
|
|
||||||
import { MouseEventHandler, useContext, useMemo } from "react";
|
|
||||||
import { Trans } from "react-i18next";
|
|
||||||
import { UserDetails } from "types/user";
|
|
||||||
import {
|
|
||||||
hasAddOnBonus,
|
|
||||||
hasExceededStorageQuota,
|
|
||||||
hasPaidSubscription,
|
|
||||||
hasStripeSubscription,
|
|
||||||
isOnFreePlan,
|
|
||||||
isSubscriptionActive,
|
|
||||||
isSubscriptionCancelled,
|
|
||||||
isSubscriptionPastDue,
|
|
||||||
} from "utils/billing";
|
|
||||||
|
|
||||||
import { Typography } from "@mui/material";
|
|
||||||
import LinkButton from "components/pages/gallery/LinkButton";
|
|
||||||
import billingService from "services/billingService";
|
|
||||||
import { isFamilyAdmin, isPartOfFamily } from "utils/user/family";
|
|
||||||
|
|
||||||
export default function SubscriptionStatus({
|
|
||||||
userDetails,
|
|
||||||
}: {
|
|
||||||
userDetails: UserDetails;
|
|
||||||
}) {
|
|
||||||
const { showPlanSelectorModal } = useContext(GalleryContext);
|
|
||||||
|
|
||||||
const hasAMessage = useMemo(() => {
|
|
||||||
if (!userDetails) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
isPartOfFamily(userDetails.familyData) &&
|
|
||||||
!isFamilyAdmin(userDetails.familyData)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
hasPaidSubscription(userDetails.subscription) &&
|
|
||||||
!isSubscriptionCancelled(userDetails.subscription)
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}, [userDetails]);
|
|
||||||
|
|
||||||
const handleClick = useMemo(() => {
|
|
||||||
const eventHandler: MouseEventHandler<HTMLSpanElement> = (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (userDetails) {
|
|
||||||
if (isSubscriptionActive(userDetails.subscription)) {
|
|
||||||
if (hasExceededStorageQuota(userDetails)) {
|
|
||||||
showPlanSelectorModal();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (
|
|
||||||
hasStripeSubscription(userDetails.subscription) &&
|
|
||||||
isSubscriptionPastDue(userDetails.subscription)
|
|
||||||
) {
|
|
||||||
billingService.redirectToCustomerPortal();
|
|
||||||
} else {
|
|
||||||
showPlanSelectorModal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return eventHandler;
|
|
||||||
}, [userDetails]);
|
|
||||||
|
|
||||||
if (!hasAMessage) {
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const messages = [];
|
|
||||||
if (!hasAddOnBonus(userDetails.bonusData)) {
|
|
||||||
if (isSubscriptionActive(userDetails.subscription)) {
|
|
||||||
if (isOnFreePlan(userDetails.subscription)) {
|
|
||||||
messages.push(
|
|
||||||
<Trans
|
|
||||||
i18nKey={"FREE_SUBSCRIPTION_INFO"}
|
|
||||||
values={{
|
|
||||||
date: userDetails.subscription?.expiryTime,
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
} else if (isSubscriptionCancelled(userDetails.subscription)) {
|
|
||||||
messages.push(
|
|
||||||
t("RENEWAL_CANCELLED_SUBSCRIPTION_INFO", {
|
|
||||||
date: userDetails.subscription?.expiryTime,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
messages.push(
|
|
||||||
<Trans
|
|
||||||
i18nKey={"SUBSCRIPTION_EXPIRED_MESSAGE"}
|
|
||||||
components={{
|
|
||||||
a: <LinkButton onClick={handleClick} />,
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasExceededStorageQuota(userDetails) && messages.length === 0) {
|
|
||||||
messages.push(
|
|
||||||
<Trans
|
|
||||||
i18nKey={"STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO"}
|
|
||||||
components={{
|
|
||||||
a: <LinkButton onClick={handleClick} />,
|
|
||||||
}}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box px={1} pt={0.5}>
|
|
||||||
<Typography
|
|
||||||
variant="small"
|
|
||||||
color={"text.muted"}
|
|
||||||
onClick={handleClick && handleClick}
|
|
||||||
sx={{ cursor: handleClick && "pointer" }}
|
|
||||||
>
|
|
||||||
{messages}
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,222 +0,0 @@
|
||||||
import log from "@/next/log";
|
|
||||||
import RecoveryKey from "@ente/shared/components/RecoveryKey";
|
|
||||||
import {
|
|
||||||
ACCOUNTS_PAGES,
|
|
||||||
PHOTOS_PAGES as PAGES,
|
|
||||||
} from "@ente/shared/constants/pages";
|
|
||||||
import TwoFactorModal from "components/TwoFactor/Modal";
|
|
||||||
import { t } from "i18next";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { AppContext } from "pages/_app";
|
|
||||||
import { useContext, useState } from "react";
|
|
||||||
// import mlIDbStorage from 'services/ml/db';
|
|
||||||
import {
|
|
||||||
configurePasskeyRecovery,
|
|
||||||
isPasskeyRecoveryEnabled,
|
|
||||||
} from "@ente/accounts/services/passkey";
|
|
||||||
import { APPS, CLIENT_PACKAGE_NAMES } from "@ente/shared/apps/constants";
|
|
||||||
import ThemeSwitcher from "@ente/shared/components/ThemeSwitcher";
|
|
||||||
import { getRecoveryKey } from "@ente/shared/crypto/helpers";
|
|
||||||
import {
|
|
||||||
encryptToB64,
|
|
||||||
generateEncryptionKey,
|
|
||||||
} from "@ente/shared/crypto/internal/libsodium";
|
|
||||||
import { getAccountsURL } from "@ente/shared/network/api";
|
|
||||||
import { THEME_COLOR } from "@ente/shared/themes/constants";
|
|
||||||
import { EnteMenuItem } from "components/Menu/EnteMenuItem";
|
|
||||||
import { WatchFolder } from "components/WatchFolder";
|
|
||||||
import isElectron from "is-electron";
|
|
||||||
import { getAccountsToken } from "services/userService";
|
|
||||||
import { getDownloadAppMessage } from "utils/ui";
|
|
||||||
import { isInternalUser } from "utils/user";
|
|
||||||
import Preferences from "./Preferences";
|
|
||||||
|
|
||||||
export default function UtilitySection({ closeSidebar }) {
|
|
||||||
const router = useRouter();
|
|
||||||
const appContext = useContext(AppContext);
|
|
||||||
const {
|
|
||||||
setDialogMessage,
|
|
||||||
startLoading,
|
|
||||||
watchFolderView,
|
|
||||||
setWatchFolderView,
|
|
||||||
themeColor,
|
|
||||||
setThemeColor,
|
|
||||||
} = appContext;
|
|
||||||
|
|
||||||
const [recoverModalView, setRecoveryModalView] = useState(false);
|
|
||||||
const [twoFactorModalView, setTwoFactorModalView] = useState(false);
|
|
||||||
const [preferencesView, setPreferencesView] = useState(false);
|
|
||||||
|
|
||||||
const openPreferencesOptions = () => setPreferencesView(true);
|
|
||||||
const closePreferencesOptions = () => setPreferencesView(false);
|
|
||||||
|
|
||||||
const openRecoveryKeyModal = () => setRecoveryModalView(true);
|
|
||||||
const closeRecoveryKeyModal = () => setRecoveryModalView(false);
|
|
||||||
|
|
||||||
const openTwoFactorModal = () => setTwoFactorModalView(true);
|
|
||||||
const closeTwoFactorModal = () => setTwoFactorModalView(false);
|
|
||||||
|
|
||||||
const openWatchFolder = () => {
|
|
||||||
if (isElectron()) {
|
|
||||||
setWatchFolderView(true);
|
|
||||||
} else {
|
|
||||||
setDialogMessage(getDownloadAppMessage());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const closeWatchFolder = () => setWatchFolderView(false);
|
|
||||||
|
|
||||||
const redirectToChangePasswordPage = () => {
|
|
||||||
closeSidebar();
|
|
||||||
router.push(PAGES.CHANGE_PASSWORD);
|
|
||||||
};
|
|
||||||
|
|
||||||
const redirectToChangeEmailPage = () => {
|
|
||||||
closeSidebar();
|
|
||||||
router.push(PAGES.CHANGE_EMAIL);
|
|
||||||
};
|
|
||||||
|
|
||||||
const redirectToAccountsPage = async () => {
|
|
||||||
closeSidebar();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// check if the user has passkey recovery enabled
|
|
||||||
const recoveryEnabled = await isPasskeyRecoveryEnabled();
|
|
||||||
if (!recoveryEnabled) {
|
|
||||||
// let's create the necessary recovery information
|
|
||||||
const recoveryKey = await getRecoveryKey();
|
|
||||||
|
|
||||||
const resetSecret = await generateEncryptionKey();
|
|
||||||
|
|
||||||
const encryptionResult = await encryptToB64(
|
|
||||||
resetSecret,
|
|
||||||
recoveryKey,
|
|
||||||
);
|
|
||||||
|
|
||||||
await configurePasskeyRecovery(
|
|
||||||
resetSecret,
|
|
||||||
encryptionResult.encryptedData,
|
|
||||||
encryptionResult.nonce,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const accountsToken = await getAccountsToken();
|
|
||||||
|
|
||||||
window.open(
|
|
||||||
`${getAccountsURL()}${
|
|
||||||
ACCOUNTS_PAGES.ACCOUNT_HANDOFF
|
|
||||||
}?package=${CLIENT_PACKAGE_NAMES.get(
|
|
||||||
APPS.PHOTOS,
|
|
||||||
)}&token=${accountsToken}`,
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
log.error("failed to redirect to accounts page", e);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const redirectToDeduplicatePage = () => router.push(PAGES.DEDUPLICATE);
|
|
||||||
|
|
||||||
const somethingWentWrong = () =>
|
|
||||||
setDialogMessage({
|
|
||||||
title: t("ERROR"),
|
|
||||||
content: t("RECOVER_KEY_GENERATION_FAILED"),
|
|
||||||
close: { variant: "critical" },
|
|
||||||
});
|
|
||||||
|
|
||||||
const toggleTheme = () => {
|
|
||||||
setThemeColor((themeColor) =>
|
|
||||||
themeColor === THEME_COLOR.DARK
|
|
||||||
? THEME_COLOR.LIGHT
|
|
||||||
: THEME_COLOR.DARK,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isElectron() && (
|
|
||||||
<EnteMenuItem
|
|
||||||
onClick={openWatchFolder}
|
|
||||||
variant="secondary"
|
|
||||||
label={t("WATCH_FOLDERS")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<EnteMenuItem
|
|
||||||
variant="secondary"
|
|
||||||
onClick={openRecoveryKeyModal}
|
|
||||||
label={t("RECOVERY_KEY")}
|
|
||||||
/>
|
|
||||||
{isInternalUser() && (
|
|
||||||
<EnteMenuItem
|
|
||||||
onClick={toggleTheme}
|
|
||||||
variant="secondary"
|
|
||||||
label={t("CHOSE_THEME")}
|
|
||||||
endIcon={
|
|
||||||
<ThemeSwitcher
|
|
||||||
themeColor={themeColor}
|
|
||||||
setThemeColor={setThemeColor}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<EnteMenuItem
|
|
||||||
variant="secondary"
|
|
||||||
onClick={openTwoFactorModal}
|
|
||||||
label={t("TWO_FACTOR")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{isInternalUser() && (
|
|
||||||
<EnteMenuItem
|
|
||||||
variant="secondary"
|
|
||||||
onClick={redirectToAccountsPage}
|
|
||||||
label={t("PASSKEYS")}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<EnteMenuItem
|
|
||||||
variant="secondary"
|
|
||||||
onClick={redirectToChangePasswordPage}
|
|
||||||
label={t("CHANGE_PASSWORD")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EnteMenuItem
|
|
||||||
variant="secondary"
|
|
||||||
onClick={redirectToChangeEmailPage}
|
|
||||||
label={t("CHANGE_EMAIL")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EnteMenuItem
|
|
||||||
variant="secondary"
|
|
||||||
onClick={redirectToDeduplicatePage}
|
|
||||||
label={t("DEDUPLICATE_FILES")}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<EnteMenuItem
|
|
||||||
variant="secondary"
|
|
||||||
onClick={openPreferencesOptions}
|
|
||||||
label={t("PREFERENCES")}
|
|
||||||
/>
|
|
||||||
<RecoveryKey
|
|
||||||
appContext={appContext}
|
|
||||||
show={recoverModalView}
|
|
||||||
onHide={closeRecoveryKeyModal}
|
|
||||||
somethingWentWrong={somethingWentWrong}
|
|
||||||
/>
|
|
||||||
<TwoFactorModal
|
|
||||||
show={twoFactorModalView}
|
|
||||||
onHide={closeTwoFactorModal}
|
|
||||||
closeSidebar={closeSidebar}
|
|
||||||
setLoading={startLoading}
|
|
||||||
/>
|
|
||||||
{isElectron() && (
|
|
||||||
<WatchFolder
|
|
||||||
open={watchFolderView}
|
|
||||||
onClose={closeWatchFolder}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Preferences
|
|
||||||
open={preferencesView}
|
|
||||||
onClose={closePreferencesOptions}
|
|
||||||
onRootClose={closeSidebar}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,13 +1,93 @@
|
||||||
import { Divider, Stack } from "@mui/material";
|
import log from "@/next/log";
|
||||||
|
import { savedLogs } from "@/next/log-web";
|
||||||
|
import {
|
||||||
|
configurePasskeyRecovery,
|
||||||
|
isPasskeyRecoveryEnabled,
|
||||||
|
} from "@ente/accounts/services/passkey";
|
||||||
|
import { APPS, CLIENT_PACKAGE_NAMES } from "@ente/shared/apps/constants";
|
||||||
|
import { SpaceBetweenFlex } from "@ente/shared/components/Container";
|
||||||
|
import { EnteLogo } from "@ente/shared/components/EnteLogo";
|
||||||
|
import EnteSpinner from "@ente/shared/components/EnteSpinner";
|
||||||
|
import RecoveryKey from "@ente/shared/components/RecoveryKey";
|
||||||
|
import ThemeSwitcher from "@ente/shared/components/ThemeSwitcher";
|
||||||
|
import {
|
||||||
|
ACCOUNTS_PAGES,
|
||||||
|
PHOTOS_PAGES as PAGES,
|
||||||
|
} from "@ente/shared/constants/pages";
|
||||||
|
import { getRecoveryKey } from "@ente/shared/crypto/helpers";
|
||||||
|
import {
|
||||||
|
encryptToB64,
|
||||||
|
generateEncryptionKey,
|
||||||
|
} from "@ente/shared/crypto/internal/libsodium";
|
||||||
|
import { useLocalState } from "@ente/shared/hooks/useLocalState";
|
||||||
|
import { getAccountsURL } from "@ente/shared/network/api";
|
||||||
|
import { LS_KEYS, getData, setData } from "@ente/shared/storage/localStorage";
|
||||||
|
import { THEME_COLOR } from "@ente/shared/themes/constants";
|
||||||
|
import { downloadAsFile } from "@ente/shared/utils";
|
||||||
|
import ArchiveOutlined from "@mui/icons-material/ArchiveOutlined";
|
||||||
|
import CategoryIcon from "@mui/icons-material/Category";
|
||||||
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
|
import DeleteOutline from "@mui/icons-material/DeleteOutline";
|
||||||
|
import LockOutlined from "@mui/icons-material/LockOutlined";
|
||||||
|
import VisibilityOff from "@mui/icons-material/VisibilityOff";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Divider,
|
||||||
|
IconButton,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
styled,
|
||||||
|
} from "@mui/material";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import DeleteAccountModal from "components/DeleteAccountModal";
|
||||||
|
import { EnteDrawer } from "components/EnteDrawer";
|
||||||
|
import { EnteMenuItem } from "components/Menu/EnteMenuItem";
|
||||||
|
import TwoFactorModal from "components/TwoFactor/Modal";
|
||||||
|
import { WatchFolder } from "components/WatchFolder";
|
||||||
|
import LinkButton from "components/pages/gallery/LinkButton";
|
||||||
|
import { NoStyleAnchor } from "components/pages/sharedAlbum/GoToEnte";
|
||||||
|
import {
|
||||||
|
ARCHIVE_SECTION,
|
||||||
|
DUMMY_UNCATEGORIZED_COLLECTION,
|
||||||
|
TRASH_SECTION,
|
||||||
|
} from "constants/collection";
|
||||||
|
import { t } from "i18next";
|
||||||
|
import isElectron from "is-electron";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { AppContext } from "pages/_app";
|
||||||
|
import { GalleryContext } from "pages/gallery";
|
||||||
|
import {
|
||||||
|
MouseEventHandler,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
|
import { Trans } from "react-i18next";
|
||||||
|
import billingService from "services/billingService";
|
||||||
|
import { getUncategorizedCollection } from "services/collectionService";
|
||||||
|
import exportService from "services/export";
|
||||||
|
import { getAccountsToken, getUserDetailsV2 } from "services/userService";
|
||||||
import { CollectionSummaries } from "types/collection";
|
import { CollectionSummaries } from "types/collection";
|
||||||
import DebugSection from "./DebugSection";
|
import { UserDetails } from "types/user";
|
||||||
import ExitSection from "./ExitSection";
|
import {
|
||||||
import HeaderSection from "./Header";
|
hasAddOnBonus,
|
||||||
import HelpSection from "./HelpSection";
|
hasExceededStorageQuota,
|
||||||
import ShortcutSection from "./ShortcutSection";
|
hasPaidSubscription,
|
||||||
import UtilitySection from "./UtilitySection";
|
hasStripeSubscription,
|
||||||
import { DrawerSidebar } from "./styledComponents";
|
isOnFreePlan,
|
||||||
import UserDetailsSection from "./userDetailsSection";
|
isSubscriptionActive,
|
||||||
|
isSubscriptionCancelled,
|
||||||
|
isSubscriptionPastDue,
|
||||||
|
} from "utils/billing";
|
||||||
|
import { openLink } from "utils/common";
|
||||||
|
import { getDownloadAppMessage } from "utils/ui";
|
||||||
|
import { isInternalUser } from "utils/user";
|
||||||
|
import { isFamilyAdmin, isPartOfFamily } from "utils/user/family";
|
||||||
|
import { testUpload } from "../../../tests/upload.test";
|
||||||
|
import { MemberSubscriptionManage } from "../MemberSubscriptionManage";
|
||||||
|
import Preferences from "./Preferences";
|
||||||
|
import SubscriptionCard from "./SubscriptionCard";
|
||||||
|
|
||||||
interface Iprops {
|
interface Iprops {
|
||||||
collectionSummaries: CollectionSummaries;
|
collectionSummaries: CollectionSummaries;
|
||||||
|
@ -40,3 +120,658 @@ export default function Sidebar({
|
||||||
</DrawerSidebar>
|
</DrawerSidebar>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DrawerSidebar = styled(EnteDrawer)(({ theme }) => ({
|
||||||
|
"& .MuiPaper-root": {
|
||||||
|
padding: theme.spacing(1.5),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
DrawerSidebar.defaultProps = { anchor: "left" };
|
||||||
|
|
||||||
|
interface HeaderSectionProps {
|
||||||
|
closeSidebar: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const HeaderSection: React.FC<HeaderSectionProps> = ({ closeSidebar }) => {
|
||||||
|
return (
|
||||||
|
<SpaceBetweenFlex mt={0.5} mb={1} pl={1.5}>
|
||||||
|
<EnteLogo />
|
||||||
|
<IconButton
|
||||||
|
aria-label="close"
|
||||||
|
onClick={closeSidebar}
|
||||||
|
color="secondary"
|
||||||
|
>
|
||||||
|
<CloseIcon fontSize="small" />
|
||||||
|
</IconButton>
|
||||||
|
</SpaceBetweenFlex>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface UserDetailsSectionProps {
|
||||||
|
sidebarView: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserDetailsSection: React.FC<UserDetailsSectionProps> = ({
|
||||||
|
sidebarView,
|
||||||
|
}) => {
|
||||||
|
const galleryContext = useContext(GalleryContext);
|
||||||
|
|
||||||
|
const [userDetails, setUserDetails] = useLocalState<UserDetails>(
|
||||||
|
LS_KEYS.USER_DETAILS,
|
||||||
|
);
|
||||||
|
const [memberSubscriptionManageView, setMemberSubscriptionManageView] =
|
||||||
|
useState(false);
|
||||||
|
|
||||||
|
const openMemberSubscriptionManage = () =>
|
||||||
|
setMemberSubscriptionManageView(true);
|
||||||
|
const closeMemberSubscriptionManage = () =>
|
||||||
|
setMemberSubscriptionManageView(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!sidebarView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const main = async () => {
|
||||||
|
const userDetails = await getUserDetailsV2();
|
||||||
|
setUserDetails(userDetails);
|
||||||
|
setData(LS_KEYS.SUBSCRIPTION, userDetails.subscription);
|
||||||
|
setData(LS_KEYS.FAMILY_DATA, userDetails.familyData);
|
||||||
|
setData(LS_KEYS.USER, {
|
||||||
|
...getData(LS_KEYS.USER),
|
||||||
|
email: userDetails.email,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
main();
|
||||||
|
}, [sidebarView]);
|
||||||
|
|
||||||
|
const isMemberSubscription = useMemo(
|
||||||
|
() =>
|
||||||
|
userDetails &&
|
||||||
|
isPartOfFamily(userDetails.familyData) &&
|
||||||
|
!isFamilyAdmin(userDetails.familyData),
|
||||||
|
[userDetails],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSubscriptionCardClick = () => {
|
||||||
|
if (isMemberSubscription) {
|
||||||
|
openMemberSubscriptionManage();
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
hasStripeSubscription(userDetails.subscription) &&
|
||||||
|
isSubscriptionPastDue(userDetails.subscription)
|
||||||
|
) {
|
||||||
|
billingService.redirectToCustomerPortal();
|
||||||
|
} else {
|
||||||
|
galleryContext.showPlanSelectorModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Box px={0.5} mt={2} pb={1.5} mb={1}>
|
||||||
|
<Typography px={1} pb={1} color="text.muted">
|
||||||
|
{userDetails ? (
|
||||||
|
userDetails.email
|
||||||
|
) : (
|
||||||
|
<Skeleton animation="wave" />
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
|
||||||
|
<SubscriptionCard
|
||||||
|
userDetails={userDetails}
|
||||||
|
onClick={handleSubscriptionCardClick}
|
||||||
|
/>
|
||||||
|
<SubscriptionStatus userDetails={userDetails} />
|
||||||
|
</Box>
|
||||||
|
{isMemberSubscription && (
|
||||||
|
<MemberSubscriptionManage
|
||||||
|
userDetails={userDetails}
|
||||||
|
open={memberSubscriptionManageView}
|
||||||
|
onClose={closeMemberSubscriptionManage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface SubscriptionStatusProps {
|
||||||
|
userDetails: UserDetails;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SubscriptionStatus: React.FC<SubscriptionStatusProps> = ({
|
||||||
|
userDetails,
|
||||||
|
}) => {
|
||||||
|
const { showPlanSelectorModal } = useContext(GalleryContext);
|
||||||
|
|
||||||
|
const hasAMessage = useMemo(() => {
|
||||||
|
if (!userDetails) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
isPartOfFamily(userDetails.familyData) &&
|
||||||
|
!isFamilyAdmin(userDetails.familyData)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
hasPaidSubscription(userDetails.subscription) &&
|
||||||
|
!isSubscriptionCancelled(userDetails.subscription)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}, [userDetails]);
|
||||||
|
|
||||||
|
const handleClick = useMemo(() => {
|
||||||
|
const eventHandler: MouseEventHandler<HTMLSpanElement> = (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
if (userDetails) {
|
||||||
|
if (isSubscriptionActive(userDetails.subscription)) {
|
||||||
|
if (hasExceededStorageQuota(userDetails)) {
|
||||||
|
showPlanSelectorModal();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (
|
||||||
|
hasStripeSubscription(userDetails.subscription) &&
|
||||||
|
isSubscriptionPastDue(userDetails.subscription)
|
||||||
|
) {
|
||||||
|
billingService.redirectToCustomerPortal();
|
||||||
|
} else {
|
||||||
|
showPlanSelectorModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return eventHandler;
|
||||||
|
}, [userDetails]);
|
||||||
|
|
||||||
|
if (!hasAMessage) {
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
let message: React.ReactNode;
|
||||||
|
if (!hasAddOnBonus(userDetails.bonusData)) {
|
||||||
|
if (isSubscriptionActive(userDetails.subscription)) {
|
||||||
|
if (isOnFreePlan(userDetails.subscription)) {
|
||||||
|
message = (
|
||||||
|
<Trans
|
||||||
|
i18nKey={"FREE_SUBSCRIPTION_INFO"}
|
||||||
|
values={{
|
||||||
|
date: userDetails.subscription?.expiryTime,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
} else if (isSubscriptionCancelled(userDetails.subscription)) {
|
||||||
|
message = t("RENEWAL_CANCELLED_SUBSCRIPTION_INFO", {
|
||||||
|
date: userDetails.subscription?.expiryTime,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message = (
|
||||||
|
<Trans
|
||||||
|
i18nKey={"SUBSCRIPTION_EXPIRED_MESSAGE"}
|
||||||
|
components={{
|
||||||
|
a: <LinkButton onClick={handleClick} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message && hasExceededStorageQuota(userDetails)) {
|
||||||
|
message = (
|
||||||
|
<Trans
|
||||||
|
i18nKey={"STORAGE_QUOTA_EXCEEDED_SUBSCRIPTION_INFO"}
|
||||||
|
components={{
|
||||||
|
a: <LinkButton onClick={handleClick} />,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message) return <></>;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box px={1} pt={0.5}>
|
||||||
|
<Typography
|
||||||
|
variant="small"
|
||||||
|
color={"text.muted"}
|
||||||
|
onClick={handleClick && handleClick}
|
||||||
|
sx={{ cursor: handleClick && "pointer" }}
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ShortcutSectionProps {
|
||||||
|
closeSidebar: () => void;
|
||||||
|
collectionSummaries: CollectionSummaries;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ShortcutSection: React.FC<ShortcutSectionProps> = ({
|
||||||
|
closeSidebar,
|
||||||
|
collectionSummaries,
|
||||||
|
}) => {
|
||||||
|
const galleryContext = useContext(GalleryContext);
|
||||||
|
const [uncategorizedCollectionId, setUncategorizedCollectionID] =
|
||||||
|
useState<number>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const main = async () => {
|
||||||
|
const unCategorizedCollection = await getUncategorizedCollection();
|
||||||
|
if (unCategorizedCollection) {
|
||||||
|
setUncategorizedCollectionID(unCategorizedCollection.id);
|
||||||
|
} else {
|
||||||
|
setUncategorizedCollectionID(DUMMY_UNCATEGORIZED_COLLECTION);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
main();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const openUncategorizedSection = () => {
|
||||||
|
galleryContext.setActiveCollectionID(uncategorizedCollectionId);
|
||||||
|
closeSidebar();
|
||||||
|
};
|
||||||
|
|
||||||
|
const openTrashSection = () => {
|
||||||
|
galleryContext.setActiveCollectionID(TRASH_SECTION);
|
||||||
|
closeSidebar();
|
||||||
|
};
|
||||||
|
|
||||||
|
const openArchiveSection = () => {
|
||||||
|
galleryContext.setActiveCollectionID(ARCHIVE_SECTION);
|
||||||
|
closeSidebar();
|
||||||
|
};
|
||||||
|
|
||||||
|
const openHiddenSection = () => {
|
||||||
|
galleryContext.openHiddenSection(() => {
|
||||||
|
closeSidebar();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EnteMenuItem
|
||||||
|
startIcon={<CategoryIcon />}
|
||||||
|
onClick={openUncategorizedSection}
|
||||||
|
variant="captioned"
|
||||||
|
label={t("UNCATEGORIZED")}
|
||||||
|
subText={collectionSummaries
|
||||||
|
.get(uncategorizedCollectionId)
|
||||||
|
?.fileCount.toString()}
|
||||||
|
/>
|
||||||
|
<EnteMenuItem
|
||||||
|
startIcon={<ArchiveOutlined />}
|
||||||
|
onClick={openArchiveSection}
|
||||||
|
variant="captioned"
|
||||||
|
label={t("ARCHIVE_SECTION_NAME")}
|
||||||
|
subText={collectionSummaries
|
||||||
|
.get(ARCHIVE_SECTION)
|
||||||
|
?.fileCount.toString()}
|
||||||
|
/>
|
||||||
|
<EnteMenuItem
|
||||||
|
startIcon={<VisibilityOff />}
|
||||||
|
onClick={openHiddenSection}
|
||||||
|
variant="captioned"
|
||||||
|
label={t("HIDDEN")}
|
||||||
|
subIcon={<LockOutlined />}
|
||||||
|
/>
|
||||||
|
<EnteMenuItem
|
||||||
|
startIcon={<DeleteOutline />}
|
||||||
|
onClick={openTrashSection}
|
||||||
|
variant="captioned"
|
||||||
|
label={t("TRASH")}
|
||||||
|
subText={collectionSummaries
|
||||||
|
.get(TRASH_SECTION)
|
||||||
|
?.fileCount.toString()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface UtilitySectionProps {
|
||||||
|
closeSidebar: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UtilitySection: React.FC<UtilitySectionProps> = ({ closeSidebar }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const appContext = useContext(AppContext);
|
||||||
|
const {
|
||||||
|
setDialogMessage,
|
||||||
|
startLoading,
|
||||||
|
watchFolderView,
|
||||||
|
setWatchFolderView,
|
||||||
|
themeColor,
|
||||||
|
setThemeColor,
|
||||||
|
} = appContext;
|
||||||
|
|
||||||
|
const [recoverModalView, setRecoveryModalView] = useState(false);
|
||||||
|
const [twoFactorModalView, setTwoFactorModalView] = useState(false);
|
||||||
|
const [preferencesView, setPreferencesView] = useState(false);
|
||||||
|
|
||||||
|
const openPreferencesOptions = () => setPreferencesView(true);
|
||||||
|
const closePreferencesOptions = () => setPreferencesView(false);
|
||||||
|
|
||||||
|
const openRecoveryKeyModal = () => setRecoveryModalView(true);
|
||||||
|
const closeRecoveryKeyModal = () => setRecoveryModalView(false);
|
||||||
|
|
||||||
|
const openTwoFactorModal = () => setTwoFactorModalView(true);
|
||||||
|
const closeTwoFactorModal = () => setTwoFactorModalView(false);
|
||||||
|
|
||||||
|
const openWatchFolder = () => {
|
||||||
|
if (isElectron()) {
|
||||||
|
setWatchFolderView(true);
|
||||||
|
} else {
|
||||||
|
setDialogMessage(getDownloadAppMessage());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const closeWatchFolder = () => setWatchFolderView(false);
|
||||||
|
|
||||||
|
const redirectToChangePasswordPage = () => {
|
||||||
|
closeSidebar();
|
||||||
|
router.push(PAGES.CHANGE_PASSWORD);
|
||||||
|
};
|
||||||
|
|
||||||
|
const redirectToChangeEmailPage = () => {
|
||||||
|
closeSidebar();
|
||||||
|
router.push(PAGES.CHANGE_EMAIL);
|
||||||
|
};
|
||||||
|
|
||||||
|
const redirectToAccountsPage = async () => {
|
||||||
|
closeSidebar();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// check if the user has passkey recovery enabled
|
||||||
|
const recoveryEnabled = await isPasskeyRecoveryEnabled();
|
||||||
|
if (!recoveryEnabled) {
|
||||||
|
// let's create the necessary recovery information
|
||||||
|
const recoveryKey = await getRecoveryKey();
|
||||||
|
|
||||||
|
const resetSecret = await generateEncryptionKey();
|
||||||
|
|
||||||
|
const encryptionResult = await encryptToB64(
|
||||||
|
resetSecret,
|
||||||
|
recoveryKey,
|
||||||
|
);
|
||||||
|
|
||||||
|
await configurePasskeyRecovery(
|
||||||
|
resetSecret,
|
||||||
|
encryptionResult.encryptedData,
|
||||||
|
encryptionResult.nonce,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountsToken = await getAccountsToken();
|
||||||
|
|
||||||
|
window.open(
|
||||||
|
`${getAccountsURL()}${
|
||||||
|
ACCOUNTS_PAGES.ACCOUNT_HANDOFF
|
||||||
|
}?package=${CLIENT_PACKAGE_NAMES.get(
|
||||||
|
APPS.PHOTOS,
|
||||||
|
)}&token=${accountsToken}`,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
log.error("failed to redirect to accounts page", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const redirectToDeduplicatePage = () => router.push(PAGES.DEDUPLICATE);
|
||||||
|
|
||||||
|
const somethingWentWrong = () =>
|
||||||
|
setDialogMessage({
|
||||||
|
title: t("ERROR"),
|
||||||
|
content: t("RECOVER_KEY_GENERATION_FAILED"),
|
||||||
|
close: { variant: "critical" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
setThemeColor((themeColor) =>
|
||||||
|
themeColor === THEME_COLOR.DARK
|
||||||
|
? THEME_COLOR.LIGHT
|
||||||
|
: THEME_COLOR.DARK,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isElectron() && (
|
||||||
|
<EnteMenuItem
|
||||||
|
onClick={openWatchFolder}
|
||||||
|
variant="secondary"
|
||||||
|
label={t("WATCH_FOLDERS")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<EnteMenuItem
|
||||||
|
variant="secondary"
|
||||||
|
onClick={openRecoveryKeyModal}
|
||||||
|
label={t("RECOVERY_KEY")}
|
||||||
|
/>
|
||||||
|
{isInternalUser() && (
|
||||||
|
<EnteMenuItem
|
||||||
|
onClick={toggleTheme}
|
||||||
|
variant="secondary"
|
||||||
|
label={t("CHOSE_THEME")}
|
||||||
|
endIcon={
|
||||||
|
<ThemeSwitcher
|
||||||
|
themeColor={themeColor}
|
||||||
|
setThemeColor={setThemeColor}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<EnteMenuItem
|
||||||
|
variant="secondary"
|
||||||
|
onClick={openTwoFactorModal}
|
||||||
|
label={t("TWO_FACTOR")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{isInternalUser() && (
|
||||||
|
<EnteMenuItem
|
||||||
|
variant="secondary"
|
||||||
|
onClick={redirectToAccountsPage}
|
||||||
|
label={t("PASSKEYS")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<EnteMenuItem
|
||||||
|
variant="secondary"
|
||||||
|
onClick={redirectToChangePasswordPage}
|
||||||
|
label={t("CHANGE_PASSWORD")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EnteMenuItem
|
||||||
|
variant="secondary"
|
||||||
|
onClick={redirectToChangeEmailPage}
|
||||||
|
label={t("CHANGE_EMAIL")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EnteMenuItem
|
||||||
|
variant="secondary"
|
||||||
|
onClick={redirectToDeduplicatePage}
|
||||||
|
label={t("DEDUPLICATE_FILES")}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<EnteMenuItem
|
||||||
|
variant="secondary"
|
||||||
|
onClick={openPreferencesOptions}
|
||||||
|
label={t("PREFERENCES")}
|
||||||
|
/>
|
||||||
|
<RecoveryKey
|
||||||
|
appContext={appContext}
|
||||||
|
show={recoverModalView}
|
||||||
|
onHide={closeRecoveryKeyModal}
|
||||||
|
somethingWentWrong={somethingWentWrong}
|
||||||
|
/>
|
||||||
|
<TwoFactorModal
|
||||||
|
show={twoFactorModalView}
|
||||||
|
onHide={closeTwoFactorModal}
|
||||||
|
closeSidebar={closeSidebar}
|
||||||
|
setLoading={startLoading}
|
||||||
|
/>
|
||||||
|
{isElectron() && (
|
||||||
|
<WatchFolder
|
||||||
|
open={watchFolderView}
|
||||||
|
onClose={closeWatchFolder}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Preferences
|
||||||
|
open={preferencesView}
|
||||||
|
onClose={closePreferencesOptions}
|
||||||
|
onRootClose={closeSidebar}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const HelpSection: React.FC = () => {
|
||||||
|
const { setDialogMessage } = useContext(AppContext);
|
||||||
|
const { openExportModal } = useContext(GalleryContext);
|
||||||
|
|
||||||
|
const openRoadmap = () =>
|
||||||
|
openLink("https://github.com/ente-io/ente/discussions", true);
|
||||||
|
|
||||||
|
const contactSupport = () => openLink("mailto:support@ente.io", true);
|
||||||
|
|
||||||
|
function openExport() {
|
||||||
|
if (isElectron()) {
|
||||||
|
openExportModal();
|
||||||
|
} else {
|
||||||
|
setDialogMessage(getDownloadAppMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EnteMenuItem
|
||||||
|
onClick={openRoadmap}
|
||||||
|
label={t("REQUEST_FEATURE")}
|
||||||
|
variant="secondary"
|
||||||
|
/>
|
||||||
|
<EnteMenuItem
|
||||||
|
onClick={contactSupport}
|
||||||
|
labelComponent={
|
||||||
|
<NoStyleAnchor href="mailto:support@ente.io">
|
||||||
|
<Typography fontWeight={"bold"}>
|
||||||
|
{t("SUPPORT")}
|
||||||
|
</Typography>
|
||||||
|
</NoStyleAnchor>
|
||||||
|
}
|
||||||
|
variant="secondary"
|
||||||
|
/>
|
||||||
|
<EnteMenuItem
|
||||||
|
onClick={openExport}
|
||||||
|
label={t("EXPORT")}
|
||||||
|
endIcon={
|
||||||
|
exportService.isExportInProgress() && (
|
||||||
|
<EnteSpinner size="20px" />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
variant="secondary"
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const ExitSection: React.FC = () => {
|
||||||
|
const { setDialogMessage, logout } = useContext(AppContext);
|
||||||
|
|
||||||
|
const [deleteAccountModalView, setDeleteAccountModalView] = useState(false);
|
||||||
|
|
||||||
|
const closeDeleteAccountModal = () => setDeleteAccountModalView(false);
|
||||||
|
const openDeleteAccountModal = () => setDeleteAccountModalView(true);
|
||||||
|
|
||||||
|
const confirmLogout = () => {
|
||||||
|
setDialogMessage({
|
||||||
|
title: t("LOGOUT_MESSAGE"),
|
||||||
|
proceed: {
|
||||||
|
text: t("LOGOUT"),
|
||||||
|
action: logout,
|
||||||
|
variant: "critical",
|
||||||
|
},
|
||||||
|
close: { text: t("CANCEL") },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EnteMenuItem
|
||||||
|
onClick={confirmLogout}
|
||||||
|
color="critical"
|
||||||
|
label={t("LOGOUT")}
|
||||||
|
variant="secondary"
|
||||||
|
/>
|
||||||
|
<EnteMenuItem
|
||||||
|
onClick={openDeleteAccountModal}
|
||||||
|
color="critical"
|
||||||
|
variant="secondary"
|
||||||
|
label={t("DELETE_ACCOUNT")}
|
||||||
|
/>
|
||||||
|
<DeleteAccountModal
|
||||||
|
open={deleteAccountModalView}
|
||||||
|
onClose={closeDeleteAccountModal}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const DebugSection: React.FC = () => {
|
||||||
|
const appContext = useContext(AppContext);
|
||||||
|
const [appVersion, setAppVersion] = useState<string | undefined>();
|
||||||
|
|
||||||
|
const electron = globalThis.electron;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
electron?.appVersion().then((v) => setAppVersion(v));
|
||||||
|
});
|
||||||
|
|
||||||
|
const confirmLogDownload = () =>
|
||||||
|
appContext.setDialogMessage({
|
||||||
|
title: t("DOWNLOAD_LOGS"),
|
||||||
|
content: <Trans i18nKey={"DOWNLOAD_LOGS_MESSAGE"} />,
|
||||||
|
proceed: {
|
||||||
|
text: t("DOWNLOAD"),
|
||||||
|
variant: "accent",
|
||||||
|
action: downloadLogs,
|
||||||
|
},
|
||||||
|
close: {
|
||||||
|
text: t("CANCEL"),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const downloadLogs = () => {
|
||||||
|
log.info("Downloading logs");
|
||||||
|
if (electron) electron.openLogDirectory();
|
||||||
|
else downloadAsFile(`debug_logs_${Date.now()}.txt`, savedLogs());
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<EnteMenuItem
|
||||||
|
onClick={confirmLogDownload}
|
||||||
|
variant="mini"
|
||||||
|
label={t("DOWNLOAD_UPLOAD_LOGS")}
|
||||||
|
/>
|
||||||
|
{appVersion && (
|
||||||
|
<Typography
|
||||||
|
py={"14px"}
|
||||||
|
px={"16px"}
|
||||||
|
color="text.muted"
|
||||||
|
variant="mini"
|
||||||
|
>
|
||||||
|
{appVersion}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{isInternalUser() && (
|
||||||
|
<EnteMenuItem
|
||||||
|
variant="secondary"
|
||||||
|
onClick={testUpload}
|
||||||
|
label={"Test Upload"}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
import CircleIcon from "@mui/icons-material/Circle";
|
|
||||||
import { styled } from "@mui/material";
|
|
||||||
import { EnteDrawer } from "components/EnteDrawer";
|
|
||||||
|
|
||||||
export const DrawerSidebar = styled(EnteDrawer)(({ theme }) => ({
|
|
||||||
"& .MuiPaper-root": {
|
|
||||||
padding: theme.spacing(1.5),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
DrawerSidebar.defaultProps = { anchor: "left" };
|
|
||||||
|
|
||||||
export const DotSeparator = styled(CircleIcon)`
|
|
||||||
font-size: 4px;
|
|
||||||
margin: 0 ${({ theme }) => theme.spacing(1)};
|
|
||||||
color: inherit;
|
|
||||||
`;
|
|
|
@ -1,96 +0,0 @@
|
||||||
import { useLocalState } from "@ente/shared/hooks/useLocalState";
|
|
||||||
import { LS_KEYS, getData, setData } from "@ente/shared/storage/localStorage";
|
|
||||||
import { Box, Skeleton } from "@mui/material";
|
|
||||||
import Typography from "@mui/material/Typography";
|
|
||||||
import { GalleryContext } from "pages/gallery";
|
|
||||||
import { useContext, useEffect, useMemo, useState } from "react";
|
|
||||||
import billingService from "services/billingService";
|
|
||||||
import { getUserDetailsV2 } from "services/userService";
|
|
||||||
import { UserDetails } from "types/user";
|
|
||||||
import { hasStripeSubscription, isSubscriptionPastDue } from "utils/billing";
|
|
||||||
import { isFamilyAdmin, isPartOfFamily } from "utils/user/family";
|
|
||||||
import { MemberSubscriptionManage } from "../MemberSubscriptionManage";
|
|
||||||
import SubscriptionCard from "./SubscriptionCard";
|
|
||||||
import SubscriptionStatus from "./SubscriptionStatus";
|
|
||||||
|
|
||||||
export default function UserDetailsSection({ sidebarView }) {
|
|
||||||
const galleryContext = useContext(GalleryContext);
|
|
||||||
|
|
||||||
const [userDetails, setUserDetails] = useLocalState<UserDetails>(
|
|
||||||
LS_KEYS.USER_DETAILS,
|
|
||||||
);
|
|
||||||
const [memberSubscriptionManageView, setMemberSubscriptionManageView] =
|
|
||||||
useState(false);
|
|
||||||
|
|
||||||
const openMemberSubscriptionManage = () =>
|
|
||||||
setMemberSubscriptionManageView(true);
|
|
||||||
const closeMemberSubscriptionManage = () =>
|
|
||||||
setMemberSubscriptionManageView(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!sidebarView) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const main = async () => {
|
|
||||||
const userDetails = await getUserDetailsV2();
|
|
||||||
setUserDetails(userDetails);
|
|
||||||
setData(LS_KEYS.SUBSCRIPTION, userDetails.subscription);
|
|
||||||
setData(LS_KEYS.FAMILY_DATA, userDetails.familyData);
|
|
||||||
setData(LS_KEYS.USER, {
|
|
||||||
...getData(LS_KEYS.USER),
|
|
||||||
email: userDetails.email,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
main();
|
|
||||||
}, [sidebarView]);
|
|
||||||
|
|
||||||
const isMemberSubscription = useMemo(
|
|
||||||
() =>
|
|
||||||
userDetails &&
|
|
||||||
isPartOfFamily(userDetails.familyData) &&
|
|
||||||
!isFamilyAdmin(userDetails.familyData),
|
|
||||||
[userDetails],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleSubscriptionCardClick = () => {
|
|
||||||
if (isMemberSubscription) {
|
|
||||||
openMemberSubscriptionManage();
|
|
||||||
} else {
|
|
||||||
if (
|
|
||||||
hasStripeSubscription(userDetails.subscription) &&
|
|
||||||
isSubscriptionPastDue(userDetails.subscription)
|
|
||||||
) {
|
|
||||||
billingService.redirectToCustomerPortal();
|
|
||||||
} else {
|
|
||||||
galleryContext.showPlanSelectorModal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Box px={0.5} mt={2} pb={1.5} mb={1}>
|
|
||||||
<Typography px={1} pb={1} color="text.muted">
|
|
||||||
{userDetails ? (
|
|
||||||
userDetails.email
|
|
||||||
) : (
|
|
||||||
<Skeleton animation="wave" />
|
|
||||||
)}
|
|
||||||
</Typography>
|
|
||||||
|
|
||||||
<SubscriptionCard
|
|
||||||
userDetails={userDetails}
|
|
||||||
onClick={handleSubscriptionCardClick}
|
|
||||||
/>
|
|
||||||
<SubscriptionStatus userDetails={userDetails} />
|
|
||||||
</Box>
|
|
||||||
{isMemberSubscription && (
|
|
||||||
<MemberSubscriptionManage
|
|
||||||
userDetails={userDetails}
|
|
||||||
open={memberSubscriptionManageView}
|
|
||||||
onClose={closeMemberSubscriptionManage}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -4,7 +4,6 @@ import { DialogBoxAttributes } from "@ente/shared/components/DialogBox/types";
|
||||||
import AutoAwesomeOutlinedIcon from "@mui/icons-material/AutoAwesomeOutlined";
|
import AutoAwesomeOutlinedIcon from "@mui/icons-material/AutoAwesomeOutlined";
|
||||||
import InfoOutlined from "@mui/icons-material/InfoRounded";
|
import InfoOutlined from "@mui/icons-material/InfoRounded";
|
||||||
import { Link } from "@mui/material";
|
import { Link } from "@mui/material";
|
||||||
import { OPEN_STREET_MAP_LINK } from "components/Sidebar/EnableMap";
|
|
||||||
import { t } from "i18next";
|
import { t } from "i18next";
|
||||||
import { Trans } from "react-i18next";
|
import { Trans } from "react-i18next";
|
||||||
import { Subscription } from "types/billing";
|
import { Subscription } from "types/billing";
|
||||||
|
@ -143,7 +142,12 @@ export const getMapEnableConfirmationDialog = (
|
||||||
<Trans
|
<Trans
|
||||||
i18nKey={"ENABLE_MAP_DESCRIPTION"}
|
i18nKey={"ENABLE_MAP_DESCRIPTION"}
|
||||||
components={{
|
components={{
|
||||||
a: <Link target="_blank" href={OPEN_STREET_MAP_LINK} />,
|
a: (
|
||||||
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
href="https://www.openstreetmap.org/"
|
||||||
|
/>
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
|
|
Loading…
Reference in a new issue