SMYN: IRCColors support and various bug fixes. (#786)

* Add IRC support and fix bugs.

* Fix DMs.

* Adjust minimum speed.

* Add mention support for IRCColors when using SMYN.
This commit is contained in:
Etorix 2026-02-17 13:23:36 -08:00 • committed by GitHub
parent 49d4090dbf
commit 566ef6cb97
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 135 additions and 73 deletions

View file

@ -13,6 +13,7 @@ import { definePluginSettings } from "@api/Settings";
import { Button, TextButton } from "@components/Button";
import ErrorBoundary from "@components/ErrorBoundary";
import { Heading } from "@components/Heading";
import ircColors from "@plugins/ircColors";
import mentionAvatars from "@plugins/mentionAvatars";
import { Devs, EquicordDevs } from "@utils/constants";
import { classNameFactory, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/index";
@ -61,6 +62,48 @@ function adjustHex(color: string, percent: number): string {
return `#${newColor}`;
}
function normalizeHexColor(color: string): string | null {
const trimmed = color.trim();
if (/^#[\da-f]{3}$/iu.test(trimmed)) {
return `#${trimmed.slice(1).split("").map(c => c + c).join("")}`;
}
if (/^#[\da-f]{6}$/iu.test(trimmed)) {
return trimmed;
}
return null;
}
function cssColorToHex(color: string): string | null {
const colorTestDiv = document.createElement("div");
colorTestDiv.style.color = color;
document.body.appendChild(colorTestDiv);
const computedColor = getComputedStyle(colorTestDiv).color;
colorTestDiv.remove();
const match = computedColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i);
if (!match) return null;
const r = Math.max(0, Math.min(255, parseInt(match[1], 10)));
const g = Math.max(0, Math.min(255, parseInt(match[2], 10)));
const b = Math.max(0, Math.min(255, parseInt(match[3], 10)));
return `#${((r << 16) | (g << 8) | b).toString(16).padStart(6, "0")}`;
}
function toHexColor(color: string | number | null | undefined): string | null {
if (!color) return null;
if (typeof color === "number") { return `#${color.toString(16).padStart(6, "0")}`; }
const normalized = normalizeHexColor(color);
if (normalized) return normalized;
return cssColorToHex(color);
}
function validColor(color: string) {
const trimmedColor = color.trim();
@ -83,7 +126,8 @@ function validColor(color: string) {
}
function resolveColor(
user: User | GuildMember,
colorStrings: colorStringsType,
displayNameStyles: { effectId: number; colors: number[]; } | null | undefined,
savedColor: string,
canUseGradient: boolean,
inGuild: boolean,
@ -92,44 +136,26 @@ function resolveColor(
if (!savedColor.trim()) { return null; }
let gradient: any = null;
let primaryColor: any = savedColor;
let secondaryColor: any = savedColor;
let tertiaryColor: any = savedColor;
let primaryAdjusted: any = savedColor;
let secondaryAdjusted: any = savedColor;
let tertiaryAdjusted: any = savedColor;
let primaryColor: any = null;
let secondaryColor: any = null;
let tertiaryColor: any = null;
let primaryAdjusted: any = null;
let secondaryAdjusted: any = null;
let tertiaryAdjusted: any = null;
if (savedColor.toLowerCase().includes("role")) {
const percentage = roleColorPattern.exec(savedColor)?.[1] || "";
if (percentage && isNaN(parseInt(percentage))) return null;
const colorStrings = (user as any)?.colorStrings || {};
const displayStyles = !inGuild ? (user as any)?.displayNameStyles || {} : {};
primaryColor = forceWhite ? "#ffffff" : (toHexColor(colorStrings?.primaryColor) || (!inGuild && toHexColor(displayNameStyles?.colors?.[0])) || "#ffffff");
secondaryColor = forceWhite ? null : (toHexColor(colorStrings?.secondaryColor) || (!inGuild && toHexColor(displayNameStyles?.colors?.[1])) || null);
tertiaryColor = forceWhite ? null : (toHexColor(colorStrings?.tertiaryColor) || (!inGuild && toHexColor(displayNameStyles?.colors?.[2])) || null);
primaryColor = forceWhite ? "#ffffff" : colorStrings.primaryColor || displayStyles?.colors?.[0] || "#ffffff";
if (typeof primaryColor === "number") primaryColor = `#${primaryColor.toString(16).padStart(6, "0")}`;
secondaryColor = forceWhite ? null : colorStrings.secondaryColor || displayStyles?.colors?.[1] || null;
if (typeof secondaryColor === "number") secondaryColor = `#${secondaryColor.toString(16).padStart(6, "0")}`;
tertiaryColor = forceWhite ? null : colorStrings.tertiaryColor || displayStyles?.colors?.[2] || null;
if (typeof tertiaryColor === "number") tertiaryColor = `#${tertiaryColor.toString(16).padStart(6, "0")}`;
primaryAdjusted = primaryColor;
secondaryAdjusted = secondaryColor;
tertiaryAdjusted = tertiaryColor;
if (primaryColor && percentage) {
primaryAdjusted = adjustHex(primaryColor, parseInt(percentage));
}
if (secondaryColor && percentage) {
secondaryAdjusted = adjustHex(secondaryColor, parseInt(percentage));
}
if (tertiaryColor && percentage) {
tertiaryAdjusted = adjustHex(tertiaryColor, parseInt(percentage));
}
primaryAdjusted = percentage ? adjustHex(primaryColor, parseInt(percentage)) : primaryColor;
secondaryAdjusted = secondaryColor && percentage ? adjustHex(secondaryColor, parseInt(percentage)) : secondaryColor;
tertiaryAdjusted = tertiaryColor && percentage ? adjustHex(tertiaryColor, parseInt(percentage)) : tertiaryColor;
} else {
primaryColor = savedColor;
}
gradient = !canUseGradient || !secondaryColor || forceWhite
@ -164,7 +190,7 @@ function resolveColor(
"--custom-gradient-color-2": secondaryColor || primaryColor,
"--custom-gradient-color-3": tertiaryColor || primaryColor,
"background-image": gradient,
"animation": "smyn-animation 1.5s linear infinite"
"animation": "smyn-animation var(--smyn-gradient-duration) linear infinite"
},
static: {
original: {
@ -300,6 +326,8 @@ interface mentionProps {
interface messageProps {
message: Message | null | undefined;
colorString?: string;
colorStrings: colorStringsType;
userOverride?: User;
isRepliedMessage?: boolean;
withMentionPrefix?: boolean;
@ -312,6 +340,8 @@ interface memberListProfileReactionProps {
tags?: any;
}
type colorStringsType = { primaryColor: string | null, secondaryColor: string | null, tertiaryColor: string | null; } | null | undefined;
function getMemberListProfilesReactionsVoiceName(
props: memberListProfileReactionProps,
): [string | null, JSX.Element | null, string | null] {
@ -343,7 +373,7 @@ function getMessageName(props: messageProps): [string | null, JSX.Element | null
const member = isWebhook ? null : target && channel ? GuildMemberStore.getMember(channel.guild_id, target.id) : null;
const author = user && member ? { ...user, ...member } : user || member || null;
const mentionSymbol = hideDefaultAtSign && (!isRepliedMessage || replies) ? "" : withMentionPrefix ? "@" : "";
return renderUsername(author, channel?.id || null, message?.id || null, isRepliedMessage ? "replies" : "messages", mentionSymbol, false, !!channel?.guild_id);
return renderUsername(author, channel?.id || null, message?.id || null, isRepliedMessage ? "replies" : "messages", mentionSymbol, false, !!channel?.guild_id, props.colorString, props.colorStrings);
}
function getMessageNameElement(props: messageProps): JSX.Element | null {
@ -362,7 +392,20 @@ function getMentionNameElement(props: mentionProps): JSX.Element | null {
const member = channel ? GuildMemberStore.getMember(channel.guild_id, userId) : null;
const author = user && member ? { ...user, ...member } : user || member || null;
const mentionSymbol = hideDefaultAtSign && mentions ? "" : "@";
return renderUsername(author, channelId || null, nestedProps?.messageId || null, "mentions", mentionSymbol, false, !!channel?.guild_id)[1];
let colorString: string | undefined = undefined;
let colorStrings: colorStringsType = undefined;
if (isPluginEnabled(ircColors.name)) {
const color = ircColors.calculateNameColorForMessageContext({ message: { author: { id: userId } }, channel });
if (color) {
colorString = color;
colorStrings = { primaryColor: color, secondaryColor: null, tertiaryColor: null };
}
}
return renderUsername(author, channelId || null, nestedProps?.messageId || null, "mentions", mentionSymbol, false, !!channel?.guild_id, colorString, colorStrings)[1];
}
function getEffectType(effectId: number | null | undefined): string | null {
@ -408,7 +451,9 @@ function renderUsername(
type: "messages" | "replies" | "mentions" | "membersList" | "profilesPopout" | "profilesTooltip" | "reactionsTooltip" | "reactionsPopout" | "voiceChannel",
mentionSymbol: string,
hookless: boolean,
inGuild: boolean
inGuild: boolean,
colorString?: string,
colorStrings?: { primaryColor: string | null, secondaryColor: string | null, tertiaryColor: string | null; } | null
): [string | null, JSX.Element | null, string | null] {
const isMessage = type === "messages";
const isReply = type === "replies";
@ -435,27 +480,38 @@ function renderUsername(
? hoveringReactionPopoutSet.has((author as User).id)
: false;
const authorDisplayNameStyles = (!inGuild && (author as any)?.displayNameStyles) || null;
if (colorString && !colorStrings) {
colorStrings = {
primaryColor: colorString,
secondaryColor: null,
tertiaryColor: null
};
}
const ircColorsEnabled = isPluginEnabled(ircColors.name);
const authorColorStrings = colorStrings || (author as any)?.colorStrings || null;
const authorDisplayNameStyles = (!inGuild && !ircColorsEnabled && (author as any)?.displayNameStyles) || null;
const effectType = authorDisplayNameStyles ? getEffectType(authorDisplayNameStyles.effectId) : null;
const effectCSSVars = authorDisplayNameStyles ? computeEffectCSSVars(authorDisplayNameStyles) : {};
const hasEffect = !!effectType;
const needsEffectDataAttr = effectType === "neon" || effectType === "toon" || effectType === "pop";
const shouldShowEffect = hasEffect && isHovering;
const shouldAnimateEffect = shouldShowEffect && !AccessibilityStore.useReducedMotion;
const shouldUseDMDefault = !inGuild && !isHovering;
const shouldUseDMDefault = !inGuild && !ircColorsEnabled && !isHovering;
const canUseGradient = ((author as GuildMember)?.guildId ? (GuildStore.getGuild((author as GuildMember).guildId) ?? {}).premiumFeatures?.features.includes("ENHANCED_ROLE_COLORS") : !inGuild);
const useTopRoleStyle = isMention || isReactionsPopout || channel?.isDM() || channel?.isGroupDM();
const topRoleStyle = author ? resolveColor(author, "Role", canUseGradient, inGuild, shouldUseDMDefault) : null;
const topRoleStyle = author ? resolveColor(authorColorStrings, authorDisplayNameStyles, "Role", canUseGradient, inGuild, shouldUseDMDefault) : null;
const hasGradient = !!topRoleStyle?.gradient && Object.keys(topRoleStyle.gradient).length > 0;
const textMutedValue = getComputedStyle(document.documentElement)?.getPropertyValue("--text-muted")?.trim() || "#72767d";
const options = splitTemplate(includedNames);
const resolvedUsernameColor = author ? resolveColor(author, usernameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
const resolvedDisplayNameColor = author ? resolveColor(author, displayNameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
const resolvedNicknameColor = author ? resolveColor(author, nicknameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
const resolvedFriendNameColor = author ? resolveColor(author, friendNameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
const resolvedCustomNameColor = author ? resolveColor(author, customNameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
const resolvedUsernameColor = author ? resolveColor(authorColorStrings, authorDisplayNameStyles, usernameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
const resolvedDisplayNameColor = author ? resolveColor(authorColorStrings, authorDisplayNameStyles, displayNameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
const resolvedNicknameColor = author ? resolveColor(authorColorStrings, authorDisplayNameStyles, nicknameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
const resolvedFriendNameColor = author ? resolveColor(authorColorStrings, authorDisplayNameStyles, friendNameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
const resolvedCustomNameColor = author ? resolveColor(authorColorStrings, authorDisplayNameStyles, customNameColor.trim(), canUseGradient, inGuild, shouldUseDMDefault) : null;
const affixColor = { color: textMutedValue, "-webkit-text-fill-color": textMutedValue, isolation: "isolate", "white-space": "pre", "font-family": "var(--font-primary)", "letter-spacing": "normal" };
const { username, display, nick, friend, custom } = getProcessedNames(author, truncateAllNamesWithStreamerMode, discriminators, inGuild, friendNameOnlyInDirectMessages, customNameOnlyInDirectMessages);
@ -580,6 +636,7 @@ function renderUsername(
}
const remainingNames = [first, second, third, fourth, fifth].filter(Boolean);
first = remainingNames.shift();
second = remainingNames.shift();
third = remainingNames.shift();
@ -591,37 +648,40 @@ function renderUsername(
const shouldAnimateSecondaryNames = animateGradients && !ignoreGradients;
const firstDataText = mentionSymbol + first.name;
const secondDataText = second && shouldAnimateSecondaryNames ? (second.prefix + second.name + second.suffix) : "";
const thirdDataText = third && shouldAnimateSecondaryNames ? (third.prefix + third.name + third.suffix) : "";
const fourthDataText = fourth && shouldAnimateSecondaryNames ? (fourth.prefix + fourth.name + fourth.suffix) : "";
const fifthDataText = fifth && shouldAnimateSecondaryNames ? (fifth.prefix + fifth.name + fifth.suffix) : "";
const secondDataText = second && shouldAnimateSecondaryNames ? second.name : "";
const thirdDataText = third && shouldAnimateSecondaryNames ? third.name : "";
const fourthDataText = fourth && shouldAnimateSecondaryNames ? fourth.name : "";
const fifthDataText = fifth && shouldAnimateSecondaryNames ? fifth.name : "";
const allDataText = [firstDataText, secondDataText, thirdDataText, fourthDataText, fifthDataText].filter(Boolean).join(nameSeparator).trim();
// Only mentions and reactions popouts should patch in the gradient glow or else a double glow will appear on messages.
const hoveringClass = (isHovering ? " show-me-your-name-gradient-hovered" : "");
const hoveringClass = (isHovering ? " smyn-gradient-hovered" : "");
const gradientClasses = useTopRoleStyle
? "show-me-your-name-gradient show-me-your-name-gradient-inherit-bg" + hoveringClass
: "show-me-your-name-gradient show-me-your-name-gradient-unset-bg" + hoveringClass;
? "smyn-gradient smyn-gradient-inherit-bg" + hoveringClass
: "smyn-gradient smyn-gradient-unset-bg" + hoveringClass;
const firstGroupClasses = "show-me-your-name-name-group show-me-your-name-first-name-group";
const secondGroupClasses = "show-me-your-name-name-group show-me-your-name-second-name-group";
const thirdGroupClasses = "show-me-your-name-name-group show-me-your-name-third-name-group";
const fourthGroupClasses = "show-me-your-name-name-group show-me-your-name-fourth-name-group";
const fifthGroupClasses = "show-me-your-name-name-group show-me-your-name-fifth-name-group";
const firstNameClasses = "show-me-your-name-name show-me-your-name-first-name";
const secondNameClasses = "show-me-your-name-name show-me-your-name-second-name";
const thirdNameClasses = "show-me-your-name-name show-me-your-name-third-name";
const fourthNameClasses = "show-me-your-name-name show-me-your-name-fourth-name";
const fifthNameClasses = "show-me-your-name-name show-me-your-name-fifth-name";
const prefixClasses = "show-me-your-name-affix show-me-your-name-prefix";
const suffixClasses = "show-me-your-name-affix show-me-your-name-suffix";
const firstGroupClasses = "smyn-name-group smyn-first-name-group";
const secondGroupClasses = "smyn-name-group smyn-second-name-group";
const thirdGroupClasses = "smyn-name-group smyn-third-name-group";
const fourthGroupClasses = "smyn-name-group smyn-fourth-name-group";
const fifthGroupClasses = "smyn-name-group smyn-fifth-name-group";
const firstNameClasses = "smyn-name smyn-first-name";
const secondNameClasses = "smyn-name smyn-second-name";
const thirdNameClasses = "smyn-name smyn-third-name";
const fourthNameClasses = "smyn-name smyn-fourth-name";
const fifthNameClasses = "smyn-name smyn-fifth-name";
const prefixClasses = "smyn-affix smyn-prefix";
const suffixClasses = "smyn-affix smyn-suffix";
const animationDuration = Math.max(1, 1.5 * (first.name.length / 12));
const topLevelStyle = {
// Allows names to wrap in reaction popouts.
...(isReactionsPopout
? { display: "flex", flexWrap: "wrap", lineHeight: "1.1em", fontSize: "0.9em" }
: {}),
...(hasEffect ? effectCSSVars : {})
...(hasEffect ? effectCSSVars : {}),
"--smyn-gradient-duration": `${animationDuration}s`
} as React.CSSProperties;
const nameElement = (
@ -630,7 +690,7 @@ function renderUsername(
...topLevelStyle,
...(topRoleStyle?.normal.original || {})
}}
className="show-me-your-name-container"
className="smyn-container"
>
{mentionSymbol && <span>{mentionSymbol}</span>}
{(
@ -667,9 +727,7 @@ function renderUsername(
].map(({ name, dataText, groupClass, nameClass, position }) => name && (
<span
key={position}
className={SMYNC(groupClass, { [gradientClasses]: shouldGradientGlow && shouldAnimateSecondaryNames })}
data-text={shouldGradientGlow && dataText ? dataText : undefined}
style={(shouldGradientGlow && shouldAnimateSecondaryNames ? name.style.gradient.animated : undefined) as React.CSSProperties}
className={SMYNC(groupClass)}
>
<span style={affixColor as React.CSSProperties} className={prefixClasses}>
<span>{nameSeparator}</span>
@ -677,10 +735,12 @@ function renderUsername(
<span
// On non-primary names, allow disabling the effects completely, or just their animation & glow.
className={SMYNC(nameClass, {
[gradientClasses]: shouldGradientGlow && shouldAnimateSecondaryNames,
"smyn-effect-container": shouldShowEffect && !ignoreGradients,
[`smyn-effect-${effectType}`]: shouldShowEffect && !ignoreGradients,
"smyn-effect-animated": shouldAnimateEffect && shouldAnimateSecondaryNames
})}
data-text={shouldGradientGlow && dataText ? dataText : undefined}
data-username-with-effects={needsEffectDataAttr && shouldShowEffect && !ignoreGradients ? name.name : undefined}
style={{
...(ignoreFonts ? { "font-family": "var(--font-primary)", "letter-spacing": "normal" } : {}),
@ -814,7 +874,7 @@ function CustomNicknameModal({ modalProps, user }: { modalProps: ModalProps; use
style={{ width: "100%" }}
/>
<TextButton
className="show-me-your-name-reset-button"
className="smyn-reset-button"
onClick={async () => {
setValue("");
delete customNicknames[user.id];
@ -826,7 +886,7 @@ function CustomNicknameModal({ modalProps, user }: { modalProps: ModalProps; use
</TextButton>
<div style={{ paddingTop: "10px", flexGrow: 0 }}></div>
</ModalContent>
<ModalFooter className="show-me-your-name-modal-footer-container">
<ModalFooter className="smyn-modal-footer-container">
<Button
variant="primary"
onClick={async () => {
@ -1026,8 +1086,8 @@ export default definePlugin({
replacement: [
{
// Replace names in messages and replies.
match: /(onContextMenu:\i,children:)(.{0,250}?),"data-text":(\i\+\i)/,
replace: "$1$self.getMessageNameElement(arguments[0])??($2),\"data-text\":$self.getMessageNameText(arguments[0])??($3)"
match: /(?<=colorString:(\i),colorStrings:(\i).{0,900}?)style:\i\(\),(onClick:\i,onContextMenu:\i,children:)(.{0,250}?),"data-text":(\i\+\i)/,
replace: "$3$self.getMessageNameElement({...arguments[0],colorString:$1,colorStrings:$2})??($4),\"data-text\":$self.getMessageNameText(arguments[0])??($5)"
},
{
// Pass the message object to the should-animate checker.

View file

@ -31,10 +31,12 @@
.smyn-gradient-unset-bg::after {
background: unset;
-webkit-text-fill-color: currentcolor;
}
.smyn-gradient-inherit-bg::after {
background: inherit;
-webkit-text-fill-color: transparent;
}
.smyn-gradient-hovered::after {