Fix WhoReacted crash & future proof some plugins

This commit is contained in:
Vendicated 2026-05-05 10:41:42 +02:00
parent 8cfb152684
commit 923719b56a
No known key found for this signature in database
GPG key ID: D66986BAF75ECF18
13 changed files with 121 additions and 122 deletions

View file

@ -17,6 +17,7 @@
*/
import ErrorBoundary from "@components/ErrorBoundary";
import { Logger } from "@utils/Logger";
import { JSX, ReactNode } from "react";
export type MessageAccessoryFactory = (props: Record<string, any>) => ReactNode;
@ -46,23 +47,28 @@ export function _modifyAccessories(
elements: JSX.Element[],
props: Record<string, any>
) {
for (const [key, accessory] of accessories.entries()) {
const res = (
<ErrorBoundary noop message={`Failed to render ${key} Message Accessory`} key={key}>
<accessory.render {...props} />
</ErrorBoundary>
);
try {
for (const [key, accessory] of accessories.entries()) {
const res = (
<ErrorBoundary noop message={`Failed to render ${key} Message Accessory`} key={key}>
<accessory.render {...props} />
</ErrorBoundary>
);
elements.splice(
accessory.position != null
? accessory.position < 0
? elements.length + accessory.position
: accessory.position
: elements.length,
0,
res
);
elements.splice(
accessory.position != null
? accessory.position < 0
? elements.length + accessory.position
: accessory.position
: elements.length,
0,
res
);
}
return elements;
} catch (e) {
new Logger("MessageAccessories").error("Failed to modify message accessories", e);
return elements;
}
return elements;
}

View file

@ -28,7 +28,7 @@ export default definePlugin({
find: "#{intl::REMOVE_ATTACHMENT_BODY}",
replacement: {
match: /children:(\[[^\]]{0,100}?this.renderSuppressConfirmModal[^\]]{0,100}?\])/,
replace: "children:Vencord.Api.MessageAccessories._modifyAccessories($1,this.props)",
replace: "children:Vencord.Api.MessageAccessories._modifyAccessories($1,this?.props)",
},
},
],

View file

@ -60,7 +60,7 @@ export default definePlugin({
replacement: [
{
match: /(\.renderReactions\(\i\).+?className:)/,
replace: '$&(this.props?.channel?.nsfw?"vc-nsfw-img ":"")+'
replace: '$&(this?.props?.channel?.nsfw?"vc-nsfw-img ":"")+'
}
]
}

View file

@ -66,6 +66,8 @@ function formatDuration(ms: number) {
return res;
}
export default definePlugin({
name: "CallTimer",
description: "Adds a timer to vcs",
@ -82,21 +84,17 @@ export default definePlugin({
replacement: {
// in renderConnectionStatus()
match: /(renderConnectionStatus\(\).{0,1000}?lineClamp:1,children:)(\i)(?=,|}\))/,
replace: "$1[$2,$self.renderTimer(this.props?.channel?.id)]"
replace: "$1[$2,$self.renderTimer({ channelId: this?.props?.channel?.id })]"
}
}],
renderTimer(channelId: string) {
return <ErrorBoundary noop>
<this.Timer channelId={channelId} />
</ErrorBoundary>;
},
renderTimer: ErrorBoundary.wrap(({ channelId }: { channelId: string; }) => {
const time = useTimer({ deps: [channelId] });
Timer({ channelId }: { channelId: string; }) {
const time = useTimer({
deps: [channelId]
});
return <p style={{ margin: 0, fontFamily: "var(--font-code)" }}>{formatDuration(time)}</p>;
}
return (
<p style={{ margin: 0, fontFamily: "var(--font-code)" }}>
{formatDuration(time)}
</p>
);
}, { noop: true }),
});

View file

