BetterForwards overhaul (#1184)

* BetterForwards: overhaul

* BetterForwards: fix formatting

* BetterForwards: fix embed counter

* Update src/equicordplugins/betterForwards/components.tsx

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* BetterForwards: reorder imports

* BetterForwards: non-null assertion

* BetterForwards: fix footer rendering

* BetterForwards: Improve forward UI

* BetterForwards: Various bug fixes

* BetterForwards: Minor changes

* BetterForwards: Minor changes

* BetterForwards: Improved message formatting

* BetterForwards: nah actually delete the readme, no one reads this anyway

* BetterForwards: Fix styles

* Update components.tsx

* BetterForwards: Errorboundary

* BetterForwards: Another errorboundary

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: thororen <78185467+thororen1234@users.noreply.github.com>
This commit is contained in:
Davri 2026-06-18 00:00:36 +02:00 • committed by GitHub
parent 665e43f048
commit e3277bfea2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 597 additions and 136 deletions

View file

@ -1,13 +0,0 @@
# Better Forwards
# Dont Follow Forwards
Don't follow forwarded messages after sending them.
## Before
![before](https://github.com/user-attachments/assets/13187c7c-37ea-41cc-bfbc-98c9f517c69b)
## After
![after](https://github.com/user-attachments/assets/1b3972d9-fa8e-47c5-a39f-94f4963d94fa)

View file

@ -0,0 +1,247 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2026 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { BaseText } from "@components/BaseText";
import { Flex, FlexProps } from "@components/Flex";
import { RightArrow } from "@components/Icons";
import { iconsModule } from "@equicordplugins/_core/concatenatedModules";
import { MessageAttachment } from "@vencord/discord-types";
import { ChannelType } from "@vencord/discord-types/enums";
import { findByCodeLazy, findComponentByCodeLazy, findCssClassesLazy } from "@webpack";
import { ChannelStore, DateUtils, GuildStore, IconUtils, match, NavigationRouter, Popout, React, SelectedGuildStore, SnowflakeUtils, useMemo, useRef, UserStore, useStateFromStores } from "@webpack/common";
import { PropsWithChildren } from "react";
import { cl, ForwardOptionsContext, ForwardOptionsState } from ".";
type AttachmentType = "IMAGE" | "VIDEO" | "CLIP" | "AUDIO" | "VISUAL_PLACEHOLDER" | "PLAINTEXT_PREVIEW" | "OTHER" | "INVALID";
const tagClasses = findCssClassesLazy("tagList", "tagGroup", "tag");
const ServerProfileComponent = findComponentByCodeLazy("{guildProfile:", "GUILD_PROFILE");
const getAttachmentType: (attachment: MessageAttachment, inlineAttachmentMedia?: boolean) => AttachmentType = findByCodeLazy('"PLAINTEXT_PREVIEW":"OTHER"');
export function GuildName({ guildId }: { guildId: string; }) {
const guild = useStateFromStores(
[GuildStore, SelectedGuildStore],
() => {
const current = SelectedGuildStore.getGuildId();
return current !== guildId ? GuildStore.getGuild(guildId) : null;
},
[guildId]
);
const icon = useMemo(() => {
if (!guild?.icon) return null;
return IconUtils.getGuildIconURL({ id: guildId, icon: guild.icon, canAnimate: true, size: 16 });
}, [guildId, guild?.icon]);
const guildDivRef = useRef(null);
return (
guild && (
<Popout
position="top"
renderPopout={() => <ServerProfileComponent guildId={guildId} />}
targetElementRef={guildDivRef}
>
{popoutProps => (
<div ref={guildDivRef} className={cl("footer-element")} {...popoutProps}>
{icon && <img src={icon} alt={`Server icon for ${guild.name}`} className={cl("guild-icon")} />}
<BaseText size="sm" weight="medium" className={cl("footer-text")}>
{guild ? guild.name : "View server"}
</BaseText>
<RightArrow width={12} height={12} fill="var(--text-muted)" />
</div>
)}
</Popout>
)
);
}
export function ChannelName({ guildId, channelId, messageId }: { guildId?: string; channelId: string; messageId: string; }) {
const name = useStateFromStores(
[ChannelStore, UserStore],
() => {
const channel = ChannelStore.getChannel(channelId);
if (!channel) return null;
return match(channel.type)
.with(ChannelType.DM, () => {
const user = UserStore.getUser(channel.recipients[0]);
return `@${user.globalName || user.username}`;
})
.with(ChannelType.GROUP_DM, () => {
if (channel.name) return channel.name;
const users = channel.recipients.map(r => UserStore.getUser(r));
return users.map(u => u.globalName || u.username).join(", ");
})
.with(
ChannelType.ANNOUNCEMENT_THREAD,
ChannelType.PRIVATE_THREAD,
ChannelType.PUBLIC_THREAD,
() => channel.name
)
.otherwise(() => `#${channel.name}`);
},
[channelId]
);
return (
name && (
<div
className={cl("footer-element")}
onClick={() => NavigationRouter.transitionTo(`/channels/${guildId ?? "@me"}/${channelId}/${messageId}`)}
>
<BaseText size="sm" weight="medium" className={cl("footer-text")}>
{name}
</BaseText>
<RightArrow width={12} height={12} fill="var(--text-muted)" />
</div>
)
);
}
export function Timestamp({ snowflake }: { snowflake: string; }) {
const formatted = useMemo(
() => DateUtils.calendarFormat(new Date(SnowflakeUtils.extractTimestamp(snowflake))),
[snowflake]
);
return (
<div className={cl("footer-element")} style={{ pointerEvents: "none" }}>
<BaseText size="sm" weight="medium" className={cl("footer-text")}>
{formatted}
</BaseText>
</div>
);
}
export function ForwardPicker() {
const state = React.useContext(ForwardOptionsContext);
if (state.message.embeds.length + state.message.attachments.length === 0) return null;
return (
<Flex gap={12} flexDirection="column">
{state.message.attachments.length > 0 && <AttachmentPicker {...state} />}
{state.message.embeds.length > 0 && <EmbedPicker {...state} />}
</Flex>
);
}
export function EmbedPicker({ message, opts, setOpts, hasOpts, defaultOpts }: ForwardOptionsState) {
const embeds = useMemo(() => {
let id = 0;
return message.embeds.map(({ rawTitle, rawDescription, image, images = image ? [image] : [], video }, i) => {
const current = {
title: rawTitle?.trim() || rawDescription?.trim() || `Embed ${i + 1}`,
subEmbeds: [] as { id: number; name: string; isMainEmbed: boolean; }[]
};
if (images.length > 0) {
// The "main" embed is the first embed with the same url (in 99% cases), which is used for displaying embed metadata (title, description, etc).
// It's only possible to tell it apart in the raw API message source since the client groups all related embeds together.
current.subEmbeds = images.map((image, si) => ({
id: id++,
name: `${si === 0 ? "Embed + " : ""}Image ${images.length > 1 ? `${si + 1} ` : ""}(${image!.width} x ${image!.height})`,
isMainEmbed: si === 0
}));
} else if (video) {
current.subEmbeds = [{ id: id++, name: "Embed + Video", isMainEmbed: true }];
} else {
current.subEmbeds = [{ id: id++, name: "Embed", isMainEmbed: true }];
}
return current;
});
}, [message]);
const { EmbedIcon, ImageIcon } = iconsModule;
return embeds.map(({ title, subEmbeds }) => (
<Flex gap={4} flexDirection="column" key={subEmbeds[0].id}>
<BaseText
size="sm"
color="text-subtle"
className={cl("embed-name")}
style={{ opacity: !hasOpts ? 0.5 : undefined }}
>
{title}
</BaseText>
<TagContainer>
{subEmbeds.map(({ id, name, isMainEmbed }) => {
const Icon = isMainEmbed ? EmbedIcon : ImageIcon;
return (
<Tag
key={id}
id={id}
source={hasOpts ? (opts.onlyEmbedIndices ?? []) : defaultOpts.onlyEmbedIndices}
onChange={data => setOpts(prev => ({ ...prev, onlyEmbedIndices: data }))}
disabled={!hasOpts}
>
{Icon && <Icon size="xs" style={{ flexShrink: 0 }} />}
<BaseText size="sm">{name}</BaseText>
</Tag>
);
})}
</TagContainer>
</Flex>
));
}
export function AttachmentPicker({ message, opts, setOpts, hasOpts, defaultOpts }: ForwardOptionsState) {
return (
<TagContainer>
{message.attachments.map(attachment => (
<Tag
key={attachment.id}
id={attachment.id}
source={hasOpts ? opts.onlyAttachmentIds ?? [] : defaultOpts.onlyAttachmentIds}
onChange={data => setOpts(prev => ({ ...prev, onlyAttachmentIds: data }))}
disabled={!hasOpts}
>
<AttachmentIcon attachment={attachment} />
<BaseText size="sm">{attachment.filename}</BaseText>
</Tag>
))}
</TagContainer>
);
}
function TagContainer(props: FlexProps) {
return <Flex gap={8} flexWrap="wrap" className={tagClasses.tagGroup} data-layout="inline" {...props} />;
}
function Tag<T>({ id, children, source, onChange, disabled }: { id: T; source: T[]; onChange: (data: T[]) => void; disabled?: boolean; } & PropsWithChildren) {
const selected = useMemo(() => source.includes(id), [source, id]);
return (
<div
className={tagClasses.tag}
data-selection-mode="multiple"
data-selected={!disabled &&selected ? "true" : undefined}
onClick={() => onChange(selected ? source.filter(x => x !== id) : [...source, id])}
style={{ textWrap: "wrap", opacity: disabled ? .5 : undefined }}
inert={disabled}
>
{children}
</div>
);
}
const icons: Partial<Record<AttachmentType, string>> = {
IMAGE: "Image",
VIDEO: "Video",
CLIP: "Clips",
AUDIO: "Music",
PLAINTEXT_PREVIEW: "A"
};
function AttachmentIcon({ attachment }: { attachment: MessageAttachment; }) {
const Icon = useMemo(() => {
const type = getAttachmentType(attachment, true);
return iconsModule[(icons[type] ?? "ImageFile") + "Icon"];
}, [attachment]);
return Icon && <Icon size="xs" style={{ flexShrink: 0 }} />;
}

View file

@ -1,99 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings, migratePluginSettings } from "@api/Settings";
import { Devs } from "@utils/constants";
import { sendMessage } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
import { Channel, Message } from "@vencord/discord-types";
import { MouseEvent } from "react";
let ignore = false;
// Taken From Signature :)
const settings = definePluginSettings({
forwardPreface: {
description: "What should forwarded from be prefaced with",
type: OptionType.SELECT,
options: [
{ label: ">", value: ">", default: true },
{ label: "-#", value: "-#" }
]
},
dontFollowForwards: {
description: "After forwarding a single message, don't jump to it. Hold shift to ignore this behavior",
displayName: "Don't Follow Forwards",
type: OptionType.BOOLEAN,
default: false,
restartNeeded: true,
}
});
migratePluginSettings("BetterForwards", "ForwardAnywhere");
export default definePlugin({
name: "BetterForwards",
description: "If a forward fails, send it as a normal message. Also allows nsfw forwards. See settings for various other improvements to forwarding.",
tags: ["Chat", "Utility"],
authors: [Devs.thororen, Devs.sadan],
settings,
patches: [
{
find: "#{intl::MESSAGE_FORWARDING_NSFW_NOT_ALLOWED}",
replacement: {
match: /(\{if\().{0,50}(\)return.{0,25}#{intl::MESSAGE_FORWARDING_NSFW_NOT_ALLOWED})/,
replace: "$1false$2",
}
},
{
find: "#{intl::MESSAGE_ACTION_FORWARD_TO}",
replacement: {
match: /(?<=let (\i)=.{0,25}rejected.{0,25}\);)(?=.{0,25}message:(\i))/,
replace: "if ($1) return $self.sendForward($1,$2);",
}
},
{
find: "#{intl::MESSAGE_FORWARD_MESSAGE_PLACEHOLDER}",
predicate: () => settings.store.dontFollowForwards,
replacement: [
{
match: /(?<=transitionToDestination:)(1===\i\.length)(?=,|\})/,
replace: "$self.shouldTransition($1)"
},
{
// there are two useCallbacks with clearDraft in this module
// we need to anchor to the one that is used as an onClick handler
match: /((\i)=\i\.useCallback\(\()(\)=>\{)(null!=\i&&\i\.\i\.clearDraft)(?=.{1500,2000}onClick:\2)/,
replace: (_, beforeParen, _1, beforeBody, body) => `${beforeParen}vencordArg1${beforeBody}$self.setShift(vencordArg1);${body}`
}
]
}
],
sendForward(channels: Channel[], message: Message) {
const chunkSize = 5;
channels.forEach(c => {
if (message.attachments.length) {
for (let i = 0; i < message.attachments.length; i += chunkSize) {
const group = message.attachments.slice(i, i + chunkSize);
const text = `${message.content}\nAttachments:\n${group.map(a => a.url).join("\n")}\n${settings.store.forwardPreface} Forwarded from <#${message.channel_id}>`;
sendMessage(c.id, { content: text });
}
} else {
sendMessage(c.id, {
content: `${message.content}\n${settings.store.forwardPreface} Forwarded from <#${message.channel_id}>`
});
}
});
},
shouldTransition(origCond: boolean): boolean {
return ignore ? origCond : false;
},
setShift(event: MouseEvent | undefined) {
ignore = !!event?.shiftKey;
}
});

View file

@ -0,0 +1,297 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { definePluginSettings, migratePluginSettings } from "@api/Settings";
import { BaseText } from "@components/BaseText";
import ErrorBoundary from "@components/ErrorBoundary";
import { Flex } from "@components/Flex";
import { InfoIcon } from "@components/Icons";
import { Margins } from "@components/margins";
import { Devs, EquicordDevs } from "@utils/constants";
import { classNameFactory } from "@utils/css";
import { sendMessage } from "@utils/discord";
import definePlugin, { OptionType } from "@utils/types";
import { Message } from "@vencord/discord-types";
import { proxyLazyWebpack } from "@webpack";
import { ChannelActionCreators, ChannelStore, Checkbox, React, Tooltip, useMemo, useState } from "@webpack/common";
import { Dispatch, MouseEvent, ReactNode, SetStateAction } from "react";
import { ChannelName, ForwardPicker, GuildName, Timestamp } from "./components";
import managedStyle from "./style.css?managed";
export const cl = classNameFactory("vc-betterforwards-");
export interface ForwardOptions {
onlyEmbedIndices?: number[];
onlyAttachmentIds?: string[];
}
export interface ForwardOptionsState {
opts: ForwardOptions;
setOpts: Dispatch<SetStateAction<ForwardOptions>>;
defaultOpts: Required<ForwardOptions>;
hasOpts: boolean;
message: Message;
}
export const ForwardOptionsContext = proxyLazyWebpack(() =>
React.createContext<ForwardOptionsState & { message: Message }>({
opts: {},
setOpts: () => {},
defaultOpts: { onlyAttachmentIds: [], onlyEmbedIndices: [] },
hasOpts: false,
message: {} as Message
})
);
let ignore = false;
const getId = ({ id, type }: { id: string; type: string; }) => {
if (type !== "user") return id;
return (
ChannelStore.getDMFromUserId(id) ??
(ChannelActionCreators.getOrEnsurePrivateChannel(id) as Promise<string | void>)
);
};
// Taken From Signature :)
const settings = definePluginSettings({
resendOnFail: {
description: "This will attempt to resend a forwarded message if the forward fails. Could cause unintentional pings or text spam. Bypasses NSFW restrictions.",
type: OptionType.BOOLEAN,
default: true,
restartNeeded: true
},
forwardPreface: {
description: "What should the forwarded message be prefaced with",
type: OptionType.SELECT,
hidden: () => !settings.store.resendOnFail,
options: [
{ label: ">", value: ">", default: true },
{ label: "-#", value: "-#" }
]
},
dontFollowForwards: {
description: "After forwarding a single message, don't jump to it. Hold shift to ignore this behavior.",
displayName: "Don't Follow Forwards",
type: OptionType.BOOLEAN,
default: false,
restartNeeded: true
},
selfForward: {
description: "Show the current channel in the forward list popup.",
type: OptionType.BOOLEAN,
default: false,
restartNeeded: true
}
});
migratePluginSettings("BetterForwards", "ForwardAnywhere");
export default definePlugin({
name: "BetterForwards",
description: "Message forward utilities including NSFW bypass and UI improvements.",
tags: ["Chat", "Utility"],
searchTerms: ["selfForward", "betterForwardMeta"],
authors: [Devs.thororen, Devs.sadan, Devs.nin0dev, EquicordDevs.VillainsRule, EquicordDevs.davri],
settings,
managedStyle,
patches: [
{
find: "#{intl::MESSAGE_FORWARDING_NSFW_NOT_ALLOWED}",
predicate: () => settings.store.resendOnFail,
replacement: {
match: /(\{if\().{0,50}(\)return.{0,25}#{intl::MESSAGE_FORWARDING_NSFW_NOT_ALLOWED})/,
replace: "$1false$2"
}
},
{
find: "#{intl::MESSAGE_ACTION_FORWARD_TO}",
replacement: [
{
match: /(?<=hasContextMessage:null!=(\i)&&.{100,150}?let (\i)=.{0,25}rejected.{0,25}\);)(?=.{0,25}message:(\i))/,
replace: (_, additionalMessage, channels, message) =>
`if(${channels}.length>0)return await $self.sendForward(${additionalMessage},${channels},${message},__state.opts);`,
predicate: () => settings.store.resendOnFail
},
{
match: /(?<=source:\i,)\.\.\.(\i)\}=(\i),/,
replace: "__state,...$1}=$self.useProps($2),"
},
{
match: /\(0,\i\.jsx\)\(\i,\{message:\i,forwardOptions:\i,channel:\i\}\)/,
replace: "$self.renderWrapper(__state,$&)"
},
{
match: /(?<=#{intl::CHECKPOINT_2025}.{50,100}?)\i>0&&\(.{200,250}?\}\)\]\}\)/,
replace: "$self.renderForwardPicker()"
},
{
match: /(?<=transitionToDestination:)(1===\i\.length)(?=,|\})/,
replace: "$self.shouldTransition($1)",
predicate: () => settings.store.dontFollowForwards
},
{
// there are two useCallbacks with clearDraft in this module
// we need to anchor to the one that is used as an onClick handler
match: /((\i)=\i\.useCallback\(\()(\)=>\{)(null!=\i&&\i\.\i\.clearDraft)(?=.{1500,2000}onClick:\2)/,
replace: (_, beforeParen, _1, beforeBody, body) =>
`${beforeParen}vencordArg1${beforeBody}$self.setShift(vencordArg1);${body}`,
predicate: () => settings.store.dontFollowForwards
}
]
},
{
find: 'location:"ForwardFooter"',
replacement: {
match: /let{message:\i,snapshot:\i,index:\i}=(\i)/,
replace: "return $self.renderForwardFooter($1);$&"
}
},
{
find: ".getChannelHistory(),",
predicate: () => settings.store.selfForward,
replacement: {
match: /\i.id\]/,
replace: "]"
}
}
],
async sendForward(additionalMessage: string | null, channels: { id: string; type: string }[], message: Message, options: ForwardOptions) {
const contentMessage = message.messageSnapshots[0]?.message ?? message;
const newLine = `\n${settings.store.forwardPreface} `;
const prefix = `${newLine}*Forwarded from <#${message.channel_id}>*${newLine}${contentMessage.content.trim().replaceAll("\n", newLine)}`;
const suffix = additionalMessage ? `\n${additionalMessage.trim()}` : "";
const attIds = options.onlyAttachmentIds;
const attachments = attIds
? contentMessage.attachments.filter(a => attIds.includes(a.id))
: contentMessage.attachments;
const ids = (await Promise.all(channels.map(getId))).filter(Boolean) as string[];
const chunkSize = 5;
ids.forEach(id => {
if (attachments.length > 0) {
for (let i = 0; i < attachments.length; i += chunkSize) {
const group = attachments.slice(i, i + chunkSize);
let text = i === 0 ? `${prefix}${newLine}Attachments:${newLine}` : newLine;
text += `${group.map(a => a.url).join(newLine)}`;
if (i + chunkSize >= attachments.length) text += suffix;
sendMessage(id, { content: text });
}
} else {
sendMessage(id, { content: prefix + suffix });
}
});
},
shouldTransition(origCond: boolean): boolean {
return ignore ? origCond : false;
},
setShift(event: MouseEvent | undefined) {
ignore = !!event?.shiftKey;
},
renderForwardFooter({ message }: { message: Message }) {
if (!message.messageReference) return null;
const { guild_id, channel_id, message_id } = message.messageReference;
return (
<ErrorBoundary noop>
<div className={cl("footer")}>
{guild_id && <GuildName guildId={guild_id} />}
<ChannelName messageId={message_id} channelId={channel_id} guildId={guild_id} />
<Timestamp snowflake={message_id} />
</div>
</ErrorBoundary>
);
},
useProps(props: { message: Message; forwardOptions?: ForwardOptions }) {
const message = props.message.messageSnapshots[0]?.message ?? props.message;
const [opts, setOpts] = useState(() => {
if (!props.forwardOptions || !props.forwardOptions.onlyEmbedIndices)
return props.forwardOptions ?? ({} as ForwardOptions);
let id = 0;
const embedsIds = new Set(props.forwardOptions.onlyEmbedIndices as number[]);
// Discord incorrectly assumes that embed indices directly map to whole embeds, this is an attempt to fix that
const onlyEmbedIndices = message.embeds
.flatMap((e, i) => e.images?.map(() => ({ id: id++, eId: i })) ?? { id: id++, eId: i })
.filter(({ eId }) => embedsIds.has(eId))
.map(({ id }) => id);
return { ...props.forwardOptions, onlyEmbedIndices };
});
const defaultOpts = useMemo(
() => ({
onlyAttachmentIds: message.attachments.map(a => a.id),
onlyEmbedIndices: message.embeds.flatMap(e => e.images ?? [{}]).map((_, i) => i)
}),
[message]
);
const hasOpts = !!opts.onlyAttachmentIds || !!opts.onlyEmbedIndices;
const forwardOptions = useMemo(() => {
if (!hasOpts) return opts;
const fixed = { onlyAttachmentIds: [], onlyEmbedIndices: [], ...opts };
// Server-side validation can be bypassed by specifying a fake attachment id
if (fixed.onlyAttachmentIds.length + fixed.onlyEmbedIndices.length === 0) fixed.onlyAttachmentIds = ["0"];
// If the embed indices are in the incorrect order, embed metadata could get stripped out by the client
fixed.onlyEmbedIndices = fixed.onlyEmbedIndices.toSorted((a, b) => a - b);
return fixed;
}, [opts, hasOpts]);
const state = useMemo(
() => ({ message, opts, setOpts, defaultOpts, hasOpts }),
[message, opts, defaultOpts, hasOpts]
);
return { ...props, forwardOptions, __state: state };
},
renderWrapper(state: ForwardOptionsState, children: ReactNode) {
const { message, hasOpts, setOpts, defaultOpts } = state;
return (
<ErrorBoundary noop>
<ForwardOptionsContext.Provider value={state}>
{children}
{message.embeds.length + message.attachments.length > 0 && (
<Flex className={Margins.top16}>
<Checkbox value={!hasOpts} onChange={() => setOpts(!hasOpts ? defaultOpts : {})} size={20}>
<BaseText size="sm">Forward everything</BaseText>
</Checkbox>
<Tooltip text="Message text will not be forwarded when this option is disabled">
{props => <InfoIcon {...props} color="var(--text-muted)" width={20} height={20} />}
</Tooltip>
</Flex>
)}
</ForwardOptionsContext.Provider>
</ErrorBoundary>
);
},
renderForwardPicker() {
return (
<ErrorBoundary noop>
<ForwardPicker />
</ErrorBoundary>
);
}
});

View file

@ -0,0 +1,53 @@
.vc-betterforwards-footer {
margin-top: 3px;
margin-left: -2px;
.vc-betterforwards-footer-text {
font-weight: 400;
font-size: 14px;
color: var(--text-muted);
}
.vc-betterforwards-guild-icon {
margin-block: 0;
border-radius: 4px;
width: 16px;
}
.vc-betterforwards-footer-element {
&:hover {
.vc-betterforwards-footer-text {
color: var(--interactive-text-hover);
}
svg path {
fill: var(--interactive-text-hover) !important;
}
background-color: var(--interactive-background-hover);
cursor: pointer;
}
&:first-of-type {
margin-left: 0;
}
align-items: center;
align-self: flex-start;
display: inline-flex;
gap: 2px;
padding: 1px 0 1px 2px;
border-radius: 4px;
vertical-align: middle;
}
}
.vc-betterforwards-embed-name {
overflow: hidden;
text-wrap: nowrap;
text-overflow: ellipsis;
}
[class^="forwardPreview"] > [class^="contentWrapper"] {
overflow: hidden;
}

View file

@ -1,24 +0,0 @@
/*
* Vencord, a Discord client mod
* Copyright (c) 2025 Vendicated and contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { EquicordDevs } from "@utils/constants";
import definePlugin from "@utils/types";
export default definePlugin({
name: "SelfForward",
description: "Adds the current channel to the forward list popup",
tags: ["Utility"],
authors: [EquicordDevs.VillainsRule],
patches: [
{
find: ".getChannelHistory(),",
replacement: [{
match: /\i.id\]/,
replace: "]"
}]
}
]
});