diff --git a/src/plugins/showMeYourName/index.tsx b/src/plugins/showMeYourName/index.tsx index bbafb58d32..1de05201c5 100644 --- a/src/plugins/showMeYourName/index.tsx +++ b/src/plugins/showMeYourName/index.tsx @@ -4,48 +4,152 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ -import "./styles.css"; - import { definePluginSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; -import { Message, User } from "discord-types/general"; - -interface UsernameProps { - author: { nick: string; }; - message: Message; - withMentionPrefix?: boolean; - isRepliedMessage: boolean; - userOverride?: User; +import { findStoreLazy } from "@webpack"; +import { ChannelStore, GuildMemberStore, UserStore } from "@webpack/common"; +import { GuildMember, User } from "discord-types/general"; + +const StreamerModeStore = findStoreLazy("StreamerModeStore"); +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; + +function validColor(color: string) { + const trimmedColor = color.trim(); + if (!trimmedColor) return color.length > 0; + + if (trimmedColor.toLowerCase() === "role") return true; + + if (color.toLowerCase().includes("role")) { + const percentage = parseInt(roleColorPattern.exec(color)?.[1] || ""); + return !isNaN(percentage) && percentage <= 100 && percentage >= -100; + } + + const colorTestDiv = document.createElement("div"); + colorTestDiv.style.borderColor = color; + + const isValid = colorTestDiv.style.borderColor !== "" && colorPattern.test(color); + colorTestDiv.remove(); + + return isValid; +} + +function resolveColor(user: User | GuildMember, savedColor: string, fallbackColor: string) { + if (!savedColor.trim()) return { color: fallbackColor }; + + if (savedColor.toLowerCase().includes("role")) { + const percentageText = roleColorPattern.exec(savedColor)?.[1] || ""; + if (percentageText && isNaN(parseInt(percentageText))) return { color: fallbackColor }; + + const percentage = percentageText ? 1 + (parseInt(percentageText) / 100) : 1; + const roleColor = (user as GuildMember)?.colorString || null; + if (!roleColor) return { color: fallbackColor }; + + return { color: roleColor, filter: `brightness(${percentage})` }; + } else { + return { color: savedColor }; + } +} + +function validTemplate(value: string) { + const items = value.trim().split(/\s+/); + if (items.length > 3 || !items.length) return false; + + const invalidItems = items.some(item => !/(?:\{(nick|display|user)\})/i.test(item)); + if (invalidItems) return false; + + const affixes = parseAffixes(value); + if (!affixes.order.length || affixes.order.length !== items.length) return false; + + return ["nick", "display", "user"].every(name => { + const { prefix, suffix } = affixes[name]; + return prefix.length <= 3 && suffix.length <= 3 && (!prefix || symbolPattern.test(prefix)) && (!suffix || symbolPattern.test(suffix)); + }); +} + +function parseAffixes(template: string) { + const affixes = { + order: [] as string[], + nick: { included: false, prefix: "", suffix: "" }, + display: { included: false, prefix: "", suffix: "" }, + user: { included: false, prefix: "", suffix: "" } + }; + + const types = ["nick", "display", "user"]; + + template.split(/\s+/).forEach(item => { + types.forEach(type => { + if (item.includes(`{${type}}`)) { + const [prefix, , suffix] = item.split(/(?:\{(nick|display|user)\})/i); + affixes.order.push(type); + affixes[type] = { included: true, prefix, suffix }; + } + }); + }); + + return affixes; } const settings = definePluginSettings({ - mode: { - type: OptionType.SELECT, - description: "How to display usernames and nicks", - options: [ - { label: "Username then nickname", value: "user-nick", default: true }, - { label: "Nickname then username", value: "nick-user" }, - { label: "Username only", value: "user" }, - ], + replies: { + type: OptionType.BOOLEAN, + default: false, + description: "Also display extra names in replies.", }, - displayNames: { + mentions: { type: OptionType.BOOLEAN, - description: "Use display names in place of usernames", - default: false + default: false, + description: "Also display extra names in mentions.", + restartNeeded: true }, - inReplies: { + hideDefaultAtSign: { type: OptionType.BOOLEAN, default: false, - description: "Also apply functionality to reply previews", + description: "Hide the default '@' symbol before the name in mentions and replies. Only applied if either feature is enabled.", + }, + respectStreamerMode: { + type: OptionType.BOOLEAN, + default: true, + description: "Truncate usernames in Streamer Mode as Discord does everywhere else.", + }, + removeDuplicates: { + type: OptionType.BOOLEAN, + default: true, + description: "If any of the names are equivalent, remove them, leaving only the unique names.", + }, + includedNames: { + type: OptionType.STRING, + description: "The order to display usernames, nicknames, and display names. Use the following placeholders: {nick}, {display}, {user}. You can have up to three prefixes and three suffixes per name.", + default: "{nick} [{display}] (@{user})", + isValid: validTemplate, + }, + nicknameColor: { + type: OptionType.STRING, + description: "The color to use for the nickname. Leave blank for default. Accepts hex(a), rgb(a), or hsl(a) input. Use \"Role\" to follow the user's top role color. Use \"Role+-#\" to adjust the brightness by that percentage (ex: \"Role+15\")", + default: "Role-25", + isValid: validColor, }, + displayNameColor: { + type: OptionType.STRING, + description: "The color to use for the display name. Leave blank for default. Accepts hex(a), rgb(a), or hsl(a) input. Use \"Role\" to follow the user's top role color. Use \"Role+-#\" to adjust the brightness by that percentage (ex: \"Role+15\")", + default: "Role-25", + isValid: validColor, + }, + usernameColor: { + type: OptionType.STRING, + description: "The color to use for the username. Leave blank for default. Accepts hex(a), rgb(a), or hsl(a) input. Use \"Role\" to follow the user's top role color. Use \"Role+-#\" to adjust the brightness by that percentage (ex: \"Role+15\")", + default: "Role-25", + isValid: validColor, + } }); export default definePlugin({ name: "ShowMeYourName", - description: "Display usernames next to nicks, or no nicks at all", - authors: [Devs.Rini, Devs.TheKodeToad], + description: "Display any permutation of nicknames, display names, and usernames in chat.", + authors: [Devs.Rini, Devs.TheKodeToad, Devs.Etorix, Devs.sadan], patches: [ { find: '?"@":""', @@ -54,31 +158,126 @@ export default definePlugin({ replace: "$self.renderUsername(arguments[0])}" } }, + { + find: "missing user\"", + predicate: () => settings.store.mentions, + replacement: { + match: /"@"\.concat\(null!=(\i)\?\i:(\i)\)/, + replace: "$self.renderUsername(arguments[0])" + } + }, ], settings, - renderUsername: ErrorBoundary.wrap(({ author, message, isRepliedMessage, withMentionPrefix, userOverride }: UsernameProps) => { + renderUsername: ErrorBoundary.wrap((props: any) => { + const { replies, hideDefaultAtSign, respectStreamerMode, removeDuplicates, includedNames, nicknameColor, displayNameColor, usernameColor } = settings.use(); + + const renderType = props.className === "mention" ? "mention" : "message"; + let author: any = null; + let isRepliedMessage = false; + let mentionSymbol = ""; + + if (renderType === "mention") { + const channel = ChannelStore.getChannel(props.channelId) || {}; + const usr = UserStore.getUser(props.userId) || {}; + const mem = GuildMemberStore.getMember(channel.guild_id, props.userId) || {}; + author = usr && mem ? { ...usr, ...mem } : usr || mem || null; + isRepliedMessage = false; + mentionSymbol = hideDefaultAtSign ? "" : "@"; + } else if (renderType === "message") { + // props.message.author only has a globalName attribute. + // props.author only has a nick attribute, but it is overwritten by the globalName if no nickname is set. + // getUser only has a globalName attribute. + // getMember only has a nick attribute, and it is null if no nickname is set. + // Therefore just using the author props is not enough for an accurate result and we instead need to combine the results of getUser and getMember. + const channel = ChannelStore.getChannel(props.message.channel_id) || {}; + const athr = props.userOverride ? props.userOverride : props.message.author; + const usr = UserStore.getUser(athr.id) || {}; + const mem = GuildMemberStore.getMember(channel.guild_id, athr.id) || {}; + author = usr && mem ? { ...usr, ...mem } : usr || mem || null; + isRepliedMessage = props.isRepliedMessage; + mentionSymbol = hideDefaultAtSign ? "" : props.withMentionPrefix ? "@" : ""; + } + + if (!author) { + return <>{mentionSymbol}Unknown; + } + + const username = StreamerModeStore.enabled && respectStreamerMode ? author.username[0] + "..." : author.username; + const display = StreamerModeStore.enabled && respectStreamerMode && author.globalName?.toLowerCase() === author.username.toLowerCase() ? author.globalName[0] + "..." : author.globalName || ""; + const nick = StreamerModeStore.enabled && respectStreamerMode && author.nick?.toLowerCase() === author.username.toLowerCase() ? author.nick[0] + "..." : author.nick || ""; + try { - const user = userOverride ?? message.author; - let { username } = user; - if (settings.store.displayNames) - username = (user as any).globalName || username; + if (isRepliedMessage && !replies) { + return <>{mentionSymbol}{nick || display || username}; + } + + const textMutedValue = getComputedStyle(document.documentElement)?.getPropertyValue("--text-muted")?.trim() || "#72767d"; + const affixes = parseAffixes(includedNames); + const resolvedUsernameColor = resolveColor(author, usernameColor.trim(), textMutedValue); + const resolvedNicknameColor = resolveColor(author, nicknameColor.trim(), textMutedValue); + const resolvedDisplayNameColor = resolveColor(author, displayNameColor.trim(), textMutedValue); + const affixColor = { color: getComputedStyle(document.documentElement)?.getPropertyValue("--text-muted")?.trim() || "#72767d" }; - const { nick } = author; - const prefix = withMentionPrefix ? "@" : ""; + const values = { + "user": { "value": username, "prefix": affixes.user.prefix, "suffix": affixes.user.suffix, "color": resolvedUsernameColor }, + "display": { "value": display, "prefix": affixes.display.prefix, "suffix": affixes.display.suffix, "color": resolvedDisplayNameColor }, + "nick": { "value": nick, "prefix": affixes.nick.prefix, "suffix": affixes.nick.suffix, "color": resolvedNicknameColor } + }; - if (isRepliedMessage && !settings.store.inReplies || username.toLowerCase() === nick.toLowerCase()) - return <>{prefix}{nick}; + let { order } = affixes; + order.includes("nick") && !values.nick.value && !order.includes("display") && values.display.value ? order[order.indexOf("nick")] = "display" : null; + order.includes("display") && !values.display.value && !order.includes("user") && values.user.value ? order[order.indexOf("display")] = "user" : null; + order = order.filter((name: string) => values[name].value); - if (settings.store.mode === "user-nick") - return <>{prefix}{username} {nick}; + const first = order.shift() || "user"; + let second = order.shift() || null; + let third = order.shift() || null; - if (settings.store.mode === "nick-user") - return <>{prefix}{nick} {username}; + if (removeDuplicates) { + // If third is the same as second, remove it, unless third is the username, then prioritize it. + second && third && values[third].value.toLowerCase() === values[second].value.toLowerCase() ? third === "user" ? second = null : third = null : null; + // If second is the same as first, remove it. + second && values[second].value.toLowerCase() === values[first].value.toLowerCase() ? second = null : null; + // If third is the same as first, remove it. + third && values[third].value.toLowerCase() === values[first].value.toLowerCase() ? third = null : null; + } - return <>{prefix}{username}; - } catch { - return <>{author?.nick}; + return ( + <> + {mentionSymbol && {mentionSymbol}} + {( + + {values[first].value} + + )} + {second && ( + +   + + {values[second].prefix} + + {values[second].value} + + {values[second].suffix} + + )} + {third && ( + +   + + {values[third].prefix} + + {values[third].value} + + {values[third].suffix} + + )} + + ); + } catch (e) { + console.error(e); + return <>{mentionSymbol}{StreamerModeStore.enabled && respectStreamerMode ? ((nick || display || username)[0] + "...") : (nick || display || username)}; } }, { noop: true }), }); diff --git a/src/plugins/showMeYourName/styles.css b/src/plugins/showMeYourName/styles.css deleted file mode 100644 index 7a1455d908..0000000000 --- a/src/plugins/showMeYourName/styles.css +++ /dev/null @@ -1,11 +0,0 @@ -.vc-smyn-suffix { - color: var(--text-muted); -} - -.vc-smyn-suffix::before { - content: "("; -} - -.vc-smyn-suffix::after { - content: ")"; -} diff --git a/src/utils/constants.ts b/src/utils/constants.ts index e758259125..2be25440c4 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -579,6 +579,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "jamesbt365", id: 158567567487795200n, }, + Etorix: { + name: "Etorix", + id: 94597845868355584n, + }, } satisfies Record); // iife so #__PURE__ works correctly