diff --git a/src/components/common/messaging/Message.tsx b/src/components/common/messaging/Message.tsx index c2f5df74..8b740c8d 100644 --- a/src/components/common/messaging/Message.tsx +++ b/src/components/common/messaging/Message.tsx @@ -24,15 +24,7 @@ import MessageBase, { import Attachment from "./attachments/Attachment"; import { MessageReply } from "./attachments/MessageReply"; import Embed from "./embed/Embed"; -import EmbedInvite from "./embed/EmbedInvite"; - -const INVITE_PATHS = [ - location.hostname + "/invite", - "app.revolt.chat/invite", - "nightly.revolt.chat/invite", - "local.revolt.chat/invite", - "rvlt.gg" -] +import InviteList from "./embed/EmbedInvite"; interface Props { attachContext?: boolean; @@ -151,28 +143,7 @@ const Message = observer( )} {replacement ?? } - {(() => { - let isInvite = false; - INVITE_PATHS.forEach(path => { - if (content.includes(path)) { - isInvite = true; - } - }) - if (isInvite) { - const inviteRegex = new RegExp("(?:" + INVITE_PATHS.map((path, index) => path.split(".").join("\\.") + (index !== INVITE_PATHS.length - 1 ? "|" : "")).join("") + ")/([A-Za-z0-9]*)", "g"); - if (inviteRegex.test(content)) { - let results: string[] = []; - let match: RegExpExecArray | null; - inviteRegex.lastIndex = 0; - while ((match = inviteRegex.exec(content)) !== null) { - if (!results.includes(match[match.length - 1])) { - results.push(match[match.length - 1]); - } - } - return results.map(code => ); - } - } - })()} + {!queued && } {queued?.error && ( )} diff --git a/src/components/common/messaging/embed/Embed.tsx b/src/components/common/messaging/embed/Embed.tsx index c53ebdfb..b0caf741 100644 --- a/src/components/common/messaging/embed/Embed.tsx +++ b/src/components/common/messaging/embed/Embed.tsx @@ -22,7 +22,7 @@ const MAX_PREVIEW_SIZE = 150; export default function Embed({ embed }: Props) { const client = useClient(); - const { openScreen } = useIntermediate(); + const { openScreen, openLink } = useIntermediate(); const maxWidth = Math.min( useContext(MessageAreaWidthContext) - CONTAINER_PADDING, MAX_EMBED_WIDTH, @@ -111,6 +111,10 @@ export default function Embed({ embed }: Props) { {embed.title && ( + openLink(e.currentTarget.href) && + e.preventDefault() + } href={embed.url} target={"_blank"} className={styles.title} diff --git a/src/components/common/messaging/embed/EmbedInvite.tsx b/src/components/common/messaging/embed/EmbedInvite.tsx index 8a1ee19c..ff079fc2 100644 --- a/src/components/common/messaging/embed/EmbedInvite.tsx +++ b/src/components/common/messaging/embed/EmbedInvite.tsx @@ -1,8 +1,9 @@ -import styled from "styled-components"; - import { autorun } from "mobx"; +import { observer } from "mobx-react-lite"; import { useHistory } from "react-router-dom"; import { RetrievedInvite } from "revolt-api/types/Invites"; +import { Message } from "revolt.js/dist/maps/Messages"; +import styled from "styled-components"; import { useContext, useEffect, useState } from "preact/hooks"; @@ -10,8 +11,6 @@ import { defer } from "../../../../lib/defer"; import { dispatch } from "../../../../redux"; -import { useClient } from "../../../../context/revoltjs/RevoltClient"; - import { AppContext, ClientStatus, @@ -21,10 +20,8 @@ import { takeError } from "../../../../context/revoltjs/util"; import ServerIcon from "../../../../components/common/ServerIcon"; import Button from "../../../../components/ui/Button"; -import Overline from "../../../ui/Overline"; import Preloader from "../../../ui/Preloader"; - const EmbedInviteBase = styled.div` width: 400px; height: 80px; @@ -35,22 +32,25 @@ const EmbedInviteBase = styled.div` padding: 0 12px; margin-top: 2px; `; + const EmbedInviteDetails = styled.div` flex-grow: 1; padding-left: 12px; `; + const EmbedInviteName = styled.div` font-weight: bold; `; + const EmbedInviteMemberCount = styled.div` font-size: 0.8em; `; type Props = { - code: string -} + code: string; +}; -export default function EmbedInvite(props: Props) { +export function EmbedInvite(props: Props) { const history = useHistory(); const client = useContext(AppContext); const status = useContext(StatusContext); @@ -72,90 +72,120 @@ export default function EmbedInvite(props: Props) { .catch((err) => setError(takeError(err))); } }, [client, code, invite, status]); - + if (typeof invite === "undefined") { return error ? ( - + - - Invalid invite! - + Invalid invite! ) : ( - ) + ); } - - return - - - - {invite.server_name} - - - {invite.member_count} members - - - {processing ? ( -
- -
- ) : ( - - )} -
+ defer(() => { + if (server) { + dispatch({ + type: "UNREADS_MARK_MULTIPLE_READ", + channels: server.channel_ids, + }); + + history.push( + `/server/${server._id}/channel/${invite.channel_id}`, + ); + } + }); + + dispose(); + }); + } + + await client.joinInvite(code); + setProcessing(false); + } catch (err) { + setError(takeError(err)); + setProcessing(false); + } + }}> + {client.servers.get(invite.server_id) ? "Joined" : "Join"} + + )} + + ); } + +const INVITE_PATHS = [ + `${location.hostname}/invite`, + "app.revolt.chat/invite", + "nightly.revolt.chat/invite", + "local.revolt.chat/invite", + "rvlt.gg", +]; + +const RE_INVITE = new RegExp( + `(?:${INVITE_PATHS.map((x) => x.replaceAll(".", "\\.")).join( + "|", + )})/([A-Za-z0-9]*)`, + "g", +); + +export default observer(({ message }: { message: Message }) => { + if (typeof message.content !== "string") return null; + const matches = [...message.content.matchAll(RE_INVITE)]; + + if (matches.length > 0) { + const entries = [ + ...new Set(matches.slice(0, 5).map((x) => x[1])), + ].slice(0, 5); + + return ( + <> + {entries.map((entry) => ( + + ))} + + ); + } + + return null; +}); diff --git a/src/components/markdown/Renderer.tsx b/src/components/markdown/Renderer.tsx index 5e84d2c1..d484cc2d 100644 --- a/src/components/markdown/Renderer.tsx +++ b/src/components/markdown/Renderer.tsx @@ -17,6 +17,7 @@ import styles from "./Markdown.module.scss"; import { useCallback, useContext } from "preact/hooks"; import { internalEmit } from "../../lib/eventEmitter"; +import { determineLink } from "../../lib/links"; import { getState } from "../../redux"; @@ -35,13 +36,6 @@ declare global { } } -const ALLOWED_ORIGINS = [ - location.hostname, - "app.revolt.chat", - "nightly.revolt.chat", - "local.revolt.chat", -]; - // Handler for code block copy. if (typeof window !== "undefined") { window.copycode = function (element: HTMLDivElement) { @@ -100,7 +94,7 @@ const RE_CHANNELS = /<#([A-z0-9]{26})>/g; export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) { const client = useContext(AppContext); - const { openScreen } = useIntermediate(); + const { openLink } = useIntermediate(); if (typeof content === "undefined") return null; if (content.length === 0) return null; @@ -142,24 +136,15 @@ export default function Renderer({ content, disallowBigEmoji }: MarkdownProps) { } }, []); - const handleLink = useCallback((ev: MouseEvent) => { - if (ev.currentTarget) { - const element = ev.currentTarget as HTMLAnchorElement; - const url = new URL(element.href, location.href); - const pathname = url.pathname; - - if (pathname.startsWith("/@")) { - const id = pathname.substr(2); - if (/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}/.test(id)) { - ev.preventDefault(); - internalEmit("Intermediate", "openProfile", id); - } - } else { - ev.preventDefault(); - internalEmit("Intermediate", "navigate", pathname); + const handleLink = useCallback( + (ev: MouseEvent) => { + if (ev.currentTarget) { + const element = ev.currentTarget as HTMLAnchorElement; + if (openLink(element.href)) ev.preventDefault(); } - } - }, []); + }, + [openLink], + ); return ( ("a").forEach( (element) => { element.removeEventListener("click", handleLink); + element.addEventListener("click", handleLink); element.removeAttribute("data-type"); element.removeAttribute("target"); - let internal, - url: URL | null = null; - const href = element.href; - if (href) { - try { - url = new URL(href, location.href); - - if ( - ALLOWED_ORIGINS.includes(url.hostname) - ) { - internal = true; - element.addEventListener( - "click", - handleLink, - ); - - if (url.pathname.startsWith("/@")) { - element.setAttribute( - "data-type", - "mention", - ); - } - } - } catch (err) {} - } - - if (!internal) { - element.setAttribute("target", "_blank"); - element.onclick = (ev) => { - const { trustedLinks } = getState(); - if ( - !url || - !trustedLinks.domains?.includes( - url.hostname, - ) - ) { - ev.preventDefault(); - openScreen({ - id: "external_link_prompt", - link: href, - }); - } - }; + const link = determineLink(element.href); + switch (link.type) { + case "profile": { + element.setAttribute( + "data-type", + "mention", + ); + break; + } + case "external": { + element.setAttribute("target", "_blank"); + break; + } } }, ); diff --git a/src/context/intermediate/Intermediate.tsx b/src/context/intermediate/Intermediate.tsx index 05211969..b2fb4d6d 100644 --- a/src/context/intermediate/Intermediate.tsx +++ b/src/context/intermediate/Intermediate.tsx @@ -1,6 +1,7 @@ import { Prompt } from "react-router"; import { useHistory } from "react-router-dom"; import type { Attachment } from "revolt-api/types/Autumn"; +import { Bot } from "revolt-api/types/Bots"; import type { EmbedImage } from "revolt-api/types/January"; import { Channel } from "revolt.js/dist/maps/Channels"; import { Message } from "revolt.js/dist/maps/Messages"; @@ -11,12 +12,14 @@ import { createContext } from "preact"; import { useContext, useEffect, useMemo, useState } from "preact/hooks"; import { internalSubscribe } from "../../lib/eventEmitter"; +import { determineLink } from "../../lib/links"; + +import { getState } from "../../redux"; import { Action } from "../../components/ui/Modal"; import { Children } from "../../types/Preact"; import Modals from "./Modals"; -import { Bot } from "revolt-api/types/Bots"; export type Screen = | { id: "none" } @@ -103,9 +106,11 @@ export const IntermediateContext = createContext({ }); export const IntermediateActionsContext = createContext<{ + openLink: (href?: string) => boolean; openScreen: (screen: Screen) => void; writeClipboard: (text: string) => void; }>({ + openLink: null!, openScreen: null!, writeClipboard: null!, }); @@ -125,6 +130,37 @@ export default function Intermediate(props: Props) { const actions = useMemo(() => { return { + openLink: (href?: string) => { + 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": { + const { trustedLinks } = getState(); + if ( + !trustedLinks.domains?.includes(link.url.hostname) + ) { + openScreen({ + id: "external_link_prompt", + link: link.href, + }); + return true; + } + + return false; + } + default: { + return true; + } + } + }, openScreen: (screen: Screen) => openScreen(screen), writeClipboard: (text: string) => { if (navigator.clipboard) { @@ -134,6 +170,7 @@ export default function Intermediate(props: Props) { } }, }; + // eslint-disable-next-line }, []); useEffect(() => { diff --git a/src/lib/links.ts b/src/lib/links.ts new file mode 100644 index 00000000..d915a43c --- /dev/null +++ b/src/lib/links.ts @@ -0,0 +1,43 @@ +type LinkType = + | { type: "profile"; id: string } + | { type: "navigate"; path: string } + | { type: "external"; href: string; url: URL } + | { type: "none" }; + +const ALLOWED_ORIGINS = [ + location.hostname, + "app.revolt.chat", + "nightly.revolt.chat", + "local.revolt.chat", +]; + +export function determineLink(href?: string): LinkType { + let internal, + url: URL | null = null; + + if (href) { + try { + url = new URL(href, location.href); + + if (ALLOWED_ORIGINS.includes(url.hostname)) { + const path = url.pathname; + if (path.startsWith("/@")) { + const id = path.substr(2); + if (/[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}/.test(id)) { + return { type: "profile", id }; + } + } else { + return { type: "navigate", path }; + } + + internal = true; + } + } catch (err) {} + + if (!internal && url) { + return { type: "external", href, url }; + } + } + + return { type: "none" }; +} diff --git a/src/lib/renderer/simple/SimpleRenderer.ts b/src/lib/renderer/simple/SimpleRenderer.ts index 02153003..cd7d2b81 100644 --- a/src/lib/renderer/simple/SimpleRenderer.ts +++ b/src/lib/renderer/simple/SimpleRenderer.ts @@ -73,7 +73,6 @@ export const SimpleRenderer: RendererRoutines = { }); }, edit: async (renderer) => { - console.log("EDIT!!"); renderer.emitScroll({ type: "StayAtBottom", smooth: false,