Plugin

WhoReacted

Renders the avatars of users who reacted to a message

Reactions Chat Appearance
index.tsx
Download

Source

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