Merge branch 'main' into generic_group_by

This commit is contained in:
Neeraj Gupta 2024-05-22 15:18:44 +05:30
commit ce6160a06a
22 changed files with 1071 additions and 1109 deletions

View file

@ -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"}
/>
)}
</>
);
}

View file

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

View file

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

View file

@ -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}
/>
</>
);
}

View file

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

View file

@ -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"
/>
</>
);
}

View 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>
);
}

View file

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

View file

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

View file

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

View file

@ -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}
/>
);
};

View file

@ -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()}
/>
</>
);
}

View file

@ -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"
/>
);
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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}
/>
</>
);
}

View file

@ -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"}
/>
)}
</>
);
};

View file

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

View file

@ -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}
/>
)}
</>
);
}

View file

@ -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/"
/>
),
}} }}
/> />
), ),