Selaa lähdekoodia

[web] Inline sidebar code (#1803)

... to make it more manageable.
Manav Rathi 1 vuosi sitten
vanhempi
commit
f60e750848
22 muutettua tiedostoa jossa 1071 lisäystä ja 1109 poistoa
  1. 0 69
      web/apps/photos/src/components/Sidebar/DebugSection.tsx
  2. 0 35
      web/apps/photos/src/components/Sidebar/DisableMap.tsx
  3. 0 43
      web/apps/photos/src/components/Sidebar/EnableMap.tsx
  4. 0 47
      web/apps/photos/src/components/Sidebar/ExitSection.tsx
  5. 0 23
      web/apps/photos/src/components/Sidebar/Header.tsx
  6. 0 62
      web/apps/photos/src/components/Sidebar/HelpSection.tsx
  7. 226 0
      web/apps/photos/src/components/Sidebar/MapSetting.tsx
  8. 0 76
      web/apps/photos/src/components/Sidebar/MapSetting/ModifyMapEnabled.tsx
  9. 0 82
      web/apps/photos/src/components/Sidebar/MapSetting/index.tsx
  10. 60 3
      web/apps/photos/src/components/Sidebar/Preferences.tsx
  11. 0 61
      web/apps/photos/src/components/Sidebar/Preferences/LanguageSelector.tsx
  12. 0 102
      web/apps/photos/src/components/Sidebar/ShortcutSection.tsx
  13. 0 11
      web/apps/photos/src/components/Sidebar/SubscriptionCard/backgroundOverlay.tsx
  14. 0 15
      web/apps/photos/src/components/Sidebar/SubscriptionCard/clickOverlay.tsx
  15. 28 3
      web/apps/photos/src/components/Sidebar/SubscriptionCard/index.tsx
  16. 7 1
      web/apps/photos/src/components/Sidebar/SubscriptionCard/styledComponents.tsx
  17. 0 130
      web/apps/photos/src/components/Sidebar/SubscriptionStatus/index.tsx
  18. 0 222
      web/apps/photos/src/components/Sidebar/UtilitySection.tsx
  19. 744 9
      web/apps/photos/src/components/Sidebar/index.tsx
  20. 0 17
      web/apps/photos/src/components/Sidebar/styledComponents.tsx
  21. 0 96
      web/apps/photos/src/components/Sidebar/userDetailsSection.tsx
  22. 6 2
      web/apps/photos/src/utils/ui/index.tsx

+ 0 - 69
web/apps/photos/src/components/Sidebar/DebugSection.tsx

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

+ 0 - 35
web/apps/photos/src/components/Sidebar/DisableMap.tsx

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

+ 0 - 43
web/apps/photos/src/components/Sidebar/EnableMap.tsx

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

+ 0 - 47
web/apps/photos/src/components/Sidebar/ExitSection.tsx

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

+ 0 - 23
web/apps/photos/src/components/Sidebar/Header.tsx

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

+ 0 - 62
web/apps/photos/src/components/Sidebar/HelpSection.tsx

@@ -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 - 0
web/apps/photos/src/components/Sidebar/MapSetting.tsx

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

+ 0 - 76
web/apps/photos/src/components/Sidebar/MapSetting/ModifyMapEnabled.tsx

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

+ 0 - 82
web/apps/photos/src/components/Sidebar/MapSetting/index.tsx

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

+ 60 - 3
web/apps/photos/src/components/Sidebar/Preferences/index.tsx → web/apps/photos/src/components/Sidebar/Preferences.tsx

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

+ 0 - 61
web/apps/photos/src/components/Sidebar/Preferences/LanguageSelector.tsx

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

+ 0 - 102
web/apps/photos/src/components/Sidebar/ShortcutSection.tsx

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

+ 0 - 11
web/apps/photos/src/components/Sidebar/SubscriptionCard/backgroundOverlay.tsx

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

+ 0 - 15
web/apps/photos/src/components/Sidebar/SubscriptionCard/clickOverlay.tsx

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

+ 28 - 3
web/apps/photos/src/components/Sidebar/SubscriptionCard/index.tsx

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

+ 7 - 1
web/apps/photos/src/components/Sidebar/SubscriptionCard/styledComponents.tsx

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

+ 0 - 130
web/apps/photos/src/components/Sidebar/SubscriptionStatus/index.tsx

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

+ 0 - 222
web/apps/photos/src/components/Sidebar/UtilitySection.tsx

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

+ 744 - 9
web/apps/photos/src/components/Sidebar/index.tsx

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

+ 0 - 17
web/apps/photos/src/components/Sidebar/styledComponents.tsx

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

+ 0 - 96
web/apps/photos/src/components/Sidebar/userDetailsSection.tsx

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

+ 6 - 2
web/apps/photos/src/utils/ui/index.tsx

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