commit
f60e750848
22 changed files with 1071 additions and 1109 deletions
|
@ -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 { Box, DialogProps, Stack } from "@mui/material";
|
||||
import DropdownInput from "components/DropdownInput";
|
||||
import { EnteDrawer } from "components/EnteDrawer";
|
||||
import { EnteMenuItem } from "components/Menu/EnteMenuItem";
|
||||
import Titlebar from "components/Titlebar";
|
||||
import { t } from "i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import AdvancedSettings from "../AdvancedSettings";
|
||||
import MapSettings from "../MapSetting";
|
||||
import { LanguageSelector } from "./LanguageSelector";
|
||||
import AdvancedSettings from "./AdvancedSettings";
|
||||
import MapSettings from "./MapSetting";
|
||||
|
||||
export default function Preferences({ open, onClose, onRootClose }) {
|
||||
const [advancedSettingsView, setAdvancedSettingsView] = useState(false);
|
||||
|
@ -76,3 +83,53 @@ export default function Preferences({ open, onClose, onRootClose }) {
|
|||
</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 { UserDetails } from "types/user";
|
||||
import { BackgroundOverlay } from "./backgroundOverlay";
|
||||
import { ClickOverlay } from "./clickOverlay";
|
||||
|
||||
import { SubscriptionCardContentOverlay } from "./contentOverlay";
|
||||
|
||||
const SUBSCRIPTION_CARD_SIZE = 152;
|
||||
|
@ -32,3 +31,29 @@ export default function SubscriptionCard({ userDetails, onClick }: Iprops) {
|
|||
</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 { DotSeparator } from "../styledComponents";
|
||||
|
||||
export const Progressbar = styled(LinearProgress)(() => ({
|
||||
".MuiLinearProgress-bar": {
|
||||
|
@ -13,6 +13,12 @@ Progressbar.defaultProps = {
|
|||
variant: "determinate",
|
||||
};
|
||||
|
||||
const DotSeparator = styled(CircleIcon)`
|
||||
font-size: 4px;
|
||||
margin: 0 ${({ theme }) => theme.spacing(1)};
|
||||
color: inherit;
|
||||
`;
|
||||
|
||||
export const LegendIndicator = styled(DotSeparator)`
|
||||
font-size: 8.71px;
|
||||
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 DebugSection from "./DebugSection";
|
||||
import ExitSection from "./ExitSection";
|
||||
import HeaderSection from "./Header";
|
||||
import HelpSection from "./HelpSection";
|
||||
import ShortcutSection from "./ShortcutSection";
|
||||
import UtilitySection from "./UtilitySection";
|
||||
import { DrawerSidebar } from "./styledComponents";
|
||||
import UserDetailsSection from "./userDetailsSection";
|
||||
import { UserDetails } from "types/user";
|
||||
import {
|
||||
hasAddOnBonus,
|
||||
hasExceededStorageQuota,
|
||||
hasPaidSubscription,
|
||||
hasStripeSubscription,
|
||||
isOnFreePlan,
|
||||
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 {
|
||||
collectionSummaries: CollectionSummaries;
|
||||
|
@ -40,3 +120,658 @@ export default function Sidebar({
|
|||
</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 InfoOutlined from "@mui/icons-material/InfoRounded";
|
||||
import { Link } from "@mui/material";
|
||||
import { OPEN_STREET_MAP_LINK } from "components/Sidebar/EnableMap";
|
||||
import { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
import { Subscription } from "types/billing";
|
||||
|
@ -143,7 +142,12 @@ export const getMapEnableConfirmationDialog = (
|
|||
<Trans
|
||||
i18nKey={"ENABLE_MAP_DESCRIPTION"}
|
||||
components={{
|
||||
a: <Link target="_blank" href={OPEN_STREET_MAP_LINK} />,
|
||||
a: (
|
||||
<Link
|
||||
target="_blank"
|
||||
href="https://www.openstreetmap.org/"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
|
|
Loading…
Add table
Reference in a new issue