Plugin
WhoReacted
Renders the avatars of users who reacted to a message
1
/*2
* FiveCord — a Discord client mod3
* Copyright (c) 2025 FiveCord4
* SPDX-License-Identifier: GPL-3.0-or-later5
*/6
7
import ErrorBoundary from "@components/ErrorBoundary";8
import { Devs } from "@utils/constants";9
import { sleep } from "@utils/misc";10
import { Queue } from "@utils/Queue";11
import { useForceUpdater } from "@utils/react";12
import definePlugin from "@utils/types";13
import { CustomEmoji, Message, ReactionEmoji, User } from "@vencord/discord-types";14
import { ChannelStore, Constants, FluxDispatcher, React, RestAPI, useEffect, useLayoutEffect, UserStore, UserSummaryItem } from "@webpack/common";15
16
interface ReactionCacheEntry {17
fetched: boolean;18
users: Map<string, User>;19
}20
21
interface ReactionProps {22
message: Message;23
emoji: CustomEmoji;24
type: number;25
}26
27
let Scroll: any = null;28
const queue = new Queue();29
let reactions: Record<string, ReactionCacheEntry> = {};30
31
function 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
type38
},39
oldFormErrors: true40
})41
.then(res => {42
for (const user of res.body) {43
FluxDispatcher.dispatch({44
type: "USER_UPDATE",45
user46
});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: type56
});57
})58
.catch(console.error)59
.finally(() => sleep(250));60
}61
62
function 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
73
function handleClickAvatar(event: React.UIEvent<HTMLElement, Event>) {74
event.stopPropagation();75
}76
77
function ReactionUsers({ message, emoji, type }: ReactionProps) {78
const forceUpdate = useForceUpdater();79
80
useLayoutEffect(() => { class="ts-cmt">// bc need to prevent autoscrolling81
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
<div101
style={{ marginLeft: "0.5em", transform: "scale(0.9)" }}102
>103
<div onClick={handleClickAvatar} onKeyDown={handleClickAvatar}>104
<UserSummaryItem105
users={users}106
guildId={ChannelStore.getChannel(message.channel_id)?.guild_id}107
renderIcon={false}108
max={5}109
showDefaultAvatarsForNullUsers110
showUserPopout111
/>112
</div>113
</div>114
);115
}116
117
export 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 > 10150
? null151
: <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