diff --git a/src/components/markdown/Renderer.tsx b/src/components/markdown/Renderer.tsx index 9fe876b4..5e84d2c1 100644 --- a/src/components/markdown/Renderer.tsx +++ b/src/components/markdown/Renderer.tsx @@ -18,13 +18,15 @@ import { useCallback, useContext } from "preact/hooks"; import { internalEmit } from "../../lib/eventEmitter"; +import { getState } from "../../redux"; + +import { useIntermediate } from "../../context/intermediate/Intermediate"; import { AppContext } from "../../context/revoltjs/RevoltClient"; import { generateEmoji } from "../common/Emoji"; import { emojiDictionary } from "../../assets/emojis"; import { MarkdownProps } from "./Markdown"; -import {useIntermediate} from "../../context/intermediate/Intermediate"; // TODO: global.d.ts file for defining globals declare global { @@ -35,9 +37,9 @@ declare global { const ALLOWED_ORIGINS = [ location.hostname, - 'app.revolt.chat', - 'nightly.revolt.chat', - 'local.revolt.chat', + "app.revolt.chat", + "nightly.revolt.chat", + "local.revolt.chat", ]; // Handler for code block copy. @@ -176,13 +178,16 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) { element.removeAttribute("data-type"); element.removeAttribute("target"); - let internal; + let internal, + url: URL | null = null; const href = element.href; if (href) { try { - const url = new URL(href, location.href); + url = new URL(href, location.href); - if (ALLOWED_ORIGINS.includes(url.hostname)) { + if ( + ALLOWED_ORIGINS.includes(url.hostname) + ) { internal = true; element.addEventListener( "click", @@ -202,12 +207,20 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) { if (!internal) { element.setAttribute("target", "_blank"); element.onclick = (ev) => { - ev.preventDefault(); - openScreen({ - id: "external_link_prompt", - link: href - }) - } + const { trustedLinks } = getState(); + if ( + !url || + !trustedLinks.domains?.includes( + url.hostname, + ) + ) { + ev.preventDefault(); + openScreen({ + id: "external_link_prompt", + link: href, + }); + } + }; } }, ); diff --git a/src/context/intermediate/modals/ExternalLinkPrompt.tsx b/src/context/intermediate/modals/ExternalLinkPrompt.tsx index b0f5e6df..244e4990 100644 --- a/src/context/intermediate/modals/ExternalLinkPrompt.tsx +++ b/src/context/intermediate/modals/ExternalLinkPrompt.tsx @@ -1,6 +1,7 @@ import { Text } from "preact-i18n"; import Modal from "../../../components/ui/Modal"; +import { dispatch } from "../../../redux"; interface Props { onClose: () => void; @@ -29,8 +30,23 @@ export function ExternalLinkModal({ onClose, link }: Props) { confirmation: false, children: "Cancel", }, + { + onClick: () => { + try { + const url = new URL(link); + dispatch({ + type: "TRUSTED_LINKS_ADD_DOMAIN", + domain: url.hostname + }); + } catch(e) {} + window.open(link, "_blank"); + onClose(); + }, + plain: true, + children: , + } ]}> -
+
{link} ); diff --git a/src/redux/index.ts b/src/redux/index.ts index cab197fd..3edab32a 100644 --- a/src/redux/index.ts +++ b/src/redux/index.ts @@ -14,6 +14,7 @@ import { QueuedMessage } from "./reducers/queue"; import { SectionToggle } from "./reducers/section_toggle"; import { Settings } from "./reducers/settings"; import { SyncOptions } from "./reducers/sync"; +import { TrustedLinks } from "./reducers/trusted_links"; import { Unreads } from "./reducers/unreads"; export type State = { @@ -29,6 +30,7 @@ export type State = { lastOpened: LastOpened; notifications: Notifications; sectionToggle: SectionToggle; + trustedLinks: TrustedLinks; }; // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -59,6 +61,7 @@ store.subscribe(() => { lastOpened, notifications, sectionToggle, + trustedLinks, } = store.getState() as State; localForage.setItem("state", { @@ -74,6 +77,7 @@ store.subscribe(() => { lastOpened, notifications, sectionToggle, + trustedLinks, }); }); diff --git a/src/redux/reducers/index.ts b/src/redux/reducers/index.ts index e2b29cd6..c7040552 100644 --- a/src/redux/reducers/index.ts +++ b/src/redux/reducers/index.ts @@ -12,6 +12,7 @@ import { sectionToggle, SectionToggleAction } from "./section_toggle"; import { config, ConfigAction } from "./server_config"; import { settings, SettingsAction } from "./settings"; import { sync, SyncAction } from "./sync"; +import { trustedLinks, TrustedLinksAction } from "./trusted_links"; import { unreads, UnreadsAction } from "./unreads"; export default combineReducers({ @@ -27,6 +28,7 @@ export default combineReducers({ lastOpened, notifications, sectionToggle, + trustedLinks, }); export type Action = @@ -42,4 +44,5 @@ export type Action = | LastOpenedAction | NotificationsAction | SectionToggleAction + | TrustedLinksAction | { type: "__INIT"; state: State }; diff --git a/src/redux/reducers/trusted_links.ts b/src/redux/reducers/trusted_links.ts new file mode 100644 index 00000000..4675b3cd --- /dev/null +++ b/src/redux/reducers/trusted_links.ts @@ -0,0 +1,37 @@ +export interface TrustedLinks { + domains?: string[]; +} + +export type TrustedLinksAction = + | { type: undefined } + | { + type: "TRUSTED_LINKS_ADD_DOMAIN"; + domain: string; + } + | { + type: "TRUSTED_LINKS_REMOVE_DOMAIN"; + domain: string; + }; + +export function trustedLinks( + state = {} as TrustedLinks, + action: TrustedLinksAction, +): TrustedLinks { + switch (action.type) { + case "TRUSTED_LINKS_ADD_DOMAIN": + return { + ...state, + domains: [ + ...(state.domains ?? []).filter((v) => v !== action.domain), + action.domain, + ], + }; + case "TRUSTED_LINKS_REMOVE_DOMAIN": + return { + ...state, + domains: state.domains?.filter((v) => v !== action.domain), + }; + default: + return state; + } +}