feat(modal): port LinkWarning

This commit is contained in:
Paul Makles 2022-06-18 14:19:31 +01:00
parent 241b9cd27b
commit b7be9f8c03
11 changed files with 84 additions and 51 deletions

View file

@ -107,6 +107,7 @@
"eslint-config-preact": "^1.1.4", "eslint-config-preact": "^1.1.4",
"eslint-plugin-jsdoc": "^39.3.2", "eslint-plugin-jsdoc": "^39.3.2",
"eventemitter3": "^4.0.7", "eventemitter3": "^4.0.7",
"history": "4",
"json-stringify-deterministic": "^1.0.2", "json-stringify-deterministic": "^1.0.2",
"localforage": "^1.9.0", "localforage": "^1.9.0",
"lodash.defaultsdeep": "^4.6.1", "lodash.defaultsdeep": "^4.6.1",

5
src/context/history.ts Normal file
View file

@ -0,0 +1,5 @@
import { createBrowserHistory } from "history";
export const history = createBrowserHistory({
basename: import.meta.env.BASE_URL,
});

View file

@ -1,4 +1,4 @@
import { BrowserRouter as Router, Link } from "react-router-dom"; import { Router, Link } from "react-router-dom";
import { ContextMenuTrigger } from "preact-context-menu"; import { ContextMenuTrigger } from "preact-context-menu";
import { Text } from "preact-i18n"; import { Text } from "preact-i18n";
@ -10,6 +10,7 @@ import { hydrateState } from "../mobx/State";
import Locale from "./Locale"; import Locale from "./Locale";
import Theme from "./Theme"; import Theme from "./Theme";
import { history } from "./history";
import Intermediate from "./intermediate/Intermediate"; import Intermediate from "./intermediate/Intermediate";
import ModalRenderer from "./modals/ModalRenderer"; import ModalRenderer from "./modals/ModalRenderer";
import Client from "./revoltjs/RevoltClient"; import Client from "./revoltjs/RevoltClient";
@ -36,7 +37,7 @@ export default function Context({ children }: { children: Children }) {
if (!ready) return <Preloader type="spinner" />; if (!ready) return <Preloader type="spinner" />;
return ( return (
<Router basename={import.meta.env.BASE_URL}> <Router history={history}>
<UIProvider value={uiContext}> <UIProvider value={uiContext}>
<Locale> <Locale>
<Intermediate> <Intermediate>

View file

@ -141,35 +141,7 @@ export default function Intermediate(props: Props) {
const actions = useMemo(() => { const actions = useMemo(() => {
return { return {
openLink: (href?: string, trusted?: boolean) => { openLink: (href?: string, trusted?: boolean) => {
const link = determineLink(href); return modalController.openLink(href, trusted);
switch (link.type) {
case "profile": {
openScreen({ id: "profile", user_id: link.id });
return true;
}
case "navigate": {
history.push(link.path);
return true;
}
case "external": {
if (
!trusted &&
!settings.security.isTrustedOrigin(
link.url.hostname,
)
) {
openScreen({
id: "external_link_prompt",
link: link.href,
});
} else {
window.open(link.href, "_blank", "noreferrer");
}
}
}
return true;
}, },
openScreen: (screen: Screen) => openScreen(screen), openScreen: (screen: Screen) => openScreen(screen),
writeClipboard: (a: string) => modalController.writeText(a), writeClipboard: (a: string) => modalController.writeText(a),

View file

@ -1,6 +1,5 @@
//import { isModalClosing } from "../../components/ui/Modal"; //import { isModalClosing } from "../../components/ui/Modal";
import { Screen } from "./Intermediate"; import { Screen } from "./Intermediate";
import { ExternalLinkModal } from "./modals/ExternalLinkPrompt";
import { InputModal } from "./modals/Input"; import { InputModal } from "./modals/Input";
import { OnboardingModal } from "./modals/Onboarding"; import { OnboardingModal } from "./modals/Onboarding";
import { PromptModal } from "./modals/Prompt"; import { PromptModal } from "./modals/Prompt";
@ -23,8 +22,6 @@ export default function Modals({ screen, openScreen }: Props) {
return <InputModal onClose={onClose} {...screen} />; return <InputModal onClose={onClose} {...screen} />;
case "onboarding": case "onboarding":
return <OnboardingModal onClose={onClose} {...screen} />; return <OnboardingModal onClose={onClose} {...screen} />;
case "external_link_prompt":
return <ExternalLinkModal onClose={onClose} {...screen} />;
} }
return null; return null;

View file

@ -2,35 +2,32 @@ import { Text } from "preact-i18n";
import { Modal } from "@revoltchat/ui"; import { Modal } from "@revoltchat/ui";
import { noopTrue } from "../../../lib/js";
import { useApplicationState } from "../../../mobx/State"; import { useApplicationState } from "../../../mobx/State";
import { useIntermediate } from "../Intermediate"; import { ModalProps } from "../types";
interface Props { export default function LinkWarning({
onClose: () => void; link,
link: string; callback,
} ...props
}: ModalProps<"link_warning">) {
export function ExternalLinkModal({ onClose, link }: Props) {
const { openLink } = useIntermediate();
const settings = useApplicationState().settings; const settings = useApplicationState().settings;
return ( return (
<Modal <Modal
onClose={onClose} {...props}
title={<Text id={"app.special.modals.external_links.title"} />} title={<Text id={"app.special.modals.external_links.title"} />}
actions={[ actions={[
{ {
onClick: () => { onClick: callback,
openLink(link, true);
onClose();
},
confirmation: true, confirmation: true,
palette: "accent", palette: "accent",
children: "Continue", children: "Continue",
}, },
{ {
onClick: onClose, onClick: noopTrue,
confirmation: false, confirmation: false,
children: "Cancel", children: "Cancel",
}, },
@ -41,8 +38,7 @@ export function ExternalLinkModal({ onClose, link }: Props) {
settings.security.addTrustedOrigin(url.hostname); settings.security.addTrustedOrigin(url.hostname);
} catch (e) {} } catch (e) {}
openLink(link, true); return callback();
onClose();
}, },
palette: "plain", palette: "plain",
children: ( children: (

View file

@ -8,9 +8,16 @@ import {
import type { Client, API } from "revolt.js"; import type { Client, API } from "revolt.js";
import { ulid } from "ulid"; import { ulid } from "ulid";
import { determineLink } from "../../lib/links";
import { getApplicationState, useApplicationState } from "../../mobx/State";
import { history } from "../history";
// import { determineLink } from "../../lib/links";
import Changelog from "./components/Changelog"; import Changelog from "./components/Changelog";
import Clipboard from "./components/Clipboard"; import Clipboard from "./components/Clipboard";
import Error from "./components/Error"; import Error from "./components/Error";
import LinkWarning from "./components/LinkWarning";
import MFAEnableTOTP from "./components/MFAEnableTOTP"; import MFAEnableTOTP from "./components/MFAEnableTOTP";
import MFAFlow from "./components/MFAFlow"; import MFAFlow from "./components/MFAFlow";
import MFARecovery from "./components/MFARecovery"; import MFARecovery from "./components/MFARecovery";
@ -156,12 +163,46 @@ class ModalControllerExtended extends ModalController<Modal> {
}); });
} }
} }
openLink(href?: string, trusted?: boolean) {
const link = determineLink(href);
const settings = getApplicationState().settings;
switch (link.type) {
case "profile": {
// TODO: port Profile
// openScreen({ 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({ export const modalController = new ModalControllerExtended({
changelog: Changelog, changelog: Changelog,
clipboard: Clipboard, clipboard: Clipboard,
error: Error, error: Error,
link_warning: LinkWarning,
mfa_flow: MFAFlow, mfa_flow: MFAFlow,
mfa_recovery: MFARecovery, mfa_recovery: MFARecovery,
mfa_enable_totp: MFAEnableTOTP, mfa_enable_totp: MFAEnableTOTP,

View file

@ -51,6 +51,11 @@ export type Modal = {
type: "clipboard"; type: "clipboard";
text: string; text: string;
} }
| {
type: "link_warning";
link: string;
callback: () => true;
}
| { | {
type: "signed_out"; type: "signed_out";
} }

View file

@ -311,3 +311,11 @@ export async function hydrateState() {
export function useApplicationState() { export function useApplicationState() {
return state; return state;
} }
/**
* Get the application state
* @returns Application state
*/
export function getApplicationState() {
return state;
}

View file

@ -2,6 +2,8 @@ import { makeAutoObservable, computed, action } from "mobx";
import Settings from "../Settings"; import Settings from "../Settings";
const TRUSTED_DOMAINS = ["revolt.chat", "revolt.wtf", "gifbox.me", "rvlt.gg"];
/** /**
* Helper class for changing security options. * Helper class for changing security options.
*/ */
@ -27,6 +29,10 @@ export default class SSecurity {
} }
@computed isTrustedOrigin(origin: string) { @computed isTrustedOrigin(origin: string) {
if (TRUSTED_DOMAINS.find((x) => origin.endsWith(x))) {
return true;
}
return this.settings.get("security:trustedOrigins")?.includes(origin); return this.settings.get("security:trustedOrigins")?.includes(origin);
} }
} }

View file

@ -3574,6 +3574,7 @@ __metadata:
eslint-plugin-jsdoc: ^39.3.2 eslint-plugin-jsdoc: ^39.3.2
eventemitter3: ^4.0.7 eventemitter3: ^4.0.7
fs-extra: ^10.0.0 fs-extra: ^10.0.0
history: 4
json-stringify-deterministic: ^1.0.2 json-stringify-deterministic: ^1.0.2
klaw: ^3.0.0 klaw: ^3.0.0
localforage: ^1.9.0 localforage: ^1.9.0
@ -5043,7 +5044,7 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"history@npm:^4.9.0": "history@npm:4, history@npm:^4.9.0":
version: 4.10.1 version: 4.10.1
resolution: "history@npm:4.10.1" resolution: "history@npm:4.10.1"
dependencies: dependencies: