166 lines
4.2 KiB
TypeScript
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,
|
|
},
|
|
};
|
|
};
|