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 ? (
-
- ) : (
-
+ )}
+
+ );
}
+
+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,