diff --git a/package.json b/package.json
index 4db1eacf..62141717 100644
--- a/package.json
+++ b/package.json
@@ -73,7 +73,7 @@
"@hcaptcha/react-hcaptcha": "^0.3.6",
"@insertish/vite-plugin-babel-macros": "^1.0.5",
"@preact/preset-vite": "^2.0.0",
- "@revoltchat/ui": "1.0.40",
+ "@revoltchat/ui": "1.0.43",
"@rollup/plugin-replace": "^2.4.2",
"@styled-icons/boxicons-logos": "^10.38.0",
"@styled-icons/boxicons-regular": "^10.38.0",
diff --git a/src/context/modals/ModalRenderer.tsx b/src/context/modals/ModalRenderer.tsx
index ab5c0582..0092d93b 100644
--- a/src/context/modals/ModalRenderer.tsx
+++ b/src/context/modals/ModalRenderer.tsx
@@ -1,5 +1,22 @@
import { observer } from "mobx-react-lite";
+import { useEffect } from "preact/hooks";
+
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;
+});
diff --git a/src/context/modals/components/Changelog.tsx b/src/context/modals/components/Changelog.tsx
index 059ed776..fd6c0498 100644
--- a/src/context/modals/components/Changelog.tsx
+++ b/src/context/modals/components/Changelog.tsx
@@ -40,6 +40,7 @@ function RenderLog({ post }: { post: ChangelogPost }) {
export default function Changelog({
initial,
onClose,
+ signal,
}: ModalProps<"changelog">) {
const [log, setLog] = useState(initial);
@@ -86,7 +87,8 @@ export default function Changelog({
)
}
actions={actions}
- onClose={onClose}>
+ onClose={onClose}
+ signal={signal}>
{entry ? (
) : (
diff --git a/src/context/modals/components/MFAEnableTOTP.tsx b/src/context/modals/components/MFAEnableTOTP.tsx
index 0e18ef74..32caa8fd 100644
--- a/src/context/modals/components/MFAEnableTOTP.tsx
+++ b/src/context/modals/components/MFAEnableTOTP.tsx
@@ -31,6 +31,7 @@ export default function MFAEnableTOTP({
secret,
callback,
onClose,
+ signal,
}: ModalProps<"mfa_enable_totp">) {
const uri = `otpauth://totp/Revolt:${identifier}?secret=${secret}&issuer=Revolt`;
const [value, setValue] = useState("");
@@ -61,7 +62,9 @@ export default function MFAEnableTOTP({
onClose={() => {
callback();
onClose();
- }}>
+ }}
+ signal={signal}
+ nonDismissable>
diff --git a/src/context/modals/components/MFAFlow.tsx b/src/context/modals/components/MFAFlow.tsx
index ab0a0180..21bd2e0f 100644
--- a/src/context/modals/components/MFAFlow.tsx
+++ b/src/context/modals/components/MFAFlow.tsx
@@ -81,7 +81,11 @@ function ResponseEntry({
/**
* 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(
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={() => {
props.callback();
onClose();
diff --git a/src/context/modals/components/MFARecovery.tsx b/src/context/modals/components/MFARecovery.tsx
index 27cc37ca..bed9afd1 100644
--- a/src/context/modals/components/MFARecovery.tsx
+++ b/src/context/modals/components/MFARecovery.tsx
@@ -32,6 +32,7 @@ export default function MFARecovery({
codes,
client,
onClose,
+ signal,
}: ModalProps<"mfa_recovery">) {
// Keep track of changes to recovery codes
const [known, setCodes] = useState(codes);
@@ -69,7 +70,8 @@ export default function MFARecovery({
onClick: reset,
},
]}
- onClose={onClose}>
+ onClose={onClose}
+ signal={signal}>
{known.map((code) => (
{code}
diff --git a/src/context/modals/index.tsx b/src/context/modals/index.tsx
index dd10f78d..61bc3e01 100644
--- a/src/context/modals/index.tsx
+++ b/src/context/modals/index.tsx
@@ -31,8 +31,10 @@ class ModalController {
makeObservable(this, {
stack: observable,
push: action,
+ pop: action,
remove: action,
rendered: computed,
+ isVisible: computed,
});
}
@@ -50,6 +52,16 @@ class ModalController {
];
}
+ /**
+ * 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
*/
@@ -66,6 +78,8 @@ class ModalController {
{this.stack.map((modal) => {
const Component = this.components[modal.type];
return (
+ // ESLint does not understand spread operator
+ // eslint-disable-next-line
this.remove(modal.key!)}
@@ -75,6 +89,10 @@ class ModalController {
>
);
}
+
+ get isVisible() {
+ return this.stack.length > 0;
+ }
}
/**
diff --git a/src/context/modals/types.ts b/src/context/modals/types.ts
index 5b1e061d..be3127af 100644
--- a/src/context/modals/types.ts
+++ b/src/context/modals/types.ts
@@ -39,4 +39,5 @@ export type Modal = {
export type ModalProps = Modal & { type: T } & {
onClose: () => void;
+ signal?: "close" | "confirm";
};
diff --git a/src/mobx/State.ts b/src/mobx/State.ts
index 6a9c7cfb..ffc2ebbb 100644
--- a/src/mobx/State.ts
+++ b/src/mobx/State.ts
@@ -150,7 +150,6 @@ export default class State {
() => stringify(store.toJSON()),
async (value) => {
try {
- console.log(id, "updated!");
// Save updated store to local storage.
await localforage.setItem(id, JSON.parse(value));
diff --git a/src/pages/settings/GenericSettings.tsx b/src/pages/settings/GenericSettings.tsx
index f4f4fc34..0c6e5d94 100644
--- a/src/pages/settings/GenericSettings.tsx
+++ b/src/pages/settings/GenericSettings.tsx
@@ -13,6 +13,8 @@ import { isTouchscreenDevice } from "../../lib/isTouchscreenDevice";
import { useApplicationState } from "../../mobx/State";
+import { modalController } from "../../context/modals";
+
import ButtonItem from "../../components/navigation/items/ButtonItem";
interface Props {
@@ -61,6 +63,8 @@ export function GenericSettings({
useEffect(() => {
function keyDown(e: KeyboardEvent) {
if (e.key === "Escape") {
+ if (modalController.isVisible) return;
+
exitSettings();
}
}
diff --git a/yarn.lock b/yarn.lock
index 4ebbce01..88c78fb3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -2231,9 +2231,9 @@ __metadata:
languageName: node
linkType: hard
-"@revoltchat/ui@npm:1.0.40":
- version: 1.0.40
- resolution: "@revoltchat/ui@npm:1.0.40"
+"@revoltchat/ui@npm:1.0.43":
+ version: 1.0.43
+ resolution: "@revoltchat/ui@npm:1.0.43"
dependencies:
"@styled-icons/boxicons-logos": ^10.38.0
"@styled-icons/boxicons-regular": ^10.38.0
@@ -2246,7 +2246,7 @@ __metadata:
react-device-detect: "*"
react-virtuoso: "*"
revolt.js: "*"
- checksum: bc0bc906cdb22e8a31c862d1e87f8bd5c46cb463aa23ad773e9c683514fbe0e52ac44e9eab41dd6aa6e8e207050f9ab0590d6e51b2a4d8af6c0fb2ea899d789f
+ checksum: d6a6d0cb4a2f08fea45a4d61e5599894012fbb591472ef95d34ee8ddc9e66cfdc7626e94360b7c104e59d3c64a7d0bd674d6a42f5c3cefc723574db8c1aee64e
languageName: node
linkType: hard
@@ -3539,7 +3539,7 @@ __metadata:
"@hcaptcha/react-hcaptcha": ^0.3.6
"@insertish/vite-plugin-babel-macros": ^1.0.5
"@preact/preset-vite": ^2.0.0
- "@revoltchat/ui": 1.0.40
+ "@revoltchat/ui": 1.0.43
"@rollup/plugin-replace": ^2.4.2
"@styled-icons/boxicons-logos": ^10.38.0
"@styled-icons/boxicons-regular": ^10.38.0