stoat-for-desktop/components/markdown/plugins/mentions.tsx

224 lines
5.6 KiB
TypeScript

import { Match, Switch } from "solid-js";
import { Handler } from "mdast-util-to-hast";
import { cva } from "styled-system/css";
import { styled } from "styled-system/jsx";
import { Plugin } from "unified";
import { visit } from "unist-util-visit";
import { UserContextMenu } from "@revolt/app";
import { useClient } from "@revolt/client";
import { useSmartParams } from "@revolt/routing";
import { Avatar, ColouredText, iconSize } from "@revolt/ui";
import MdAt from "@material-design-icons/svg/filled/alternate_email.svg?component-solid";
import { useUser } from "../users";
export function RenderMention(props: {
mentions?: string;
disabled?: boolean;
}) {
return (
<Switch fallback={<span>Invalid Mention Element</span>}>
<Match when={props.mentions?.startsWith("user:")}>
<UserMention
userId={props.mentions!.substring(5)}
disabled={props.disabled}
/>
</Match>
<Match when={props.mentions === "everyone"}>
<span class={mention()}>
<MdAt {...iconSize(16)} />
everyone
</span>
</Match>
<Match when={props.mentions === "online"}>
<span class={mention()}>
<MdAt {...iconSize(16)} />
online
</span>
</Match>
<Match when={props.mentions?.startsWith("role:")}>
<RoleMention roleId={props.mentions!.substring(5)} />
</Match>
</Switch>
);
}
export function UserMention(props: { userId: string; disabled?: boolean }) {
const user = useUser(() => props.userId);
return (
<Switch
fallback={<span class={mention({ valid: false })}>Unknown User</span>}
>
<Match when={user().user}>
<div
class={mention({ isLink: true })}
use:floating={
props.disabled
? undefined
: {
userCard: user().user
? {
user: user().user!,
member: user().member,
}
: undefined,
contextMenu: () => (
<UserContextMenu
user={user().user!}
member={user().member}
/>
),
}
}
>
<Avatar size={16} src={user().avatar} fallback={user().username} />
<ColouredText colour={user().colour!}>{user().username}</ColouredText>
</div>
</Match>
</Switch>
);
}
export function RoleMention(props: { roleId: string }) {
// some jank involved...
const client = useClient();
const params = useSmartParams();
const role = () =>
client().servers.get(params().serverId!)?.roles.get(props.roleId);
return (
<Switch
fallback={<span class={mention({ valid: false })}>Unknown Role</span>}
>
<Match when={role()}>
<div class={mention()}>
<RoleIcon
style={{
background:
role()!.colour ?? "var(--md-sys-color-outline-variant)",
}}
/>
<ColouredText colour={role()!.colour!}>{role()!.name}</ColouredText>
</div>
</Match>
</Switch>
);
}
const RoleIcon = styled("div", {
base: {
margin: "1px",
width: "14px",
height: "14px",
aspectRatio: "1/1",
borderRadius: "100%",
},
});
const RE_MENTION =
/(<@[0-9ABCDEFGHJKMNPQRSTVWXYZ]{26}>|@everyone|@online|<%[0-9ABCDEFGHJKMNPQRSTVWXYZ]{26}>)/;
export const remarkMentions: Plugin = () => (tree) => {
visit(
tree,
"text",
(
node: { type: "text"; value: string },
idx,
parent: { children: unknown[] },
) => {
const elements = node.value.split(RE_MENTION);
if (elements.length === 1) return; // no matches
const newNodes = elements.map((value, index) =>
index % 2
? value.startsWith("<@")
? {
type: "mention",
mentions: "user:" + value.substring(2, value.length - 1),
}
: value === "@everyone"
? {
type: "mention",
mentions: "everyone",
}
: value === "@online"
? {
type: "mention",
mentions: "online",
}
: {
type: "mention",
mentions: "role:" + value.substring(2, value.length - 1),
}
: {
type: "text",
value,
},
);
parent.children.splice(idx, 1, ...newNodes);
return idx + newNodes.length;
},
);
};
export const mentionHandler: Handler = (h, node) => {
return {
type: "element" as const,
tagName: "mention",
children: [],
properties: {
mentions: node.mentions,
},
};
};
export const mention = cva({
base: {
verticalAlign: "bottom",
gap: "4px",
paddingLeft: "2px",
paddingRight: "6px",
alignItems: "center",
display: "inline-flex",
fontWeight: 600,
borderRadius: "var(--borderRadius-lg)",
color: "var(--md-sys-color-on-primary-container)",
background: "var(--md-sys-color-primary-container)",
},
variants: {
isLink: {
true: {
cursor: "pointer",
},
},
valid: {
false: {
paddingLeft: "6px",
cursor: "not-allowed",
},
},
inEditor: {
true: {
color: "var(--md-sys-color-on-primary)",
background: "var(--md-sys-color-primary)",
"& img": {
width: "16px",
height: "16px",
objectFit: "cover",
borderRadius: "var(--borderRadius-full)",
},
},
},
},
});