@ -94,7 +94,7 @@ export default definePlugin({
// https://regex101.com/r/07gpzP/1
// ($1 renderHeaderContent=function { ... switch (x) ... case FAVORITES:return) ($2) ($3 case default: ... return r.jsx(($<searchComp>), {...props}))
match: /(renderHeaderContent\(\).{1,150}FAVORITES:return)(.{1,150});(case.{1,200}default:.{0,50}?return\(0,\i\.jsx\)\((?<searchComp>\i\.\i),)/,
replace: "$1 this.state.resultType === 'Favorites' ? $self.renderSearchBar(this, $<searchComp>) : $2;$3"
replace: "$1 this?.state?.resultType === 'Favorites' ? $self.renderSearchBar(this, $<searchComp>) : $2;$3"
},
{
// to persist filtered favorites when component re-renders.

View file

@ -33,7 +33,7 @@ export default definePlugin({
find: ".handleImageLoad)",
replacement: {
match: /getSrc\(\i\)\{/,
replace: "$&var _vcSrc=$self.getSrc(this.props,arguments[1]);if(_vcSrc)return _vcSrc;"
replace: "$&var _vcSrc=$self.getSrc(this?.props,arguments[1]);if(_vcSrc)return _vcSrc;"
}
}
],

View file

@ -31,7 +31,7 @@ export default definePlugin({
find: "handleSelectGIF=",
replacement: {
match: /handleSelectGIF=(\i)=>\{/,
replace: "$&if (!this.props.className) return $self.handleSelect($1);"
replace: "$&if (!this?.props?.className) return $self.handleSelect($1);"
}
}],

View file

@ -30,7 +30,7 @@ export default definePlugin({
find: "hasThemeColors(){",
replacement: {
match: /get canUsePremiumProfileCustomization\(\){return /,
replace: "$&$self.isCurrentUser(this.userId)&&"
replace: "$&$self.isCurrentUser(this?.userId)&&"
}
},
],

View file

@ -250,9 +250,6 @@ function RoleContextMenu({ guild, roleId, onClose }: { guild: Guild; roleId: str
label={getIntlMessage("VIEW_AS_ROLE")}
icon={ViewAsRoleIcon}
action={() => {
const role = GuildRoleStore.getRole(guild.id, roleId);
if (!role) return;
onClose();
FluxDispatcher.dispatch({
type: "IMPERSONATE_UPDATE",

View file

@ -114,7 +114,7 @@ export default definePlugin({
{
// Override scrollToChannel to properly account for pinned channels
match: /(?<=scrollTo\(\{to:\i\}\):\(\i\+=)(\d+)\*\(.+?(?=,)/,
replace: "$self.getScrollOffset(arguments[0],$1,this.props.padding,this.state.preRenderedChildren,$&)"
replace: "$self.getScrollOffset(arguments[0],$1,this?.props?.padding,this?.state?.preRenderedChildren,$&)"
},
{
match: /(scrollToChannel\(\i\){.{1,300})(this\.props\.privateChannelIds)/,

View file

@ -139,7 +139,7 @@ export default definePlugin({
"renderInviteButton",
].map(func => ({
match: new RegExp(`(?<=${func}\\(\\){)`, "g"), // Global because Discord has multiple declarations of the same functions
replace: "if($self.isHiddenChannel(this.props.channel))return null;"
replace: "if($self.isHiddenChannel(this?.props?.channel))return null;"
}))
]
},
@ -149,7 +149,7 @@ export default definePlugin({
// Render null instead of the buttons if the channel is hidden
replacement: {
match: /(?<=renderOpenChatButton(?:",|=)\(\)=>{)/,
replace: "if($self.isHiddenChannel(this.props.channel))return null;"
replace: "if($self.isHiddenChannel(this?.props?.channel))return null;"
}
},
{
@ -245,11 +245,11 @@ export default definePlugin({
},
{
match: /(?<=renderSidebar\(\){)/,
replace: "if($self.isHiddenChannel(this.props.channel))return null;"
replace: "if($self.isHiddenChannel(this?.props?.channel))return null;"
},
{
match: /(?<=renderChat\(\){)/,
replace: "if($self.isHiddenChannel(this.props.channel))return $self.HiddenChannelLockScreen(this.props.channel);"
replace: "if($self.isHiddenChannel(this?.props?.channel))return $self.HiddenChannelLockScreen(this?.props?.channel);"
}
]
},
@ -355,22 +355,22 @@ export default definePlugin({
{
// Render our HiddenChannelLockScreen component instead of the main voice channel component
match: /renderContent\(\i\){.+?this\.renderVoiceChannelEffects.+?children:/,
replace: "$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel,true)?$self.HiddenChannelLockScreen(this.props.channel):"
replace: "$&!this?.props?.inCall&&$self.isHiddenChannel(this?.props?.channel,true)?$self.HiddenChannelLockScreen(this?.props?.channel):"
},
{
// Disable gradients for the HiddenChannelLockScreen of voice channels
match: /renderContent\(\i\){.+?disableGradients:/,
replace: "$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel,true)||"
replace: "$&!this?.props?.inCall&&$self.isHiddenChannel(this?.props?.channel,true)||"
},
{
// Disable useless components for the HiddenChannelLockScreen of voice channels
match: /(?:{|,)render(?!Header|ExternalHeader).{0,30}?:/g,
replace: "$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel,true)?()=>null:"
replace: "$&!this?.props?.inCall&&$self.isHiddenChannel(this?.props?.channel,true)?()=>null:"
},
{
// Disable bad CSS class which mess up hidden voice channels styling
match: /(?=\i\|\|\i!==\i\.\i\.FULL_SCREEN.{0,100}?this\._callContainerRef)/,
replace: '$&!this.props.inCall&&$self.isHiddenChannel(this.props.channel,true)?"":'
replace: '$&!this?.props?.inCall&&$self.isHiddenChannel(this?.props?.channel,true)?"":'
}
]
},

View file

@ -24,7 +24,7 @@ export default definePlugin({
find: ".tabBarRef",
replacement: {
match: /style:this\.getStyle\(\),role:"tab"/,
replace: "$&,'data-tab-id':this.props.id"
replace: "$&,'data-tab-id':this?.props?.id"
}
},
@ -59,7 +59,7 @@ export default definePlugin({
],
getAvatarStyles(src: string | null) {
if (!src || src.startsWith("data:")) return {};
if (typeof src !== "string" || src.startsWith("data:")) return {};
return Object.fromEntries(
[128, 256, 512, 1024, 2048, 4096].map(size => [

View file

@ -25,6 +25,29 @@ import definePlugin from "@utils/types";
import { CustomEmoji, Message, ReactionEmoji, User } from "@vencord/discord-types";
import { ChannelStore, Constants, FluxDispatcher, React, RestAPI, useEffect, useLayoutEffect, UserStore, UserSummaryItem } from "@webpack/common";
interface ReactionCacheEntry {
fetched: boolean;
users: Map<string, User>;
}
interface ReactionProps {
message: Message;
readOnly: boolean;
isLurking: boolean;
isPendingMember: boolean;
useChatFontScaling: boolean;
emoji: CustomEmoji;
count: number;
burst_user_ids: any[];
burst_count: number;
burst_colors: any[];
burst_me: boolean;
me: boolean;
type: number;
hideEmoji: boolean;
remainingBurstCurrency: number;
}
let Scroll: any = null;
const queue = new Queue();
let reactions: Record<string, ReactionCacheEntry> = {};
@ -75,6 +98,46 @@ function handleClickAvatar(event: React.UIEvent<HTMLElement, Event>) {
event.stopPropagation();
}
function ReactionUsers({ message, emoji, type }: ReactionProps) {
const forceUpdate = useForceUpdater();
useLayoutEffect(() => { // bc need to prevent autoscrolling
if (Scroll?.scrollCounter > 0) {
Scroll.setAutomaticAnchor(null);
}
});
useEffect(() => {
const cb = (e: any) => {
if (e?.messageId === message.id)
forceUpdate();
};
FluxDispatcher.subscribe("MESSAGE_REACTION_ADD_USERS", cb);
return () => FluxDispatcher.unsubscribe("MESSAGE_REACTION_ADD_USERS", cb);
}, [message.id, forceUpdate]);
const reactions = getReactionsWithQueue(message, emoji, type);
const users = Array.from(reactions, ([id]) => UserStore.getUser(id)).filter(Boolean);
return (
<div
style={{ marginLeft: "0.5em", transform: "scale(0.9)" }}
>
<div onClick={handleClickAvatar} onKeyDown={handleClickAvatar}>
<UserSummaryItem
users={users}
guildId={ChannelStore.getChannel(message.channel_id)?.guild_id}
renderIcon={false}
max={5}
showDefaultAvatarsForNullUsers
showUserPopout
/>
</div>
</div>
);
}
export default definePlugin({
name: "WhoReacted",
description: "Renders the avatars of users who reacted to a message",
@ -86,7 +149,7 @@ export default definePlugin({
find: ",reactionRef:",
replacement: {
match: /(\i)\?null:\(0,\i\.jsx\)\(\i\.\i,{className:\i\.reactionCount,.*?}\),/,
replace: "$&$1?null:$self.renderUsers(this.props),"
replace: "$&$1?null:$self.renderUsers(this?.props),"
}
},
{
@ -106,82 +169,17 @@ export default definePlugin({
}
],
renderUsers: ErrorBoundary.wrap((props: ReactionProps) => {
return props.message.reactions.length > 10
? null
: <ReactionUsers {...props} />;
}, { noop: true }),
setScrollObj(scroll: any) {
Scroll = scroll;
},
renderUsers(props: RootObject) {
return props.message.reactions.length > 10 ? null : (
<ErrorBoundary noop>
<this.UsersComponent {...props} />
</ErrorBoundary>
);
},
UsersComponent({ message, emoji, type }: RootObject) {
const forceUpdate = useForceUpdater();
useLayoutEffect(() => { // bc need to prevent autoscrolling
if (Scroll?.scrollCounter > 0) {
Scroll.setAutomaticAnchor(null);
}
});
useEffect(() => {
const cb = (e: any) => {
if (e?.messageId === message.id)
forceUpdate();
};
FluxDispatcher.subscribe("MESSAGE_REACTION_ADD_USERS", cb);
return () => FluxDispatcher.unsubscribe("MESSAGE_REACTION_ADD_USERS", cb);
}, [message.id, forceUpdate]);
const reactions = getReactionsWithQueue(message, emoji, type);
const users = Array.from(reactions, ([id]) => UserStore.getUser(id)).filter(Boolean);
return (
<div
style={{ marginLeft: "0.5em", transform: "scale(0.9)" }}
>
<div onClick={handleClickAvatar} onKeyDown={handleClickAvatar}>
<UserSummaryItem
users={users}
guildId={ChannelStore.getChannel(message.channel_id)?.guild_id}
renderIcon={false}
max={5}
showDefaultAvatarsForNullUsers
showUserPopout
/>
</div>
</div>
);
},
set reactions(value: any) {
reactions = value;
}
});
interface ReactionCacheEntry {
fetched: boolean;
users: Map<string, User>;
}
interface RootObject {
message: Message;
readOnly: boolean;
isLurking: boolean;
isPendingMember: boolean;
useChatFontScaling: boolean;
emoji: CustomEmoji;
count: number;
burst_user_ids: any[];
burst_count: number;
burst_colors: any[];
burst_me: boolean;
me: boolean;
type: number;
hideEmoji: boolean;
remainingBurstCurrency: number;
}