diff --git a/src/equicordplugins/globalNicknames/index.tsx b/src/equicordplugins/globalNicknames/index.tsx deleted file mode 100644 index e0c71c174..000000000 --- a/src/equicordplugins/globalNicknames/index.tsx +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Vencord, a Discord client mod - * Copyright (c) 2024 Vendicated and contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -import { NavContextMenuPatchCallback } from "@api/ContextMenu"; -import { DataStore } from "@api/index"; -import { definePluginSettings } from "@api/Settings"; -import { Button } from "@components/Button"; -import ErrorBoundary from "@components/ErrorBoundary"; -import { Heading } from "@components/Heading"; -import { EquicordDevs } from "@utils/constants"; -import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/modal"; -import definePlugin, { OptionType } from "@utils/types"; -import { Message, User } from "@vencord/discord-types"; -import { findComponentByCodeLazy } from "@webpack"; -import { Menu, React, TextInput, UserStore } from "@webpack/common"; - -const UserBoxIcon = findComponentByCodeLazy("0-3-3H5Zm10 6a3"); - -type NicknamesData = Record; - -let nicknames: NicknamesData = {}; - -const settings = definePluginSettings({ - enableMessages: { - type: OptionType.BOOLEAN, - description: "Show global nicknames in chat messages.", - default: true, - restartNeeded: true - }, - enableMemberList: { - type: OptionType.BOOLEAN, - description: "Show global nicknames in the member list.", - default: true, - restartNeeded: true - }, - enableVoiceChannels: { - type: OptionType.BOOLEAN, - description: "Show global nicknames in voice channels.", - default: true, - restartNeeded: true - }, - enableTypingIndicator: { - type: OptionType.BOOLEAN, - description: "Show global nicknames in the typing indicator.", - default: true, - restartNeeded: true - } -}); - -function NicknameModal({ modalProps, user }: { modalProps: ModalProps; user: User; }) { - const [value, setValue] = React.useState(nicknames[user.id] ?? ""); - - return ( - - - - Change Nickname - - - - -
- - Nickname - - -
-
- - - - -
- ); -} - -const userContextPatch: NavContextMenuPatchCallback = (children, { user }) => { - if (!user) return; - - const currentUser = UserStore.getCurrentUser(); - if (!currentUser || user.id === currentUser.id) return; - - children.push( - , - nicknames[user.id] ? ( - { - delete nicknames[user.id]; - await DataStore.set("GlobalNicknames", nicknames); - }} - /> - ) : ( - openModal(props => ( - - - - ))} - /> - ) - ); -}; - -export default definePlugin({ - name: "GlobalNicknames", - description: "Set custom nicknames for any user globally.", - authors: [EquicordDevs.Prism], - dependencies: ["ContextMenuAPI"], - settings, - - patches: [ - { - find: '="SYSTEM_TAG"', - predicate: () => settings.store.enableMessages, - replacement: { - match: /children:(.+?),"data-text":/, - replace: 'children:$self.patchMessageName(arguments[0])??($1),"data-text":' - } - }, - { - find: "let{colorRoleName:", - predicate: () => settings.store.enableMemberList, - replacement: { - match: /,name:(null!=\i\?\i:\i),colorStrings/, - replace: ",name:$self.patchName($1,arguments[0].user)||($1),colorStrings" - } - }, - { - find: ",connectUserDragSource:", - predicate: () => settings.store.enableVoiceChannels, - replacement: { - match: /nick:(\i),collapsed/, - replace: "nick:$self.patchName($1,arguments[0].user)||$1,collapsed" - } - }, - { - find: "#{intl::THREE_USERS_TYPING}", - predicate: () => settings.store.enableTypingIndicator, - replacement: { - match: /(\i\.\i\.getName\(\i\.guild_id,\i\.id),(\i)\)\)/, - replace: "$1,$self.patchTypingName($2)))" - } - } - ], - - async start() { - const data = await DataStore.get("GlobalNicknames"); - nicknames = data ?? {}; - }, - - patchMessageName(props: { message: Message; }): string | null { - const userId = props?.message?.author?.id; - return userId ? nicknames[userId] ?? null : null; - }, - - patchName(_: string, user: User): string | null { - return nicknames[user.id] ?? null; - }, - - patchTypingName(user: User): User { - const nickname = nicknames[user.id]; - if (!nickname) return user; - return { ...user, globalName: nickname, username: nickname } as User; - }, - - contextMenus: { - "user-context": userContextPatch - } -}); diff --git a/src/plugins/showMeYourName/index.tsx b/src/plugins/showMeYourName/index.tsx index 6f6234525..30ce3c6e8 100644 --- a/src/plugins/showMeYourName/index.tsx +++ b/src/plugins/showMeYourName/index.tsx @@ -4,6 +4,7 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +import { DataStore } from "@api/index"; import "./style.css"; import { definePluginSettings, Settings } from "@api/Settings"; @@ -11,15 +12,21 @@ import { Devs, EquicordDevs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { GuildMember, Message, User } from "@vencord/discord-types"; import { findByCodeLazy, findStoreLazy } from "@webpack"; -import { ChannelStore, GuildMemberStore, GuildStore, MessageStore, RelationshipStore, StreamerModeStore, useEffect, UserStore } from "@webpack/common"; +import { ChannelStore, GuildMemberStore, GuildStore, Menu, MessageStore, RelationshipStore, StreamerModeStore, TextInput, useEffect, UserStore, useState } from "@webpack/common"; import { JSX } from "react"; +import { findGroupChildrenByChildId, NavContextMenuPatchCallback } from "@api/ContextMenu"; +import { Button, ErrorBoundary, Heading, TextButton } from "@components/index"; +import { ModalCloseButton, ModalContent, ModalFooter, ModalHeader, ModalProps, ModalRoot, openModal } from "@utils/index"; const wrapEmojis = findByCodeLazy("lastIndex;return"); 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; -const templatePattern = /(?:\{(?:friend|nick|display|user)(?:,\s*(?:friend|nick|display|user))*\})/iu; +const templatePattern = /(?:\{(?:custom|friend|nick|display|user)(?:,\s*(?:custom|friend|nick|display|user))*\})/iu; + +type CustomNicknameData = Record; +let customNicknames: CustomNicknameData = {}; function adjustHex(color: string, percent: number): string { let hex = color.replace("#", ""); @@ -208,8 +215,8 @@ function validTemplate(value: string) { suffix.length > 3 || (!!prefix && !symbolPattern.test(prefix)) || (!!suffix && !symbolPattern.test(suffix)) || - targetProcessedNames.length > 4 || - targetProcessedNames.some(name => !["friend", "nick", "display", "user"].includes(name.trim())) + targetProcessedNames.length > 5 || + targetProcessedNames.some(name => !["custom", "friend", "nick", "display", "user"].includes(name.trim())) ); }); @@ -218,7 +225,13 @@ function validTemplate(value: string) { } } -function getProcessedNames(author: any, truncateAllNamesWithStreamerMode: boolean, discriminators: boolean): [string | null, string | null, string | null, string | null] { +function getProcessedNames(author: any, truncateAllNamesWithStreamerMode: boolean, discriminators: boolean): { + username: string | null; + display: string | null; + nick: string | null; + friend: string | null; + custom: string | null; +} { let discriminator: string | null = null; if (discriminators) { @@ -254,7 +267,13 @@ function getProcessedNames(author: any, truncateAllNamesWithStreamerMode: boolea ? friendName[0] + "..." : friendName as string; - return [username, display, nick, friend]; + const customName: string | null = customNicknames[author.id] || null; + const custom: string | null = !customName ? null + : StreamerModeStore.enabled && (truncateAllNamesWithStreamerMode || customName.toLowerCase() === author.username.toLowerCase()) + ? customName[0] + "..." + : customName; + + return { username, display, nick, friend, custom }; } interface mentionProps { @@ -351,8 +370,8 @@ 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", "friendNameColor", "nicknameColor", "displayNameColor", "usernameColor", "nameSeparator", "triggerNameRerender"]); - const { messages, replies, mentions, memberList, profilePopout, reactions, discriminators, hideDefaultAtSign, truncateAllNamesWithStreamerMode, removeDuplicates, ignoreGradients, ignoreFonts, animateGradients, includedNames, friendNameColor, nicknameColor, displayNameColor, usernameColor, nameSeparator, triggerNameRerender } = config; + 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 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"; @@ -361,14 +380,16 @@ function renderUsername( 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 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] = getProcessedNames(author, truncateAllNamesWithStreamerMode, discriminators); + const { username, display, nick, friend, custom } = getProcessedNames(author, truncateAllNamesWithStreamerMode, discriminators); const names: Record = { user: [username, resolvedUsernameColor], display: [display, resolvedDisplayNameColor], nick: [nick, resolvedNicknameColor], friend: [friend, resolvedFriendNameColor], + custom: [custom, resolvedCustomNameColor] }; const outputs: any[] = []; @@ -421,6 +442,7 @@ function renderUsername( let second = outputs.shift(); let third = outputs.shift(); let fourth = outputs.shift(); + let fifth = outputs.shift(); const firstValueWrapped = hookless ? (first.name || "") @@ -438,10 +460,15 @@ function renderUsername( ((fourth ?? {}).name || "") : wrapEmojis((fourth ?? {}).name || ""); + const fifthValueWrapped = hookless ? + ((fifth ?? {}).name || "") + : wrapEmojis((fifth ?? {}).name || ""); + first.wrapped = firstValueWrapped; second && (second.wrapped = secondValueWrapped); third && (third.wrapped = thirdValueWrapped); fourth && (fourth.wrapped = fourthValueWrapped); + fifth && (fifth.wrapped = fifthValueWrapped); if (isMessage && !messages) { return [null, null, null]; @@ -462,10 +489,14 @@ function renderUsername( return [fallbackText, <>{fallbackText}, fallbackText]; } - const prioritizeUsername = (new Set([first, second, third, fourth].filter(Boolean).map(pos => pos.name.toLowerCase())).size > 1); + const prioritizeUsername = (new Set([first, second, third, fourth, fifth].filter(Boolean).map(pos => pos.name.toLowerCase())).size > 1); if (removeDuplicates) { // Remove duplicates from back to front. Prioritize the earlier name, unless it's the username and there's more than one unique option, then prioritize it. + fifth && fourth && fifth.name.toLowerCase() === fourth.name.toLowerCase() ? fifth.type === "user" && prioritizeUsername ? fourth = null : fifth = null : null; + fifth && third && fifth.name.toLowerCase() === third.name.toLowerCase() ? fifth.type === "user" && prioritizeUsername ? third = null : fifth = null : null; + fifth && second && fifth.name.toLowerCase() === second.name.toLowerCase() ? fifth.type === "user" && prioritizeUsername ? second = null : fifth = null : null; + fifth && first && fifth.name.toLowerCase() === first.name.toLowerCase() ? fifth.type === "user" && prioritizeUsername ? first = null : fifth = null : null; fourth && third && fourth.name.toLowerCase() === third.name.toLowerCase() ? fourth.type === "user" && prioritizeUsername ? third = null : fourth = null : null; fourth && second && fourth.name.toLowerCase() === second.name.toLowerCase() ? fourth.type === "user" && prioritizeUsername ? second = null : fourth = null : null; fourth && first && fourth.name.toLowerCase() === first.name.toLowerCase() ? fourth.type === "user" && prioritizeUsername ? first = null : fourth = null : null; @@ -474,11 +505,12 @@ function renderUsername( second && first && second.name.toLowerCase() === first.name.toLowerCase() ? second.type === "user" && prioritizeUsername ? first = null : second = null : null; } - const remainingNames = [first, second, third, fourth].filter(Boolean); + const remainingNames = [first, second, third, fourth, fifth].filter(Boolean); first = remainingNames.shift(); second = remainingNames.shift(); third = remainingNames.shift(); fourth = remainingNames.shift(); + fifth = remainingNames.shift(); const useTopRoleStyle = isMention || isReactionsPopout; const topRoleStyle = resolveColor(author, "Role", "", canUseGradient); @@ -500,7 +532,8 @@ function renderUsername( 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 allDataText = [firstDataText, secondDataText, thirdDataText, fourthDataText].filter(Boolean).join(nameSeparator).trim(); + const fifthDataText = fifth && shouldAnimateSecondaryNames ? (fifth.prefix + fifth.name + fifth.suffix) : ""; + 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" : ""); @@ -512,10 +545,12 @@ function renderUsername( 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"; @@ -549,85 +584,39 @@ function renderUsername( {first.wrapped} )} - {second && ( + {[ + { name: second, dataText: secondDataText, groupClass: secondGroupClasses, nameClass: secondNameClasses, position: "second" }, + { name: third, dataText: thirdDataText, groupClass: thirdGroupClasses, nameClass: thirdNameClasses, position: "third" }, + { name: fourth, dataText: fourthDataText, groupClass: fourthGroupClasses, nameClass: fourthNameClasses, position: "fourth" }, + { name: fifth, dataText: fifthDataText, groupClass: fifthGroupClasses, nameClass: fifthNameClasses, position: "fifth" } + ].map(({ name, dataText, groupClass, nameClass, position }) => name && ( {nameSeparator} - {second.prefix} + {name.prefix} - {second.wrapped} + {name.wrapped} - {second.suffix} + {name.suffix} - )} - {third && ( - - - {nameSeparator} - {third.prefix} - - {third.wrapped} - - {third.suffix} - - )} - {fourth && ( - - - {nameSeparator} - {fourth.prefix} - - {fourth.wrapped} - - {fourth.suffix} - - )} + ))} ); @@ -688,6 +677,98 @@ function removeHoveringReactionPopout(id: string) { settings.store.triggerNameRerender = !settings.store.triggerNameRerender; } +function CustomNicknameModal({ modalProps, user }: { modalProps: ModalProps; user: User; }) { + const [value, setValue] = useState(customNicknames[user.id] ?? ""); + + return ( + + + + {customNicknames[user.id] ? "Change SMYN Nickname" : "Add SMYN Nickname"} + + + + + + {"Set a custom SMYN nickname for this user. Make use of it by specifying {custom} in the SMYN template settings."} + +
+ + SMYN Nickname + + + { + setValue(""); + delete customNicknames[user.id]; + await DataStore.set("SMYNCustomNicknames", customNicknames); + settings.store.triggerNameRerender = !settings.store.triggerNameRerender; + }} + > + Reset SMYN Nickname + +
+
+ + + + +
+ ); +} + +const userContextPatch: NavContextMenuPatchCallback = (children, { user }) => { + if (!user) return; + + const currentUser = UserStore.getCurrentUser(); + if (!currentUser || user.id === currentUser.id) return; + + const group = findGroupChildrenByChildId("user-profile", children); + + !group && children.push(); + (group || children).push( + openModal(props => ( + + + + ))} + /> + ); +}; + const settings = definePluginSettings({ messages: { type: OptionType.BOOLEAN, @@ -747,17 +828,17 @@ const settings = definePluginSettings({ ignoreFonts: { type: OptionType.BOOLEAN, default: false, - description: "For the second, third, and fourth names, use GG SANS regardless of the user's custom font.", + description: "For the non-primary names, use Discord's default fonts regardless of the user's custom nitro font.", }, ignoreGradients: { type: OptionType.BOOLEAN, default: true, - description: "For the second, third, and fourth names, if the role has a gradient, ignore it in favor of the value below.", + description: "For the non-primary names, if the role has a gradient, ignore it in favor of the color set below.", }, animateGradients: { type: OptionType.BOOLEAN, default: false, - description: "For the second, third, and fourth 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, animate it. This is disabled by \"Ignore Gradients\" and reduced motion.", }, nameSeparator: { type: OptionType.STRING, @@ -766,10 +847,16 @@ const settings = definePluginSettings({ }, includedNames: { type: OptionType.STRING, - description: "The order to display usernames, display names, nicknames, and friend names. Use the following placeholders: {user}, {display}, {nick}, {friend}. You can provide multiple name options to use as fallbacks if one is unavailable by separating them with commas as such: {friend, nick, display}. You can have up to three prefixes and three suffixes per name.", - default: "{friend, nick} [{display}] (@{user})", + 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.", + default: "{custom, friend, nick} [{display}] (@{user})", isValid: validTemplate, }, + customNameColor: { + type: OptionType.STRING, + description: "The color to use for the custom name you assigned a user if it's not the first displayed. 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, + }, friendNameColor: { type: OptionType.STRING, description: "The color to use for a friend's nickname if it's not the first displayed. 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\")", @@ -804,8 +891,9 @@ const settings = definePluginSettings({ export default definePlugin({ name: "ShowMeYourName", - description: "Display any permutation of nicknames, display names, and usernames in chat.", - authors: [Devs.Rini, Devs.TheKodeToad, EquicordDevs.Etorix, Devs.sadan], + description: "Display any permutation of custom nicknames, friend nicknames, server nicknames, display names, and usernames in chat.", + authors: [Devs.Rini, Devs.TheKodeToad, EquicordDevs.Etorix, Devs.sadan, EquicordDevs.Prism], + tags: ["SMYN", "Nicknames", "Custom Nicknames",], isModified: true, settings, @@ -943,6 +1031,15 @@ export default definePlugin({ } ], + async start() { + const data = await DataStore.get("SMYNCustomNicknames"); + customNicknames = data ?? {}; + }, + + contextMenus: { + "user-context": userContextPatch + }, + flux: { RELATIONSHIP_UPDATE(data) { // Allows rerendering when changing friend names. diff --git a/src/plugins/showMeYourName/style.css b/src/plugins/showMeYourName/style.css index 58bd29db7..45cfb18a0 100644 --- a/src/plugins/showMeYourName/style.css +++ b/src/plugins/showMeYourName/style.css @@ -37,4 +37,14 @@ .show-me-your-name-gradient-hovered::after { opacity: .7; transition: opacity .1s ease-in-out +} + +.show-me-your-name-modal-footer-container button { + width: 100%; +} + +.show-me-your-name-reset-button { + margin-top: 8px; + font-size: 14px; + font-weight: 500; } \ No newline at end of file