[web] Dialog enhancements - Part x/x (#3657)
This commit is contained in:
@@ -1,11 +1,9 @@
|
||||
import { staticAppTitle } from "@/base/app";
|
||||
import { CustomHead } from "@/base/components/Head";
|
||||
import {
|
||||
type MiniDialogAttributes,
|
||||
MiniDialog,
|
||||
} from "@/base/components/MiniDialog";
|
||||
import { AttributedMiniDialog } from "@/base/components/MiniDialog";
|
||||
import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator";
|
||||
import { AppNavbar } from "@/base/components/Navbar";
|
||||
import { useAttributedMiniDialog } from "@/base/components/utils/mini-dialog";
|
||||
import { setupI18n } from "@/base/i18n";
|
||||
import { disableDiskLogs } from "@/base/log";
|
||||
import { logUnhandledErrorsAndRejections } from "@/base/log-web";
|
||||
@@ -16,7 +14,7 @@ import { CssBaseline } from "@mui/material";
|
||||
import { ThemeProvider } from "@mui/material/styles";
|
||||
import { t } from "i18next";
|
||||
import type { AppProps } from "next/app";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { AppContext } from "../types/context";
|
||||
|
||||
import "styles/global.css";
|
||||
@@ -24,10 +22,7 @@ import "styles/global.css";
|
||||
const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
const [isI18nReady, setIsI18nReady] = useState<boolean>(false);
|
||||
const [showNavbar, setShowNavbar] = useState(false);
|
||||
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = useState<
|
||||
MiniDialogAttributes | undefined
|
||||
>();
|
||||
const [dialogBoxV2View, setDialogBoxV2View] = useState(false);
|
||||
const { showMiniDialog, miniDialogProps } = useAttributedMiniDialog();
|
||||
|
||||
useEffect(() => {
|
||||
disableDiskLogs();
|
||||
@@ -36,16 +31,13 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
return () => logUnhandledErrorsAndRejections(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setDialogBoxV2View(true);
|
||||
}, [dialogBoxAttributeV2]);
|
||||
|
||||
const closeDialogBoxV2 = () => setDialogBoxV2View(false);
|
||||
|
||||
const appContext = {
|
||||
showNavBar: setShowNavbar,
|
||||
setDialogBoxAttributesV2,
|
||||
};
|
||||
const appContext = useMemo(
|
||||
() => ({
|
||||
showNavBar: setShowNavbar,
|
||||
showMiniDialog,
|
||||
}),
|
||||
[showMiniDialog],
|
||||
);
|
||||
|
||||
const title = isI18nReady ? t("title_accounts") : staticAppTitle;
|
||||
|
||||
@@ -55,12 +47,7 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
|
||||
<ThemeProvider theme={getTheme(THEME_COLOR.DARK, "photos")}>
|
||||
<CssBaseline enableColorScheme />
|
||||
<MiniDialog
|
||||
sx={{ zIndex: 1600 }}
|
||||
open={dialogBoxV2View}
|
||||
onClose={closeDialogBoxV2}
|
||||
attributes={dialogBoxAttributeV2}
|
||||
/>
|
||||
<AttributedMiniDialog {...miniDialogProps} />
|
||||
|
||||
<AppContext.Provider value={appContext}>
|
||||
{!isI18nReady && (
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { EnteDrawer } from "@/base/components/EnteDrawer";
|
||||
import { MenuItemDivider, MenuItemGroup } from "@/base/components/Menu";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import { errorDialogAttributes } from "@/base/components/utils/mini-dialog";
|
||||
import log from "@/base/log";
|
||||
import { ensure } from "@/utils/ensure";
|
||||
import { CenteredFlex } from "@ente/shared/components/Container";
|
||||
@@ -34,7 +35,7 @@ import {
|
||||
import { useAppContext } from "../../types/context";
|
||||
|
||||
const Page: React.FC = () => {
|
||||
const { showNavBar, setDialogBoxAttributesV2 } = useAppContext();
|
||||
const { showNavBar, showMiniDialog } = useAppContext();
|
||||
|
||||
const [token, setToken] = useState<string | undefined>();
|
||||
const [passkeys, setPasskeys] = useState<Passkey[]>([]);
|
||||
@@ -44,12 +45,8 @@ const Page: React.FC = () => {
|
||||
>();
|
||||
|
||||
const showPasskeyFetchFailedErrorDialog = useCallback(() => {
|
||||
setDialogBoxAttributesV2({
|
||||
title: t("error"),
|
||||
content: t("passkey_fetch_failed"),
|
||||
close: {},
|
||||
});
|
||||
}, [setDialogBoxAttributesV2]);
|
||||
showMiniDialog(errorDialogAttributes(t("passkey_fetch_failed")));
|
||||
}, [showMiniDialog]);
|
||||
|
||||
useEffect(() => {
|
||||
showNavBar(true);
|
||||
@@ -263,34 +260,26 @@ const ManagePasskeyDrawer: React.FC<ManagePasskeyDrawerProps> = ({
|
||||
passkey,
|
||||
onUpdateOrDeletePasskey,
|
||||
}) => {
|
||||
const { setDialogBoxAttributesV2 } = useAppContext();
|
||||
const { showMiniDialog } = useAppContext();
|
||||
|
||||
const [showRenameDialog, setShowRenameDialog] = useState(false);
|
||||
|
||||
const showDeleteConfirmationDialog = useCallback(() => {
|
||||
const handleDelete = async (setLoading: (value: boolean) => void) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await deletePasskey(ensure(token), ensure(passkey).id);
|
||||
onUpdateOrDeletePasskey();
|
||||
} catch (e) {
|
||||
log.error("Failed to delete passkey", e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
setDialogBoxAttributesV2({
|
||||
title: t("delete_passkey"),
|
||||
content: t("delete_passkey_confirmation"),
|
||||
proceed: {
|
||||
text: t("delete"),
|
||||
action: handleDelete,
|
||||
variant: "critical",
|
||||
},
|
||||
close: { text: t("cancel") },
|
||||
});
|
||||
}, [token, passkey, onUpdateOrDeletePasskey, setDialogBoxAttributesV2]);
|
||||
const showDeleteConfirmationDialog = useCallback(
|
||||
() =>
|
||||
showMiniDialog({
|
||||
title: t("delete_passkey"),
|
||||
message: t("delete_passkey_confirmation"),
|
||||
continue: {
|
||||
text: t("delete"),
|
||||
color: "critical",
|
||||
action: async () => {
|
||||
await deletePasskey(ensure(token), ensure(passkey).id);
|
||||
onUpdateOrDeletePasskey();
|
||||
},
|
||||
},
|
||||
}),
|
||||
[showMiniDialog, token, passkey, onUpdateOrDeletePasskey],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import type { MiniDialogAttributes } from "@/base/components/MiniDialog";
|
||||
import type { AccountsContextT } from "@/accounts/types/context";
|
||||
import { ensure } from "@/utils/ensure";
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
/**
|
||||
* The type of the context for pages in the accounts app.
|
||||
*/
|
||||
interface AppContextT {
|
||||
/** Show or hide the app's navigation bar. */
|
||||
showNavBar: (show: boolean) => void;
|
||||
setDialogBoxAttributesV2: (attrs: MiniDialogAttributes) => void;
|
||||
}
|
||||
type AppContextT = Omit<AccountsContextT, "logout">;
|
||||
|
||||
/**
|
||||
* The React {@link Context} available to all nodes in the React tree.
|
||||
|
||||
@@ -2,12 +2,13 @@ import { accountLogout } from "@/accounts/services/logout";
|
||||
import type { AccountsContextT } from "@/accounts/types/context";
|
||||
import { clientPackageName, staticAppTitle } from "@/base/app";
|
||||
import { CustomHead } from "@/base/components/Head";
|
||||
import {
|
||||
type MiniDialogAttributes,
|
||||
MiniDialog,
|
||||
} from "@/base/components/MiniDialog";
|
||||
import { AttributedMiniDialog } from "@/base/components/MiniDialog";
|
||||
import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator";
|
||||
import { AppNavbar } from "@/base/components/Navbar";
|
||||
import {
|
||||
genericErrorDialogAttributes,
|
||||
useAttributedMiniDialog,
|
||||
} from "@/base/components/utils/mini-dialog";
|
||||
import { setupI18n } from "@/base/i18n";
|
||||
import {
|
||||
logStartupBanner,
|
||||
@@ -39,6 +40,7 @@ import React, {
|
||||
useState,
|
||||
} from "react";
|
||||
import LoadingBar, { type LoadingBarRef } from "react-top-loading-bar";
|
||||
|
||||
import "../../public/css/global.css";
|
||||
|
||||
/**
|
||||
@@ -68,10 +70,8 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
const [showNavbar, setShowNavBar] = useState(false);
|
||||
const isLoadingBarRunning = useRef<boolean>(false);
|
||||
const loadingBar = useRef<LoadingBarRef>(null);
|
||||
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = useState<
|
||||
MiniDialogAttributes | undefined
|
||||
>();
|
||||
const [dialogBoxV2View, setDialogBoxV2View] = useState(false);
|
||||
|
||||
const { showMiniDialog, miniDialogProps } = useAttributedMiniDialog();
|
||||
const [themeColor, setThemeColor] = useLocalState(
|
||||
LS_KEYS.THEME,
|
||||
THEME_COLOR.DARK,
|
||||
@@ -111,16 +111,13 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setDialogBoxV2View(true);
|
||||
}, [dialogBoxAttributeV2]);
|
||||
|
||||
const showNavBar = (show: boolean) => setShowNavBar(show);
|
||||
|
||||
const startLoading = () => {
|
||||
!isLoadingBarRunning.current && loadingBar.current?.continuousStart();
|
||||
isLoadingBarRunning.current = true;
|
||||
};
|
||||
|
||||
const finishLoading = () => {
|
||||
setTimeout(() => {
|
||||
isLoadingBarRunning.current && loadingBar.current?.complete();
|
||||
@@ -128,14 +125,8 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const closeDialogBoxV2 = () => setDialogBoxV2View(false);
|
||||
|
||||
const somethingWentWrong = () =>
|
||||
setDialogBoxAttributesV2({
|
||||
title: t("error"),
|
||||
close: { variant: "critical" },
|
||||
content: t("generic_error_retry"),
|
||||
});
|
||||
showMiniDialog(genericErrorDialogAttributes());
|
||||
|
||||
const logout = () => {
|
||||
void accountLogout().then(() => router.push("/"));
|
||||
@@ -144,7 +135,7 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
const appContext = {
|
||||
logout,
|
||||
showNavBar,
|
||||
setDialogBoxAttributesV2,
|
||||
showMiniDialog,
|
||||
startLoading,
|
||||
finishLoading,
|
||||
themeColor,
|
||||
@@ -167,12 +158,7 @@ const App: React.FC<AppProps> = ({ Component, pageProps }) => {
|
||||
|
||||
<LoadingBar color="#51cd7c" ref={loadingBar} />
|
||||
|
||||
<MiniDialog
|
||||
sx={{ zIndex: 1600 }}
|
||||
open={dialogBoxV2View}
|
||||
onClose={closeDialogBoxV2}
|
||||
attributes={dialogBoxAttributeV2}
|
||||
/>
|
||||
<AttributedMiniDialog {...miniDialogProps} />
|
||||
|
||||
<AppContext.Provider value={appContext}>
|
||||
{(loading || !isI18nReady) && (
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { sessionExpiredDialogAttributes } from "@/accounts/components/LoginComponents";
|
||||
import { stashRedirect } from "@/accounts/services/redirect";
|
||||
import { EnteLogo } from "@/base/components/EnteLogo";
|
||||
import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator";
|
||||
@@ -7,7 +8,6 @@ import {
|
||||
HorizontalFlex,
|
||||
VerticallyCentered,
|
||||
} from "@ente/shared/components/Container";
|
||||
import { sessionExpiredDialogAttributes } from "@ente/shared/components/LoginComponents";
|
||||
import OverflowMenu from "@ente/shared/components/OverflowMenu/menu";
|
||||
import { OverflowMenuOption } from "@ente/shared/components/OverflowMenu/option";
|
||||
import { AUTH_PAGES as PAGES } from "@ente/shared/constants/pages";
|
||||
@@ -23,7 +23,7 @@ import { getAuthCodes } from "services/remote";
|
||||
import { AppContext } from "./_app";
|
||||
|
||||
const Page: React.FC = () => {
|
||||
const { logout, showNavBar, setDialogBoxAttributesV2 } = ensure(
|
||||
const { logout, showNavBar, showMiniDialog } = ensure(
|
||||
useContext(AppContext),
|
||||
);
|
||||
const router = useRouter();
|
||||
@@ -32,7 +32,7 @@ const Page: React.FC = () => {
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
||||
const showSessionExpiredDialog = () =>
|
||||
setDialogBoxAttributesV2(sessionExpiredDialogAttributes(logout));
|
||||
showMiniDialog(sessionExpiredDialogAttributes(logout));
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCodes = async () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { checkSessionValidity } from "@/accounts/services/session";
|
||||
import {
|
||||
DialogBoxV2,
|
||||
TitledMiniDialog,
|
||||
type MiniDialogAttributes,
|
||||
} from "@/base/components/MiniDialog";
|
||||
import log from "@/base/log";
|
||||
@@ -24,8 +24,7 @@ export default function AuthenticateUserModal({
|
||||
onClose,
|
||||
onAuthenticate,
|
||||
}: Iprops) {
|
||||
const { setDialogMessage, setDialogBoxAttributesV2, logout } =
|
||||
useContext(AppContext);
|
||||
const { setDialogMessage, showMiniDialog, logout } = useContext(AppContext);
|
||||
const [user, setUser] = useState<User>();
|
||||
const [keyAttributes, setKeyAttributes] = useState<KeyAttributes>();
|
||||
|
||||
@@ -46,7 +45,7 @@ export default function AuthenticateUserModal({
|
||||
const session = await checkSessionValidity();
|
||||
if (session.status != "valid") {
|
||||
onClose();
|
||||
setDialogBoxAttributesV2(
|
||||
showMiniDialog(
|
||||
passwordChangedElsewhereDialogAttributes(logout),
|
||||
);
|
||||
}
|
||||
@@ -55,7 +54,7 @@ export default function AuthenticateUserModal({
|
||||
// potentially transient issues.
|
||||
log.warn("Ignoring error when determining session validity", e);
|
||||
}
|
||||
}, [setDialogBoxAttributesV2, logout]);
|
||||
}, [showMiniDialog, logout]);
|
||||
|
||||
useEffect(() => {
|
||||
const main = async () => {
|
||||
@@ -98,13 +97,11 @@ export default function AuthenticateUserModal({
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogBoxV2
|
||||
<TitledMiniDialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
sx={{ position: "absolute" }}
|
||||
attributes={{
|
||||
title: t("password"),
|
||||
}}
|
||||
title={t("password")}
|
||||
>
|
||||
<VerifyMasterPasswordForm
|
||||
buttonText={t("AUTHENTICATE")}
|
||||
@@ -113,7 +110,7 @@ export default function AuthenticateUserModal({
|
||||
keyAttributes={keyAttributes}
|
||||
submitButtonProps={{ sx: { mb: 0 } }}
|
||||
/>
|
||||
</DialogBoxV2>
|
||||
</TitledMiniDialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -128,11 +125,10 @@ const passwordChangedElsewhereDialogAttributes = (
|
||||
onLogin: () => void,
|
||||
): MiniDialogAttributes => ({
|
||||
title: t("password_changed_elsewhere"),
|
||||
content: t("password_changed_elsewhere_message"),
|
||||
proceed: {
|
||||
message: t("password_changed_elsewhere_message"),
|
||||
continue: {
|
||||
text: t("login"),
|
||||
action: onLogin,
|
||||
variant: "accent",
|
||||
},
|
||||
close: { text: t("cancel") },
|
||||
cancel: false,
|
||||
});
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { DialogBoxV2 } from "@/base/components/MiniDialog";
|
||||
import { TitledMiniDialog } from "@/base/components/MiniDialog";
|
||||
import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator";
|
||||
import { boxSeal } from "@/base/crypto/libsodium";
|
||||
import log from "@/base/log";
|
||||
import type { Collection } from "@/media/collection";
|
||||
import { photosDialogZIndex } from "@/new/photos/components/z-index";
|
||||
import { loadCast } from "@/new/photos/utils/chromecast-sender";
|
||||
import SingleInputForm, {
|
||||
type SingleInputFormProps,
|
||||
@@ -135,11 +136,11 @@ export const AlbumCastDialog: React.FC<AlbumCastDialogProps> = ({
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<DialogBoxV2
|
||||
<TitledMiniDialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
attributes={{ title: t("cast_album_to_tv") }}
|
||||
sx={{ zIndex: 1600 }}
|
||||
title={t("cast_album_to_tv")}
|
||||
sx={{ zIndex: photosDialogZIndex }}
|
||||
>
|
||||
{view == "choose" && (
|
||||
<Stack sx={{ py: 1, gap: 4 }}>
|
||||
@@ -213,6 +214,6 @@ export const AlbumCastDialog: React.FC<AlbumCastDialogProps> = ({
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</DialogBoxV2>
|
||||
</TitledMiniDialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DialogBoxV2 } from "@/base/components/MiniDialog";
|
||||
import { TitledMiniDialog } from "@/base/components/MiniDialog";
|
||||
import SingleInputForm, {
|
||||
type SingleInputFormProps,
|
||||
} from "@ente/shared/components/SingleInputForm";
|
||||
@@ -39,12 +39,10 @@ export default function CollectionNamer({ attributes, ...props }: Props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogBoxV2
|
||||
<TitledMiniDialog
|
||||
open={props.show}
|
||||
onClose={props.onHide}
|
||||
attributes={{
|
||||
title: attributes.title,
|
||||
}}
|
||||
title={attributes.title}
|
||||
>
|
||||
<SingleInputForm
|
||||
callback={onSubmit}
|
||||
@@ -55,6 +53,6 @@ export default function CollectionNamer({ attributes, ...props }: Props) {
|
||||
submitButtonProps={{ sx: { mt: 1, mb: 2 } }}
|
||||
secondaryButtonAction={props.onHide}
|
||||
/>
|
||||
</DialogBoxV2>
|
||||
</TitledMiniDialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { DialogBoxV2 } from "@/base/components/MiniDialog";
|
||||
import { TitledMiniDialog } from "@/base/components/MiniDialog";
|
||||
import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton";
|
||||
import { LoadingButton } from "@/base/components/mui/LoadingButton";
|
||||
import log from "@/base/log";
|
||||
import { AppContext } from "@/new/photos/types/context";
|
||||
import { initiateEmail } from "@/new/photos/utils/web";
|
||||
import { Link, Stack, useMediaQuery } from "@mui/material";
|
||||
@@ -28,7 +27,7 @@ interface FormValues {
|
||||
}
|
||||
|
||||
const DeleteAccountModal = ({ open, onClose }: Iprops) => {
|
||||
const { setDialogBoxAttributesV2, logout } = useContext(AppContext);
|
||||
const { showMiniDialog, onGenericError, logout } = useContext(AppContext);
|
||||
const { authenticateUser } = useContext(GalleryContext);
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
@@ -39,13 +38,6 @@ const DeleteAccountModal = ({ open, onClose }: Iprops) => {
|
||||
|
||||
const isMobile = useMediaQuery("(max-width: 428px)");
|
||||
|
||||
const somethingWentWrong = () =>
|
||||
setDialogBoxAttributesV2({
|
||||
title: t("error"),
|
||||
close: { variant: "critical" },
|
||||
content: t("generic_error_retry"),
|
||||
});
|
||||
|
||||
const initiateDelete = async (
|
||||
{ reason, feedback }: FormValues,
|
||||
{ setFieldError }: FormikHelpers<FormValues>,
|
||||
@@ -76,80 +68,61 @@ const DeleteAccountModal = ({ open, onClose }: Iprops) => {
|
||||
askToMailForDeletion();
|
||||
}
|
||||
} catch (e) {
|
||||
log.error("Error while initiating account deletion", e);
|
||||
somethingWentWrong();
|
||||
onGenericError(e);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const confirmAccountDeletion = () => {
|
||||
setDialogBoxAttributesV2({
|
||||
showMiniDialog({
|
||||
title: t("delete_account"),
|
||||
content: <Trans i18nKey="delete_account_confirm_message" />,
|
||||
proceed: {
|
||||
message: <Trans i18nKey="delete_account_confirm_message" />,
|
||||
continue: {
|
||||
text: t("delete"),
|
||||
color: "critical",
|
||||
action: solveChallengeAndDeleteAccount,
|
||||
variant: "critical",
|
||||
},
|
||||
close: { text: t("cancel") },
|
||||
});
|
||||
};
|
||||
|
||||
const askToMailForDeletion = () => {
|
||||
const emailID = "account-deletion@ente.io";
|
||||
|
||||
setDialogBoxAttributesV2({
|
||||
showMiniDialog({
|
||||
title: t("delete_account"),
|
||||
content: (
|
||||
message: (
|
||||
<Trans
|
||||
i18nKey="delete_account_manually_message"
|
||||
components={{ a: <Link href={`mailto:${emailID}`} /> }}
|
||||
values={{ emailID }}
|
||||
/>
|
||||
),
|
||||
proceed: {
|
||||
continue: {
|
||||
text: t("delete"),
|
||||
color: "critical",
|
||||
action: () => initiateEmail(emailID),
|
||||
variant: "critical",
|
||||
},
|
||||
close: { text: t("cancel") },
|
||||
});
|
||||
};
|
||||
|
||||
const solveChallengeAndDeleteAccount = async (
|
||||
setLoading: (value: boolean) => void,
|
||||
) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const decryptedChallenge = await decryptDeleteAccountChallenge(
|
||||
deleteAccountChallenge.current,
|
||||
);
|
||||
const { reason, feedback } = reasonAndFeedbackRef.current;
|
||||
await deleteAccount(decryptedChallenge, reason, feedback);
|
||||
logout();
|
||||
} catch (e) {
|
||||
log.error("solveChallengeAndDeleteAccount failed", e);
|
||||
somethingWentWrong();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
const solveChallengeAndDeleteAccount = async () => {
|
||||
const decryptedChallenge = await decryptDeleteAccountChallenge(
|
||||
deleteAccountChallenge.current,
|
||||
);
|
||||
const { reason, feedback } = reasonAndFeedbackRef.current;
|
||||
await deleteAccount(decryptedChallenge, reason, feedback);
|
||||
logout();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<DialogBoxV2
|
||||
<TitledMiniDialog
|
||||
fullWidth
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
fullScreen={isMobile}
|
||||
attributes={{
|
||||
title: t("delete_account"),
|
||||
secondary: {
|
||||
action: onClose,
|
||||
text: t("cancel"),
|
||||
},
|
||||
}}
|
||||
title={t("delete_account")}
|
||||
>
|
||||
<Formik<FormValues>
|
||||
initialValues={{
|
||||
@@ -222,7 +195,7 @@ const DeleteAccountModal = ({ open, onClose }: Iprops) => {
|
||||
</form>
|
||||
)}
|
||||
</Formik>
|
||||
</DialogBoxV2>
|
||||
</TitledMiniDialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DialogBoxV2 } from "@/base/components/MiniDialog";
|
||||
import { TitledMiniDialog } from "@/base/components/MiniDialog";
|
||||
import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton";
|
||||
import { ItemCard, PreviewItemTile } from "@/new/photos/components/Tiles";
|
||||
import { EnteFile } from "@/new/photos/types/file";
|
||||
import { FlexWrapper } from "@ente/shared/components/Container";
|
||||
@@ -54,20 +55,13 @@ const ExportPendingList = (props: Iprops) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<DialogBoxV2
|
||||
<TitledMiniDialog
|
||||
open={props.isOpen}
|
||||
onClose={props.onClose}
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: { maxWidth: "444px" },
|
||||
}}
|
||||
attributes={{
|
||||
title: t("PENDING_ITEMS"),
|
||||
close: {
|
||||
action: props.onClose,
|
||||
text: t("close"),
|
||||
},
|
||||
}}
|
||||
title={t("PENDING_ITEMS")}
|
||||
>
|
||||
<ItemList
|
||||
maxHeight={240}
|
||||
@@ -77,7 +71,14 @@ const ExportPendingList = (props: Iprops) => {
|
||||
getItemTitle={getItemTitle}
|
||||
generateItemKey={generateItemKey}
|
||||
/>
|
||||
</DialogBoxV2>
|
||||
<FocusVisibleButton
|
||||
fullWidth
|
||||
color={"secondary"}
|
||||
onClick={props.onClose}
|
||||
>
|
||||
{t("close")}
|
||||
</FocusVisibleButton>
|
||||
</TitledMiniDialog>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { photosDialogZIndex } from "@/new/photos/components/z-index";
|
||||
import { AppContext } from "@/new/photos/types/context";
|
||||
import Notification from "components/Notification";
|
||||
import { t } from "i18next";
|
||||
@@ -123,7 +124,7 @@ export const FilesDownloadProgress: React.FC<FilesDownloadProgressProps> = ({
|
||||
horizontal="left"
|
||||
sx={{
|
||||
"&&": { bottom: `${index * 80 + 20}px` },
|
||||
zIndex: 1600,
|
||||
zIndex: photosDialogZIndex,
|
||||
}}
|
||||
open={isFilesDownloadStarted(attributes)}
|
||||
onClose={handleClose(attributes)}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DialogBoxV2 } from "@/base/components/MiniDialog";
|
||||
import { TitledMiniDialog } from "@/base/components/MiniDialog";
|
||||
import { photosDialogZIndex } from "@/new/photos/components/z-index";
|
||||
import SingleInputForm, {
|
||||
type SingleInputFormProps,
|
||||
} from "@ente/shared/components/SingleInputForm";
|
||||
@@ -23,13 +24,11 @@ export const FileNameEditDialog = ({
|
||||
}
|
||||
};
|
||||
return (
|
||||
<DialogBoxV2
|
||||
sx={{ zIndex: 1600 }}
|
||||
<TitledMiniDialog
|
||||
sx={{ zIndex: photosDialogZIndex }}
|
||||
open={isInEditMode}
|
||||
onClose={closeEditMode}
|
||||
attributes={{
|
||||
title: t("rename_file"),
|
||||
}}
|
||||
title={t("rename_file")}
|
||||
>
|
||||
<SingleInputForm
|
||||
initialValue={filename}
|
||||
@@ -41,6 +40,6 @@ export const FileNameEditDialog = ({
|
||||
secondaryButtonAction={closeEditMode}
|
||||
submitButtonProps={{ sx: { mt: 1, mb: 2 } }}
|
||||
/>
|
||||
</DialogBoxV2>
|
||||
</TitledMiniDialog>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { EnteDrawer } from "@/base/components/EnteDrawer";
|
||||
import type { MiniDialogAttributes } from "@/base/components/MiniDialog";
|
||||
import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator";
|
||||
import { Titlebar } from "@/base/components/Titlebar";
|
||||
import { EllipsizedTypography } from "@/base/components/Typography";
|
||||
@@ -18,7 +19,7 @@ import {
|
||||
UnclusteredFaceList,
|
||||
} from "@/new/photos/components/PeopleList";
|
||||
import { PhotoDateTimePicker } from "@/new/photos/components/PhotoDateTimePicker";
|
||||
import { photoSwipeZIndex } from "@/new/photos/components/PhotoViewer";
|
||||
import { fileInfoDrawerZIndex } from "@/new/photos/components/z-index";
|
||||
import { tagNumericValue, type RawExifTags } from "@/new/photos/services/exif";
|
||||
import {
|
||||
AnnotatedFacesForFile,
|
||||
@@ -53,12 +54,9 @@ import LinkButton from "components/pages/gallery/LinkButton";
|
||||
import { t } from "i18next";
|
||||
import { GalleryContext } from "pages/gallery";
|
||||
import React, { useContext, useEffect, useMemo, useState } from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import { changeFileName, updateExistingFilePubMetadata } from "utils/file";
|
||||
import { PublicCollectionGalleryContext } from "utils/publicCollectionGallery";
|
||||
import {
|
||||
getMapDisableConfirmationDialog,
|
||||
getMapEnableConfirmationDialog,
|
||||
} from "utils/ui";
|
||||
import { FileNameEditDialog } from "./FileNameEditDialog";
|
||||
import InfoItem from "./InfoItem";
|
||||
import MapBox from "./MapBox";
|
||||
@@ -101,7 +99,7 @@ export const FileInfo: React.FC<FileInfoProps> = ({
|
||||
closePhotoViewer,
|
||||
onSelectPerson,
|
||||
}) => {
|
||||
const { mapEnabled, updateMapEnabled, setDialogBoxAttributesV2 } =
|
||||
const { mapEnabled, updateMapEnabled, showMiniDialog } =
|
||||
useContext(AppContext);
|
||||
const galleryContext = useContext(GalleryContext);
|
||||
const publicCollectionGalleryContext = useContext(
|
||||
@@ -151,13 +149,13 @@ export const FileInfo: React.FC<FileInfoProps> = ({
|
||||
};
|
||||
|
||||
const openEnableMapConfirmationDialog = () =>
|
||||
setDialogBoxAttributesV2(
|
||||
getMapEnableConfirmationDialog(() => updateMapEnabled(true)),
|
||||
showMiniDialog(
|
||||
confirmEnableMapsDialogAttributes(() => updateMapEnabled(true)),
|
||||
);
|
||||
|
||||
const openDisableMapConfirmationDialog = () =>
|
||||
setDialogBoxAttributesV2(
|
||||
getMapDisableConfirmationDialog(() => updateMapEnabled(false)),
|
||||
showMiniDialog(
|
||||
confirmDisableMapsDialogAttributes(() => updateMapEnabled(false)),
|
||||
);
|
||||
|
||||
const handleSelectFace = (annotatedFaceID: AnnotatedFaceID) => {
|
||||
@@ -379,10 +377,39 @@ const parseExifInfo = (
|
||||
return info;
|
||||
};
|
||||
|
||||
const confirmEnableMapsDialogAttributes = (
|
||||
onConfirm: () => void,
|
||||
): MiniDialogAttributes => ({
|
||||
title: t("ENABLE_MAPS"),
|
||||
message: (
|
||||
<Trans
|
||||
i18nKey={"ENABLE_MAP_DESCRIPTION"}
|
||||
components={{
|
||||
a: (
|
||||
<Link
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
href="https://www.openstreetmap.org/"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
continue: { text: t("enable"), action: onConfirm },
|
||||
});
|
||||
|
||||
const confirmDisableMapsDialogAttributes = (
|
||||
onConfirm: () => void,
|
||||
): MiniDialogAttributes => ({
|
||||
title: t("DISABLE_MAPS"),
|
||||
message: <Trans i18nKey={"DISABLE_MAP_DESCRIPTION"} />,
|
||||
continue: { text: t("disable"), color: "critical", action: onConfirm },
|
||||
});
|
||||
|
||||
const FileInfoSidebar = styled((props: DialogProps) => (
|
||||
<EnteDrawer {...props} anchor="right" />
|
||||
))({
|
||||
zIndex: photoSwipeZIndex + 1,
|
||||
zIndex: fileInfoDrawerZIndex,
|
||||
"& .MuiPaper-root": {
|
||||
padding: 8,
|
||||
},
|
||||
|
||||
@@ -4,8 +4,10 @@ import {
|
||||
MenuItemGroup,
|
||||
MenuSectionTitle,
|
||||
} from "@/base/components/Menu";
|
||||
import type { MiniDialogAttributes } from "@/base/components/MiniDialog";
|
||||
import { nameAndExtension } from "@/base/file";
|
||||
import log from "@/base/log";
|
||||
import { photosDialogZIndex } from "@/new/photos/components/z-index";
|
||||
import downloadManager from "@/new/photos/services/download";
|
||||
import { AppContext } from "@/new/photos/types/context";
|
||||
import { EnteFile } from "@/new/photos/types/file";
|
||||
@@ -38,7 +40,6 @@ import type { Dispatch, MutableRefObject, SetStateAction } from "react";
|
||||
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
||||
import { getLocalCollections } from "services/collectionService";
|
||||
import uploadManager from "services/upload/uploadManager";
|
||||
import { getEditorCloseConfirmationMessage } from "utils/ui";
|
||||
import ColoursMenu from "./ColoursMenu";
|
||||
import CropMenu, { cropRegionOfCanvas, getCropRegionArgs } from "./CropMenu";
|
||||
import FreehandCropRegion from "./FreehandCropRegion";
|
||||
@@ -83,7 +84,7 @@ export interface CropBoxProps {
|
||||
}
|
||||
|
||||
const ImageEditorOverlay = (props: IProps) => {
|
||||
const appContext = useContext(AppContext);
|
||||
const { showMiniDialog } = useContext(AppContext);
|
||||
|
||||
const canvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
const originalSizeCanvasRef = useRef<HTMLCanvasElement | null>(null);
|
||||
@@ -441,9 +442,7 @@ const ImageEditorOverlay = (props: IProps) => {
|
||||
|
||||
const handleCloseWithConfirmation = () => {
|
||||
if (transformationPerformed || coloursAdjusted) {
|
||||
appContext.setDialogBoxAttributesV2(
|
||||
getEditorCloseConfirmationMessage(handleClose),
|
||||
);
|
||||
showMiniDialog(confirmEditorCloseDialogAttributes(handleClose));
|
||||
} else {
|
||||
handleClose();
|
||||
}
|
||||
@@ -522,7 +521,7 @@ const ImageEditorOverlay = (props: IProps) => {
|
||||
<Backdrop
|
||||
sx={{
|
||||
background: "#000",
|
||||
zIndex: 1600,
|
||||
zIndex: photosDialogZIndex,
|
||||
width: "100%",
|
||||
}}
|
||||
open
|
||||
@@ -732,6 +731,18 @@ const ImageEditorOverlay = (props: IProps) => {
|
||||
|
||||
export default ImageEditorOverlay;
|
||||
|
||||
const confirmEditorCloseDialogAttributes = (
|
||||
onConfirm: () => void,
|
||||
): MiniDialogAttributes => ({
|
||||
title: t("CONFIRM_EDITOR_CLOSE_MESSAGE"),
|
||||
message: t("CONFIRM_EDITOR_CLOSE_DESCRIPTION"),
|
||||
continue: {
|
||||
text: t("close"),
|
||||
color: "critical",
|
||||
action: onConfirm,
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* Create a new {@link File} with the contents of the given canvas.
|
||||
*
|
||||
|
||||
@@ -7,7 +7,7 @@ import log from "@/base/log";
|
||||
import { savedLogs } from "@/base/log-web";
|
||||
import { customAPIHost } from "@/base/origins";
|
||||
import type { CollectionSummaries } from "@/new/photos/services/collection/ui";
|
||||
import { AppContext } from "@/new/photos/types/context";
|
||||
import { AppContext, useAppContext } from "@/new/photos/types/context";
|
||||
import { initiateEmail, openURL } from "@/new/photos/utils/web";
|
||||
import { SpaceBetweenFlex } from "@ente/shared/components/Container";
|
||||
import { EnteMenuItem } from "@ente/shared/components/Menu/EnteMenuItem";
|
||||
@@ -667,7 +667,7 @@ const ExitSection: React.FC = () => {
|
||||
};
|
||||
|
||||
const DebugSection: React.FC = () => {
|
||||
const appContext = useContext(AppContext);
|
||||
const { showMiniDialog } = useAppContext();
|
||||
const [appVersion, setAppVersion] = useState<string | undefined>();
|
||||
const [host, setHost] = useState<string | undefined>();
|
||||
|
||||
@@ -679,17 +679,13 @@ const DebugSection: React.FC = () => {
|
||||
});
|
||||
|
||||
const confirmLogDownload = () =>
|
||||
appContext.setDialogMessage({
|
||||
showMiniDialog({
|
||||
title: t("DOWNLOAD_LOGS"),
|
||||
content: <Trans i18nKey={"DOWNLOAD_LOGS_MESSAGE"} />,
|
||||
proceed: {
|
||||
message: <Trans i18nKey={"DOWNLOAD_LOGS_MESSAGE"} />,
|
||||
continue: {
|
||||
text: t("download"),
|
||||
variant: "accent",
|
||||
action: downloadLogs,
|
||||
},
|
||||
close: {
|
||||
text: t("cancel"),
|
||||
},
|
||||
});
|
||||
|
||||
const downloadLogs = () => {
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { clientPackageName, staticAppTitle } from "@/base/app";
|
||||
import { CustomHead } from "@/base/components/Head";
|
||||
import type { MiniDialogAttributes } from "@/base/components/MiniDialog";
|
||||
import { MiniDialog } from "@/base/components/MiniDialog";
|
||||
import { AttributedMiniDialog } from "@/base/components/MiniDialog";
|
||||
import { ActivityIndicator } from "@/base/components/mui/ActivityIndicator";
|
||||
import { AppNavbar } from "@/base/components/Navbar";
|
||||
import {
|
||||
genericErrorDialogAttributes,
|
||||
useAttributedMiniDialog,
|
||||
} from "@/base/components/utils/mini-dialog";
|
||||
import { setupI18n } from "@/base/i18n";
|
||||
import log from "@/base/log";
|
||||
import {
|
||||
@@ -11,6 +14,7 @@ import {
|
||||
logUnhandledErrorsAndRejections,
|
||||
} from "@/base/log-web";
|
||||
import { AppUpdate } from "@/base/types/ipc";
|
||||
import { photosDialogZIndex } from "@/new/photos/components/z-index";
|
||||
import DownloadManager from "@/new/photos/services/download";
|
||||
import { runMigrations } from "@/new/photos/services/migrations";
|
||||
import { initML, isMLSupported } from "@/new/photos/services/ml";
|
||||
@@ -70,17 +74,15 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
const isLoadingBarRunning = useRef(false);
|
||||
const loadingBar = useRef(null);
|
||||
const [dialogMessage, setDialogMessage] = useState<DialogBoxAttributes>();
|
||||
const [dialogBoxAttributeV2, setDialogBoxAttributesV2] = useState<
|
||||
MiniDialogAttributes | undefined
|
||||
>();
|
||||
const [messageDialogView, setMessageDialogView] = useState(false);
|
||||
const [dialogBoxV2View, setDialogBoxV2View] = useState(false);
|
||||
const [watchFolderView, setWatchFolderView] = useState(false);
|
||||
const [watchFolderFiles, setWatchFolderFiles] = useState<FileList>(null);
|
||||
const [notificationView, setNotificationView] = useState(false);
|
||||
const closeNotification = () => setNotificationView(false);
|
||||
const [notificationAttributes, setNotificationAttributes] =
|
||||
useState<NotificationAttributes>(null);
|
||||
|
||||
const { showMiniDialog, miniDialogProps } = useAttributedMiniDialog();
|
||||
const [themeColor, setThemeColor] = useLocalState(
|
||||
LS_KEYS.THEME,
|
||||
THEME_COLOR.DARK,
|
||||
@@ -199,10 +201,6 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
setMessageDialogView(true);
|
||||
}, [dialogMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
setDialogBoxV2View(true);
|
||||
}, [dialogBoxAttributeV2]);
|
||||
|
||||
useEffect(() => {
|
||||
setNotificationView(true);
|
||||
}, [notificationAttributes]);
|
||||
@@ -210,13 +208,9 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
const showNavBar = (show: boolean) => setShowNavBar(show);
|
||||
|
||||
const updateMapEnabled = async (enabled: boolean) => {
|
||||
try {
|
||||
await updateMapEnabledStatus(enabled);
|
||||
setLocalMapEnabled(enabled);
|
||||
setMapEnabled(enabled);
|
||||
} catch (e) {
|
||||
log.error("Error while updating mapEnabled", e);
|
||||
}
|
||||
await updateMapEnabledStatus(enabled);
|
||||
setLocalMapEnabled(enabled);
|
||||
setMapEnabled(enabled);
|
||||
};
|
||||
|
||||
const startLoading = () => {
|
||||
@@ -234,7 +228,6 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
() => setMessageDialogView(false),
|
||||
[],
|
||||
);
|
||||
const closeDialogBoxV2 = () => setDialogBoxV2View(false);
|
||||
|
||||
// Use `onGenericError` instead.
|
||||
const somethingWentWrong = useCallback(
|
||||
@@ -244,20 +237,13 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
close: { variant: "critical" },
|
||||
content: t("generic_error_retry"),
|
||||
}),
|
||||
[setDialogMessage],
|
||||
[],
|
||||
);
|
||||
|
||||
const onGenericError = useCallback(
|
||||
(e: unknown) => (
|
||||
log.error("Error", e),
|
||||
setDialogBoxAttributesV2({
|
||||
title: t("error"),
|
||||
content: t("generic_error"),
|
||||
close: { variant: "critical" },
|
||||
})
|
||||
),
|
||||
[setDialogBoxAttributesV2],
|
||||
);
|
||||
const onGenericError = useCallback((e: unknown) => {
|
||||
log.error("Error", e);
|
||||
showMiniDialog(genericErrorDialogAttributes());
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
void photosLogout().then(() => router.push("/"));
|
||||
@@ -276,9 +262,9 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
setNotificationAttributes,
|
||||
themeColor,
|
||||
setThemeColor,
|
||||
showMiniDialog,
|
||||
somethingWentWrong,
|
||||
onGenericError,
|
||||
setDialogBoxAttributesV2,
|
||||
mapEnabled,
|
||||
updateMapEnabled, // <- changes on each render
|
||||
isCFProxyDisabled,
|
||||
@@ -301,17 +287,15 @@ export default function App({ Component, pageProps }: AppProps) {
|
||||
<LoadingBar color="#51cd7c" ref={loadingBar} />
|
||||
|
||||
<DialogBox
|
||||
sx={{ zIndex: 1600 }}
|
||||
sx={{ zIndex: photosDialogZIndex }}
|
||||
size="xs"
|
||||
open={messageDialogView}
|
||||
onClose={closeMessageDialog}
|
||||
attributes={dialogMessage}
|
||||
/>
|
||||
<MiniDialog
|
||||
sx={{ zIndex: 1600 }}
|
||||
open={dialogBoxV2View}
|
||||
onClose={closeDialogBoxV2}
|
||||
attributes={dialogBoxAttributeV2}
|
||||
<AttributedMiniDialog
|
||||
sx={{ zIndex: photosDialogZIndex }}
|
||||
{...miniDialogProps}
|
||||
/>
|
||||
|
||||
<Notification
|
||||
|
||||
@@ -4,7 +4,6 @@ import { openURL } from "@/new/photos/utils/web";
|
||||
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 { t } from "i18next";
|
||||
import { Trans } from "react-i18next";
|
||||
import { Subscription } from "types/billing";
|
||||
@@ -135,55 +134,3 @@ export const getSessionExpiredMessage = (
|
||||
variant: "accent",
|
||||
},
|
||||
});
|
||||
|
||||
export const getMapEnableConfirmationDialog = (
|
||||
enableMapHelper,
|
||||
): DialogBoxAttributes => ({
|
||||
title: t("ENABLE_MAPS"),
|
||||
content: (
|
||||
<Trans
|
||||
i18nKey={"ENABLE_MAP_DESCRIPTION"}
|
||||
components={{
|
||||
a: (
|
||||
<Link
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
href="https://www.openstreetmap.org/"
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
),
|
||||
proceed: {
|
||||
action: enableMapHelper,
|
||||
text: t("enable"),
|
||||
variant: "accent",
|
||||
},
|
||||
close: { text: t("cancel") },
|
||||
});
|
||||
|
||||
export const getMapDisableConfirmationDialog = (
|
||||
disableMapHelper,
|
||||
): DialogBoxAttributes => ({
|
||||
title: t("DISABLE_MAPS"),
|
||||
content: <Trans i18nKey={"DISABLE_MAP_DESCRIPTION"} />,
|
||||
proceed: {
|
||||
action: disableMapHelper,
|
||||
text: t("disable"),
|
||||
variant: "accent",
|
||||
},
|
||||
close: { text: t("cancel") },
|
||||
});
|
||||
|
||||
export const getEditorCloseConfirmationMessage = (
|
||||
doClose: () => void,
|
||||
): DialogBoxAttributes => ({
|
||||
title: t("CONFIRM_EDITOR_CLOSE_MESSAGE"),
|
||||
content: t("CONFIRM_EDITOR_CLOSE_DESCRIPTION"),
|
||||
proceed: {
|
||||
action: doClose,
|
||||
text: t("close"),
|
||||
variant: "critical",
|
||||
},
|
||||
close: { text: t("cancel") },
|
||||
});
|
||||
|
||||
@@ -5,16 +5,17 @@ import {
|
||||
} from "@/accounts/services/passkey";
|
||||
import type { MiniDialogAttributes } from "@/base/components/MiniDialog";
|
||||
import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton";
|
||||
import { genericErrorDialogAttributes } from "@/base/components/utils/mini-dialog";
|
||||
import log from "@/base/log";
|
||||
import { customAPIHost } from "@/base/origins";
|
||||
import { VerticallyCentered } from "@ente/shared/components/Container";
|
||||
import FormPaper from "@ente/shared/components/Form/FormPaper";
|
||||
import FormPaperFooter from "@ente/shared/components/Form/FormPaper/Footer";
|
||||
import LinkButton from "@ente/shared/components/LinkButton";
|
||||
import { CircularProgress, Stack, Typography, styled } from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { VerticallyCentered } from "./Container";
|
||||
import FormPaper from "./Form/FormPaper";
|
||||
import FormPaperFooter from "./Form/FormPaper/Footer";
|
||||
import LinkButton from "./LinkButton";
|
||||
|
||||
export const PasswordHeader: React.FC<React.PropsWithChildren> = ({
|
||||
children,
|
||||
@@ -74,7 +75,7 @@ interface VerifyingPasskeyProps {
|
||||
onRetry: () => void;
|
||||
/** Perform the (possibly app specific) logout sequence. */
|
||||
logout: () => void;
|
||||
setDialogBoxAttributesV2: (attrs: MiniDialogAttributes) => void;
|
||||
showMiniDialog: (attrs: MiniDialogAttributes) => void;
|
||||
}
|
||||
|
||||
export const VerifyingPasskey: React.FC<VerifyingPasskeyProps> = ({
|
||||
@@ -82,7 +83,7 @@ export const VerifyingPasskey: React.FC<VerifyingPasskeyProps> = ({
|
||||
email,
|
||||
onRetry,
|
||||
logout,
|
||||
setDialogBoxAttributesV2,
|
||||
showMiniDialog,
|
||||
}) => {
|
||||
type VerificationStatus = "waiting" | "checking" | "pending";
|
||||
const [verificationStatus, setVerificationStatus] =
|
||||
@@ -104,11 +105,11 @@ export const VerifyingPasskey: React.FC<VerifyingPasskeyProps> = ({
|
||||
else router.push(await saveCredentialsAndNavigateTo(response));
|
||||
} catch (e) {
|
||||
log.error("Passkey verification status check failed", e);
|
||||
setDialogBoxAttributesV2(
|
||||
showMiniDialog(
|
||||
e instanceof Error &&
|
||||
e.message == passkeySessionExpiredErrorMessage
|
||||
? sessionExpiredDialogAttributes(logout)
|
||||
: genericErrorAttributes(),
|
||||
: genericErrorDialogAttributes(),
|
||||
);
|
||||
setVerificationStatus("waiting");
|
||||
}
|
||||
@@ -194,11 +195,10 @@ const ButtonStack = styled("div")`
|
||||
`;
|
||||
|
||||
/**
|
||||
* {@link DialogBoxAttributesV2} for showing the error when the user's session
|
||||
* has expired.
|
||||
* {@link MiniDialogAttributes} for showing asking the user to login again when
|
||||
* their session has expired.
|
||||
*
|
||||
* It asks them to login again. There is one button, which allows them to
|
||||
* logout.
|
||||
* There is one button, which allows them to logout.
|
||||
*
|
||||
* @param onLogin Called when the user presses the "Login" button on the error
|
||||
* dialog.
|
||||
@@ -207,20 +207,11 @@ export const sessionExpiredDialogAttributes = (
|
||||
onLogin: () => void,
|
||||
): MiniDialogAttributes => ({
|
||||
title: t("SESSION_EXPIRED"),
|
||||
content: t("SESSION_EXPIRED_MESSAGE"),
|
||||
message: t("SESSION_EXPIRED_MESSAGE"),
|
||||
nonClosable: true,
|
||||
proceed: {
|
||||
continue: {
|
||||
text: t("login"),
|
||||
action: onLogin,
|
||||
variant: "accent",
|
||||
},
|
||||
});
|
||||
|
||||
/**
|
||||
* {@link DialogBoxAttributesV2} for showing a generic error.
|
||||
*/
|
||||
const genericErrorAttributes = (): MiniDialogAttributes => ({
|
||||
title: t("error"),
|
||||
close: { variant: "critical" },
|
||||
content: t("generic_error_retry"),
|
||||
cancel: false,
|
||||
});
|
||||
@@ -7,12 +7,6 @@ import { ensure } from "@/utils/ensure";
|
||||
import { VerticallyCentered } from "@ente/shared/components/Container";
|
||||
import FormPaper from "@ente/shared/components/Form/FormPaper";
|
||||
import LinkButton from "@ente/shared/components/LinkButton";
|
||||
import {
|
||||
LoginFlowFormFooter,
|
||||
PasswordHeader,
|
||||
VerifyingPasskey,
|
||||
sessionExpiredDialogAttributes,
|
||||
} from "@ente/shared/components/LoginComponents";
|
||||
import VerifyMasterPasswordForm, {
|
||||
type VerifyMasterPasswordFormProps,
|
||||
} from "@ente/shared/components/VerifyMasterPasswordForm";
|
||||
@@ -46,6 +40,12 @@ import { t } from "i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { getSRPAttributes } from "../api/srp";
|
||||
import {
|
||||
LoginFlowFormFooter,
|
||||
PasswordHeader,
|
||||
VerifyingPasskey,
|
||||
sessionExpiredDialogAttributes,
|
||||
} from "../components/LoginComponents";
|
||||
import { PAGES } from "../constants/pages";
|
||||
import {
|
||||
openPasskeyVerificationURL,
|
||||
@@ -66,7 +66,7 @@ import type { PageProps } from "../types/page";
|
||||
import type { SRPAttributes } from "../types/srp";
|
||||
|
||||
const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
const { logout, showNavBar, setDialogBoxAttributesV2 } = appContext;
|
||||
const { logout, showNavBar, showMiniDialog } = appContext;
|
||||
|
||||
const [srpAttributes, setSrpAttributes] = useState<SRPAttributes>();
|
||||
const [keyAttributes, setKeyAttributes] = useState<KeyAttributes>();
|
||||
@@ -82,7 +82,7 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
|
||||
const validateSession = useCallback(async () => {
|
||||
const showSessionExpiredDialog = () =>
|
||||
setDialogBoxAttributesV2(sessionExpiredDialogAttributes(logout));
|
||||
showMiniDialog(sessionExpiredDialogAttributes(logout));
|
||||
|
||||
try {
|
||||
const session = await checkSessionValidity();
|
||||
@@ -114,7 +114,7 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
// potentially transient issues.
|
||||
log.warn("Ignoring error when determining session validity", e);
|
||||
}
|
||||
}, [setDialogBoxAttributesV2, logout]);
|
||||
}, [showMiniDialog, logout]);
|
||||
|
||||
useEffect(() => {
|
||||
const main = async () => {
|
||||
@@ -354,7 +354,7 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
onRetry={() =>
|
||||
openPasskeyVerificationURL(passkeyVerificationData)
|
||||
}
|
||||
{...{ logout, setDialogBoxAttributesV2 }}
|
||||
{...{ logout, showMiniDialog }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ const bip39 = require("bip39");
|
||||
bip39.setDefaultWordlist("english");
|
||||
|
||||
const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
const { showNavBar, setDialogBoxAttributesV2 } = appContext;
|
||||
const { showNavBar, showMiniDialog } = appContext;
|
||||
|
||||
const [keyAttributes, setKeyAttributes] = useState<
|
||||
KeyAttributes | undefined
|
||||
@@ -98,10 +98,11 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
};
|
||||
|
||||
const showNoRecoveryKeyMessage = () =>
|
||||
setDialogBoxAttributesV2({
|
||||
showMiniDialog({
|
||||
title: t("sorry"),
|
||||
close: {},
|
||||
content: t("NO_RECOVERY_KEY_MESSAGE"),
|
||||
message: t("NO_RECOVERY_KEY_MESSAGE"),
|
||||
continue: { color: "secondary" },
|
||||
cancel: false,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -42,7 +42,7 @@ export interface RecoverPageProps {
|
||||
}
|
||||
|
||||
const Page: React.FC<RecoverPageProps> = ({ appContext, twoFactorType }) => {
|
||||
const { logout } = appContext;
|
||||
const { showMiniDialog, logout } = appContext;
|
||||
|
||||
const [encryptedTwoFactorSecret, setEncryptedTwoFactorSecret] =
|
||||
useState<Omit<B64EncryptionResult, "key"> | null>(null);
|
||||
@@ -70,10 +70,7 @@ const Page: React.FC<RecoverPageProps> = ({ appContext, twoFactorType }) => {
|
||||
const resp = await recoverTwoFactor(sid, twoFactorType);
|
||||
setDoesHaveEncryptedRecoveryKey(!!resp.encryptedSecret);
|
||||
if (!resp.encryptedSecret) {
|
||||
showContactSupportDialog({
|
||||
text: t("GO_BACK"),
|
||||
action: router.back,
|
||||
});
|
||||
showContactSupportDialog({ action: router.back });
|
||||
} else {
|
||||
setEncryptedTwoFactorSecret({
|
||||
encryptedData: resp.encryptedSecret,
|
||||
@@ -89,10 +86,7 @@ const Page: React.FC<RecoverPageProps> = ({ appContext, twoFactorType }) => {
|
||||
} else {
|
||||
log.error("two factor recovery page setup failed", e);
|
||||
setDoesHaveEncryptedRecoveryKey(false);
|
||||
showContactSupportDialog({
|
||||
text: t("GO_BACK"),
|
||||
action: router.back,
|
||||
});
|
||||
showContactSupportDialog({ action: router.back });
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -146,12 +140,11 @@ const Page: React.FC<RecoverPageProps> = ({ appContext, twoFactorType }) => {
|
||||
};
|
||||
|
||||
const showContactSupportDialog = (
|
||||
dialogClose?: MiniDialogAttributes["close"],
|
||||
dialogContinue?: MiniDialogAttributes["continue"],
|
||||
) => {
|
||||
appContext.setDialogBoxAttributesV2({
|
||||
showMiniDialog({
|
||||
title: t("contact_support"),
|
||||
close: dialogClose ?? {},
|
||||
content: (
|
||||
message: (
|
||||
<Trans
|
||||
i18nKey={"NO_TWO_FACTOR_RECOVERY_KEY_MESSAGE"}
|
||||
components={{
|
||||
@@ -160,6 +153,8 @@ const Page: React.FC<RecoverPageProps> = ({ appContext, twoFactorType }) => {
|
||||
values={{ emailID: "support@ente.io" }}
|
||||
/>
|
||||
),
|
||||
continue: { color: "secondary", ...(dialogContinue ?? {}) },
|
||||
cancel: false,
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -6,10 +6,6 @@ import { VerticallyCentered } from "@ente/shared/components/Container";
|
||||
import FormPaper from "@ente/shared/components/Form/FormPaper";
|
||||
import FormPaperTitle from "@ente/shared/components/Form/FormPaper/Title";
|
||||
import LinkButton from "@ente/shared/components/LinkButton";
|
||||
import {
|
||||
LoginFlowFormFooter,
|
||||
VerifyingPasskey,
|
||||
} from "@ente/shared/components/LoginComponents";
|
||||
import SingleInputForm, {
|
||||
type SingleInputFormProps,
|
||||
} from "@ente/shared/components/SingleInputForm";
|
||||
@@ -35,6 +31,10 @@ import { useEffect, useState } from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import { getSRPAttributes } from "../api/srp";
|
||||
import { putAttributes, sendOtt, verifyOtt } from "../api/user";
|
||||
import {
|
||||
LoginFlowFormFooter,
|
||||
VerifyingPasskey,
|
||||
} from "../components/LoginComponents";
|
||||
import { PAGES } from "../constants/pages";
|
||||
import {
|
||||
openPasskeyVerificationURL,
|
||||
@@ -46,7 +46,7 @@ import type { PageProps } from "../types/page";
|
||||
import type { SRPAttributes, SRPSetupAttributes } from "../types/srp";
|
||||
|
||||
const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
const { logout, showNavBar, setDialogBoxAttributesV2 } = appContext;
|
||||
const { logout, showNavBar, showMiniDialog } = appContext;
|
||||
|
||||
const [email, setEmail] = useState("");
|
||||
const [resend, setResend] = useState(0);
|
||||
@@ -202,7 +202,7 @@ const Page: React.FC<PageProps> = ({ appContext }) => {
|
||||
onRetry={() =>
|
||||
openPasskeyVerificationURL(passkeyVerificationData)
|
||||
}
|
||||
{...{ logout, setDialogBoxAttributesV2 }}
|
||||
{...{ logout, showMiniDialog }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,9 +5,20 @@ import type { MiniDialogAttributes } from "@/base/components/MiniDialog";
|
||||
* defer to the pages provided by the accounts package.
|
||||
*/
|
||||
export interface AccountsContextT {
|
||||
/** Perform the (possibly app specific) logout sequence. */
|
||||
/**
|
||||
* Perform the (possibly app specific) logout sequence.
|
||||
*/
|
||||
logout: () => void;
|
||||
/** Show or hide the app's navigation bar. */
|
||||
/**
|
||||
* Show or hide the app's navigation bar.
|
||||
*/
|
||||
showNavBar: (show: boolean) => void;
|
||||
setDialogBoxAttributesV2: (attrs: MiniDialogAttributes) => void;
|
||||
/**
|
||||
* Show a "mini dialog" with the given attributes.
|
||||
*
|
||||
* Mini dialogs (see {@link AttributedMiniDialog}) are meant for simple
|
||||
* confirmation or notications. Their appearance and functionality can be
|
||||
* customized by providing appropriate {@link MiniDialogAttributes}.
|
||||
*/
|
||||
showMiniDialog: (attributes: MiniDialogAttributes) => void;
|
||||
}
|
||||
|
||||
@@ -1,381 +1,290 @@
|
||||
// TODO:
|
||||
/* eslint-disable @typescript-eslint/prefer-optional-chain */
|
||||
/* eslint-disable @typescript-eslint/no-unnecessary-condition */
|
||||
/* eslint-disable @typescript-eslint/prefer-nullish-coalescing */
|
||||
import { FocusVisibleButton } from "@/base/components/mui/FocusVisibleButton";
|
||||
import { LoadingButton } from "@/base/components/mui/LoadingButton";
|
||||
import { dialogCloseHandler } from "@ente/shared/components/DialogBox/TitleWithCloseButton";
|
||||
import type { ButtonProps } from "@mui/material";
|
||||
import {
|
||||
Box,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
Stack,
|
||||
Typography,
|
||||
type DialogProps,
|
||||
} from "@mui/material";
|
||||
import { t } from "i18next";
|
||||
import React, { useState } from "react";
|
||||
import log from "../log";
|
||||
|
||||
/**
|
||||
* Customize the properties of the dialog box.
|
||||
*
|
||||
* Our custom dialog box helpers are meant for small message boxes, usually
|
||||
* meant to confirm some user action. If more customization is needed, it might
|
||||
* be a better idea to reach out for a bespoke MUI {@link DialogBox} instead.
|
||||
* Customize the contents of an {@link AttributedMiniDialog}.
|
||||
*/
|
||||
export interface MiniDialogAttributes {
|
||||
icon?: React.ReactNode;
|
||||
/**
|
||||
* The dialog's title
|
||||
* The dialog's title.
|
||||
*
|
||||
* Usually this will be a string, but it can be any {@link ReactNode}. Note
|
||||
* that it always gets wrapped in a Typography element to set the font
|
||||
* style, so if your ReactNode wants to do its own thing, it'll need to
|
||||
* reset or override these customizations.
|
||||
* This will be usually be a string, but the prop accepts any React node to
|
||||
* allow passing a i18next <Trans /> component.
|
||||
*/
|
||||
title?: React.ReactNode;
|
||||
/**
|
||||
* An optional component shown next to the title.
|
||||
*/
|
||||
icon?: React.ReactNode;
|
||||
/**
|
||||
* The dialog's message.
|
||||
*
|
||||
* This will be usually be a string, but the prop accepts any React node to
|
||||
* allow passing a i18next <Trans /> component.
|
||||
*/
|
||||
message?: React.ReactNode;
|
||||
/**
|
||||
* If `true`, then clicks in the backdrop are ignored. The default behaviour
|
||||
* is to close the dialog when the background is clicked.
|
||||
*/
|
||||
staticBackdrop?: boolean;
|
||||
/**
|
||||
* If `true`, then the dialog cannot be closed (e.g. with the ESC key, or
|
||||
* clicking on the backdrop) except through one of the explicitly provided
|
||||
* actions.
|
||||
*/
|
||||
nonClosable?: boolean;
|
||||
/**
|
||||
* The dialog's content.
|
||||
*/
|
||||
content?: React.ReactNode;
|
||||
/**
|
||||
* Customize the cancel (dismiss) action button offered by the dialog box.
|
||||
* Customize the primary action button shown in the dialog.
|
||||
*
|
||||
* Usually dialog boxes should have a cancel action, but this can be skipped
|
||||
* to only show one of the other types of buttons.
|
||||
* This is provided by boxes which serve as some sort of confirmation. For
|
||||
* dialogs which are informational notifications, this is usually skipped,
|
||||
* only the {@link close} action button is configured.
|
||||
*/
|
||||
close?: {
|
||||
/** The string to use as the label for the cancel button. */
|
||||
text?: string;
|
||||
/** The color of the button. */
|
||||
variant?: ButtonProps["color"];
|
||||
continue?: {
|
||||
/**
|
||||
* The function to call when the user cancels.
|
||||
* The string to use as the label for the primary action button.
|
||||
*
|
||||
* If provided, this callback is invoked before closing the dialog.
|
||||
* Default is `t("ok")`.
|
||||
*/
|
||||
action?: () => void;
|
||||
text?: string;
|
||||
/**
|
||||
* The color of the button.
|
||||
*
|
||||
* Default is "accent".
|
||||
*/
|
||||
color?: ButtonProps["color"];
|
||||
/**
|
||||
* If `true`, the primary action button is auto focused when the dialog
|
||||
* is opened, allowing the user to confirm just by pressing ENTER.
|
||||
*/
|
||||
autoFocus?: ButtonProps["autoFocus"];
|
||||
/**
|
||||
* The function to call when the user activates the button.
|
||||
*
|
||||
* If this function returns a promise, then an activity indicator will
|
||||
* be shown on the button until the promise settles.
|
||||
*
|
||||
* If this function is not provided, or if the function completes /
|
||||
* fullfills, then then the dialog is automatically closed.
|
||||
*
|
||||
* Otherwise (that is, if the provided function throws), the dialog
|
||||
* remains open, showing a generic error.
|
||||
*
|
||||
* That's quite a mouthful, here's a flowchart:
|
||||
*
|
||||
* - Not provided: Close
|
||||
* - Provided sync:
|
||||
* - Success: Close
|
||||
* - Failure: Remain open, showing generic error
|
||||
* - Provided async:
|
||||
* - Success: Close
|
||||
* - Failure: Remain open, showing generic error
|
||||
*/
|
||||
action?: () => void | Promise<void>;
|
||||
};
|
||||
/**
|
||||
* Customize the primary action button offered by the dialog box.
|
||||
* The string to use as the label for the cancel button.
|
||||
*
|
||||
* Default is `t("cancel")`.
|
||||
*
|
||||
* Set this to `false` to omit the cancel button altogether.
|
||||
*/
|
||||
proceed?: {
|
||||
/** The string to use as the label for the primary action. */
|
||||
text: string;
|
||||
/**
|
||||
* The function to call when the user presses the primary action button.
|
||||
*
|
||||
* It is passed a {@link setLoading} function that can be used to show
|
||||
* or hide loading indicator or the primary action button.
|
||||
*/
|
||||
action:
|
||||
| (() => void | Promise<void>)
|
||||
| ((setLoading: (value: boolean) => void) => void | Promise<void>);
|
||||
variant?: ButtonProps["color"];
|
||||
disabled?: boolean;
|
||||
};
|
||||
secondary?: {
|
||||
text: string;
|
||||
action: () => void;
|
||||
variant?: ButtonProps["color"];
|
||||
disabled?: boolean;
|
||||
};
|
||||
buttons?: {
|
||||
text: string;
|
||||
action: () => void;
|
||||
variant: ButtonProps["color"];
|
||||
disabled?: boolean;
|
||||
}[];
|
||||
cancel?: string | false;
|
||||
/** The direction in which the buttons are stacked. Default is "column". */
|
||||
buttonDirection?: "row" | "column";
|
||||
}
|
||||
|
||||
type MiniDialogProps = React.PropsWithChildren<
|
||||
Omit<DialogProps, "onClose"> & {
|
||||
onClose: () => void;
|
||||
attributes?: MiniDialogAttributes;
|
||||
}
|
||||
>;
|
||||
type MiniDialogProps = Omit<DialogProps, "onClose"> & {
|
||||
onClose: () => void;
|
||||
attributes?: MiniDialogAttributes;
|
||||
};
|
||||
|
||||
/**
|
||||
* A small, mostly predefined, MUI {@link Dialog} that can be used to notify the
|
||||
* user, or ask for confirmation before actions.
|
||||
*
|
||||
* The rendered dialog can be customized by modifying the {@link attributes}
|
||||
* prop. If you find yourself wanting to customize it further, consider just
|
||||
* creating a new bespoke instantiation of a {@link Dialog}.
|
||||
* prop. If you find yourself wanting to customize it further, consider either
|
||||
* using a {@link TitledMiniDialog} or {@link Dialog}.
|
||||
*/
|
||||
export function MiniDialog({
|
||||
attributes,
|
||||
children,
|
||||
open,
|
||||
onClose,
|
||||
...props
|
||||
}: MiniDialogProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
export const AttributedMiniDialog: React.FC<
|
||||
React.PropsWithChildren<MiniDialogProps>
|
||||
> = ({ open, onClose, attributes, children, ...props }) => {
|
||||
const [phase, setPhase] = useState<"loading" | "failed" | undefined>();
|
||||
|
||||
if (!attributes) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const handleClose = dialogCloseHandler({
|
||||
staticBackdrop: attributes.staticBackdrop,
|
||||
nonClosable: attributes.nonClosable,
|
||||
onClose: onClose,
|
||||
});
|
||||
const resetPhaseAndClose = () => {
|
||||
setPhase(undefined);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
if (attributes.nonClosable) return;
|
||||
resetPhaseAndClose();
|
||||
};
|
||||
|
||||
const { PaperProps, ...rest } = props;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
...PaperProps,
|
||||
sx: {
|
||||
padding: "8px 12px",
|
||||
maxWidth: "360px",
|
||||
...PaperProps?.sx,
|
||||
},
|
||||
}}
|
||||
onClose={handleClose}
|
||||
// This is required to prevent console errors about aria-hiding a
|
||||
// focused button when the dialog is closed.
|
||||
//
|
||||
// https://github.com/mui/material-ui/issues/43106#issuecomment-2314809028
|
||||
closeAfterTransition={false}
|
||||
{...rest}
|
||||
>
|
||||
<Stack spacing={"36px"} p={"16px"}>
|
||||
<Stack spacing={"19px"}>
|
||||
{attributes.icon && (
|
||||
<Box
|
||||
sx={{
|
||||
"& > svg": {
|
||||
fontSize: "32px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{attributes.icon}
|
||||
</Box>
|
||||
)}
|
||||
{(attributes.icon ?? attributes.title) && (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
"& > svg": {
|
||||
fontSize: "32px",
|
||||
color: "text.faint",
|
||||
},
|
||||
padding: "24px 16px 16px 16px",
|
||||
}}
|
||||
>
|
||||
{attributes.title && (
|
||||
<Typography variant="large" fontWeight={"bold"}>
|
||||
<DialogTitle sx={{ "&&&": { padding: 0 } }}>
|
||||
{attributes.title}
|
||||
</DialogTitle>
|
||||
)}
|
||||
{attributes.icon}
|
||||
</Box>
|
||||
)}
|
||||
<DialogContent>
|
||||
{attributes.message && (
|
||||
<Typography
|
||||
component={
|
||||
typeof attributes.message == "string" ? "p" : "div"
|
||||
}
|
||||
color="text.muted"
|
||||
>
|
||||
{attributes.message}
|
||||
</Typography>
|
||||
)}
|
||||
{children}
|
||||
<Stack
|
||||
sx={{ paddingBlockStart: "24px", gap: "12px" }}
|
||||
direction={
|
||||
attributes.buttonDirection == "row"
|
||||
? "row-reverse"
|
||||
: "column"
|
||||
}
|
||||
>
|
||||
{phase == "failed" && (
|
||||
<Typography variant="small" color="critical.main">
|
||||
{t("generic_error")}
|
||||
</Typography>
|
||||
)}
|
||||
{children ||
|
||||
(attributes?.content && (
|
||||
<Typography color="text.muted">
|
||||
{attributes.content}
|
||||
</Typography>
|
||||
))}
|
||||
{attributes.continue && (
|
||||
<LoadingButton
|
||||
loading={phase == "loading"}
|
||||
fullWidth
|
||||
color={attributes.continue.color ?? "accent"}
|
||||
autoFocus={attributes.continue.autoFocus}
|
||||
onClick={async () => {
|
||||
setPhase("loading");
|
||||
try {
|
||||
await attributes.continue?.action?.();
|
||||
resetPhaseAndClose();
|
||||
} catch (e) {
|
||||
log.error("Error", e);
|
||||
setPhase("failed");
|
||||
}
|
||||
}}
|
||||
>
|
||||
{attributes.continue.text ?? t("ok")}
|
||||
</LoadingButton>
|
||||
)}
|
||||
{attributes.cancel !== false && (
|
||||
<FocusVisibleButton
|
||||
fullWidth
|
||||
color="secondary"
|
||||
onClick={resetPhaseAndClose}
|
||||
>
|
||||
{attributes.cancel ?? t("cancel")}
|
||||
</FocusVisibleButton>
|
||||
)}
|
||||
</Stack>
|
||||
{(attributes.proceed ||
|
||||
attributes.close ||
|
||||
attributes.buttons?.length) && (
|
||||
<Stack
|
||||
spacing={"8px"}
|
||||
direction={
|
||||
attributes.buttonDirection === "row"
|
||||
? "row-reverse"
|
||||
: "column"
|
||||
}
|
||||
flex={1}
|
||||
>
|
||||
{attributes.proceed && (
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
size="large"
|
||||
color={attributes.proceed?.variant}
|
||||
onClick={async () => {
|
||||
await attributes.proceed?.action(
|
||||
setLoading,
|
||||
);
|
||||
|
||||
onClose();
|
||||
}}
|
||||
disabled={attributes.proceed.disabled}
|
||||
>
|
||||
{attributes.proceed.text}
|
||||
</LoadingButton>
|
||||
)}
|
||||
{attributes.close && (
|
||||
<FocusVisibleButton
|
||||
size="large"
|
||||
color={attributes.close?.variant ?? "secondary"}
|
||||
onClick={() => {
|
||||
attributes.close?.action &&
|
||||
attributes.close?.action();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{attributes.close?.text ?? t("ok")}
|
||||
</FocusVisibleButton>
|
||||
)}
|
||||
{attributes.buttons &&
|
||||
attributes.buttons.map((b) => (
|
||||
<FocusVisibleButton
|
||||
size="large"
|
||||
key={b.text}
|
||||
color={b.variant}
|
||||
onClick={() => {
|
||||
b.action();
|
||||
onClose();
|
||||
}}
|
||||
disabled={b.disabled}
|
||||
>
|
||||
{b.text}
|
||||
</FocusVisibleButton>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// TODO: Sketch of a possible approach to using this. Haven't throught this
|
||||
// through, just noting down the outline inspired by an API I saw.
|
||||
// /**
|
||||
// * A React hook for simplifying use of MiniDialog within the photos app context.
|
||||
// *
|
||||
// * It relies on the presence of the {@link setDialogBoxAttributesV2} function
|
||||
// * provided by the Photos app's {@link AppContext}.
|
||||
// */
|
||||
// export const useConfirm = (attr) => {
|
||||
// const {setDialogBoxAttributesV2} = useAppContext();
|
||||
// return () => {
|
||||
// new Promise((resolve) => {
|
||||
// setDialogBoxAttributesV2(
|
||||
// proceed: {
|
||||
// action: attr.action.then(resolve)
|
||||
// }
|
||||
// )
|
||||
// }
|
||||
// }
|
||||
type TitledMiniDialogProps = Omit<DialogProps, "onClose"> & {
|
||||
onClose: () => void;
|
||||
/**
|
||||
* The dialog's title.
|
||||
*/
|
||||
title?: React.ReactNode;
|
||||
};
|
||||
|
||||
/**
|
||||
* TODO This is a duplicate of MiniDialog. This is for use by call sites that
|
||||
* were using the MiniDialog not as a dialog but as a base container. Such use
|
||||
* cases are better served by directly using the MUI {@link Dialog}, so these
|
||||
* are considered deprecated. Splitting these here so that we can streamline the
|
||||
* API for the notify/confirm case separately.
|
||||
* MiniDialog in a "shell" form.
|
||||
*
|
||||
* This is a {@link Dialog} for use at places which need more customization than
|
||||
* what {@link AttributedMiniDialog} provides, but wish to retain a similar look
|
||||
* and feel without duplicating code.
|
||||
*
|
||||
* It does three things:
|
||||
*
|
||||
* - Sets a fixed size and padding similar to {@link AttributedMiniDialog}.
|
||||
* - Takes the title as a prop, and wraps it in a {@link DialogTitle}.
|
||||
* - Wraps children in a scrollable {@link DialogContent}.
|
||||
*/
|
||||
export function DialogBoxV2({
|
||||
attributes,
|
||||
children,
|
||||
open,
|
||||
onClose,
|
||||
...props
|
||||
}: MiniDialogProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
if (!attributes) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
const handleClose = dialogCloseHandler({
|
||||
staticBackdrop: attributes.staticBackdrop,
|
||||
nonClosable: attributes.nonClosable,
|
||||
onClose: onClose,
|
||||
});
|
||||
|
||||
export const TitledMiniDialog: React.FC<
|
||||
React.PropsWithChildren<TitledMiniDialogProps>
|
||||
> = ({ open, onClose, title, children, ...props }) => {
|
||||
const { PaperProps, ...rest } = props;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
onClose={onClose}
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
...PaperProps,
|
||||
sx: {
|
||||
padding: "8px 12px",
|
||||
maxWidth: "360px",
|
||||
...PaperProps?.sx,
|
||||
},
|
||||
}}
|
||||
{...rest}
|
||||
>
|
||||
<Stack spacing={"36px"} p={"16px"}>
|
||||
<Stack spacing={"19px"}>
|
||||
{attributes.icon && (
|
||||
<Box
|
||||
sx={{
|
||||
"& > svg": {
|
||||
fontSize: "32px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
{attributes.icon}
|
||||
</Box>
|
||||
)}
|
||||
{attributes.title && (
|
||||
<Typography variant="large" fontWeight={"bold"}>
|
||||
{attributes.title}
|
||||
</Typography>
|
||||
)}
|
||||
{children ||
|
||||
(attributes?.content && (
|
||||
<Typography color="text.muted">
|
||||
{attributes.content}
|
||||
</Typography>
|
||||
))}
|
||||
</Stack>
|
||||
{(attributes.proceed ||
|
||||
attributes.close ||
|
||||
attributes.buttons?.length) && (
|
||||
<Stack
|
||||
spacing={"8px"}
|
||||
direction={
|
||||
attributes.buttonDirection === "row"
|
||||
? "row-reverse"
|
||||
: "column"
|
||||
}
|
||||
flex={1}
|
||||
>
|
||||
{attributes.proceed && (
|
||||
<LoadingButton
|
||||
loading={loading}
|
||||
size="large"
|
||||
color={attributes.proceed?.variant}
|
||||
onClick={async () => {
|
||||
await attributes.proceed?.action(
|
||||
setLoading,
|
||||
);
|
||||
|
||||
onClose();
|
||||
}}
|
||||
disabled={attributes.proceed.disabled}
|
||||
>
|
||||
{attributes.proceed.text}
|
||||
</LoadingButton>
|
||||
)}
|
||||
{attributes.close && (
|
||||
<FocusVisibleButton
|
||||
size="large"
|
||||
color={attributes.close?.variant ?? "secondary"}
|
||||
onClick={() => {
|
||||
attributes.close?.action &&
|
||||
attributes.close?.action();
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{attributes.close?.text ?? t("ok")}
|
||||
</FocusVisibleButton>
|
||||
)}
|
||||
{attributes.buttons &&
|
||||
attributes.buttons.map((b) => (
|
||||
<FocusVisibleButton
|
||||
size="large"
|
||||
key={b.text}
|
||||
color={b.variant}
|
||||
onClick={() => {
|
||||
b.action();
|
||||
onClose();
|
||||
}}
|
||||
disabled={b.disabled}
|
||||
>
|
||||
{b.text}
|
||||
</FocusVisibleButton>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
<DialogTitle sx={{ "&&&": { paddingBlock: "24px 16px" } }}>
|
||||
{title}
|
||||
</DialogTitle>
|
||||
<DialogContent>{children}</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
63
web/packages/base/components/utils/mini-dialog.tsx
Normal file
63
web/packages/base/components/utils/mini-dialog.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import ErrorOutline from "@mui/icons-material/ErrorOutline";
|
||||
import { t } from "i18next";
|
||||
import { useCallback, useState } from "react";
|
||||
import type { MiniDialogAttributes } from "../MiniDialog";
|
||||
|
||||
/**
|
||||
* A React hook for simplifying the provisioning of a {@link showMiniDialog}
|
||||
* function to inject in app contexts, and the other props that are needed for
|
||||
* to pass on to the {@link AttributedMiniDialog}.
|
||||
*/
|
||||
export const useAttributedMiniDialog = () => {
|
||||
const [miniDialogAttributes, setMiniDialogAttributes] = useState<
|
||||
MiniDialogAttributes | undefined
|
||||
>();
|
||||
|
||||
const [openMiniDialog, setOpenMiniDialog] = useState(false);
|
||||
|
||||
const showMiniDialog = useCallback((attributes: MiniDialogAttributes) => {
|
||||
setMiniDialogAttributes(attributes);
|
||||
setOpenMiniDialog(true);
|
||||
}, []);
|
||||
|
||||
const onCloseMiniDialog = useCallback(() => setOpenMiniDialog(false), []);
|
||||
|
||||
return {
|
||||
showMiniDialog,
|
||||
miniDialogProps: {
|
||||
open: openMiniDialog,
|
||||
onClose: onCloseMiniDialog,
|
||||
attributes: miniDialogAttributes,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* A convenience function to construct {@link MiniDialogAttributes} for showing
|
||||
* error dialogs.
|
||||
*
|
||||
* It takes one or two arguments.
|
||||
*
|
||||
* - If both are provided, then the first one is taken as the title and the
|
||||
* second one as the message.
|
||||
*
|
||||
* - Otherwise it sets a default title and use the only argument as the message.
|
||||
*/
|
||||
export const errorDialogAttributes = (
|
||||
messageOrTitle: string,
|
||||
optionalMessage?: string,
|
||||
): MiniDialogAttributes => {
|
||||
const title = optionalMessage ? messageOrTitle : t("error");
|
||||
const message = optionalMessage ? optionalMessage : messageOrTitle;
|
||||
|
||||
return {
|
||||
title,
|
||||
icon: <ErrorOutline />,
|
||||
message,
|
||||
continue: { color: "critical" },
|
||||
cancel: false,
|
||||
};
|
||||
};
|
||||
|
||||
export const genericErrorDialogAttributes = () =>
|
||||
errorDialogAttributes(t("generic_error"));
|
||||
@@ -13,9 +13,14 @@ module.exports = {
|
||||
"react/jsx-no-target-blank": ["warn", { allowReferrer: true }],
|
||||
/* Otherwise we need to do unnecessary boilerplating when using memo. */
|
||||
"react/display-name": "off",
|
||||
/* Apparently Fast refresh only works if a file only exports components,
|
||||
and this rule warns about that. Constants are okay though (otherwise
|
||||
we'll need to create unnecessary helper files). */
|
||||
"react-refresh/only-export-components": [
|
||||
"warn",
|
||||
{ allowConstantExport: true },
|
||||
],
|
||||
/* Next.js supports the JSX transform introduced in React 17 */
|
||||
"react/react-in-jsx-scope": "off",
|
||||
},
|
||||
};
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
import { t } from "i18next";
|
||||
import React, { useEffect, useState, useSyncExternalStore } from "react";
|
||||
import { Trans } from "react-i18next";
|
||||
import { useAppContext, type AppContextT } from "../types/context";
|
||||
import { useAppContext } from "../types/context";
|
||||
import { openURL } from "../utils/web";
|
||||
import { useWrapAsyncOperation } from "./use-wrap-async";
|
||||
|
||||
@@ -36,8 +36,6 @@ export const MLSettings: React.FC<NestedDrawerVisibilityProps> = ({
|
||||
onClose,
|
||||
onRootClose,
|
||||
}) => {
|
||||
const { setDialogBoxAttributesV2 } = useAppContext();
|
||||
|
||||
const mlStatus = useSyncExternalStore(mlStatusSubscribe, mlStatusSnapshot);
|
||||
const [openFaceConsent, setOpenFaceConsent] = useState(false);
|
||||
|
||||
@@ -68,10 +66,7 @@ export const MLSettings: React.FC<NestedDrawerVisibilityProps> = ({
|
||||
component = <EnableML onEnable={handleEnableML} />;
|
||||
} else {
|
||||
component = (
|
||||
<ManageML
|
||||
{...{ mlStatus, setDialogBoxAttributesV2 }}
|
||||
onDisableML={handleDisableML}
|
||||
/>
|
||||
<ManageML {...{ mlStatus }} onDisableML={handleDisableML} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -252,15 +247,11 @@ interface ManageMLProps {
|
||||
mlStatus: Exclude<MLStatus, { phase: "disabled" }>;
|
||||
/** Called when the user wants to disable ML. */
|
||||
onDisableML: () => void;
|
||||
/** Subset of appContext. */
|
||||
setDialogBoxAttributesV2: AppContextT["setDialogBoxAttributesV2"];
|
||||
}
|
||||
|
||||
const ManageML: React.FC<ManageMLProps> = ({
|
||||
mlStatus,
|
||||
onDisableML,
|
||||
setDialogBoxAttributesV2,
|
||||
}) => {
|
||||
const ManageML: React.FC<ManageMLProps> = ({ mlStatus, onDisableML }) => {
|
||||
const { showMiniDialog } = useAppContext();
|
||||
|
||||
const { phase, nSyncedFiles, nTotalFiles } = mlStatus;
|
||||
|
||||
let status: string;
|
||||
@@ -289,19 +280,17 @@ const ManageML: React.FC<ManageMLProps> = ({
|
||||
? `${Math.round((100 * nSyncedFiles) / nTotalFiles)}%`
|
||||
: `${nSyncedFiles} / ${nTotalFiles}`;
|
||||
|
||||
const confirmDisableML = () => {
|
||||
setDialogBoxAttributesV2({
|
||||
const confirmDisableML = () =>
|
||||
showMiniDialog({
|
||||
title: t("ml_search_disable"),
|
||||
content: t("ml_search_disable_confirm"),
|
||||
close: { text: t("cancel") },
|
||||
proceed: {
|
||||
variant: "critical",
|
||||
message: t("ml_search_disable_confirm"),
|
||||
continue: {
|
||||
text: t("disable"),
|
||||
color: "critical",
|
||||
action: onDisableML,
|
||||
},
|
||||
buttonDirection: "row",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack px={"16px"} py={"20px"} gap={4}>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { ParsedMetadataDate } from "@/media/file-metadata";
|
||||
import { photoSwipeZIndex } from "@/new/photos/components/PhotoViewer";
|
||||
import {
|
||||
LocalizationProvider,
|
||||
MobileDateTimePicker,
|
||||
@@ -7,6 +6,7 @@ import {
|
||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||
import dayjs, { Dayjs } from "dayjs";
|
||||
import React, { useState } from "react";
|
||||
import { fileInfoDrawerZIndex } from "./z-index";
|
||||
|
||||
interface PhotoDateTimePickerProps {
|
||||
/**
|
||||
@@ -105,7 +105,7 @@ export const PhotoDateTimePicker: React.FC<PhotoDateTimePickerProps> = ({
|
||||
photo viewer and the info drawer */
|
||||
dialog: {
|
||||
sx: {
|
||||
zIndex: photoSwipeZIndex + 2,
|
||||
zIndex: fileInfoDrawerZIndex + 1,
|
||||
},
|
||||
},
|
||||
}}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
/**
|
||||
* PhotoSwipe sets the zIndex of its "pswp" class to 1500. We need to go higher
|
||||
* than that for our drawers and dialogs to show above it.
|
||||
*/
|
||||
export const photoSwipeZIndex = 1500;
|
||||
@@ -92,7 +92,7 @@ const CGroupPersonOptions: React.FC<CGroupPersonOptionsProps> = ({
|
||||
cgroup,
|
||||
onSelectPerson,
|
||||
}) => {
|
||||
const { setDialogBoxAttributesV2 } = useAppContext();
|
||||
const { showMiniDialog } = useAppContext();
|
||||
|
||||
const [openAddNameInput, setOpenAddNameInput] = useState(false);
|
||||
|
||||
@@ -103,17 +103,16 @@ const CGroupPersonOptions: React.FC<CGroupPersonOptionsProps> = ({
|
||||
);
|
||||
|
||||
const handleDeletePerson = () =>
|
||||
setDialogBoxAttributesV2({
|
||||
showMiniDialog({
|
||||
title: pt("Reset person?"),
|
||||
content: pt(
|
||||
message: pt(
|
||||
"The name, face groupings and suggestions for this person will be reset",
|
||||
),
|
||||
close: { text: t("cancel") },
|
||||
proceed: {
|
||||
continue: {
|
||||
text: t("reset"),
|
||||
color: "primary",
|
||||
action: deletePerson,
|
||||
},
|
||||
buttonDirection: "row",
|
||||
});
|
||||
|
||||
const deletePerson = useWrapAsyncOperation(async () => {
|
||||
|
||||
17
web/packages/new/photos/components/z-index.tsx
Normal file
17
web/packages/new/photos/components/z-index.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* PhotoSwipe sets the zIndex of its "pswp" class to 1500. We need to go higher
|
||||
* than that for our drawers and dialogs to get them to show above it.
|
||||
*/
|
||||
export const photoSwipeZIndex = 1500;
|
||||
|
||||
/**
|
||||
* The file info drawer needs to be higher than the photo viewer.
|
||||
*/
|
||||
export const fileInfoDrawerZIndex = photoSwipeZIndex + 1;
|
||||
|
||||
/**
|
||||
* Dialogs (not necessarily always) need to be higher still so to ensure they
|
||||
* are visible above the drawer in case they are shown in response to some
|
||||
* action taken in the file info drawer.
|
||||
*/
|
||||
export const photosDialogZIndex = 1600;
|
||||
@@ -17,10 +17,10 @@ export type AppContextT = AccountsContextT & {
|
||||
* Hide the global activity indicator.
|
||||
*/
|
||||
finishLoading: () => void;
|
||||
onGenericError: (error: unknown) => void;
|
||||
somethingWentWrong: () => void;
|
||||
setDialogMessage: SetDialogBoxAttributes;
|
||||
setNotificationAttributes: SetNotificationAttributes;
|
||||
onGenericError: (error: unknown) => void;
|
||||
closeMessageDialog: () => void;
|
||||
mapEnabled: boolean;
|
||||
updateMapEnabled: (enabled: boolean) => Promise<void>;
|
||||
|
||||
@@ -52,7 +52,7 @@ export const getComponents = (
|
||||
// This is not a great choice either, usually most dialogs, for
|
||||
// one reason or the other, will need to customize this padding
|
||||
// anyway. But not resetting it to 16px leaves it at the MUI
|
||||
// defaults, which just don't work with our designs.
|
||||
// defaults, which just doesn't work well with our designs.
|
||||
"& .MuiDialogTitle-root": {
|
||||
// MUI default is '16px 24px'.
|
||||
padding: "16px",
|
||||
@@ -66,9 +66,9 @@ export const getComponents = (
|
||||
overflowY: "auto",
|
||||
},
|
||||
"& .MuiDialogActions-root": {
|
||||
// MUI default is way since they cluster the buttons to the
|
||||
// right, our designs usually want the buttons to align with
|
||||
// the heading / content.
|
||||
// MUI default is way off for us since they cluster the
|
||||
// buttons to the right, while our designs usually want the
|
||||
// buttons to align with the heading / content.
|
||||
padding: "16px",
|
||||
},
|
||||
".MuiDialogTitle-root + .MuiDialogContent-root": {
|
||||
|
||||
Reference in New Issue
Block a user