feat: re-work modal behaviour to be more natural

This commit is contained in:
Paul Makles 2022-06-18 11:22:37 +01:00
parent 63d5f6bb7d
commit 0ee7b73d61
11 changed files with 72 additions and 12 deletions

View file

@ -73,7 +73,7 @@
"@hcaptcha/react-hcaptcha": "^0.3.6", "@hcaptcha/react-hcaptcha": "^0.3.6",
"@insertish/vite-plugin-babel-macros": "^1.0.5", "@insertish/vite-plugin-babel-macros": "^1.0.5",
"@preact/preset-vite": "^2.0.0", "@preact/preset-vite": "^2.0.0",
"@revoltchat/ui": "1.0.40", "@revoltchat/ui": "1.0.43",
"@rollup/plugin-replace": "^2.4.2", "@rollup/plugin-replace": "^2.4.2",
"@styled-icons/boxicons-logos": "^10.38.0", "@styled-icons/boxicons-logos": "^10.38.0",
"@styled-icons/boxicons-regular": "^10.38.0", "@styled-icons/boxicons-regular": "^10.38.0",

View file

@ -1,5 +1,22 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useEffect } from "preact/hooks";
import { modalController } from "."; import { modalController } from ".";
export default observer(() => modalController.rendered); 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;
});

View file

