chore: refactor account UI

This commit is contained in:
Paul Makles 2022-06-12 15:07:30 +01:00
parent 8103cc03cf
commit 645e1af6db
7 changed files with 145 additions and 207 deletions

View file

@ -162,5 +162,8 @@
"repository": "https://github.com/revoltchat/revite.git",
"author": "Paul <paulmakles@gmail.com>",
"license": "MIT",
"packageManager": "yarn@3.2.0"
"packageManager": "yarn@3.2.0",
"resolutions": {
"@revoltchat/ui": "portal:../components"
}
}

View file

@ -4,12 +4,7 @@ import { ContextMenuTrigger } from "preact-context-menu";
import { Text } from "preact-i18n";
import { useEffect, useState } from "preact/hooks";
import {
LinkProvider,
Preloader,
TextProvider,
TrigProvider,
} from "@revoltchat/ui";
import { Preloader, UIProvider } from "@revoltchat/ui";
import { hydrateState } from "../mobx/State";
@ -20,6 +15,13 @@ import ModalRenderer from "./modals/ModalRenderer";
import Client from "./revoltjs/RevoltClient";
import SyncManager from "./revoltjs/SyncManager";
const uiContext = {
Link,
Text: Text as any,
Trigger: ContextMenuTrigger,
emitAction: () => {},
};
/**
* This component provides all of the application's context layers.
* @param param0 Provided children
@ -35,21 +37,17 @@ export default function Context({ children }: { children: Children }) {
return (
<Router basename={import.meta.env.BASE_URL}>
<LinkProvider value={Link}>
<TextProvider value={Text as any}>
<TrigProvider value={ContextMenuTrigger}>
<Locale>
<Intermediate>
<Client>
{children}
<SyncManager />
</Client>
</Intermediate>
<ModalRenderer />
</Locale>
</TrigProvider>
</TextProvider>
</LinkProvider>
<UIProvider value={uiContext}>
<Locale>
<Intermediate>
<Client>
{children}
<SyncManager />
</Client>
</Intermediate>
<ModalRenderer />
</Locale>
</UIProvider>
<Theme />
</Router>
);

View file

@ -3,7 +3,12 @@ import { Key, Keyboard } from "@styled-icons/boxicons-solid";
import { API } from "revolt.js";
import { Text } from "preact-i18n";
import { useCallback, useEffect, useState } from "preact/hooks";
import {
useCallback,
useEffect,
useLayoutEffect,
useState,
} from "preact/hooks";
import {
Category,
@ -15,8 +20,6 @@ import {
import { noopTrue } from "../../../lib/js";
import { useApplicationState } from "../../../mobx/State";
import { ModalProps } from "../types";
const ICONS: Record<API.MFAMethod, React.FC<any>> = {
@ -34,12 +37,13 @@ function ResponseEntry({
value?: API.MFAResponse;
onChange: (v: API.MFAResponse) => void;
}) {
if (type === "Password") {
return (
<>
<Category compact>
<Text id={`login.${type.toLowerCase()}`} />
</Category>
return (
<>
<Category compact>
<Text id={`login.${type.toLowerCase()}`} />
</Category>
{type === "Password" && (
<InputBox
type="password"
value={(value as { password: string })?.password}
@ -47,56 +51,51 @@ function ResponseEntry({
onChange({ password: e.currentTarget.value })
}
/>
</>
);
} else {
return null;
}
)}
</>
);
}
/**
* MFA ticket creation flow
*/
export default function MFAFlow({
callback,
onClose,
...props
}: ModalProps<"mfa_flow">) {
const state = useApplicationState();
export default function MFAFlow({ onClose, ...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) {
let ticket;
if (props.state === "known") {
ticket = await props.client.api.put(
const ticket = await props.client.api.put(
"/auth/mfa/ticket",
response,
);
props.callback(ticket);
} else {
ticket = await state.config
.createClient()
.api.put("/auth/mfa/ticket", response, {
headers: {
"X-MFA-Ticket": props.ticket.token,
},
});
props.callback(response);
}
callback(ticket);
return true;
}
@ -122,8 +121,12 @@ export default function MFAFlow({
},
{
palette: "plain",
children: "Back",
onClick: () => setSelected(undefined),
children:
methods!.length === 1 ? "Cancel" : "Back",
onClick: () =>
methods!.length === 1
? true
: void setSelected(undefined),
},
]
: [

View file

@ -5,16 +5,16 @@ export type Modal = {
} & (
| ({
type: "mfa_flow";
callback: (ticket: API.MFATicket) => void;
} & (
| {
state: "known";
client: Client;
callback: (ticket: API.MFATicket) => void;
}
| {
state: "unknown";
available_methods: API.MFAMethod[];
ticket: API.MFATicket & { validated: false };
callback: (response: API.MFAResponse) => void;
}
))
| {

View file

@ -4,6 +4,7 @@ import { API } from "revolt.js";
import { useApplicationState } from "../../../mobx/State";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { modalController } from "../../../context/modals";
import { Form } from "./Form";
@ -43,14 +44,34 @@ export function FormLogin() {
// This should be replaced in the future.
const client = state.config.createClient();
await client.fetchConfiguration();
const session = await client.api.post("/auth/session/login", {
let session = await client.api.post("/auth/session/login", {
...data,
friendly_name,
});
if (session.result !== "Success") {
alert("unsupported!");
return;
if (session.result === "MFA") {
const { allowed_methods } = session;
let mfa_response: API.MFAResponse = await new Promise(
(callback) =>
modalController.push({
type: "mfa_flow",
state: "unknown",
available_methods: allowed_methods,
callback,
}),
);
session = await client.api.post("/auth/session/login", {
mfa_response,
mfa_ticket: session.ticket,
friendly_name,
});
if (session.result === "MFA") {
// unreachable code
return;
}
}
const s = session;

View file

@ -1,22 +1,19 @@
import { At, Key, Block } from "@styled-icons/boxicons-regular";
import {
Envelope,
HelpCircle,
Lock,
Trash,
Pencil,
} from "@styled-icons/boxicons-solid";
import { Envelope, Lock, Trash, Pencil } from "@styled-icons/boxicons-solid";
import { observer } from "mobx-react-lite";
import { useHistory } from "react-router-dom";
import { API } from "revolt.js";
import { Link } from "react-router-dom";
import styles from "./Panes.module.scss";
import { Text } from "preact-i18n";
import { useContext, useEffect, useState } from "preact/hooks";
import { Button, CategoryButton, LineDivider, Tip } from "@revoltchat/ui";
import { stopPropagation } from "../../../lib/stopPropagation";
import {
AccountDetail,
CategoryButton,
Column,
HiddenValue,
Tip,
} from "@revoltchat/ui";
import { useIntermediate } from "../../../context/intermediate/Intermediate";
import { modalController } from "../../../context/modals";
@ -27,26 +24,14 @@ import {
useClient,
} from "../../../context/revoltjs/RevoltClient";
import Tooltip from "../../../components/common/Tooltip";
import UserIcon from "../../../components/common/user/UserIcon";
export const Account = observer(() => {
const { openScreen, writeClipboard } = useIntermediate();
const { openScreen } = useIntermediate();
const logOut = useContext(LogOutContext);
const status = useContext(StatusContext);
const client = useClient();
const [email, setEmail] = useState("...");
const [revealEmail, setRevealEmail] = useState(false);
const [profile, setProfile] = useState<undefined | API.UserProfile>(
undefined,
);
const history = useHistory();
function switchPage(to: string) {
history.replace(`/settings/${to}`);
}
useEffect(() => {
if (email === "..." && status === ClientStatus.ONLINE) {
@ -54,124 +39,48 @@ export const Account = observer(() => {
.get("/auth/account/")
.then((account) => setEmail(account.email));
}
if (profile === undefined && status === ClientStatus.ONLINE) {
client
.user!.fetchProfile()
.then((profile) => setProfile(profile ?? {}));
}
}, [client, email, profile, status]);
}, [client, email, status]);
return (
<div className={styles.user}>
<div className={styles.banner}>
<div className={styles.container}>
<UserIcon
className={styles.avatar}
target={client.user!}
size={72}
onClick={() => switchPage("profile")}
/>
<div className={styles.userDetail}>
<div className={styles.userContainer}>
<UserIcon
className={styles.tinyavatar}
target={client.user!}
size={25}
onClick={() => switchPage("profile")}
/>
<div className={styles.username}>
@{client.user!.username}
</div>
</div>
<div className={styles.userid}>
<Tooltip
content={
<Text id="app.settings.pages.account.unique_id" />
}>
<HelpCircle size={16} />
</Tooltip>
<Tooltip content={<Text id="app.special.copy" />}>
<a
onClick={() =>
writeClipboard(client.user!._id)
}>
{client.user!._id}
</a>
</Tooltip>
</div>
</div>
</div>
<Column group>
<AccountDetail user={client.user!} />
</Column>
{(
[
["username", client.user!.username, At],
["email", email, Envelope],
["password", "•••••••••", Key],
] as const
).map(([field, value, Icon]) => (
<CategoryButton
key={field}
icon={<Icon size={24} />}
description={
field === "email" ? (
<HiddenValue
value={value}
placeholder={"•••••••••••@••••••.•••"}
/>
) : (
value
)
}
account
action={<Pencil size={20} />}
onClick={() =>
openScreen({
id: "modify_account",
field,
})
}>
<Text id={`login.${field}`} />
</CategoryButton>
))}
<Button
onClick={() => switchPage("profile")}
palette="secondary">
<Text id="app.settings.pages.profile.edit_profile" />
</Button>
</div>
<div>
{(
[
[
"username",
client.user!.username,
<At key="at" size={24} />,
],
["email", email, <Envelope key="envelope" size={24} />],
["password", "•••••••••", <Key key="key" size={24} />],
] as const
).map(([field, value, icon]) => (
<CategoryButton
key={field}
icon={icon}
description={
field === "email" ? (
revealEmail ? (
<>
{value}{" "}
<a
style={{ fontSize: "13px" }}
onClick={(ev) =>
stopPropagation(
ev,
setRevealEmail(false),
)
}>
<Text id="app.special.modals.actions.hide" />
</a>
</>
) : (
<>
@.{" "}
<a
style={{ fontSize: "13px" }}
onClick={(ev) =>
stopPropagation(
ev,
setRevealEmail(true),
)
}>
<Text id="app.special.modals.actions.reveal" />
</a>
</>
)
) : (
value
)
}
account
action={<Pencil size={20} />}
onClick={() =>
openScreen({
id: "modify_account",
field,
})
}>
<Text id={`login.${field}`} />
</CategoryButton>
))}
</div>
<hr />
<h3>
<Text id="app.settings.pages.account.2fa.title" />
</h3>
@ -197,10 +106,13 @@ export const Account = observer(() => {
action="chevron">
View my backup codes
</CategoryButton>*/}
<hr />
<h3>
<Text id="app.settings.pages.account.manage.title" />
</h3>
<h5>
<Text id="app.settings.pages.account.manage.description" />
</h5>
@ -227,6 +139,7 @@ export const Account = observer(() => {
}>
<Text id="app.settings.pages.account.manage.disable" />
</CategoryButton>
<CategoryButton
icon={<Trash size={24} color="var(--error)" />}
description={
@ -250,13 +163,14 @@ export const Account = observer(() => {
}>
<Text id="app.settings.pages.account.manage.delete" />
</CategoryButton>
<Tip>
<span>
<Text id="app.settings.tips.account.a" />
</span>{" "}
<a onClick={() => switchPage("profile")}>
<Link to="/settings/profile" replace>
<Text id="app.settings.tips.account.b" />
</a>
</Link>
</Tip>
</div>
);

View file

@ -2220,9 +2220,9 @@ __metadata:
languageName: node
linkType: hard
"@revoltchat/ui@npm:1.0.39":
version: 1.0.39
resolution: "@revoltchat/ui@npm:1.0.39"
"@revoltchat/ui@portal:../components::locator=client%40workspace%3A.":
version: 0.0.0-use.local
resolution: "@revoltchat/ui@portal:../components::locator=client%40workspace%3A."
dependencies:
"@styled-icons/boxicons-logos": ^10.38.0
"@styled-icons/boxicons-regular": ^10.38.0
@ -2235,9 +2235,8 @@ __metadata:
react-device-detect: "*"
react-virtuoso: "*"
revolt.js: "*"
checksum: 0376ef1e6c90a139da613a0b76d498327c7bad63941d02eb27b9d5b8208f09c01fb45330fc4e0643554a298beee416814dd41fd9992750378491450c6f773ee0
languageName: node
linkType: hard
linkType: soft
"@rollup/plugin-babel@npm:^5.2.0":
version: 5.3.0