Merge branch 'dev'

This commit is contained in:
thororen1234 2026-02-13 17:12:23 -05:00
commit aa71c621f6
No known key found for this signature in database
10 changed files with 611 additions and 124 deletions

View file

@ -136,13 +136,81 @@ export async function uploadToS3(
}
}
export async function uploadToCatbox(
_: IpcMainInvokeEvent,
fileBuffer: ArrayBuffer,
filename: string,
userhash?: string
): Promise<NativeUploadResult> {
try {
const formData = new FormData();
formData.append("reqtype", "fileupload");
if (userhash) {
formData.append("userhash", userhash);
}
formData.append("fileToUpload", new Blob([fileBuffer]), filename);
const response = await fetch("https://catbox.moe/user/api.php", {
method: "POST",
body: formData
});
if (!response.ok) {
const errorText = await response.text();
return { success: false, error: `Upload failed: ${response.status} ${errorText}` };
}
const text = (await response.text()).trim();
if (!text) {
return { success: false, error: "No URL returned from upload" };
}
return { success: true, url: text };
} catch (e) {
return { success: false, error: e instanceof Error ? e.message : "Unknown error" };
}
}
export async function uploadToLitterbox(
_: IpcMainInvokeEvent,
fileBuffer: ArrayBuffer,
filename: string,
expiry: string
): Promise<NativeUploadResult> {
try {
const formData = new FormData();
formData.append("reqtype", "fileupload");
formData.append("time", expiry);
formData.append("fileToUpload", new Blob([fileBuffer]), filename);
const response = await fetch("https://litterbox.catbox.moe/resources/internals/api.php", {
method: "POST",
body: formData
});
if (!response.ok) {
const errorText = await response.text();
return { success: false, error: `Upload failed: ${response.status} ${errorText}` };
}
const text = (await response.text()).trim();
if (!text) {
return { success: false, error: "No URL returned from upload" };
}
return { success: true, url: text };
} catch (e) {
return { success: false, error: e instanceof Error ? e.message : "Unknown error" };
}
}
export async function fetchFile(
_: IpcMainInvokeEvent,
url: string
): Promise<{ success: boolean; data?: ArrayBuffer; contentType?: string; error?: string }> {
): Promise<{ success: boolean; data?: ArrayBuffer; contentType?: string; error?: string; }> {
try {

View file

@ -128,6 +128,12 @@ export const settings = definePluginSettings({
default: "24h",
hidden: true
},
catboxUserhash: {
type: OptionType.STRING,
description: "Catbox userhash for account binding",
default: "",
hidden: true
},
stripQueryParams: {
type: OptionType.BOOLEAN,
description: "Strip query params from uploaded URLs",
@ -180,6 +186,7 @@ export function SettingsComponent() {
const isEzHost = store.serviceType === ServiceType.EZHOST;
const isS3 = store.serviceType === ServiceType.S3;
const isZipline = store.serviceType === ServiceType.ZIPLINE;
const isCatbox = store.serviceType === ServiceType.CATBOX;
const isLitterbox = store.serviceType === ServiceType.LITTERBOX;
return (
@ -310,6 +317,16 @@ export function SettingsComponent() {
</>
)}
{isCatbox && (
<SettingTextInput
name="Catbox Userhash"
description="Your Catbox userhash for account binding (leave empty for anonymous uploads)"
value={store.catboxUserhash}
onChange={v => store.catboxUserhash = v}
placeholder="Your Catbox userhash"
/>
)}
{isLitterbox && (
<SettingsSection name="Litterbox Expiry" description="How long uploads are retained">
<Select

View file

@ -107,7 +107,7 @@ async function uploadToNest(fileBlob: Blob, filename: string): Promise<string> {
throw new Error(`Upload failed: ${response.status} ${errorText}`);
}
const data = await response.json() as { fileURL?: string };
const data = await response.json() as { fileURL?: string; };
if (data.fileURL) {
return data.fileURL;
@ -132,7 +132,7 @@ export function isConfigured(): boolean {
case ServiceType.NEST:
return Boolean(nestToken);
case ServiceType.EZHOST:
return Boolean((settings.store as { ezHostKey?: string }).ezHostKey);
return Boolean((settings.store as { ezHostKey?: string; }).ezHostKey);
case ServiceType.S3:
return isS3Configured();
case ServiceType.CATBOX:
@ -148,7 +148,7 @@ export function isConfigured(): boolean {
}
async function uploadToEzHost(fileBlob: Blob, filename: string): Promise<string> {
const { ezHostKey } = (settings.store as { ezHostKey?: string });
const { ezHostKey } = (settings.store as { ezHostKey?: string; });
if (!ezHostKey) throw new Error("E-Z Host API key is required");
@ -193,29 +193,27 @@ async function uploadToEzHost(fileBlob: Blob, filename: string): Promise<string>
}
async function uploadToCatbox(fileBlob: Blob, filename: string): Promise<string> {
const { catboxUserhash } = settings.store;
if (Native) {
const result = await Native.uploadToCatbox(await fileBlob.arrayBuffer(), filename, catboxUserhash || undefined);
if (!result.success || !result.url) throw new Error(result.error || "No URL returned from upload");
return result.url;
}
const formData = new FormData();
formData.append("reqtype", "fileupload");
if (catboxUserhash) formData.append("userhash", catboxUserhash);
formData.append("fileToUpload", fileBlob, filename);
const uploadUrl = Native
? "https://catbox.moe/user/api.php"
: `${CORS_PROXY}?url=${encodeURIComponent("https://catbox.moe/user/api.php")}`;
const response = await fetch(uploadUrl, {
const response = await fetch(`${CORS_PROXY}?url=${encodeURIComponent("https://catbox.moe/user/api.php")}`, {
method: "POST",
body: formData
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Upload failed: ${response.status} ${text}`);
}
if (!response.ok) throw new Error(`Upload failed: ${response.status} ${await response.text()}`);
const text = (await response.text()).trim();
if (!text) {
throw new Error("No URL returned from upload");
}
if (!text) throw new Error("No URL returned from upload");
return text;
}
@ -239,30 +237,27 @@ async function uploadTo0x0(fileBlob: Blob, filename: string): Promise<string> {
}
async function uploadToLitterbox(fileBlob: Blob, filename: string): Promise<string> {
const expiry = settings.store.litterboxExpiry || "24h";
if (Native) {
const result = await Native.uploadToLitterbox(await fileBlob.arrayBuffer(), filename, expiry);
if (!result.success || !result.url) throw new Error(result.error || "No URL returned from upload");
return result.url;
}
const formData = new FormData();
formData.append("reqtype", "fileupload");
formData.append("time", settings.store.litterboxExpiry || "24h");
formData.append("time", expiry);
formData.append("fileToUpload", fileBlob, filename);
const uploadUrl = Native
? "https://litterbox.catbox.moe/resources/internals/api.php"
: `${CORS_PROXY}?url=${encodeURIComponent("https://litterbox.catbox.moe/resources/internals/api.php")}`;
const response = await fetch(uploadUrl, {
const response = await fetch(`${CORS_PROXY}?url=${encodeURIComponent("https://litterbox.catbox.moe/resources/internals/api.php")}`, {
method: "POST",
body: formData
});
if (!response.ok) {
const text = await response.text();
throw new Error(`Upload failed: ${response.status} ${text}`);
}
if (!response.ok) throw new Error(`Upload failed: ${response.status} ${await response.text()}`);
const text = (await response.text()).trim();
if (!text) {
throw new Error("No URL returned from upload");
}
if (!text) throw new Error("No URL returned from upload");
return text;
}

View file

@ -16,8 +16,8 @@ import { classes } from "@utils/misc";
import definePlugin from "@utils/types";
import { Activity, ApplicationStream, Channel, Message, OnlineStatus, User } from "@vencord/discord-types";
import { MessageFlags } from "@vencord/discord-types/enums";
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, findCssClassesLazy, findExportedComponentLazy, findLazy } from "@webpack";
import { ChannelStore, MessageStore, Parser, RelationshipStore, SnowflakeUtils, UserStore, useStateFromStores } from "@webpack/common";
import { findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, findCssClassesLazy, findExportedComponentLazy } from "@webpack";
import { ChannelStore, ExperimentStore, MessageStore, Parser, RelationshipStore, SnowflakeUtils, UserStore, useStateFromStores } from "@webpack/common";
const cl = classNameFactory("vc-message-peek-");
@ -27,7 +27,6 @@ const MessageActions = findByPropsLazy("fetchMessages", "sendMessage");
const hasRelevantActivity: (props: ActivityCheckProps) => boolean = findByCodeLazy(".OFFLINE||", ".INVISIBLE)return!1");
const ActivityText: React.ComponentType<ActivityTextProps> = findComponentByCodeLazy("hasQuest:", "hideEmoji:");
const FavoritesServerExperiment = findLazy(m => m?.definition?.id === "2021-09_favorites_server");
const ONE_HOUR_MS = 60 * 60 * 1000;
@ -216,7 +215,7 @@ function Timestamp({ channel }: { channel: Channel; }) {
if (!lastMessage) return null;
const timestamp = SnowflakeUtils.extractTimestamp(lastMessage.id);
const className = FavoritesServerExperiment.getCurrentConfig().favoritesEnabled ? cl("timestamp-favorites") : cl("timestamp");
const className = ExperimentStore.getUserExperimentBucket("2021-09_favorites_server") >= 1 ? cl("timestamp-favorites") : cl("timestamp");
return <span className={className}>{formatRelativeTime(timestamp)}</span>;
}

View file

@ -1294,7 +1294,7 @@ export default definePlugin({
find: ",{progressTextAnimation:",
replacement: {
match: /(?<=children:\i}=)(\i)/,
replace: "Object.assign({},$1,$self.getQuestPanelPercentComplete($1))"
replace: "Object.assign({},$1,$1.quest?$self.getQuestPanelPercentComplete($1):{})"
}
},
{
@ -1302,7 +1302,7 @@ export default definePlugin({
// useful information for Quests being auto-completed.
find: '"progress-title"',
replacement: {
match: /(?<=return.{0,150}?quest:(\i),percentComplete:\i.{0,280}?"progress-title",children.{0,115}?children:)(\i.{0,50}"progress-subtitle",isTextTransition:!0,children.{0,115}?children:)/,
match: /(?<={quest:(\i).{0,250}?return.{0,150}?,percentComplete:\i.{0,280}?"progress-title",children.{0,115}?children:)(\i.{0,50}"progress-subtitle",isTextTransition:!0,children.{0,115}?children:)/,
replace: "$self.getQuestPanelTitleText($1)??$2$self.getQuestPanelSubtitleText($1)??"
}
},

View file

@ -91,7 +91,7 @@ export default definePlugin({
{
find: "useCanFavoriteChannel",
replacement: {
match: /\i\.isDM\(\)\|\|\i\.isThread\(\)/,
match: /(?<=\i\.id\)\),\i=)\i\.isThread\(\)/,
replace: "false",
}
},

View file

@ -1084,6 +1084,20 @@ const messageContextMenuPatch: NavContextMenuPatchCallback = (children, ...args:
insertSetAliasMenuItem(children, emojiRef);
};
const messageSendListener = (_channelId: string, messageObj: { content: string; }) => {
if (!messageObj.content || !Object.keys(aliasMap).length) return;
messageObj.content = messageObj.content.replace(/:([a-z0-9_]{2,32}):/gi, (match, aliasName) => {
const ref = aliasMap[aliasName.toLowerCase()];
if (!ref) return match;
if (ref.kind === "custom" && ref.id) {
return `<${ref.animated ? "a" : ""}:${ref.name}:${ref.id}>`;
}
return getUnicodeSurrogate(ref) ?? resolveUnicodeSurrogateByName(ref.name) ?? match;
});
};
export default definePlugin({
name: "FavoriteEmojiFirst",
authors: [Devs.Aria, Devs.Ven, EquicordDevs.justjxke],
@ -1097,6 +1111,7 @@ export default definePlugin({
"textarea-context": messageContextMenuPatch
},
isModified: true,
onBeforeMessageSend: messageSendListener,
patches: [
{
find: "renderResults({results:",

View file

@ -13,15 +13,19 @@ import { definePluginSettings } from "@api/Settings";
import { Button, ErrorBoundary, Heading, TextButton } from "@components/index";
import mentionAvatars from "@plugins/mentionAvatars";
import { Devs, EquicordDevs } from "@utils/constants";
import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/index";
import { classNameFactory, ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/index";
import definePlugin, { OptionType } from "@utils/types";
import { GuildMember, Message, User } from "@vencord/discord-types";
import { findByCodeLazy, findStoreLazy } from "@webpack";
import { ChannelStore, GuildMemberStore, GuildStore, Menu, MessageStore, RelationshipStore, StreamerModeStore, TextInput, useEffect, UserStore, useState } from "@webpack/common";
import { ChannelStore, GuildMemberStore, GuildStore, Menu, MessageStore, RelationshipStore, StreamerModeStore, TextInput, useEffect, useState } from "@webpack/common";
import { JSX } from "react";
const SMYNC = classNameFactory();
const UserStore = findStoreLazy("UserStore");
const wrapEmojis = findByCodeLazy("lastIndex;return");
const adjustColor = findByCodeLazy("light1", "dark1", "toonStroke");
const AccessibilityStore = findStoreLazy("AccessibilityStore");
const colorPattern = /^#(?:[\da-f]{3}){1,2}$|^#(?:[\da-f]{4}){1,2}$|(rgb|hsl)a?\((\s*-?\d+%?\s*,){2}(\s*-?\d+%?\s*)\)|(rgb|hsl)a?\((\s*-?\d+%?\s*,){3}\s*(0|(0?\.\d+)|1)\)$/iu;
const roleColorPattern = /^role((?:\+|-)\d{0,4})?$/iu;
const symbolPattern = /^[\p{S}\p{P}]{1,3}$/iu;
@ -76,22 +80,14 @@ function validColor(color: string) {
return isValid;
}
function resolveColor(user: User | GuildMember, savedColor: string, fallbackColor: string, canUseGradient: boolean): any {
const fallbackReturn = {
normal: {
original: {
color: fallbackColor,
"text-decoration-color": fallbackColor
},
adjusted: {
color: fallbackColor,
"text-decoration-color": fallbackColor
}
},
gradient: null
};
if (!savedColor.trim()) { return fallbackReturn; }
function resolveColor(
user: User | GuildMember,
savedColor: string,
canUseGradient: boolean,
inGuild: boolean,
forceWhite: boolean
): Record<string, any> | null {
if (!savedColor.trim()) { return null; }
let gradient: any = null;
let primaryColor: any = savedColor;
@ -103,18 +99,24 @@ function resolveColor(user: User | GuildMember, savedColor: string, fallbackColo
if (savedColor.toLowerCase().includes("role")) {
const percentage = roleColorPattern.exec(savedColor)?.[1] || "";
if (percentage && isNaN(parseInt(percentage))) return fallbackReturn;
if (percentage && isNaN(parseInt(percentage))) return null;
const colorStrings = (user as any)?.colorStrings || {};
primaryColor = colorStrings.primaryColor || null;
secondaryColor = colorStrings.secondaryColor || null;
tertiaryColor = colorStrings.tertiaryColor || null;
const displayStyles = !inGuild ? (user as any)?.displayNameStyles || {} : {};
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) { return fallbackReturn; }
if (primaryColor && percentage) {
primaryAdjusted = adjustHex(primaryColor, parseInt(percentage));
}
@ -128,7 +130,7 @@ function resolveColor(user: User | GuildMember, savedColor: string, fallbackColo
}
}
gradient = !canUseGradient || !secondaryColor
gradient = !canUseGradient || !secondaryColor || forceWhite
? null
: tertiaryColor
? "linear-gradient(to right,var(--custom-gradient-color-1),var(--custom-gradient-color-2),var(--custom-gradient-color-3),var(--custom-gradient-color-1))"
@ -160,7 +162,7 @@ function resolveColor(user: User | GuildMember, savedColor: string, fallbackColo
"--custom-gradient-color-2": secondaryColor || primaryColor,
"--custom-gradient-color-3": tertiaryColor || primaryColor,
"background-image": gradient,
"animation": "show-me-your-name-animation 1.5s linear infinite"
"animation": "smyn-animation 1.5s linear infinite"
},
static: {
original: {
@ -227,7 +229,14 @@ function validTemplate(value: string) {
}
}
function getProcessedNames(author: any, truncateAllNamesWithStreamerMode: boolean, discriminators: boolean): {
function getProcessedNames(
author: any,
truncateAllNamesWithStreamerMode: boolean,
discriminators: boolean,
inGuild: boolean,
friendNameOnlyInDirectMessages: boolean,
customNameOnlyInDirectMessages: boolean
): {
username: string | null;
display: string | null;
nick: string | null;
@ -263,13 +272,13 @@ function getProcessedNames(author: any, truncateAllNamesWithStreamerMode: boolea
? author.nick[0] + "..."
: author.nick as string;
const friendName: string | null = author ? RelationshipStore.getNickname(author.id) || null : null;
const friendName: string | null = (author && !(inGuild && friendNameOnlyInDirectMessages)) ? RelationshipStore.getNickname(author.id) || null : null;
const friend: string | null = !friendName ? null
: StreamerModeStore.enabled && (truncateAllNamesWithStreamerMode || friendName.toLowerCase() === author.username.toLowerCase())
? friendName[0] + "..."
: friendName as string;
const customName: string | null = !author?.id ? null : customNicknames[author.id] || null;
const customName: string | null = (author && !(inGuild && customNameOnlyInDirectMessages)) ? customNicknames[author.id] || null : null;
const custom: string | null = !customName ? null
: StreamerModeStore.enabled && (truncateAllNamesWithStreamerMode || customName.toLowerCase() === author.username.toLowerCase())
? customName[0] + "..."
@ -311,7 +320,7 @@ function getMemberListProfilesReactionsVoiceName(
const member = guildId && user ? GuildMemberStore.getMember(guildId, user.id) : null;
const author = user && member ? { ...user, ...member } : user || member || null;
const shouldHookless = type === "reactionsTooltip" || type === "profilesTooltip";
return renderUsername(author, null, null, type, "", shouldHookless);
return renderUsername(author, null, null, type, "", shouldHookless, !!guildId);
}
function getMemberListProfilesReactionsVoiceNameText(props: memberListProfileReactionProps): string | null {
@ -332,7 +341,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);
return renderUsername(author, channel?.id || null, message?.id || null, isRepliedMessage ? "replies" : "messages", mentionSymbol, false, !!channel?.guild_id);
}
function getMessageNameElement(props: messageProps): JSX.Element | null {
@ -343,7 +352,7 @@ function getMessageNameText(props: messageProps): string | null {
return getMessageName(props)[0];
}
export function getMentionNameElement(props: mentionProps): JSX.Element | null {
function getMentionNameElement(props: mentionProps): JSX.Element | null {
const { hideDefaultAtSign, mentions } = settings.use(["hideDefaultAtSign", "mentions"]);
const { channelId, userId, props: nestedProps } = props;
const channel = channelId ? ChannelStore.getChannel(channelId) || null : null;
@ -351,7 +360,43 @@ export 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)[1];
return renderUsername(author, channelId || null, nestedProps?.messageId || null, "mentions", mentionSymbol, false, !!channel?.guild_id)[1];
}
function getEffectType(effectId: number | null | undefined): string | null {
switch (effectId) {
case 1: return "solid";
// Delegate gradient effect handling to the guild
// gradient handler. This adds animation to DM
// gradients which are usually static.
// case 2: return "gradient";
case 3: return "neon";
case 4: return "toon";
case 5: return "pop";
default: return null;
}
}
function computeEffectCSSVars(styles: any): Record<string, string> {
if (!styles?.colors?.length) return {};
const toHex = (n: number) => `#${(n >>> 0).toString(16).padStart(6, "0")}`;
const primary = toHex(styles.colors[0]);
const secondary = styles.colors.length > 1 ? toHex(styles.colors[1]) : primary;
const adjusted = adjustColor(primary);
return {
"--smyn-effect-main-color": adjusted.main,
"--smyn-effect-gradient-start": primary,
"--smyn-effect-gradient-end": secondary,
"--smyn-effect-light-1": adjusted.light1,
"--smyn-effect-light-2": adjusted.light2,
"--smyn-effect-dark-1": adjusted.dark1,
"--smyn-effect-dark-2": adjusted.dark2,
"--smyn-effect-neon-stroke": adjusted.neonStroke,
"--smyn-effect-neon-flicker": `hsl(from ${adjusted.main} h calc(min(1, s) * ((s * 1.1) + 10)) 85)`,
"--smyn-effect-toon-stroke": adjusted.toonStroke,
};
}
function renderUsername(
@ -360,7 +405,8 @@ function renderUsername(
messageId: string | null,
type: "messages" | "replies" | "mentions" | "membersList" | "profilesPopout" | "profilesTooltip" | "reactionsTooltip" | "reactionsPopout" | "voiceChannel",
mentionSymbol: string,
hookless = false
hookless: boolean,
inGuild: boolean
): [string | null, JSX.Element | null, string | null] {
const isMessage = type === "messages";
const isReply = type === "replies";
@ -372,19 +418,44 @@ function renderUsername(
const isReaction = isReactionsTooltip || isReactionsPopout;
const isVoice = type === "voiceChannel";
const config = hookless ? settings.store : settings.use(["messages", "replies", "mentions", "memberList", "profilePopout", "reactions", "discriminators", "hideDefaultAtSign", "truncateAllNamesWithStreamerMode", "removeDuplicates", "ignoreGradients", "ignoreFonts", "animateGradients", "includedNames", "customNameColor", "friendNameColor", "nicknameColor", "displayNameColor", "usernameColor", "nameSeparator", "triggerNameRerender"]);
const { messages, replies, mentions, memberList, profilePopout, reactions, discriminators, hideDefaultAtSign, truncateAllNamesWithStreamerMode, removeDuplicates, ignoreGradients, ignoreFonts, animateGradients, includedNames, customNameColor, friendNameColor, nicknameColor, displayNameColor, usernameColor, nameSeparator, triggerNameRerender } = config;
const config = hookless ? settings.store : settings.use(["messages", "replies", "mentions", "memberList", "profilePopout", "reactions", "friendNameOnlyInDirectMessages", "customNameOnlyInDirectMessages", "discriminators", "hideDefaultAtSign", "truncateAllNamesWithStreamerMode", "removeDuplicates", "ignoreGradients", "ignoreFonts", "animateGradients", "includedNames", "customNameColor", "friendNameColor", "nicknameColor", "displayNameColor", "usernameColor", "nameSeparator", "triggerNameRerender"]);
const { messages, replies, mentions, memberList, profilePopout, reactions, friendNameOnlyInDirectMessages, customNameOnlyInDirectMessages, discriminators, truncateAllNamesWithStreamerMode, removeDuplicates, ignoreGradients, ignoreFonts, animateGradients, includedNames, customNameColor, friendNameColor, nicknameColor, displayNameColor, usernameColor, nameSeparator, triggerNameRerender } = config;
const channel = channelId ? ChannelStore.getChannel(channelId) || null : null;
const message = channelId && messageId ? MessageStore.getMessage(channelId, messageId) : null;
const groupId = (message as any)?.showMeYourNameGroupId || null;
const isHovering = (isMessage || isMention)
? ((messageId && hoveringMessageMap.has(messageId)) || (groupId && hoveringMessageMap.has(groupId)))
: isReply
? (messageId && hoveringRepliesMap.has(messageId)) || (groupId && hoveringRepliesMap.has(groupId))
: isReactionsPopout
? hoveringReactionPopoutSet.has((author as User).id)
: false;
const authorDisplayNameStyles = (!inGuild && (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 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 hasGradient = !!topRoleStyle?.gradient && Object.keys(topRoleStyle.gradient).length > 0;
const canUseGradient = ((author as GuildMember)?.guildId ? (GuildStore.getGuild((author as GuildMember).guildId) ?? {}).premiumFeatures?.features.includes("ENHANCED_ROLE_COLORS") : false);
const textMutedValue = getComputedStyle(document.documentElement)?.getPropertyValue("--text-muted")?.trim() || "#72767d";
const options = splitTemplate(includedNames);
const resolvedUsernameColor = author ? resolveColor(author, usernameColor.trim(), "", canUseGradient) : null;
const resolvedDisplayNameColor = author ? resolveColor(author, displayNameColor.trim(), "", canUseGradient) : null;
const resolvedNicknameColor = author ? resolveColor(author, nicknameColor.trim(), "", canUseGradient) : null;
const resolvedFriendNameColor = author ? resolveColor(author, friendNameColor.trim(), "", canUseGradient) : null;
const resolvedCustomNameColor = author ? resolveColor(author, customNameColor.trim(), "", canUseGradient) : null;
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 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);
const { username, display, nick, friend, custom } = getProcessedNames(author, truncateAllNamesWithStreamerMode, discriminators, inGuild, friendNameOnlyInDirectMessages, customNameOnlyInDirectMessages);
const names: Record<string, [string | null, object | null]> = {
user: [username, resolvedUsernameColor],
@ -416,7 +487,7 @@ function renderUsername(
}
}
if (!chosenName || !chosenStyle) {
if (!chosenName) {
continue;
}
@ -426,7 +497,7 @@ function renderUsername(
wrapped: chosenName,
type: chosenType,
suffix: suffix || "",
style: chosenStyle as React.CSSProperties
style: chosenStyle
});
}
@ -513,18 +584,6 @@ function renderUsername(
fourth = remainingNames.shift();
fifth = remainingNames.shift();
const useTopRoleStyle = isMention || isReactionsPopout;
const topRoleStyle = resolveColor(author, "Role", "", canUseGradient);
const hasGradient = !!topRoleStyle?.gradient && Object.keys(topRoleStyle.gradient).length > 0;
const message = channelId && messageId ? MessageStore.getMessage(channelId, messageId) : null;
const groupId = (message as any)?.showMeYourNameGroupId || null;
const isHovering = (isMessage || isReply || isMention)
? ((messageId && hoveringMessageMap.has(messageId)) || (groupId && hoveringMessageMap.has(groupId)))
: isReactionsPopout
? hoveringReactionPopoutSet.has((author as User).id)
: false;
const shouldGradientGlow = isHovering && hasGradient;
const shouldAnimateGradients = shouldGradientGlow && !AccessibilityStore.useReducedMotion;
const shouldAnimateSecondaryNames = animateGradients && !ignoreGradients;
@ -560,21 +619,34 @@ function renderUsername(
...(isReactionsPopout
? { display: "flex", flexWrap: "wrap", lineHeight: "1.1em", fontSize: "0.9em" }
: {}),
...(hasEffect ? effectCSSVars : {})
} as React.CSSProperties;
const nameElement = (
<span style={topLevelStyle} className="show-me-your-name-container">
<span
style={{
...topLevelStyle,
...(topRoleStyle?.normal.original || {})
}}
className="show-me-your-name-container"
>
{mentionSymbol && <span>{mentionSymbol}</span>}
{(
<span
className={(shouldGradientGlow ? (gradientClasses + " " + firstGroupClasses) : firstGroupClasses)}
className={SMYNC(firstGroupClasses, { [gradientClasses]: shouldGradientGlow })}
data-text={shouldGradientGlow ? firstDataText : undefined}
style={(shouldGradientGlow && useTopRoleStyle && topRoleStyle ? topRoleStyle.gradient.animated : undefined) as React.CSSProperties}
>
<span
className={firstNameClasses}
style={
topRoleStyle ?
className={SMYNC(firstNameClasses, {
"smyn-effect-container": shouldShowEffect,
[`smyn-effect-${effectType}`]: shouldShowEffect,
"smyn-effect-animated": shouldAnimateEffect
})}
data-username-with-effects={needsEffectDataAttr && shouldShowEffect ? first.name : undefined}
style={shouldShowEffect
? undefined
: topRoleStyle ?
shouldAnimateGradients && topRoleStyle.gradient
? topRoleStyle.gradient.animated
: topRoleStyle.gradient
@ -593,7 +665,7 @@ function renderUsername(
].map(({ name, dataText, groupClass, nameClass, position }) => name && (
<span
key={position}
className={(shouldGradientGlow && shouldAnimateSecondaryNames ? (gradientClasses + " " + groupClass) : groupClass)}
className={SMYNC(groupClass, { [gradientClasses]: shouldGradientGlow && shouldAnimateSecondaryNames })}
data-text={shouldGradientGlow && dataText ? dataText : undefined}
style={(shouldGradientGlow && shouldAnimateSecondaryNames ? name.style.gradient.animated : undefined) as React.CSSProperties}
>
@ -601,17 +673,24 @@ function renderUsername(
<span>{nameSeparator}</span>
{name.prefix}</span>
<span
// On non-primary names, allow disabling the gradients completely, or just their animation & glow.
className={nameClass}
// On non-primary names, allow disabling the effects completely, or just their animation & glow.
className={SMYNC(nameClass, {
"smyn-effect-container": shouldShowEffect && !ignoreGradients,
[`smyn-effect-${effectType}`]: shouldShowEffect && !ignoreGradients,
"smyn-effect-animated": shouldAnimateEffect && shouldAnimateSecondaryNames
})}
data-username-with-effects={needsEffectDataAttr && shouldShowEffect && !ignoreGradients ? name.name : undefined}
style={{
...(ignoreFonts ? { "font-family": "var(--font-primary)", "letter-spacing": "normal" } : {}),
...(ignoreGradients
? name.style.normal.adjusted
: shouldAnimateGradients && shouldAnimateSecondaryNames && name.style.gradient
? name.style.gradient.animated
: name.style.gradient
? name.style.gradient.static.original
: name.style.normal.adjusted)
...(name.style
? ignoreGradients
? name.style.normal.adjusted
: shouldAnimateGradients && shouldAnimateSecondaryNames && name.style.gradient
? name.style.gradient.animated
: name.style.gradient
? name.style.gradient.static.original
: name.style.normal.adjusted
: {})
}}>
{name.wrapped}</span>
<span style={affixColor as React.CSSProperties} className={suffixClasses}>
@ -625,10 +704,12 @@ function renderUsername(
}
const hoveringMessageMap = new Map<string, number>();
const hoveringRepliesMap = new Map<string, number>();
const hoveringReactionPopoutSet = new Set<string>();
function handleHoveringMessage(message: any, isHovering: boolean) {
const messageId = message?.id;
const repliedId = message?.messageReference?.message_id;
const groupId = message?.showMeYourNameGroupId ?? "";
useEffect(() => {
@ -637,9 +718,11 @@ function handleHoveringMessage(message: any, isHovering: boolean) {
if (isHovering) {
addHoveringMessage(messageId);
addHoveringMessage(groupId);
addHoveringReply(repliedId);
} else {
removeHoveringMessage(messageId);
removeHoveringMessage(groupId);
removeHoveringReply(repliedId);
}
}, [messageId, groupId, isHovering]);
}
@ -668,6 +751,30 @@ function removeHoveringMessage(id: string) {
}
}
function addHoveringReply(id: string) {
if (!id) return;
const currentCount = hoveringRepliesMap.get(id) || 0;
hoveringRepliesMap.set(id, currentCount + 1);
if (currentCount === 0) {
settings.store.triggerNameRerender = !settings.store.triggerNameRerender;
}
}
function removeHoveringReply(id: string) {
if (!id) return;
const currentCount = hoveringRepliesMap.get(id) || 0;
if (currentCount <= 1) {
hoveringRepliesMap.delete(id);
settings.store.triggerNameRerender = !settings.store.triggerNameRerender;
} else {
hoveringRepliesMap.set(id, currentCount - 1);
}
}
function addHoveringReactionPopout(id: string) {
hoveringReactionPopoutSet.add(id);
settings.store.triggerNameRerender = !settings.store.triggerNameRerender;
@ -789,7 +896,7 @@ const settings = definePluginSettings({
memberList: {
type: OptionType.BOOLEAN,
default: true,
description: "Display the first available name listed in your custom name format in the member list.",
description: "Display the first available name listed in your custom name format in the member list and DMs list.",
},
profilePopout: {
type: OptionType.BOOLEAN,
@ -834,18 +941,28 @@ const settings = definePluginSettings({
ignoreGradients: {
type: OptionType.BOOLEAN,
default: true,
description: "For the non-primary names, if the role has a gradient, ignore it in favor of the color set below.",
description: "For the non-primary names, if the role has a gradient or nitro effect, ignore it in favor of the color set below.",
},
animateGradients: {
type: OptionType.BOOLEAN,
default: false,
description: "For the non-primary names, if the role has a gradient, animate it. This is disabled by \"Ignore Gradients\" and reduced motion.",
description: "For the non-primary names, if the role has a gradient or nitro effect, animate it. This is disabled by \"Ignore Gradients\" and reduced motion.",
},
nameSeparator: {
type: OptionType.STRING,
description: "The separator to use between names. The default is a single space.",
default: " ",
},
friendNameOnlyInDirectMessages: {
type: OptionType.BOOLEAN,
default: false,
description: "Only display friend names when in DMs, and not in servers.",
},
customNameOnlyInDirectMessages: {
type: OptionType.BOOLEAN,
default: false,
description: "Only display custom names when in DMs, and not in servers.",
},
includedNames: {
type: OptionType.STRING,
description: "The order to display usernames, display names, nicknames, friend names, and custom names. Use the following placeholders: {user}, {display}, {nick}, {friend}, {custom}. You can provide multiple name options to use as fallbacks if one is unavailable by separating them with commas as such: {custom, friend, nick}. You can have up to three prefixes and three suffixes per name.",
@ -898,6 +1015,8 @@ export default definePlugin({
isModified: true,
settings,
UserStore,
patches: [
{
find: '="SYSTEM_TAG"',
@ -923,6 +1042,14 @@ export default definePlugin({
replace: "$self.getMemberListProfilesReactionsVoiceNameText({...arguments[0],type:\"membersList\"})??"
},
},
{
// Replace name in solo DM title bar and tooltip.
find: "channel.isSystemDM(),",
replacement: {
match: /(?<=}\);)(return.{0,500}?{text:)(\i,position:"bottom",children:.{0,40}?children:)(\i\?\?\i.\i.getName\(\i\))/,
replace: "const smynName=arguments[0].channel.recipients.length===1?$self.getMemberListProfilesReactionsVoiceNameText({user:$self.UserStore.getUser(arguments[0].channel.recipients[0]),type:\"profilesPopout\"})??null:null;$1smynName??$2smynName??$3"
},
},
{
// Track hovering on messages to animate gradients.
// Attach the group ID to their messages to allow animating gradients within a group.

View file

@ -1,4 +1,7 @@
@keyframes show-me-your-name-animation {
/* stylelint-disable rule-empty-line-before */
/* stylelint-disable property-no-vendor-prefix */
@keyframes smyn-animation {
0% {
background-position: 0;
}
@ -8,12 +11,12 @@
}
}
.show-me-your-name-gradient {
.smyn-gradient {
position: relative;
z-index: 0;
}
.show-me-your-name-gradient::after {
.smyn-gradient::after {
background-clip: text;
content: attr(data-text) / "";
inset: 0;
@ -26,25 +29,286 @@
z-index: -1;
}
.show-me-your-name-gradient-unset-bg::after {
.smyn-gradient-unset-bg::after {
background: unset;
}
.show-me-your-name-gradient-inherit-bg::after {
.smyn-gradient-inherit-bg::after {
background: inherit;
}
.show-me-your-name-gradient-hovered::after {
.smyn-gradient-hovered::after {
opacity: .7;
transition: opacity .1s ease-in-out
transition: opacity .1s ease-in-out;
}
.show-me-your-name-modal-footer-container button {
.smyn-modal-footer-container button {
width: 100%;
}
.show-me-your-name-reset-button {
.smyn-reset-button {
margin-top: 8px;
font-size: 14px;
font-weight: 500;
}
.smyn-container:hover {
text-decoration: underline;
}
.smyn-effect-container {
position: relative;
opacity: 1;
}
.smyn-effect-solid {
color: var(--smyn-effect-main-color);
}
.smyn-effect-gradient {
background: linear-gradient(to bottom right, var(--smyn-effect-gradient-start) 10%, var(--smyn-effect-gradient-end) 90%);
background-clip: text;
-webkit-background-clip: text;
background-size: 100% auto;
-webkit-text-fill-color: transparent;
position: relative;
z-index: 0;
}
.smyn-effect-neon {
paint-order: stroke fill;
-webkit-text-stroke-width: calc(1px + .04em);
-webkit-text-stroke-color: var(--smyn-effect-neon-stroke);
-webkit-text-fill-color: currentcolor;
color: var(--white);
margin-inline-start: calc(-1px - .04em);
margin-bottom: calc(-1px - .04em);
padding-bottom: calc(1px + .04em);
padding-inline-start: calc(1px + .04em);
position: relative;
z-index: 0;
}
.smyn-effect-neon::after {
content: attr(data-username-with-effects);
position: absolute;
inset: 0;
background: linear-gradient(to bottom left, var(--smyn-effect-light-2) 0, var(--smyn-effect-light-2) 6%, var(--smyn-effect-main-color) 20%, var(--smyn-effect-light-1) 50%, var(--smyn-effect-light-2) 56%, var(--smyn-effect-main-color) 70%, var(--smyn-effect-light-1) 100%);
background-clip: text;
-webkit-background-clip: text;
background-position: 100% 0;
background-size: 200% 200%;
-webkit-text-fill-color: transparent;
color: var(--smyn-effect-main-color);
filter: blur(calc(1px + .12em));
opacity: 0;
transition: opacity .15s ease-in-out;
-webkit-text-stroke-width: calc(1px + .04em);
-webkit-text-stroke-color: transparent;
perspective: 1px;
z-index: -1;
}
.smyn-effect-animated.smyn-effect-neon {
animation: smyn-neon-flicker 4s cubic-bezier(.24, .31, .36, .93) infinite;
animation-direction: normal;
animation-fill-mode: forwards;
}
.smyn-effect-animated.smyn-effect-neon::after {
opacity: .8;
animation: smyn-neon-glow-flicker 1666ms linear infinite;
animation-direction: normal;
animation-fill-mode: forwards;
}
.smyn-effect-toon {
--smyn-toon-stroke-width: calc(1.6px + 0.04em);
--smyn-toon-margin: calc(var(--smyn-toon-stroke-width)*-1);
paint-order: stroke fill;
position: relative;
-webkit-text-stroke-width: var(--smyn-toon-stroke-width);
-webkit-text-stroke-color: var(--smyn-effect-toon-stroke);
color: var(--smyn-effect-toon-stroke);
margin-inline: var(--smyn-toon-margin);
margin-bottom: var(--smyn-toon-margin);
padding-inline: var(--smyn-toon-stroke-width);
padding-bottom: var(--smyn-toon-stroke-width);
transition: color 266ms cubic-bezier(.43, .21, .27, .78);
}
.smyn-effect-toon::before {
background: linear-gradient(180deg, var(--white) 0, var(--smyn-effect-light-2) 8%, var(--smyn-effect-light-1) 15%, var(--smyn-effect-main-color) 25%, var(--smyn-effect-light-2) 45%, var(--smyn-effect-main-color) 55%, var(--white) 75%, var(--smyn-effect-light-2) 83%, var(--smyn-effect-light-1) 90%, var(--smyn-effect-main-color) 100%);
background-clip: text;
-webkit-background-clip: text;
background-size: 100% 400%;
content: attr(data-username-with-effects);
inset: 0;
padding-inline: var(--smyn-toon-stroke-width);
padding-bottom: var(--smyn-toon-margin);
position: absolute;
-webkit-text-fill-color: transparent;
-webkit-text-stroke-width: 0;
-webkit-text-stroke-color: transparent;
overflow: hidden;
text-overflow: ellipsis;
transition: opacity 266ms cubic-bezier(.43, .21, .27, .78);
}
.smyn-effect-pop {
--smyn-pop-stroke-width: calc(1.2px + 0.04em);
--smyn-pop-bottom-translate: 0.08em;
display: inline-block;
color: var(--white);
paint-order: stroke fill;
position: relative;
-webkit-text-stroke-color: var(--smyn-effect-dark-2);
-webkit-text-stroke-width: var(--smyn-pop-stroke-width);
margin-inline-start: calc(var(--smyn-pop-stroke-width)*-1);
margin-bottom: calc(var(--smyn-pop-stroke-width)*-1 - var(--smyn-pop-bottom-translate));
padding-bottom: calc(var(--smyn-pop-stroke-width) + var(--smyn-pop-bottom-translate));
padding-inline-start: var(--smyn-pop-stroke-width);
}
.smyn-effect-pop::before {
bottom: calc(var(--smyn-pop-bottom-translate)*-1 - var(--smyn-pop-stroke-width));
color: var(--smyn-effect-main-color);
content: attr(data-username-with-effects);
padding-inline-start: var(--smyn-pop-stroke-width);
position: absolute;
top: 0;
width: calc(100% - var(--smyn-pop-stroke-width));
z-index: -1;
-webkit-text-stroke-width: var(--smyn-pop-stroke-width);
-webkit-text-stroke-color: transparent;
background: linear-gradient(to bottom left, var(--smyn-effect-light-1) 0, var(--smyn-effect-light-1) 6%, var(--smyn-effect-main-color) 20%, var(--smyn-effect-main-color) 50%, var(--smyn-effect-light-1) 56%, var(--smyn-effect-main-color) 70%, var(--smyn-effect-main-color) 100%);
background-clip: text;
-webkit-background-clip: text;
background-position: 100% 0;
background-size: 200% 200%;
transform: translate3d(0, var(--smyn-pop-bottom-translate), 0);
-webkit-text-fill-color: transparent;
overflow: hidden;
text-decoration: none;
text-overflow: ellipsis;
}
.smyn-effect-pop:hover::before {
text-decoration: underline;
text-decoration-color: var(--smyn-effect-main-color);
text-underline-offset: calc(var(--smyn-pop-bottom-translate));
}
.smyn-effect-animated.smyn-effect-toon::before {
animation: smyn-toon-animation 4s cubic-bezier(.44, .29, .48, 1) infinite;
animation-direction: normal;
animation-fill-mode: forwards;
}
.smyn-effect-animated.smyn-effect-pop {
animation: smyn-pop-main 4s cubic-bezier(.44, .29, .48, 1) infinite;
animation-direction: normal;
animation-fill-mode: forwards;
}
.smyn-effect-animated.smyn-effect-pop::before {
animation: smyn-pop-shadow 4s cubic-bezier(.44, .29, .48, 1) infinite;
animation-direction: normal;
animation-fill-mode: forwards;
}
@keyframes smyn-neon-flicker {
0%,
15%,
18%,
20%,
23%,
25%,
50% {
color: var(--white);
}
16%,
22%,
28% {
color: var(--smyn-effect-neon-flicker);
}
51%,
100% {
color: var(--white);
}
}
@keyframes smyn-neon-glow-flicker {
0% {
background-position: 100% 0;
}
100% {
background-position: 0 100%;
}
}
@keyframes smyn-toon-animation {
0%,
5% {
background-position: 50% 0;
}
55%,
100% {
background-position: 50% 100%;
}
}
@keyframes smyn-pop-main {
0% {
transform: translateZ(0);
}
18% {
perspective: 1px;
transform: translate3d(0, -.05em, 0);
}
35% {
perspective: 1px;
transform: translate3d(0, .08em, 0);
}
50%,
100% {
perspective: 1px;
transform: translateZ(0);
}
}
@keyframes smyn-pop-shadow {
0% {
background-position: 100% 0;
perspective: 1px;
transform: translate3d(0, .08em, 0);
}
18% {
perspective: 1px;
transform: translate3d(0, .13em, 0);
}
35% {
perspective: 1px;
transform: translateZ(0);
}
50%,
100% {
background-position: 0 100%;
perspective: 1px;
transform: translate3d(0, .08em, 0);
}
}

View file

@ -83,6 +83,7 @@ export let ApplicationCommandIndexStore: t.ApplicationCommandIndexStore;
export let EditMessageStore: t.EditMessageStore;
export let QuestStore: t.QuestStore;
export let PendingReplyStore: GenericStore;
export let ExperimentStore: GenericStore;
/**
* @see jsdoc of {@link t.useStateFromStores}
@ -137,6 +138,7 @@ waitForStore("PopoutWindowStore", m => PopoutWindowStore = m);
waitForStore("ApplicationCommandIndexStore", m => ApplicationCommandIndexStore = m);
waitForStore("EditMessageStore", m => EditMessageStore = m);
waitForStore("PendingReplyStore", m => PendingReplyStore = m);
waitForStore("ExperimentStore", m => ExperimentStore = m);
waitForStore("QuestStore", m => QuestStore = m);
waitForStore("ThemeStore", m => {
ThemeStore = m;