@ -40,6 +40,7 @@ function RenderLog({ post }: { post: ChangelogPost }) {
export default function Changelog({ export default function Changelog({
initial, initial,
onClose, onClose,
signal,
}: ModalProps<"changelog">) { }: ModalProps<"changelog">) {
const [log, setLog] = useState(initial); const [log, setLog] = useState(initial);
@ -86,7 +87,8 @@ export default function Changelog({
) )
} }
actions={actions} actions={actions}
onClose={onClose}> onClose={onClose}
signal={signal}>
{entry ? ( {entry ? (
<RenderLog post={entry} /> <RenderLog post={entry} />
) : ( ) : (

View file

@ -31,6 +31,7 @@ export default function MFAEnableTOTP({
secret, secret,
callback, callback,
onClose, onClose,
signal,
}: ModalProps<"mfa_enable_totp">) { }: ModalProps<"mfa_enable_totp">) {
const uri = `otpauth://totp/Revolt:${identifier}?secret=${secret}&issuer=Revolt`; const uri = `otpauth://totp/Revolt:${identifier}?secret=${secret}&issuer=Revolt`;
const [value, setValue] = useState(""); const [value, setValue] = useState("");
@ -61,7 +62,9 @@ export default function MFAEnableTOTP({
onClose={() => { onClose={() => {
callback(); callback();
onClose(); onClose();
}}> }}
signal={signal}
nonDismissable>
<Column> <Column>
<Centred> <Centred>
<Qr> <Qr>

View file

@ -81,7 +81,11 @@ function ResponseEntry({
/** /**
* MFA ticket creation flow * MFA ticket creation flow
*/ */
export default function MFAFlow({ onClose, ...props }: ModalProps<"mfa_flow">) { export default function MFAFlow({
onClose,
signal,
...props
}: ModalProps<"mfa_flow">) {
const [methods, setMethods] = useState<API.MFAMethod[] | undefined>( const [methods, setMethods] = useState<API.MFAMethod[] | undefined>(
props.state === "unknown" ? props.available_methods : undefined, props.state === "unknown" ? props.available_methods : undefined,
); );
@ -178,6 +182,16 @@ export default function MFAFlow({ onClose, ...props }: ModalProps<"mfa_flow">) {
}, },
] ]
} }
// 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={() => { onClose={() => {
props.callback(); props.callback();
onClose(); onClose();

View file

@ -32,6 +32,7 @@ export default function MFARecovery({
codes, codes,
client, client,
onClose, onClose,
signal,
}: ModalProps<"mfa_recovery">) { }: ModalProps<"mfa_recovery">) {
// Keep track of changes to recovery codes // Keep track of changes to recovery codes
const [known, setCodes] = useState(codes); const [known, setCodes] = useState(codes);
@ -69,7 +70,8 @@ export default function MFARecovery({
onClick: reset, onClick: reset,
}, },
]} ]}
onClose={onClose}> onClose={onClose}
signal={signal}>
<List> <List>
{known.map((code) => ( {known.map((code) => (
<span key={code}>{code}</span> <span key={code}>{code}</span>

View file

@ -31,8 +31,10 @@ class ModalController<T extends Modal> {
makeObservable(this, { makeObservable(this, {
stack: observable, stack: observable,
push: action, push: action,
pop: action,
remove: action, remove: action,
rendered: computed, rendered: computed,
isVisible: computed,
}); });
} }
@ -50,6 +52,16 @@ class ModalController<T extends Modal> {
]; ];
} }
/**
* 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 the keyed modal from the stack
*/ */
@ -66,6 +78,8 @@ class ModalController<T extends Modal> {
{this.stack.map((modal) => { {this.stack.map((modal) => {
const Component = this.components[modal.type]; const Component = this.components[modal.type];
return ( return (
// ESLint does not understand spread operator
// eslint-disable-next-line
<Component <Component
{...modal} {...modal}
onClose={() => this.remove(modal.key!)} onClose={() => this.remove(modal.key!)}
@ -75,6 +89,10 @@ class ModalController<T extends Modal> {
</> </>
); );
} }
get isVisible() {
return this.stack.length > 0;
}
} }
/** /**

View file

@ -39,4 +39,5 @@ export type Modal = {
export type ModalProps<T extends Modal["type"]> = Modal & { type: T } & { export type ModalProps<T extends Modal["type"]> = Modal & { type: T } & {
onClose: () => void; onClose: () => void;
signal?: "close" | "confirm";
}; };

View file

@ -150,7 +150,6 @@ export default class State {
() => stringify(store.toJSON()), () => stringify(store.toJSON()),
async (value) => { async (value) => {
try { try {
console.log(id, "updated!");
// Save updated store to local storage. // Save updated store to local storage.
await localforage.setItem(id, JSON.parse(value)); await localforage.setItem(id, JSON.parse(value));

View file

@ -13,6 +13,8 @@ import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
import { useApplicationState } from "../../mobx/State"; import { useApplicationState } from "../../mobx/State";
import { modalController } from "../../context/modals";
import ButtonItem from "../../components/navigation/items/ButtonItem"; import ButtonItem from "../../components/navigation/items/ButtonItem";
interface Props { interface Props {
@ -61,6 +63,8 @@ export function GenericSettings({
useEffect(() => { useEffect(() => {
function keyDown(e: KeyboardEvent) { function keyDown(e: KeyboardEvent) {
if (e.key === "Escape") { if (e.key === "Escape") {
if (modalController.isVisible) return;
exitSettings(); exitSettings();
} }
} }

View file

@ -2231,9 +2231,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@revoltchat/ui@npm:1.0.40": "@revoltchat/ui@npm:1.0.43":
version: 1.0.40 version: 1.0.43
resolution: "@revoltchat/ui@npm:1.0.40" resolution: "@revoltchat/ui@npm:1.0.43"
dependencies: dependencies:
"@styled-icons/boxicons-logos": ^10.38.0 "@styled-icons/boxicons-logos": ^10.38.0
"@styled-icons/boxicons-regular": ^10.38.0 "@styled-icons/boxicons-regular": ^10.38.0
@ -2246,7 +2246,7 @@ __metadata:
react-device-detect: "*" react-device-detect: "*"
react-virtuoso: "*" react-virtuoso: "*"
revolt.js: "*" revolt.js: "*"
checksum: bc0bc906cdb22e8a31c862d1e87f8bd5c46cb463aa23ad773e9c683514fbe0e52ac44e9eab41dd6aa6e8e207050f9ab0590d6e51b2a4d8af6c0fb2ea899d789f checksum: d6a6d0cb4a2f08fea45a4d61e5599894012fbb591472ef95d34ee8ddc9e66cfdc7626e94360b7c104e59d3c64a7d0bd674d6a42f5c3cefc723574db8c1aee64e
languageName: node languageName: node
linkType: hard linkType: hard
@ -3539,7 +3539,7 @@ __metadata:
"@hcaptcha/react-hcaptcha": ^0.3.6 "@hcaptcha/react-hcaptcha": ^0.3.6
"@insertish/vite-plugin-babel-macros": ^1.0.5 "@insertish/vite-plugin-babel-macros": ^1.0.5
"@preact/preset-vite": ^2.0.0 "@preact/preset-vite": ^2.0.0
"@revoltchat/ui": 1.0.40 "@revoltchat/ui": 1.0.43
"@rollup/plugin-replace": ^2.4.2 "@rollup/plugin-replace": ^2.4.2
"@styled-icons/boxicons-logos": ^10.38.0 "@styled-icons/boxicons-logos": ^10.38.0
"@styled-icons/boxicons-regular": ^10.38.0 "@styled-icons/boxicons-regular": ^10.38.0