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

166 lines
4.2 KiB
TypeScript

import { Match, Switch, createSignal, onMount } from "solid-js";
import { Handler } from "mdast-util-to-hast";
import { cva } from "styled-system/css";
import { Plugin } from "unified";
import { visit } from "unist-util-visit";
import { useClient } from "@revolt/client";
import { Avatar, Column, Row } from "@revolt/ui";
import { CustomEmoji, Emoji, RE_CUSTOM_EMOJI } from "../emoji";
/**
* Render a custom emoji
*
* This will also display a tooltip and fallback to text if the emoji doesn't exist.
*/
export function RenderCustomEmoji(props: { id: string }) {
const [exists, setExists] = createSignal(true);
const client = useClient();
/**
* Resolve emoji
*/
const emoji = () => client()!.emojis.get(props.id);
/**
* Resolve server
*/
const server = () =>
client()!.servers.get(
(emoji()!.parent as { type: "Server"; id: string }).id,
)!;
return (
<Switch fallback={<span>{`:${emoji()?.name ?? props.id}:`}</span>}>
<Match when={exists()}>
<div
class={tooltipTrigger()}
use:floating={{
tooltip: {
placement: "top",
content: () => (
<Row align gap="lg">
<span style={{ "--emoji-size": "3em" }}>
<Emoji emoji={props.id} />
</span>
<Switch
fallback={
<>
Unknown emote
<FetchEmote id={props.id} />
</>
}
>
<Match when={emoji()?.parent.type === "Server"}>
<Column align>
<span>{`:${emoji()!.name}:`}</span>
<Switch fallback="Private Server">
<Match when={server()}>
<Row align>
<Avatar
size={14}
src={server().animatedIconURL}
/>
{server().name}
</Row>
</Match>
</Switch>
</Column>
</Match>
</Switch>
</Row>
),
aria:
emoji()?.parent.type === "Server"
? `:${emoji()!.name}: from ${
server()?.name ?? "Private Server"
}`
: "Unknown emote",
},
}}
>
<CustomEmoji id={props.id} onError={() => setExists(false)} />
</div>
</Match>
</Switch>
);
}
/**
* Container for trigger
*/
const tooltipTrigger = cva({
base: {
display: "inline-block",
},
});
/**
* Helper to fetch unknown emotes
*/
function FetchEmote(props: { id: string }) {
const client = useClient();
onMount(() => client().emojis.fetch(props.id));
return null;
}
export const remarkCustomEmoji: Plugin = () => (tree) => {
visit(
tree,
"text",
(
node: { type: "text"; value: string },
idx,
parent: { children: unknown[] },
) => {
const elements = node.value.split(RE_CUSTOM_EMOJI);
if (elements.length === 1) return; // no matches
// Generate initial node
const newNodes: (
| { type: "text"; value: string }
| {
type: "customEmoji";
id: string;
}
)[] = [
{
type: "text",
value: elements.shift()!,
},
];
// Process all timestamps
for (let i = 0; i < elements.length / 2; i++) {
// Insert components
newNodes.push({
type: "customEmoji",
id: elements[i * 2],
});
newNodes.push({
type: "text",
value: elements[i * 2 + 1],
});
}
parent.children.splice(idx, 1, ...newNodes);
return idx + newNodes.length;
},
);
};
export const customEmojiHandler: Handler = (h, node) => {
return {
type: "element" as const,
tagName: "customEmoji",
children: [],
properties: {
id: node.id,
},
};
};