Fix WhoReacted crash & future proof some plugins
This commit is contained in:
parent
8cfb152684
commit
923719b56a
13 changed files with 121 additions and 122 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)",
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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 ":"")+'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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;"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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);"
|
||||
}
|
||||
}],
|
||||
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export default definePlugin({
|
|||
find: "hasThemeColors(){",
|
||||
replacement: {
|
||||
match: /get canUsePremiumProfileCustomization\(\){return /,
|
||||
replace: "$&$self.isCurrentUser(this.userId)&&"
|
||||
replace: "$&$self.isCurrentUser(this?.userId)&&"
|
||||
}
|
||||
},
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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)/,
|
||||
|
|
|
|||
|
|
@ -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)?"":'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 => [
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue