mirror of
https://github.com/revoltchat/revite.git
synced 2024-12-24 06:32:08 -05:00
chore: refactor account UI
This commit is contained in:
parent
8103cc03cf
commit
645e1af6db
7 changed files with 145 additions and 207 deletions
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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),
|
||||
},
|
||||
]
|
||||
: [
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
))
|
||||
| {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue