Plugin

MessageLinkTooltip

Like MessageLinkEmbed but without taking space

index.tsx
Download

Source

src/plugins/messageLinkTooltip/index.tsx
1/*
2 * FiveCord — a Discord client mod
3 * Copyright (c) 2025 FiveCord
4 * SPDX-License-Identifier: GPL-3.0-or-later
5 */
6
7import "./style.css";
8
9import ErrorBoundary from "@components/ErrorBoundary";
10import { Devs } from "@utils/constants";
11import { proxyLazy } from "@utils/lazy";
12import definePlugin from "@utils/types";
13import { findComponentByCodeLazy } from "@webpack";
14import { ChannelStore, Forms, MessageStore, RestAPI, Tooltip, useEffect, useState, useStateFromStores } from "@webpack/common";
15import type { ComponentType, HTMLAttributes } from "react";
16
17declare enum SpinnerTypes {
18 WANDERING_CUBES = "wanderingCubes",
19 CHASING_DOTS = "chasingDots",
20 PULSING_ELLIPSIS = "pulsingEllipsis",
21 SPINNING_CIRCLE = "spinningCircle",
22 SPINNING_CIRCLE_SIMPLE = "spinningCircleSimple",
23 LOW_MOTION = "lowMotion",
24}
25
26type Spinner = ComponentType<Omit<HTMLAttributes<HTMLDivElement>, "children"> & {
27 type?: SpinnerTypes;
28 animated?: boolean;
29 className?: string;
30 itemClassName?: string;
31 "aria-label"?: string;
32}> & {
33 Type: typeof SpinnerTypes;
34};
35
36const { Spinner } = proxyLazy(() => Forms as any as {
37 Spinner: Spinner,
38 SpinnerTypes: typeof SpinnerTypes;
39});
40
41const ChannelMessage = findComponentByCodeLazy("renderSimpleAccessories)") as ComponentType<any>;
42
43export default definePlugin({
44 name: "MessageLinkTooltip",
45 description: "Like MessageLinkEmbed but without taking space",
46 authors: [Devs.FiveCord],
47
48 patches: [
49 {
50 find: &#039;,className:"channelMention",children:[&#039;,
51 replacement: {
52 match: /(?<=\.jsxs\)\()(\i\.default)/,
53 replace: "$self.wrapComponent(arguments[0], $1)"
54 }
55 }
56 ],
57
58 wrapComponent({ messageId, channelId }, Component: ComponentType) {
59 return props => {
60 if (messageId === undefined) return <Component {...props} />;
61 return <Tooltip
62 tooltipClassName="c98-message-link-tooltip"
63 text={
64 <ErrorBoundary>
65 <MessagePreview
66 channelId={channelId}
67 messageId={messageId}
68 />
69 </ErrorBoundary>
70 }
71 >
72 {({ onMouseEnter, onMouseLeave }) =>
73 <Component
74 {...props}
75 onMouseEnter={onMouseEnter}
76 onMouseLeave={onMouseLeave}
77 />
78 }
79 </Tooltip>;
80 };
81 }
82});
83
84function MessagePreview({ channelId, messageId }) {
85 const channel = ChannelStore.getChannel(channelId);
86 const message = useMessage(channelId, messageId);
87 // TODO handle load failure
88 if (!message) {
89 return <Spinner type={Spinner.Type.PULSING_ELLIPSIS} />;
90 }
91
92 return <ChannelMessage
93 id={`message-link-tooltip-${messageId}`}
94 message={message}
95 channel={channel}
96 subscribeToComponentDispatch={false}
97 />;
98}
99
100function useMessage(channelId, messageId) {
101 const cachedMessage = useStateFromStores(
102 // @ts-ignore
103 [MessageStore],
104 () => MessageStore.getMessage(channelId, messageId)
105 );
106 const [message, setMessage] = useState(cachedMessage);
107 useEffect(() => {
108 if (message == null)
109 (async () => {
110 const res = await RestAPI.get({
111 url: `/channels/${channelId}/messages`,
112 query: {
113 limit: 1,
114 around: messageId,
115 },
116 retries: 2,
117 });
118 const rawMessage = res.body[0];
119 const message = MessageStore.getMessages(channelId)
120 .receiveMessage(rawMessage)
121 .get(messageId);
122 setMessage(message);
123 })();
124 });
125 return message;
126}
127