Plugin

WhoReacted

Renders the avatars of users who reacted to a message

Reactions Chat Appearance
index.tsx
Download

Source

src/plugins/whoReacted/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 ErrorBoundary from "@components/ErrorBoundary";
8import { Devs } from "@utils/constants";
9import { sleep } from "@utils/misc";
10import { Queue } from "@utils/Queue";
11import { useForceUpdater } from "@utils/react";
12import definePlugin from "@utils/types";
13import { CustomEmoji, Message, ReactionEmoji, User } from "@vencord/discord-types";
14import { ChannelStore, Constants, FluxDispatcher, React, RestAPI, useEffect, useLayoutEffect, UserStore, UserSummaryItem } from "@webpack/common";
15
16interface ReactionCacheEntry {
17 fetched: boolean;
18 users: Map<string, User>;
19}
20
21interface ReactionProps {
22 message: Message;
23 emoji: CustomEmoji;
24 type: number;
25}
26
27let Scroll: any = null;
28const queue = new Queue();
29let reactions: Record<string, ReactionCacheEntry> = {};
30
31function fetchReactions(msg: Message, emoji: ReactionEmoji, type: number) {
32 const key = emoji.name + (emoji.id ? `:${emoji.id}` : "");
33 return RestAPI.get({
34 url: Constants.Endpoints.REACTIONS(msg.channel_id, msg.id, key),
35 query: {
36 limit: 100,
37 type
38 },
39 oldFormErrors: true
40 })
41 .then(res => {
42 for (const user of res.body) {
43 FluxDispatcher.dispatch({
44 type: "USER_UPDATE",
45 user
46 });
47 }
48
49 FluxDispatcher.dispatch({
50 type: "MESSAGE_REACTION_ADD_USERS",
51 channelId: msg.channel_id,
52 messageId: msg.id,
53 users: res.body,
54 emoji,
55 reactionType: type
56 });
57 })
58 .catch(console.error)
59 .finally(() => sleep(250));
60}
61
62function getReactionsWithQueue(msg: Message, e: ReactionEmoji, type: number) {
63 const key = `${msg.id}:${e.name}:${e.id ?? ""}:${type}`;
64 const cache = reactions[key] ??= { fetched: false, users: new Map() };
65 if (!cache.fetched) {
66 queue.unshift(() => fetchReactions(msg, e, type));
67 cache.fetched = true;
68 }
69
70 return cache.users;
71}
72
73function handleClickAvatar(event: React.UIEvent<HTMLElement, Event>) {
74 event.stopPropagation();
75}
76
77function ReactionUsers({ message, emoji, type }: ReactionProps) {
78 const forceUpdate = useForceUpdater();
79
80 useLayoutEffect(() => { class="ts-cmt">// bc need to prevent autoscrolling
81 if (Scroll?.scrollCounter > 0) {
82 Scroll.setAutomaticAnchor(null);
83 }
84 });
85
86 useEffect(() => {
87 const cb = (e: any) => {
88 if (e?.messageId === message.id)
89 forceUpdate();
90 };
91 FluxDispatcher.subscribe("MESSAGE_REACTION_ADD_USERS", cb);
92
93 return () => FluxDispatcher.unsubscribe("MESSAGE_REACTION_ADD_USERS", cb);
94 }, [message.id, forceUpdate]);
95
96 const reactions = getReactionsWithQueue(message, emoji, type);
97 const users = Array.from(reactions, ([id]) => UserStore.getUser(id)).filter(Boolean);
98
99 return (
100 <div
101 style={{ marginLeft: "0.5em", transform: "scale(0.9)" }}
102 >
103 <div onClick={handleClickAvatar} onKeyDown={handleClickAvatar}>
104 <UserSummaryItem
105 users={users}
106 guildId={ChannelStore.getChannel(message.channel_id)?.guild_id}
107 renderIcon={false}
108 max={5}
109 showDefaultAvatarsForNullUsers
110 showUserPopout
111 />
112 </div>
113 </div>
114 );
115}
116
117export default definePlugin({
118 name: "WhoReacted",
119 description: "Renders the avatars of users who reacted to a message",
120 tags: ["Reactions", "Chat", "Appearance"],
121 authors: [Devs.Ven, Devs.KannaDev, Devs.newwares],
122
123 patches: [
124 {
125 find: ",reactionRef:",
126 replacement: {
127 match: /(\i)\?null:\(0,\i\.jsx\)\(\i\.\i,{className:\i\.reactionCount,.*?}\),(?<=(emoji:\i,message:\i,type:\i).+?)/,
128 replace: "$&$1?null:$self.renderUsers({$2}),"
129 }
130 },
131 {
132 find: &#039;"MessageReactionsStore"&#039;,
133 replacement: {
134 match: /CONNECTION_OPEN:function\(\){(\i)={}/,
135 replace: "$&;$self.reactions=$1;"
136 }
137 },
138 {
139
140 find: "cleanAutomaticAnchor(){",
141 replacement: {
142 match: /constructor\(\i\)\{(?=.{0,100}(?:automaticAnchor|\.messages\.loadingMore))/,
143 replace: "$&$self.setScrollObj(this);"
144 }
145 }
146 ],
147
148 renderUsers: ErrorBoundary.wrap((props: ReactionProps) => {
149 return props.message.reactions.length > 10
150 ? null
151 : <ReactionUsers {...props} />;
152 }, { noop: true }),
153
154 setScrollObj(scroll: any) {
155 Scroll = scroll;
156 },
157
158 set reactions(value: any) {
159 reactions = value;
160 }
161});
162