mirror of
https://github.com/stoatchat/for-legacy-web.git
synced 2026-03-07 09:25:27 +00:00
chore(refactor): rename context/modals to controllers/modals
This commit is contained in:
2
src/context/DO_NOT_TOUCH.md
Normal file
2
src/context/DO_NOT_TOUCH.md
Normal file
@@ -0,0 +1,2 @@
|
||||
hello do not touch `intermediate` or `revoltjs` folders
|
||||
they are being rewritten
|
||||
@@ -8,11 +8,11 @@ import { Preloader, UIProvider } from "@revoltchat/ui";
|
||||
|
||||
import { hydrateState } from "../mobx/State";
|
||||
|
||||
import ModalRenderer from "../controllers/modals/ModalRenderer";
|
||||
import Locale from "./Locale";
|
||||
import Theme from "./Theme";
|
||||
import { history } from "./history";
|
||||
import Intermediate from "./intermediate/Intermediate";
|
||||
import ModalRenderer from "./modals/ModalRenderer";
|
||||
import Client from "./revoltjs/RevoltClient";
|
||||
import SyncManager from "./revoltjs/SyncManager";
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import { determineLink } from "../../lib/links";
|
||||
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
|
||||
import { modalController } from "../modals";
|
||||
import { modalController } from "../../controllers/modals/ModalController";
|
||||
import Modals from "./Modals";
|
||||
|
||||
export type Screen =
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
|
||||
import { useEffect } from "preact/hooks";
|
||||
|
||||
import { modalController } from ".";
|
||||
|
||||
export default observer(() => {
|
||||
useEffect(() => {
|
||||
function keyUp(event: KeyboardEvent) {
|
||||
if (event.key === "Escape") {
|
||||
modalController.pop("close");
|
||||
} else if (event.key === "Enter") {
|
||||
modalController.pop("confirm");
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keyup", keyUp);
|
||||
return () => document.removeEventListener("keyup", keyUp);
|
||||
}, []);
|
||||
|
||||
return modalController.rendered;
|
||||
});
|
||||
@@ -1,107 +0,0 @@
|
||||
import dayjs from "dayjs";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { useMemo, useState } from "preact/hooks";
|
||||
|
||||
import { CategoryButton, Column, Modal } from "@revoltchat/ui";
|
||||
import type { Action } from "@revoltchat/ui/esm/components/design/atoms/display/Modal";
|
||||
|
||||
import { noopTrue } from "../../../lib/js";
|
||||
|
||||
import {
|
||||
changelogEntries,
|
||||
changelogEntryArray,
|
||||
ChangelogPost,
|
||||
} from "../../../assets/changelogs";
|
||||
import { ModalProps } from "../types";
|
||||
|
||||
const Image = styled.img`
|
||||
border-radius: var(--border-radius);
|
||||
`;
|
||||
|
||||
function RenderLog({ post }: { post: ChangelogPost }) {
|
||||
return (
|
||||
<Column>
|
||||
{post.content.map((entry) =>
|
||||
typeof entry === "string" ? (
|
||||
<span>{entry}</span>
|
||||
) : (
|
||||
<Image src={entry.src} />
|
||||
),
|
||||
)}
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changelog modal
|
||||
*/
|
||||
export default function Changelog({
|
||||
initial,
|
||||
onClose,
|
||||
signal,
|
||||
}: ModalProps<"changelog">) {
|
||||
const [log, setLog] = useState(initial);
|
||||
|
||||
const entry = useMemo(
|
||||
() => (log ? changelogEntries[log] : undefined),
|
||||
[log],
|
||||
);
|
||||
|
||||
const actions = useMemo(() => {
|
||||
const arr: Action[] = [
|
||||
{
|
||||
palette: "primary",
|
||||
children: <Text id="app.special.modals.actions.close" />,
|
||||
onClick: noopTrue,
|
||||
},
|
||||
];
|
||||
|
||||
if (log) {
|
||||
arr.push({
|
||||
palette: "plain-secondary",
|
||||
children: <Text id="app.special.modals.changelogs.older" />,
|
||||
onClick: () => {
|
||||
setLog(undefined);
|
||||
return false;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return arr;
|
||||
}, [log]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={
|
||||
entry?.title ?? (
|
||||
<Text id="app.special.modals.changelogs.title" />
|
||||
)
|
||||
}
|
||||
description={
|
||||
entry ? (
|
||||
dayjs(entry.date).calendar()
|
||||
) : (
|
||||
<Text id="app.special.modals.changelogs.description" />
|
||||
)
|
||||
}
|
||||
actions={actions}
|
||||
onClose={onClose}
|
||||
signal={signal}>
|
||||
{entry ? (
|
||||
<RenderLog post={entry} />
|
||||
) : (
|
||||
<Column>
|
||||
{changelogEntryArray.map((entry, index) => (
|
||||
<CategoryButton
|
||||
key={index}
|
||||
onClick={() => setLog(index + 1)}>
|
||||
{entry.title}
|
||||
</CategoryButton>
|
||||
))}
|
||||
</Column>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import { Modal } from "@revoltchat/ui";
|
||||
|
||||
import { noopTrue } from "../../../lib/js";
|
||||
|
||||
import { ModalProps } from "../types";
|
||||
|
||||
export default function Clipboard({ text, ...props }: ModalProps<"clipboard">) {
|
||||
return (
|
||||
<Modal
|
||||
{...props}
|
||||
title={<Text id="app.special.modals.clipboard.unavailable" />}
|
||||
actions={[
|
||||
{
|
||||
onClick: noopTrue,
|
||||
confirmation: true,
|
||||
children: <Text id="app.special.modals.actions.close" />,
|
||||
},
|
||||
]}>
|
||||
{location.protocol !== "https:" && (
|
||||
<p>
|
||||
<Text id="app.special.modals.clipboard.https" />
|
||||
</p>
|
||||
)}
|
||||
<Text id="app.special.modals.clipboard.copy" />{" "}
|
||||
<code style={{ userSelect: "all", wordBreak: "break-all" }}>
|
||||
{text}
|
||||
</code>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import { Modal } from "@revoltchat/ui";
|
||||
|
||||
import { noopTrue } from "../../../lib/js";
|
||||
|
||||
import { ModalProps } from "../types";
|
||||
|
||||
export default function Error({ error, ...props }: ModalProps<"error">) {
|
||||
return (
|
||||
<Modal
|
||||
{...props}
|
||||
title={<Text id="app.special.modals.error" />}
|
||||
actions={[
|
||||
{
|
||||
onClick: noopTrue,
|
||||
confirmation: true,
|
||||
children: <Text id="app.special.modals.actions.ok" />,
|
||||
},
|
||||
{
|
||||
palette: "plain-secondary",
|
||||
onClick: () => location.reload(),
|
||||
children: <Text id="app.special.modals.actions.reload" />,
|
||||
},
|
||||
]}>
|
||||
<Text id={`error.${error}`}>{error}</Text>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import { Modal } from "@revoltchat/ui";
|
||||
|
||||
import { noopTrue } from "../../../lib/js";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
|
||||
import { ModalProps } from "../types";
|
||||
|
||||
export default function LinkWarning({
|
||||
link,
|
||||
callback,
|
||||
...props
|
||||
}: ModalProps<"link_warning">) {
|
||||
const settings = useApplicationState().settings;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
{...props}
|
||||
title={<Text id={"app.special.modals.external_links.title"} />}
|
||||
actions={[
|
||||
{
|
||||
onClick: callback,
|
||||
confirmation: true,
|
||||
palette: "accent",
|
||||
children: "Continue",
|
||||
},
|
||||
{
|
||||
onClick: noopTrue,
|
||||
confirmation: false,
|
||||
children: "Cancel",
|
||||
},
|
||||
{
|
||||
onClick: () => {
|
||||
try {
|
||||
const url = new URL(link);
|
||||
settings.security.addTrustedOrigin(url.hostname);
|
||||
} catch (e) {}
|
||||
|
||||
return callback();
|
||||
},
|
||||
palette: "plain",
|
||||
children: (
|
||||
<Text id="app.special.modals.external_links.trust_domain" />
|
||||
),
|
||||
},
|
||||
]}>
|
||||
<Text id="app.special.modals.external_links.short" /> <br />
|
||||
<a>{link}</a>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import { QRCodeSVG } from "qrcode.react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { useState } from "preact/hooks";
|
||||
|
||||
import { Category, Centred, Column, InputBox, Modal } from "@revoltchat/ui";
|
||||
|
||||
import { ModalProps } from "../types";
|
||||
|
||||
const Code = styled.code`
|
||||
user-select: all;
|
||||
`;
|
||||
|
||||
const Qr = styled.div`
|
||||
border-radius: 4px;
|
||||
background: white;
|
||||
|
||||
width: 140px;
|
||||
height: 140px;
|
||||
|
||||
display: grid;
|
||||
place-items: center;
|
||||
`;
|
||||
|
||||
/**
|
||||
* TOTP enable modal
|
||||
*/
|
||||
export default function MFAEnableTOTP({
|
||||
identifier,
|
||||
secret,
|
||||
callback,
|
||||
onClose,
|
||||
signal,
|
||||
}: ModalProps<"mfa_enable_totp">) {
|
||||
const uri = `otpauth://totp/Revolt:${identifier}?secret=${secret}&issuer=Revolt`;
|
||||
const [value, setValue] = useState("");
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<Text id="app.special.modals.mfa.enable_totp" />}
|
||||
description={<Text id="app.special.modals.mfa.prompt_totp" />}
|
||||
actions={[
|
||||
{
|
||||
palette: "primary",
|
||||
children: <Text id="app.special.modals.actions.continue" />,
|
||||
onClick: () => {
|
||||
callback(value.trim().replace(/\s/g, ""));
|
||||
return true;
|
||||
},
|
||||
confirmation: true,
|
||||
},
|
||||
{
|
||||
palette: "plain",
|
||||
children: <Text id="app.special.modals.actions.cancel" />,
|
||||
onClick: () => {
|
||||
callback();
|
||||
return true;
|
||||
},
|
||||
},
|
||||
]}
|
||||
onClose={() => {
|
||||
callback();
|
||||
onClose();
|
||||
}}
|
||||
signal={signal}
|
||||
nonDismissable>
|
||||
<Column>
|
||||
<Centred>
|
||||
<Qr>
|
||||
<QRCodeSVG
|
||||
value={uri}
|
||||
bgColor="white"
|
||||
fgColor="black"
|
||||
/>
|
||||
</Qr>
|
||||
</Centred>
|
||||
<Centred>
|
||||
<Code>{secret}</Code>
|
||||
</Centred>
|
||||
</Column>
|
||||
|
||||
<Category compact>
|
||||
<Text id="app.special.modals.mfa.enter_code" />
|
||||
</Category>
|
||||
|
||||
<InputBox
|
||||
value={value}
|
||||
onChange={(e) => setValue(e.currentTarget.value)}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,225 +0,0 @@
|
||||
import { Archive } from "@styled-icons/boxicons-regular";
|
||||
import { Key, Keyboard } from "@styled-icons/boxicons-solid";
|
||||
import { API } from "revolt.js";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useState,
|
||||
} from "preact/hooks";
|
||||
|
||||
import {
|
||||
Category,
|
||||
CategoryButton,
|
||||
InputBox,
|
||||
Modal,
|
||||
Preloader,
|
||||
} from "@revoltchat/ui";
|
||||
|
||||
import { ModalProps } from "../types";
|
||||
|
||||
/**
|
||||
* Mapping of MFA methods to icons
|
||||
*/
|
||||
const ICONS: Record<API.MFAMethod, React.FC<any>> = {
|
||||
Password: Keyboard,
|
||||
Totp: Key,
|
||||
Recovery: Archive,
|
||||
};
|
||||
|
||||
/**
|
||||
* Component for handling challenge entry
|
||||
*/
|
||||
function ResponseEntry({
|
||||
type,
|
||||
value,
|
||||
onChange,
|
||||
}: {
|
||||
type: API.MFAMethod;
|
||||
value?: API.MFAResponse;
|
||||
onChange: (v: API.MFAResponse) => void;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<Category compact>
|
||||
<Text id={`login.${type.toLowerCase()}`} />
|
||||
</Category>
|
||||
|
||||
{type === "Password" && (
|
||||
<InputBox
|
||||
type="password"
|
||||
value={(value as { password: string })?.password}
|
||||
onChange={(e) =>
|
||||
onChange({ password: e.currentTarget.value })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === "Totp" && (
|
||||
<InputBox
|
||||
value={(value as { totp_code: string })?.totp_code}
|
||||
onChange={(e) =>
|
||||
onChange({ totp_code: e.currentTarget.value })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{type === "Recovery" && (
|
||||
<InputBox
|
||||
value={(value as { recovery_code: string })?.recovery_code}
|
||||
onChange={(e) =>
|
||||
onChange({ recovery_code: e.currentTarget.value })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* MFA ticket creation flow
|
||||
*/
|
||||
export default function MFAFlow({
|
||||
onClose,
|
||||
signal,
|
||||
...props
|
||||
}: ModalProps<"mfa_flow">) {
|
||||
const [methods, setMethods] = useState<API.MFAMethod[] | undefined>(
|
||||
props.state === "unknown" ? props.available_methods : undefined,
|
||||
);
|
||||
|
||||
// Current state of the modal
|
||||
const [selectedMethod, setSelected] = useState<API.MFAMethod>();
|
||||
const [response, setResponse] = useState<API.MFAResponse>();
|
||||
|
||||
// Fetch available methods if they have not been provided.
|
||||
useEffect(() => {
|
||||
if (!methods && props.state === "known") {
|
||||
props.client.api.get("/auth/mfa/methods").then(setMethods);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Always select first available method if only one available.
|
||||
useLayoutEffect(() => {
|
||||
if (methods && methods.length === 1) {
|
||||
setSelected(methods[0]);
|
||||
}
|
||||
}, [methods]);
|
||||
|
||||
// Callback to generate a new ticket or send response back up the chain.
|
||||
const generateTicket = useCallback(async () => {
|
||||
if (response) {
|
||||
if (props.state === "known") {
|
||||
const ticket = await props.client.api.put(
|
||||
"/auth/mfa/ticket",
|
||||
response,
|
||||
);
|
||||
|
||||
props.callback(ticket);
|
||||
} else {
|
||||
props.callback(response);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [response]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<Text id="app.special.modals.confirm" />}
|
||||
description={
|
||||
<Text
|
||||
id={`app.special.modals.mfa.${
|
||||
selectedMethod ? "confirm" : "select_method"
|
||||
}`}
|
||||
/>
|
||||
}
|
||||
actions={
|
||||
selectedMethod
|
||||
? [
|
||||
{
|
||||
palette: "primary",
|
||||
children: (
|
||||
<Text id="app.special.modals.actions.confirm" />
|
||||
),
|
||||
onClick: generateTicket,
|
||||
confirmation: true,
|
||||
},
|
||||
{
|
||||
palette: "plain",
|
||||
children: (
|
||||
<Text
|
||||
id={`app.special.modals.actions.${
|
||||
methods!.length === 1
|
||||
? "cancel"
|
||||
: "back"
|
||||
}`}
|
||||
/>
|
||||
),
|
||||
onClick: () => {
|
||||
if (methods!.length === 1) {
|
||||
props.callback();
|
||||
return true;
|
||||
}
|
||||
setSelected(undefined);
|
||||
},
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
palette: "plain",
|
||||
children: (
|
||||
<Text id="app.special.modals.actions.cancel" />
|
||||
),
|
||||
onClick: () => {
|
||||
props.callback();
|
||||
return true;
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
// If we are logging in or have selected a method,
|
||||
// don't allow the user to dismiss the modal by clicking off.
|
||||
// This is to just generally prevent annoying situations
|
||||
// where you accidentally close the modal while logging in
|
||||
// or when switching to your password manager.
|
||||
nonDismissable={
|
||||
props.state === "unknown" ||
|
||||
typeof selectedMethod !== "undefined"
|
||||
}
|
||||
signal={signal}
|
||||
onClose={() => {
|
||||
props.callback();
|
||||
onClose();
|
||||
}}>
|
||||
{methods ? (
|
||||
selectedMethod ? (
|
||||
<ResponseEntry
|
||||
type={selectedMethod}
|
||||
value={response}
|
||||
onChange={setResponse}
|
||||
/>
|
||||
) : (
|
||||
methods.map((method) => {
|
||||
const Icon = ICONS[method];
|
||||
return (
|
||||
<CategoryButton
|
||||
key={method}
|
||||
action="chevron"
|
||||
icon={<Icon size={24} />}
|
||||
onClick={() => setSelected(method)}>
|
||||
<Text id={`login.${method.toLowerCase()}`} />
|
||||
</CategoryButton>
|
||||
);
|
||||
})
|
||||
)
|
||||
) : (
|
||||
<Preloader type="ring" />
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { useCallback, useState } from "preact/hooks";
|
||||
|
||||
import { Modal } from "@revoltchat/ui";
|
||||
|
||||
import { noopTrue } from "../../../lib/js";
|
||||
|
||||
import { modalController } from "..";
|
||||
import { toConfig } from "../../../components/settings/account/MultiFactorAuthentication";
|
||||
import { ModalProps } from "../types";
|
||||
|
||||
/**
|
||||
* List of recovery codes
|
||||
*/
|
||||
const List = styled.div`
|
||||
display: grid;
|
||||
text-align: center;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
font-family: var(--monospace-font), monospace;
|
||||
|
||||
span {
|
||||
user-select: text;
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* Recovery codes modal
|
||||
*/
|
||||
export default function MFARecovery({
|
||||
codes,
|
||||
client,
|
||||
onClose,
|
||||
signal,
|
||||
}: ModalProps<"mfa_recovery">) {
|
||||
// Keep track of changes to recovery codes
|
||||
const [known, setCodes] = useState(codes);
|
||||
|
||||
// Subroutine to reset recovery codes
|
||||
const reset = useCallback(async () => {
|
||||
const ticket = await modalController.mfaFlow(client);
|
||||
if (ticket) {
|
||||
const codes = await client.api.patch(
|
||||
"/auth/mfa/recovery",
|
||||
undefined,
|
||||
toConfig(ticket.token),
|
||||
);
|
||||
|
||||
setCodes(codes);
|
||||
}
|
||||
|
||||
return false;
|
||||
}, [client]);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={<Text id="app.special.modals.mfa.recovery_codes" />}
|
||||
description={<Text id="app.special.modals.mfa.save_codes" />}
|
||||
actions={[
|
||||
{
|
||||
palette: "primary",
|
||||
children: <Text id="app.special.modals.actions.done" />,
|
||||
onClick: noopTrue,
|
||||
confirmation: true,
|
||||
},
|
||||
{
|
||||
palette: "plain",
|
||||
children: <Text id="app.special.modals.actions.reset" />,
|
||||
onClick: reset,
|
||||
},
|
||||
]}
|
||||
onClose={onClose}
|
||||
signal={signal}>
|
||||
<List>
|
||||
{known.map((code) => (
|
||||
<span key={code}>{code}</span>
|
||||
))}
|
||||
</List>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,154 +0,0 @@
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { useContext, useState } from "preact/hooks";
|
||||
|
||||
import { Category, Error, Modal } from "@revoltchat/ui";
|
||||
|
||||
import { noopTrue } from "../../../lib/js";
|
||||
|
||||
import { useApplicationState } from "../../../mobx/State";
|
||||
|
||||
import FormField from "../../../pages/login/FormField";
|
||||
import { AppContext } from "../../revoltjs/RevoltClient";
|
||||
import { takeError } from "../../revoltjs/util";
|
||||
import { ModalProps } from "../types";
|
||||
|
||||
interface FormInputs {
|
||||
password: string;
|
||||
new_email: string;
|
||||
new_username: string;
|
||||
new_password: string;
|
||||
|
||||
// TODO: figure out if this is correct or not
|
||||
// it wasn't in the types before this was typed but the element itself was there
|
||||
current_password?: string;
|
||||
}
|
||||
|
||||
export default function ModifyAccount({
|
||||
field,
|
||||
...props
|
||||
}: ModalProps<"modify_account">) {
|
||||
const client = useApplicationState().client!;
|
||||
const [processing, setProcessing] = useState(false);
|
||||
const { handleSubmit, register, errors } = useForm<FormInputs>();
|
||||
const [error, setError] = useState<string | undefined>(undefined);
|
||||
|
||||
const onSubmit: SubmitHandler<FormInputs> = async ({
|
||||
password,
|
||||
new_username,
|
||||
new_email,
|
||||
new_password,
|
||||
}) => {
|
||||
if (processing) return;
|
||||
setProcessing(true);
|
||||
|
||||
try {
|
||||
if (field === "email") {
|
||||
await client.api.patch("/auth/account/change/email", {
|
||||
current_password: password,
|
||||
email: new_email,
|
||||
});
|
||||
props.onClose();
|
||||
} else if (field === "password") {
|
||||
await client.api.patch("/auth/account/change/password", {
|
||||
current_password: password,
|
||||
password: new_password,
|
||||
});
|
||||
props.onClose();
|
||||
} else if (field === "username") {
|
||||
await client.api.patch("/users/@me/username", {
|
||||
username: new_username,
|
||||
password,
|
||||
});
|
||||
props.onClose();
|
||||
}
|
||||
} catch (err) {
|
||||
setError(takeError(err));
|
||||
setProcessing(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
{...props}
|
||||
title={<Text id={`app.special.modals.account.change.${field}`} />}
|
||||
disabled={processing}
|
||||
actions={[
|
||||
{
|
||||
confirmation: true,
|
||||
onClick: () => void handleSubmit(onSubmit)(),
|
||||
children:
|
||||
field === "email" ? (
|
||||
<Text id="app.special.modals.actions.send_email" />
|
||||
) : (
|
||||
<Text id="app.special.modals.actions.update" />
|
||||
),
|
||||
},
|
||||
{
|
||||
onClick: noopTrue,
|
||||
children: <Text id="app.special.modals.actions.cancel" />,
|
||||
palette: "plain",
|
||||
},
|
||||
]}>
|
||||
{/* Preact / React typing incompatabilities */}
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleSubmit(
|
||||
onSubmit,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
)(e as any);
|
||||
}}>
|
||||
{field === "email" && (
|
||||
<FormField
|
||||
type="email"
|
||||
name="new_email"
|
||||
register={register}
|
||||
showOverline
|
||||
error={errors.new_email?.message}
|
||||
disabled={processing}
|
||||
/>
|
||||
)}
|
||||
{field === "password" && (
|
||||
<FormField
|
||||
type="password"
|
||||
name="new_password"
|
||||
register={register}
|
||||
showOverline
|
||||
error={errors.new_password?.message}
|
||||
autoComplete="new-password"
|
||||
disabled={processing}
|
||||
/>
|
||||
)}
|
||||
{field === "username" && (
|
||||
<FormField
|
||||
type="username"
|
||||
name="new_username"
|
||||
register={register}
|
||||
showOverline
|
||||
error={errors.new_username?.message}
|
||||
disabled={processing}
|
||||
/>
|
||||
)}
|
||||
<FormField
|
||||
type="current_password"
|
||||
register={register}
|
||||
showOverline
|
||||
error={errors.current_password?.message}
|
||||
autoComplete="current-password"
|
||||
disabled={processing}
|
||||
/>
|
||||
{error && (
|
||||
<Category compact>
|
||||
<Error
|
||||
error={
|
||||
<Text id="app.special.modals.account.failed" />
|
||||
}
|
||||
/>
|
||||
</Category>
|
||||
)}
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import { Modal } from "@revoltchat/ui";
|
||||
|
||||
import { noop, noopTrue } from "../../../lib/js";
|
||||
|
||||
import { APP_VERSION } from "../../../version";
|
||||
import { ModalProps } from "../types";
|
||||
|
||||
/**
|
||||
* Out-of-date indicator which instructs users
|
||||
* that their client needs to be updated
|
||||
*/
|
||||
export default function OutOfDate({
|
||||
onClose,
|
||||
version,
|
||||
}: ModalProps<"out_of_date">) {
|
||||
return (
|
||||
<Modal
|
||||
title={<Text id="app.special.modals.out_of_date.title" />}
|
||||
description={
|
||||
<>
|
||||
<Text id="app.special.modals.out_of_date.description" />
|
||||
<br />
|
||||
<Text
|
||||
id="app.special.modals.out_of_date.version"
|
||||
fields={{ client: APP_VERSION, server: version }}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
actions={[
|
||||
{
|
||||
palette: "plain",
|
||||
onClick: noop,
|
||||
children: (
|
||||
<Text id="app.special.modals.out_of_date.attempting" />
|
||||
),
|
||||
},
|
||||
{
|
||||
palette: "plain-secondary",
|
||||
onClick: noopTrue,
|
||||
children: (
|
||||
<Text id="app.special.modals.out_of_date.ignore" />
|
||||
),
|
||||
},
|
||||
]}
|
||||
onClose={onClose}
|
||||
nonDismissable
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import { Column, Modal } from "@revoltchat/ui";
|
||||
|
||||
import { Friend } from "../../../pages/friends/Friend";
|
||||
import { ModalProps } from "../types";
|
||||
|
||||
export default function PendingFriendRequests({
|
||||
users,
|
||||
...props
|
||||
}: ModalProps<"pending_friend_requests">) {
|
||||
return (
|
||||
<Modal {...props} title={<Text id="app.special.friends.pending" />}>
|
||||
<Column>
|
||||
{users.map((x) => (
|
||||
<Friend user={x!} key={x!._id} />
|
||||
))}
|
||||
</Column>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
import { X } from "@styled-icons/boxicons-regular";
|
||||
import { Save } from "@styled-icons/boxicons-solid";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { Text } from "preact-i18n";
|
||||
import { useMemo, useState } from "preact/hooks";
|
||||
|
||||
import {
|
||||
Button,
|
||||
Category,
|
||||
Centred,
|
||||
Column,
|
||||
InputBox,
|
||||
Modal,
|
||||
Row,
|
||||
Message,
|
||||
} from "@revoltchat/ui";
|
||||
|
||||
import { noop } from "../../../lib/js";
|
||||
|
||||
import { FileUploader } from "../../revoltjs/FileUploads";
|
||||
import { ModalProps } from "../types";
|
||||
|
||||
const Preview = styled(Centred)`
|
||||
flex-grow: 1;
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--secondary-background);
|
||||
|
||||
> div {
|
||||
padding: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export default observer(
|
||||
({ member, ...props }: ModalProps<"server_identity">) => {
|
||||
const [nickname, setNickname] = useState(member.nickname ?? "");
|
||||
|
||||
const message: any = useMemo(() => {
|
||||
return {
|
||||
author: member.user!,
|
||||
member: {
|
||||
...member,
|
||||
nickname,
|
||||
},
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
{...props}
|
||||
title={
|
||||
<Text
|
||||
id={"app.special.popovers.server_identity.title"}
|
||||
fields={{ server: member.server!.name }}
|
||||
/>
|
||||
}>
|
||||
<Column gap="18px">
|
||||
<Column>
|
||||
<Category compact>
|
||||
<Text id="app.special.popovers.server_identity.nickname" />
|
||||
</Category>
|
||||
<Row centred>
|
||||
<InputBox
|
||||
value={nickname}
|
||||
placeholder={member.user!.username}
|
||||
onChange={(e) =>
|
||||
setNickname(e.currentTarget.value)
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
compact="icon"
|
||||
palette="secondary"
|
||||
disabled={
|
||||
nickname === member.nickname || !nickname
|
||||
}
|
||||
onClick={() => member.edit({ nickname })}>
|
||||
<Save size={24} />
|
||||
</Button>
|
||||
<Button
|
||||
compact="icon"
|
||||
palette="secondary"
|
||||
disabled={!member.nickname}
|
||||
onClick={() =>
|
||||
member
|
||||
.edit({ remove: ["Nickname"] })
|
||||
.then(() => setNickname(""))
|
||||
}>
|
||||
<X size={24} />
|
||||
</Button>
|
||||
</Row>
|
||||
</Column>
|
||||
<Row gap="18px">
|
||||
<Column>
|
||||
<Category compact>
|
||||
<Text id="app.special.popovers.server_identity.avatar" />
|
||||
</Category>
|
||||
<FileUploader
|
||||
width={80}
|
||||
height={80}
|
||||
style="icon"
|
||||
fileType="avatars"
|
||||
behaviour="upload"
|
||||
maxFileSize={4_000_000}
|
||||
onUpload={(avatar) =>
|
||||
member.edit({ avatar }).then(noop)
|
||||
}
|
||||
remove={() =>
|
||||
member
|
||||
.edit({ remove: ["Avatar"] })
|
||||
.then(noop)
|
||||
}
|
||||
defaultPreview={member.user?.generateAvatarURL(
|
||||
{
|
||||
max_side: 256,
|
||||
},
|
||||
false,
|
||||
)}
|
||||
previewURL={member.client.generateFileURL(
|
||||
member.avatar ?? undefined,
|
||||
{ max_side: 256 },
|
||||
true,
|
||||
)}
|
||||
desaturateDefault
|
||||
/>
|
||||
</Column>
|
||||
<Column grow>
|
||||
<Category compact>Preview</Category>
|
||||
<Preview>
|
||||
<Message message={message} head />
|
||||
</Preview>
|
||||
</Column>
|
||||
</Row>
|
||||
</Column>
|
||||
</Modal>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -1,35 +0,0 @@
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import { Modal } from "@revoltchat/ui";
|
||||
|
||||
import { noopTrue } from "../../../lib/js";
|
||||
|
||||
import { ModalProps } from "../types";
|
||||
|
||||
export default function ShowToken({
|
||||
name,
|
||||
token,
|
||||
...props
|
||||
}: ModalProps<"show_token">) {
|
||||
return (
|
||||
<Modal
|
||||
{...props}
|
||||
title={
|
||||
<Text
|
||||
id={"app.special.modals.token_reveal"}
|
||||
fields={{ name }}
|
||||
/>
|
||||
}
|
||||
actions={[
|
||||
{
|
||||
onClick: noopTrue,
|
||||
confirmation: true,
|
||||
children: <Text id="app.special.modals.actions.close" />,
|
||||
},
|
||||
]}>
|
||||
<code style={{ userSelect: "all", wordBreak: "break-all" }}>
|
||||
{token}
|
||||
</code>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Text } from "preact-i18n";
|
||||
import { useCallback } from "preact/hooks";
|
||||
|
||||
import { Modal } from "@revoltchat/ui";
|
||||
|
||||
import { noopTrue } from "../../../lib/js";
|
||||
|
||||
import { ModalProps } from "../types";
|
||||
|
||||
/**
|
||||
* Confirm whether a user wants to sign out of all other sessions
|
||||
*/
|
||||
export default function SignOutSessions(
|
||||
props: ModalProps<"sign_out_sessions">,
|
||||
) {
|
||||
const onClick = useCallback(() => {
|
||||
props.onDeleting();
|
||||
props.client.api.delete("/auth/session/all").then(props.onDelete);
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
{...props}
|
||||
title={<Text id={"app.special.modals.sessions.title"} />}
|
||||
actions={[
|
||||
{
|
||||
onClick: noopTrue,
|
||||
palette: "accent",
|
||||
confirmation: true,
|
||||
children: <Text id="app.special.modals.actions.back" />,
|
||||
},
|
||||
{
|
||||
onClick,
|
||||
confirmation: true,
|
||||
children: <Text id="app.special.modals.sessions.accept" />,
|
||||
},
|
||||
]}>
|
||||
<Text id="app.special.modals.sessions.short" /> <br />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { Text } from "preact-i18n";
|
||||
|
||||
import { Modal } from "@revoltchat/ui";
|
||||
|
||||
import { noopTrue } from "../../../lib/js";
|
||||
|
||||
import { ModalProps } from "../types";
|
||||
|
||||
/**
|
||||
* Indicate that the user has been signed out of their account
|
||||
*/
|
||||
export default function SignedOut(props: ModalProps<"signed_out">) {
|
||||
return (
|
||||
<Modal
|
||||
{...props}
|
||||
title={<Text id="app.special.modals.signed_out" />}
|
||||
actions={[
|
||||
{
|
||||
onClick: noopTrue,
|
||||
confirmation: true,
|
||||
children: <Text id="app.special.modals.actions.ok" />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,219 +0,0 @@
|
||||
import {
|
||||
action,
|
||||
computed,
|
||||
makeObservable,
|
||||
observable,
|
||||
runInAction,
|
||||
} from "mobx";
|
||||
import type { Client, API } from "revolt.js";
|
||||
import { ulid } from "ulid";
|
||||
|
||||
import { determineLink } from "../../lib/links";
|
||||
|
||||
import { getApplicationState, useApplicationState } from "../../mobx/State";
|
||||
|
||||
import { history } from "../history";
|
||||
import { __thisIsAHack } from "../intermediate/Intermediate";
|
||||
// import { determineLink } from "../../lib/links";
|
||||
import Changelog from "./components/Changelog";
|
||||
import Clipboard from "./components/Clipboard";
|
||||
import Error from "./components/Error";
|
||||
import LinkWarning from "./components/LinkWarning";
|
||||
import MFAEnableTOTP from "./components/MFAEnableTOTP";
|
||||
import MFAFlow from "./components/MFAFlow";
|
||||
import MFARecovery from "./components/MFARecovery";
|
||||
import ModifyAccount from "./components/ModifyAccount";
|
||||
import OutOfDate from "./components/OutOfDate";
|
||||
import PendingFriendRequests from "./components/PendingFriendRequests";
|
||||
import ServerIdentity from "./components/ServerIdentity";
|
||||
import ShowToken from "./components/ShowToken";
|
||||
import SignOutSessions from "./components/SignOutSessions";
|
||||
import SignedOut from "./components/SignedOut";
|
||||
import { Modal } from "./types";
|
||||
|
||||
type Components = Record<string, React.FC<any>>;
|
||||
|
||||
/**
|
||||
* Handles layering and displaying modals to the user.
|
||||
*/
|
||||
class ModalController<T extends Modal> {
|
||||
stack: T[] = [];
|
||||
components: Components;
|
||||
|
||||
constructor(components: Components) {
|
||||
this.components = components;
|
||||
|
||||
makeObservable(this, {
|
||||
stack: observable,
|
||||
push: action,
|
||||
pop: action,
|
||||
remove: action,
|
||||
rendered: computed,
|
||||
isVisible: computed,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a new modal on the stack
|
||||
* @param modal Modal data
|
||||
*/
|
||||
push(modal: T) {
|
||||
this.stack = [
|
||||
...this.stack,
|
||||
{
|
||||
...modal,
|
||||
key: ulid(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the top modal from the screen
|
||||
* @param signal What action to trigger
|
||||
*/
|
||||
pop(signal: "close" | "confirm" | "force") {
|
||||
this.stack = this.stack.map((entry, index) =>
|
||||
index === this.stack.length - 1 ? { ...entry, signal } : entry,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the keyed modal from the stack
|
||||
*/
|
||||
remove(key: string) {
|
||||
this.stack = this.stack.filter((x) => x.key !== key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render modals
|
||||
*/
|
||||
get rendered() {
|
||||
return (
|
||||
<>
|
||||
{this.stack.map((modal) => {
|
||||
const Component = this.components[modal.type];
|
||||
return (
|
||||
// ESLint does not understand spread operator
|
||||
// eslint-disable-next-line
|
||||
<Component
|
||||
{...modal}
|
||||
onClose={() => this.remove(modal.key!)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a modal is currently visible
|
||||
*/
|
||||
get isVisible() {
|
||||
return this.stack.length > 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modal controller with additional helpers.
|
||||
*/
|
||||
class ModalControllerExtended extends ModalController<Modal> {
|
||||
/**
|
||||
* Perform MFA flow
|
||||
* @param client Client
|
||||
*/
|
||||
mfaFlow(client: Client) {
|
||||
return runInAction(
|
||||
() =>
|
||||
new Promise((callback: (ticket?: API.MFATicket) => void) =>
|
||||
this.push({
|
||||
type: "mfa_flow",
|
||||
state: "known",
|
||||
client,
|
||||
callback,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open TOTP secret modal
|
||||
* @param client Client
|
||||
*/
|
||||
mfaEnableTOTP(secret: string, identifier: string) {
|
||||
return runInAction(
|
||||
() =>
|
||||
new Promise((callback: (value?: string) => void) =>
|
||||
this.push({
|
||||
type: "mfa_enable_totp",
|
||||
identifier,
|
||||
secret,
|
||||
callback,
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write text to the clipboard
|
||||
* @param text Text to write
|
||||
*/
|
||||
writeText(text: string) {
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
this.push({
|
||||
type: "clipboard",
|
||||
text,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
openLink(href?: string, trusted?: boolean) {
|
||||
const link = determineLink(href);
|
||||
const settings = getApplicationState().settings;
|
||||
|
||||
switch (link.type) {
|
||||
case "profile": {
|
||||
__thisIsAHack({ id: "profile", user_id: link.id });
|
||||
break;
|
||||
}
|
||||
case "navigate": {
|
||||
history.push(link.path);
|
||||
break;
|
||||
}
|
||||
case "external": {
|
||||
if (
|
||||
!trusted &&
|
||||
!settings.security.isTrustedOrigin(link.url.hostname)
|
||||
) {
|
||||
modalController.push({
|
||||
type: "link_warning",
|
||||
link: link.href,
|
||||
callback: () => this.openLink(href, true) as true,
|
||||
});
|
||||
} else {
|
||||
window.open(link.href, "_blank", "noreferrer");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export const modalController = new ModalControllerExtended({
|
||||
changelog: Changelog,
|
||||
clipboard: Clipboard,
|
||||
error: Error,
|
||||
link_warning: LinkWarning,
|
||||
mfa_flow: MFAFlow,
|
||||
mfa_recovery: MFARecovery,
|
||||
mfa_enable_totp: MFAEnableTOTP,
|
||||
modify_account: ModifyAccount,
|
||||
out_of_date: OutOfDate,
|
||||
pending_friend_requests: PendingFriendRequests,
|
||||
server_identity: ServerIdentity,
|
||||
show_token: ShowToken,
|
||||
signed_out: SignedOut,
|
||||
sign_out_sessions: SignOutSessions,
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
import { API, Client, User, Member } from "revolt.js";
|
||||
|
||||
export type Modal = {
|
||||
key?: string;
|
||||
} & (
|
||||
| ({
|
||||
type: "mfa_flow";
|
||||
} & (
|
||||
| {
|
||||
state: "known";
|
||||
client: Client;
|
||||
callback: (ticket?: API.MFATicket) => void;
|
||||
}
|
||||
| {
|
||||
state: "unknown";
|
||||
available_methods: API.MFAMethod[];
|
||||
callback: (response?: API.MFAResponse) => void;
|
||||
}
|
||||
))
|
||||
| { type: "mfa_recovery"; codes: string[]; client: Client }
|
||||
| {
|
||||
type: "mfa_enable_totp";
|
||||
identifier: string;
|
||||
secret: string;
|
||||
callback: (code?: string) => void;
|
||||
}
|
||||
| {
|
||||
type: "out_of_date";
|
||||
version: string;
|
||||
}
|
||||
| {
|
||||
type: "changelog";
|
||||
initial?: number;
|
||||
}
|
||||
| {
|
||||
type: "sign_out_sessions";
|
||||
client: Client;
|
||||
onDelete: () => void;
|
||||
onDeleting: () => void;
|
||||
}
|
||||
| {
|
||||
type: "show_token";
|
||||
name: string;
|
||||
token: string;
|
||||
}
|
||||
| {
|
||||
type: "error";
|
||||
error: string;
|
||||
}
|
||||
| {
|
||||
type: "clipboard";
|
||||
text: string;
|
||||
}
|
||||
| {
|
||||
type: "link_warning";
|
||||
link: string;
|
||||
callback: () => true;
|
||||
}
|
||||
| {
|
||||
type: "pending_friend_requests";
|
||||
users: User[];
|
||||
}
|
||||
| {
|
||||
type: "modify_account";
|
||||
client: Client;
|
||||
field: "username" | "email" | "password";
|
||||
}
|
||||
| {
|
||||
type: "server_identity";
|
||||
member: Member;
|
||||
}
|
||||
| {
|
||||
type: "signed_out";
|
||||
}
|
||||
);
|
||||
|
||||
export type ModalProps<T extends Modal["type"]> = Modal & { type: T } & {
|
||||
onClose: () => void;
|
||||
signal?: "close" | "confirm";
|
||||
};
|
||||
@@ -13,8 +13,8 @@ import { determineFileSize } from "../../lib/fileSize";
|
||||
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
|
||||
import { modalController } from "../../controllers/modals/ModalController";
|
||||
import { useIntermediate } from "../intermediate/Intermediate";
|
||||
import { modalController } from "../modals";
|
||||
import { AppContext } from "./RevoltClient";
|
||||
import { takeError } from "./util";
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Preloader } from "@revoltchat/ui";
|
||||
|
||||
import { useApplicationState } from "../../mobx/State";
|
||||
|
||||
import { modalController } from "../modals";
|
||||
import { modalController } from "../../controllers/modals/ModalController";
|
||||
import { registerEvents } from "./events";
|
||||
import { takeError } from "./util";
|
||||
|
||||
|
||||
Reference in New Issue
Block a user