diff --git a/package.json b/package.json index 62141717..21f82367 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,7 @@ "eslint-config-preact": "^1.1.4", "eslint-plugin-jsdoc": "^39.3.2", "eventemitter3": "^4.0.7", + "history": "4", "json-stringify-deterministic": "^1.0.2", "localforage": "^1.9.0", "lodash.defaultsdeep": "^4.6.1", diff --git a/src/context/history.ts b/src/context/history.ts new file mode 100644 index 00000000..5e816997 --- /dev/null +++ b/src/context/history.ts @@ -0,0 +1,5 @@ +import { createBrowserHistory } from "history"; + +export const history = createBrowserHistory({ + basename: import.meta.env.BASE_URL, +}); diff --git a/src/context/index.tsx b/src/context/index.tsx index f30f885c..663fdfaa 100644 --- a/src/context/index.tsx +++ b/src/context/index.tsx @@ -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 { Text } from "preact-i18n"; @@ -10,6 +10,7 @@ import { hydrateState } from "../mobx/State"; import Locale from "./Locale"; import Theme from "./Theme"; +import { history } from "./history"; import Intermediate from "./intermediate/Intermediate"; import ModalRenderer from "./modals/ModalRenderer"; import Client from "./revoltjs/RevoltClient"; @@ -36,7 +37,7 @@ export default function Context({ children }: { children: Children }) { if (!ready) return ; return ( - + diff --git a/src/context/intermediate/Intermediate.tsx b/src/context/intermediate/Intermediate.tsx index b58c37de..906e4c95 100644 --- a/src/context/intermediate/Intermediate.tsx +++ b/src/context/intermediate/Intermediate.tsx @@ -141,35 +141,7 @@ export default function Intermediate(props: Props) { const actions = useMemo(() => { return { openLink: (href?: string, trusted?: boolean) => { - const link = determineLink(href); - - 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; + return modalController.openLink(href, trusted); }, openScreen: (screen: Screen) => openScreen(screen), writeClipboard: (a: string) => modalController.writeText(a), diff --git a/src/context/intermediate/Modals.tsx b/src/context/intermediate/Modals.tsx index a2a91507..4f59c974 100644 --- a/src/context/intermediate/Modals.tsx +++ b/src/context/intermediate/Modals.tsx @@ -1,6 +1,5 @@ //import { isModalClosing } from "../../components/ui/Modal"; import { Screen } from "./Intermediate"; -import { ExternalLinkModal } from "./modals/ExternalLinkPrompt"; import { InputModal } from "./modals/Input"; import { OnboardingModal } from "./modals/Onboarding"; import { PromptModal } from "./modals/Prompt"; @@ -23,8 +22,6 @@ export default function Modals({ screen, openScreen }: Props) { return ; case "onboarding": return ; - case "external_link_prompt": - return ; } return null; diff --git a/src/context/intermediate/modals/ExternalLinkPrompt.tsx b/src/context/modals/components/LinkWarning.tsx similarity index 70% rename from src/context/intermediate/modals/ExternalLinkPrompt.tsx rename to src/context/modals/components/LinkWarning.tsx index 48e336ac..04a9e876 100644 --- a/src/context/intermediate/modals/ExternalLinkPrompt.tsx +++ b/src/context/modals/components/LinkWarning.tsx @@ -2,35 +2,32 @@ import { Text } from "preact-i18n"; import { Modal } from "@revoltchat/ui"; +import { noopTrue } from "../../../lib/js"; + import { useApplicationState } from "../../../mobx/State"; -import { useIntermediate } from "../Intermediate"; +import { ModalProps } from "../types"; -interface Props { - onClose: () => void; - link: string; -} - -export function ExternalLinkModal({ onClose, link }: Props) { - const { openLink } = useIntermediate(); +export default function LinkWarning({ + link, + callback, + ...props +}: ModalProps<"link_warning">) { const settings = useApplicationState().settings; return ( } actions={[ { - onClick: () => { - openLink(link, true); - onClose(); - }, + onClick: callback, confirmation: true, palette: "accent", children: "Continue", }, { - onClick: onClose, + onClick: noopTrue, confirmation: false, children: "Cancel", }, @@ -41,8 +38,7 @@ export function ExternalLinkModal({ onClose, link }: Props) { settings.security.addTrustedOrigin(url.hostname); } catch (e) {} - openLink(link, true); - onClose(); + return callback(); }, palette: "plain", children: ( diff --git a/src/context/modals/index.tsx b/src/context/modals/index.tsx index 1b0670fd..6a2485d0 100644 --- a/src/context/modals/index.tsx +++ b/src/context/modals/index.tsx @@ -8,9 +8,16 @@ import { import type { Client, API } from "revolt.js"; 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 Clipboard from "./components/Clipboard"; import Error from "./components/Error"; +import LinkWarning from "./components/LinkWarning"; import MFAEnableTOTP from "./components/MFAEnableTOTP"; import MFAFlow from "./components/MFAFlow"; import MFARecovery from "./components/MFARecovery"; @@ -156,12 +163,46 @@ class ModalControllerExtended extends ModalController { }); } } + + 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({ changelog: Changelog, clipboard: Clipboard, error: Error, + link_warning: LinkWarning, mfa_flow: MFAFlow, mfa_recovery: MFARecovery, mfa_enable_totp: MFAEnableTOTP, diff --git a/src/context/modals/types.ts b/src/context/modals/types.ts index 1b5a36e7..d6bb5883 100644 --- a/src/context/modals/types.ts +++ b/src/context/modals/types.ts @@ -51,6 +51,11 @@ export type Modal = { type: "clipboard"; text: string; } + | { + type: "link_warning"; + link: string; + callback: () => true; + } | { type: "signed_out"; } diff --git a/src/mobx/State.ts b/src/mobx/State.ts index ffc2ebbb..b67f03a8 100644 --- a/src/mobx/State.ts +++ b/src/mobx/State.ts @@ -311,3 +311,11 @@ export async function hydrateState() { export function useApplicationState() { return state; } + +/** + * Get the application state + * @returns Application state + */ +export function getApplicationState() { + return state; +} diff --git a/src/mobx/stores/helpers/SSecurity.ts b/src/mobx/stores/helpers/SSecurity.ts index a57d8d1f..0fce7f93 100644 --- a/src/mobx/stores/helpers/SSecurity.ts +++ b/src/mobx/stores/helpers/SSecurity.ts @@ -2,6 +2,8 @@ import { makeAutoObservable, computed, action } from "mobx"; import Settings from "../Settings"; +const TRUSTED_DOMAINS = ["revolt.chat", "revolt.wtf", "gifbox.me", "rvlt.gg"]; + /** * Helper class for changing security options. */ @@ -27,6 +29,10 @@ export default class SSecurity { } @computed isTrustedOrigin(origin: string) { + if (TRUSTED_DOMAINS.find((x) => origin.endsWith(x))) { + return true; + } + return this.settings.get("security:trustedOrigins")?.includes(origin); } } diff --git a/yarn.lock b/yarn.lock index 88c78fb3..5a43c7b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3574,6 +3574,7 @@ __metadata: eslint-plugin-jsdoc: ^39.3.2 eventemitter3: ^4.0.7 fs-extra: ^10.0.0 + history: 4 json-stringify-deterministic: ^1.0.2 klaw: ^3.0.0 localforage: ^1.9.0 @@ -5043,7 +5044,7 @@ __metadata: languageName: node linkType: hard -"history@npm:^4.9.0": +"history@npm:4, history@npm:^4.9.0": version: 4.10.1 resolution: "history@npm:4.10.1" dependencies: