diff --git a/src/plugins/enhancedUserTags/README.md b/src/plugins/enhancedUserTags/README.md new file mode 100644 index 000000000..04a5ed7c3 --- /dev/null +++ b/src/plugins/enhancedUserTags/README.md @@ -0,0 +1,12 @@ +# EnhancedUserTags + +Replaces and extends default tags (Official, Original Poster, etc.) with a crown icon, the type of which depends on the user's permissions + +## Preview + +![preview](https://i.imgur.com/HRnjNPB.png) +![preview](https://i.imgur.com/KUP5K9S.png) +![preview](https://i.imgur.com/He6sLru.png) +![preview](https://i.imgur.com/IBlGqg4.png) +![preview](https://i.imgur.com/6ORXZj8.png) +![preview](https://i.imgur.com/qkmmYoi.png) diff --git a/src/plugins/enhancedUserTags/colors.ts b/src/plugins/enhancedUserTags/colors.ts new file mode 100644 index 000000000..99c57957a --- /dev/null +++ b/src/plugins/enhancedUserTags/colors.ts @@ -0,0 +1,67 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { get, set } from "@api/DataStore"; + +import { createColoredCrownIcon, tagIcons } from "./components/Icons"; +import { TAGS } from "./tag"; +import { ColoredTag, CSSHex, CustomColoredTag, TagColors } from "./types"; + +const STORE_KEY = "EnhancedUserTagColors"; + +const DEFAULT_TAG_COLORS: TagColors = { + [TAGS.THREAD_CREATOR]: "#D9A02D", + [TAGS.POST_CREATOR]: "#D9A02D", + [TAGS.MODERATOR]: "#AA6000", + [TAGS.ADMINISTRATOR]: "#E0E0E0", + [TAGS.GROUP_OWNER]: "#D9A02D", + [TAGS.GUILD_OWNER]: "#D9A02D", + + [TAGS.BOT]: "#0BDA51", + [TAGS.WEBHOOK]: "#5865F2", +}; + +const tagColors: TagColors = new Proxy({ ...DEFAULT_TAG_COLORS }, { + // auto recreate tags on color change + // mb there's some way to re-render component by providing props into React.memo instead recreating it + // but sadly don't have much exp with react + set: (target, tag, color) => { + // no need to recreate component if color have no changes + if (color !== target[tag]) { + target[tag] = color; + tagIcons[tag] = createColoredCrownIcon(tag as any as CustomColoredTag); + } + + return true; + }, +}); + +async function initColors() { + const savedColors = await get>(STORE_KEY); + + if (!savedColors) { + await set(STORE_KEY, { ...tagColors }); + } else { + Object.assign(tagColors, savedColors); + } +} +initColors(); + +export const getColor = (tag: ColoredTag): CSSHex => { + return tagColors[tag]; +}; + +export const setColor = async (tag: CustomColoredTag, color: CSSHex) => { + tagColors[tag] = color; + + await set(STORE_KEY, { ...tagColors }); +}; + +export const resetColor = async (tag: CustomColoredTag) => { + tagColors[tag] = DEFAULT_TAG_COLORS[tag]; + + await set(STORE_KEY, { ...tagColors }); +}; diff --git a/src/plugins/enhancedUserTags/components/ColorSettings.tsx b/src/plugins/enhancedUserTags/components/ColorSettings.tsx new file mode 100644 index 000000000..3b1f4bd2b --- /dev/null +++ b/src/plugins/enhancedUserTags/components/ColorSettings.tsx @@ -0,0 +1,139 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { findComponentByCodeLazy } from "@webpack"; +import { Button, Forms, Text, useMemo, useState } from "@webpack/common"; + +import { getColor, resetColor, setColor } from "../colors"; +import { TAG_NAMES, TAGS } from "../tag"; +import { CustomColoredTag } from "../types"; +import { hex2number, number2hex } from "../util/hex"; +import { ColorlessCrownIcon, HalfedCrownIcon } from "./Icons"; + +const ColorPicker = findComponentByCodeLazy(".Messages.USER_SETTINGS_PROFILE_COLOR_SELECT_COLOR", ".BACKGROUND_PRIMARY)"); + +const SUGGESTED_COLORS = [ + "#AA6000", "#E0E0E0", "#D9A02D", "#0BDA51", "#5865F2", + "#5865F2", "#C41E3A", "#BF40BF", "#5D3FD3", "#B2BEB5", +]; + +export default function ColorSettings() { + return ( +
+ Custom Tags Color +
+ { + ([TAGS.MODERATOR, TAGS.ADMINISTRATOR, TAGS.BOT, TAGS.WEBHOOK] as CustomColoredTag[]).map(tag => { + const [curColor, setCurColor] = useState(hex2number(getColor(tag))); + const crownStyle = useMemo(() => { + return { + display: "flex", + position: "relative", + width: "16px", + height: "16px", + alignSelf: "center", + color: number2hex(curColor) + }; + }, [curColor]); + + return ( +
+ + {TAG_NAMES[tag]} + +
+ + + + { + tag !== TAGS.WEBHOOK ? null : + + { + + } + + } +
+
+ +
+ + +
+
+
+ ); + }) + } +
+
+ ); +} diff --git a/src/plugins/enhancedUserTags/components/EnhancedUserTag.tsx b/src/plugins/enhancedUserTags/components/EnhancedUserTag.tsx new file mode 100644 index 000000000..91c14e53c --- /dev/null +++ b/src/plugins/enhancedUserTags/components/EnhancedUserTag.tsx @@ -0,0 +1,46 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Tooltip } from "@webpack/common"; + +import { TagDetails } from "../tag"; +import { HalfedCrownIcon } from "./Icons"; + +export default function EnhancedUserTag(tagDetails: TagDetails & { + style: React.CSSProperties; +}) { + // only original discord tags have no text + // and them already have `span` wrapper with styles so no need to wrap into extra one + if (!tagDetails.text) + return ; + + // for custom official tag + if (tagDetails.gap) + tagDetails.style.gap = "4px"; + + return + {({ onMouseEnter, onMouseLeave }) => ( + + + { + tagDetails.halfGold ? : null + } + + )} + ; +} diff --git a/src/plugins/enhancedUserTags/components/Icons.tsx b/src/plugins/enhancedUserTags/components/Icons.tsx new file mode 100644 index 000000000..215879682 --- /dev/null +++ b/src/plugins/enhancedUserTags/components/Icons.tsx @@ -0,0 +1,144 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { LazyComponent } from "@utils/lazyReact"; +import { React, Tooltip } from "@webpack/common"; + +import { getColor } from "../colors"; +import { TAGS } from "../tag"; +import { ColoredTag } from "../types"; + +export const createColoredCrownIcon = (tag: ColoredTag) => LazyComponent( + () => React.memo( + () => ( + + + + ) + ) +); + +export const ColorlessCrownIcon = LazyComponent( + () => React.memo( + () => ( + + + + ) + ) +); + +export const HalfedCrownIcon = LazyComponent( + () => React.memo( + () => ( + + + + ) + ) +); + +export const ClydeIcon = LazyComponent( + () => React.memo( + () => ( + + + + + + + + + + + ) + ) +); + +// that's maximum of my creative vision :) +export const OfficialIcon = LazyComponent( + () => React.memo( + () => ( + <> + + + + + + + + ) + ) +); + +export const AutoModIcon = LazyComponent( + () => React.memo( + () => ( + <> + + + + + + + + + + + + ) + ) +); + +export const ErrorIcon = LazyComponent( + () => React.memo( + () => ( + ( + + {({ onMouseEnter, onMouseLeave }) => ( + + + + + + + )} + + ) + ) + ) +); + +export const tagIcons = { + [TAGS.THREAD_CREATOR]: createColoredCrownIcon(TAGS.THREAD_CREATOR), + [TAGS.POST_CREATOR]: createColoredCrownIcon(TAGS.POST_CREATOR), + [TAGS.MODERATOR]: createColoredCrownIcon(TAGS.MODERATOR), + [TAGS.ADMINISTRATOR]: createColoredCrownIcon(TAGS.ADMINISTRATOR), + [TAGS.GROUP_OWNER]: createColoredCrownIcon(TAGS.GROUP_OWNER), + [TAGS.GUILD_OWNER]: createColoredCrownIcon(TAGS.GUILD_OWNER), + + [TAGS.BOT]: createColoredCrownIcon(TAGS.BOT), + [TAGS.WEBHOOK]: createColoredCrownIcon(TAGS.WEBHOOK), + [TAGS.CLYDE]: ClydeIcon, + [TAGS.AUTOMOD]: AutoModIcon, + [TAGS.OFFICIAL]: OfficialIcon, +}; diff --git a/src/plugins/enhancedUserTags/components/OriginalSystemTag.tsx b/src/plugins/enhancedUserTags/components/OriginalSystemTag.tsx new file mode 100644 index 000000000..99ee4a34c --- /dev/null +++ b/src/plugins/enhancedUserTags/components/OriginalSystemTag.tsx @@ -0,0 +1,67 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { LazyComponent } from "@utils/lazyReact"; +import { findByCodeLazy, findByPropsLazy } from "@webpack"; +import { React } from "@webpack/common"; + +const OriginalDiscordTag: ({ + className, + type, + verified, + useRemSizes, +}: { + className: string; + type: number; + verified?: boolean; + useRemSizes?: boolean; +}) => React.JSX.Element = findByCodeLazy(".Messages.DISCORD_SYSTEM_MESSAGE_BOT_TAG_TOOLTIP_OFFICIAL", ".SYSTEM_DM_TAG_OFFICIAL"); + +const DISCORD_TAG_TYPES: { + SYSTEM_DM: 2; +} = findByPropsLazy("SYSTEM_DM", "STAFF_ONLY_DM"); + +const USERNAME_COMPONENT_CLASS_NAMES: { + decorator: string; +} = findByPropsLazy("decorator", "avatarWithText"); + +export const OriginalUsernameSystemTag = LazyComponent( + () => React.memo( + () => + ) +); + +const MESSAGE_COMPONENT_CLASS_NAMES: { + botTagCompact: string; + botTagCozy: string; +} = findByPropsLazy("botTagCompact", "botTagCozy"); + +export const OriginalMessageSystemTag = LazyComponent( + () => React.memo( + () => + ) +); + +const AUTOMOD_MESSAGE_COMPONENT_CLASS_NAMES: { + systemTag: string; +} = findByPropsLazy("systemTag", "alertActionIcon"); + +export const OriginalAutoModMessageTag = LazyComponent( + () => React.memo( + () => + ) +); diff --git a/src/plugins/enhancedUserTags/index.tsx b/src/plugins/enhancedUserTags/index.tsx new file mode 100644 index 000000000..d603ef95f --- /dev/null +++ b/src/plugins/enhancedUserTags/index.tsx @@ -0,0 +1,336 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import ErrorBoundary from "@components/ErrorBoundary"; +import { Devs } from "@utils/constants"; +import { getCurrentGuild } from "@utils/discord"; +import definePlugin from "@utils/types"; +import { ChannelStore, GuildStore } from "@webpack/common"; +import type { Message } from "discord-types/general"; + +import EnhancedUserTag from "./components/EnhancedUserTag"; +import { ErrorIcon, tagIcons } from "./components/Icons"; +import { OriginalAutoModMessageTag, OriginalMessageSystemTag, OriginalUsernameSystemTag } from "./components/OriginalSystemTag"; +import settings from "./settings"; +import { getTagDetails, GetTagDetailsReturn, TAG_NAMES, TAGS } from "./tag"; +import { Channel, User } from "./types"; +import { isAutoContentModerationMessage, isAutoModMessage, isSystemMessage } from "./util/system"; + +const MembersList = ({ + user, + channel, + guildId, +}: { + user: User; + channel: Channel; + guildId: string; +}) => { + const guild = GuildStore.getGuild(guildId); + + const tagDetails = getTagDetails({ + user, + // not sure that in members list need to append permissions from current channel to compute user permissions + channel: guild ? null : channel, + guild, + }); + + if (!tagDetails) return; + + return ; +}; + +const ChannelChat = ({ + message, + compact, + usernameClassName, +}: { + message?: Message; + compact?: boolean; + usernameClassName?: string; +}) => { + if (!message) return; + + const channel = ChannelStore.getChannel(message.channel_id) as Channel; + + if (!channel) return; + + // not sure that much ppl wants group owner icon for a single user in the whole dm group chat + if (channel.isGroupDM()) return; + + let tagDetails: GetTagDetailsReturn; + + // Shows tag also in user message inside AutoMod messages + if (isAutoModMessage(message)) { + // Original official tag for AutoMod is hardcoded and handles in `AutoMod` patch + // so here we apply tag only for users inside `has blocked a message in` message + if (!isAutoContentModerationMessage(message) || !usernameClassName) { + return; + } + + tagDetails = getTagDetails({ + user: message.author as User, + channel, + guild: GuildStore.getGuild(channel.guild_id), + }); + + // Community Updates (isSystemMessage instead .system cos there're two CU bots and for first one .system and etc are false) + } else if (isSystemMessage(message)) { + if (settings.store.originalOfficialTag) + return ; + + tagDetails = { + icon: tagIcons[TAGS.OFFICIAL], + text: `${TAG_NAMES[TAGS.OFFICIAL]} Message`, + gap: true, + }; + } else { + tagDetails = getTagDetails({ + user: message.author as User, + channel, + guild: GuildStore.getGuild(channel.guild_id), + }); + } + + if (!tagDetails) return; + + return ; +}; + +const AutoMod = ({ + compact, +}: { + compact?: boolean; +}) => { + return settings.store.originalAutoModTag ? : ; +}; + +const VoiceChannels = ({ + user, + guildId, +}: { + user: User; + guildId: string; +}) => { + const tagDetails = getTagDetails({ + user, + guild: GuildStore.getGuild(guildId) + }); + + if (!tagDetails) return; + + return ; +}; + +const UserProfile = ({ user, tags }: { + user: User; + tags?: { + props?: { + displayProfile?: { + _guildMemberProfile?: any; + }; + }; + }; +}) => { + // Show nothing in user `Main Profile` + // p.s. there's no other way to check if profile is server or main + const displayProfile = tags?.props?.displayProfile; + const isMainProfile = displayProfile ? !displayProfile._guildMemberProfile : false; + + if (!user.bot && isMainProfile) return; + + const tagDetails = getTagDetails({ + user, + guild: getCurrentGuild() + }); + + if (!tagDetails) return; + + return ; +}; + +const DMsList = ({ + channel, + user +}: { + channel: Channel; + user: User; +}) => { + if (channel.isSystemDM()) + return settings.store.originalOfficialTag ? : ; + + if (user?.bot && settings.store.botTagInDmsList) + return ; +}; + +export default definePlugin({ + name: "EnhancedUserTags", + description: "Replaces and extends default tags (Official, Original Poster, etc.) with a crown icon, the type of which depends on the user's permissions", + authors: [Devs.Vishnya], + + settings, + + patches: [ + // Members List + { + find: ".Messages.GUILD_OWNER,", + replacement: [ + // Remove original owner crown icon + { + match: /=\(\)=>null.{1,80}\.Messages\.GUILD_OWNER.*?\.ownerIcon.*?:null,/, + replace: "=()=>null,", + }, + // Add new tag + { + match: /=\(\)=>{let.{1,30}isClyde.*?isVerifiedBot.*?},/, + replace: "=()=>$self.membersList(arguments[0]),", + }, + ], + }, + // Chat + { + // Remove original tag + find: ".clanTagChiplet,profileViewedAnalytics:", + replacement: [ + { + // Cozy view + match: /:null,null==\i\|\|\i\?null:\i,/, + replace: ":null,", + }, + { + // Compact view + match: /children:\[null!=.{1,50}?" ".*?:null,/, + replace: "children:[", + }, + ], + }, + { + // Add new one + find: ".nitroAuthorBadgeTootip,", + replacement: { + match: /"span",{id:\i.{1,30}?\i}\),/, + replace: "$&$self.channelChat(arguments[0]),", + }, + + }, + { + // AutoMod + find: ".SYSTEM_DM,className", // x5 + all: true, + noWarn: true, + replacement: { + match: /(GUILD_AUTOMOD_USERNAME}\),).{1,30}.SYSTEM_DM.*?}\)/, + replace: "$1$self.autoMod(arguments[0])", + }, + }, + // Guild channels list > Voice user + { + find: ".WATCH_STREAM_WATCHING,", + replacement: { + match: /isSelf:\i}\)}\):null/, + replace: "$&,$self.voiceChannels(this.props)", + }, + }, + // Popout/modal profile + { + find: ".Messages.USER_PROFILE_PRONOUNS", + replacement: { + match: /null!=\i&&\(0.{1,35}.isVerifiedBot\(\)}\)/, + replace: "$self.userProfile(arguments[0]),", + }, + }, + // DMs list + { + find: "PrivateChannel.renderAvatar: Invalid prop configuration", + replacement: { + match: /decorators:\i.isSystemDM\(\).{1,80}}\):null/, + replace: "decorators:$self.dmsList(arguments[0])" + }, + }, + ], + + membersList: ErrorBoundary.wrap(MembersList, { fallback: () => }), + + channelChat: ErrorBoundary.wrap(ChannelChat, { fallback: () => }), + + autoMod: ErrorBoundary.wrap(AutoMod, { fallback: () => }), + + voiceChannels: ErrorBoundary.wrap(VoiceChannels, { fallback: () => }), + + userProfile: ErrorBoundary.wrap(UserProfile, { fallback: () => }), + + dmsList: ErrorBoundary.wrap(DMsList, { fallback: () => }), +}); diff --git a/src/plugins/enhancedUserTags/settings.tsx b/src/plugins/enhancedUserTags/settings.tsx new file mode 100644 index 000000000..0896e7b20 --- /dev/null +++ b/src/plugins/enhancedUserTags/settings.tsx @@ -0,0 +1,44 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import { OptionType } from "@utils/types"; + +import ColorSettings from "./components/ColorSettings"; + +export default definePluginSettings({ + ignoreYourself: { + type: OptionType.BOOLEAN, + description: + "Don't add tag to yourself", + default: false, + }, + botTagInDmsList: { + type: OptionType.BOOLEAN, + description: + "Show bot tag in DMs list", + default: true, + }, + originalOfficialTag: { + type: OptionType.BOOLEAN, + description: + "Use the original `Official Discord` tag for official messages", + default: false, + }, + originalAutoModTag: { + type: OptionType.BOOLEAN, + description: + "Use the original `Official Discord` tag for AutoMod messages", + default: false, + }, + colorSettings: { + type: OptionType.COMPONENT, + description: "", + component: () => { + return ; + } + } +}); diff --git a/src/plugins/enhancedUserTags/tag.tsx b/src/plugins/enhancedUserTags/tag.tsx new file mode 100644 index 000000000..83819003f --- /dev/null +++ b/src/plugins/enhancedUserTags/tag.tsx @@ -0,0 +1,264 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { UserStore } from "@webpack/common"; +import { Guild } from "discord-types/general"; + +import { tagIcons } from "./components/Icons"; +import { OriginalMessageSystemTag, OriginalUsernameSystemTag } from "./components/OriginalSystemTag"; +import settings from "./settings"; +import { Channel, User } from "./types"; +import { computePermissions, MODERATOR_PERMISSIONS_BITS, PERMISSIONS_BITS } from "./util/permissions"; + +export enum TAGS { + THREAD_CREATOR = 1, + POST_CREATOR, + MODERATOR, + ADMINISTRATOR, + GROUP_OWNER, // DM group + GUILD_OWNER, + + BOT, + WEBHOOK, + CLYDE, + AUTOMOD, + OFFICIAL, +} + +export const TAG_NAMES = { + [TAGS.THREAD_CREATOR]: "Thread Creator", + [TAGS.POST_CREATOR]: "Post Creator", + [TAGS.MODERATOR]: "Moderator", + [TAGS.ADMINISTRATOR]: "Administrator", + [TAGS.GROUP_OWNER]: "Group Owner", + [TAGS.GUILD_OWNER]: "Server Owner", + + [TAGS.BOT]: "Bot", + [TAGS.WEBHOOK]: "Webhook", + [TAGS.CLYDE]: "Clyde", + [TAGS.AUTOMOD]: "Official AutoMod Message", + [TAGS.OFFICIAL]: "Official Discord", +}; + +// i18n.MODERATE_MEMBERS and i18n.MANAGE_GUILD is "" so I decided to define them all manually +// Array instead dict to have strict order +const MODERATOR_PERMISSIONS_NAMES: [bigint, string][] = [ + [PERMISSIONS_BITS.MANAGE_GUILD, "Manage Guild"], + [PERMISSIONS_BITS.MANAGE_CHANNELS, "Manage Channels"], + [PERMISSIONS_BITS.MANAGE_ROLES, "Manage Roles"], + [PERMISSIONS_BITS.MANAGE_MESSAGES, "Manage Messages"], + [PERMISSIONS_BITS.BAN_MEMBERS, "Ban Members"], + [PERMISSIONS_BITS.KICK_MEMBERS, "Kick Members"], + [PERMISSIONS_BITS.MODERATE_MEMBERS, "Timeout Members"], +]; + +const permissions2Text = (permissions: bigint): string => { + return MODERATOR_PERMISSIONS_NAMES.filter(([bit]) => permissions & bit) + .map(([, name], i, array) => + i === array.length - 1 ? name : `${name}, ${i % 2 === 0 ? "\n" : ""}` + ) + .join(""); +}; + +export interface TagDetails { + icon: React.ComponentType; + text?: string | null; + gap?: boolean | null; + halfGold?: boolean | null; +} + +export type GetTagDetailsReturn = TagDetails | undefined | null; + +const getTagDeailtsForPostOrThread = ({ + tag, + perms, + isBot, + isGuildOwner, + isAdministrator, + isModerator, +}: { + tag: TAGS.POST_CREATOR | TAGS.THREAD_CREATOR, + perms: bigint; + isBot: boolean; + isGuildOwner: boolean; + isAdministrator: boolean; + isModerator: boolean; +}): GetTagDetailsReturn => { + if (isBot) { + if (isAdministrator) + return { + icon: tagIcons[TAGS.BOT], + text: `${TAG_NAMES[tag]} | ${TAG_NAMES[TAGS.BOT]} (${TAG_NAMES[TAGS.ADMINISTRATOR]})`, + halfGold: true, + }; + + if (isModerator) + return { + icon: tagIcons[TAGS.BOT], + text: `${TAG_NAMES[tag]} | ${TAG_NAMES[TAGS.BOT]} (${permissions2Text(perms)})`, + halfGold: true, + }; + + return { + icon: tagIcons[TAGS.BOT], + text: `${TAG_NAMES[tag]} | ${TAG_NAMES[TAGS.BOT]}`, + halfGold: true, + }; + } + + if (isGuildOwner) + return { + icon: tagIcons[tag], + text: `${TAG_NAMES[tag]} | ${TAG_NAMES[TAGS.GUILD_OWNER]}`, + }; + + if (isAdministrator) + return { + icon: tagIcons[TAGS.ADMINISTRATOR], + text: `${TAG_NAMES[tag]} | ${TAG_NAMES[TAGS.ADMINISTRATOR]}`, + halfGold: true, + }; + + if (isModerator) + return { + icon: tagIcons[TAGS.MODERATOR], + text: `${TAG_NAMES[tag]} | ${TAG_NAMES[TAGS.MODERATOR]} (${permissions2Text(perms)})`, + halfGold: true, + }; + + return { + icon: tagIcons[tag], + text: TAG_NAMES[tag], + }; +}; + +export const getTagDetails = ({ + user, + channel, + guild, +}: { + user?: User | null; + channel?: Channel | null; + guild?: Guild | null; +}): GetTagDetailsReturn => { + if (!user) return; + + if (user.id === UserStore.getCurrentUser().id && settings.store.ignoreYourself) + return; + + if (user.bot) { + if (user.isSystemUser()) { + if (settings.store.originalOfficialTag) { + return { + icon: channel ? OriginalMessageSystemTag : OriginalUsernameSystemTag, + }; + } + + return { + icon: tagIcons[TAGS.OFFICIAL], + text: `${TAG_NAMES[TAGS.OFFICIAL]} ${(channel?.isDM() || guild) ? "Message" : "Account"}`, + gap: true, + }; + } + + if (user.isClyde()) + return { + icon: tagIcons[TAGS.CLYDE], + text: TAG_NAMES[TAGS.CLYDE], + }; + + if (user.isNonUserBot()) + return { + icon: tagIcons[TAGS.WEBHOOK], + text: TAG_NAMES[TAGS.WEBHOOK], + }; + + if (!guild || channel?.isDM() || channel?.isGroupDM()) + return { + icon: tagIcons[TAGS.BOT], + text: TAG_NAMES[TAGS.BOT], + }; + } else { + if (channel?.isGroupDM()) + return channel.ownerId === user.id ? { + icon: tagIcons[TAGS.GROUP_OWNER], + text: TAG_NAMES[TAGS.GROUP_OWNER], + } : null; + + if (channel?.isDM()) return; + + if (!guild) return; + } + + const perms = computePermissions({ + user: user, + context: guild, + overwrites: channel ? channel.permissionOverwrites : null, + }); + + const isGuildOwner = guild.ownerId === user.id; + + const isAdministrator = !!(perms & PERMISSIONS_BITS.ADMINISTRATOR); + + const isModerator = !!(perms & MODERATOR_PERMISSIONS_BITS); + + // I'm 99% sure that only forumPost/thread can have `ownerId` (+ groupDM, but the check for that is above) + // so for 100% I made this shit code :) + // correct me please if my 99% is actually 100% + if (channel && channel.ownerId === user.id) { + if (channel.isForumPost()) + return getTagDeailtsForPostOrThread({ + tag: TAGS.POST_CREATOR, + perms, + isBot: user.bot, + isGuildOwner, + isAdministrator, + isModerator, + }); + + if (channel.isThread()) + return getTagDeailtsForPostOrThread({ + tag: TAGS.THREAD_CREATOR, + perms, + isBot: user.bot, + isGuildOwner, + isAdministrator, + isModerator, + }); + } + + if (isGuildOwner) + return { + icon: tagIcons[TAGS.GUILD_OWNER], + text: TAG_NAMES[TAGS.GUILD_OWNER], + }; + + if (isAdministrator) { + if (user.bot) + return { + icon: tagIcons[TAGS.BOT], + text: `${TAG_NAMES[TAGS.BOT]} (${TAG_NAMES[TAGS.ADMINISTRATOR]})`, + }; + else + return { + icon: tagIcons[TAGS.ADMINISTRATOR], + text: TAG_NAMES[TAGS.ADMINISTRATOR], + }; + } + + if (isModerator) { + if (user.bot) + return { + icon: tagIcons[TAGS.BOT], + text: `${TAG_NAMES[TAGS.BOT]} (${permissions2Text(perms)})`, + }; + else + return { + icon: tagIcons[TAGS.MODERATOR], + text: `${TAG_NAMES[TAGS.MODERATOR]} (${permissions2Text(perms)})`, + }; + } +}; diff --git a/src/plugins/enhancedUserTags/types.d.ts b/src/plugins/enhancedUserTags/types.d.ts new file mode 100644 index 000000000..2f6ee02ef --- /dev/null +++ b/src/plugins/enhancedUserTags/types.d.ts @@ -0,0 +1,29 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import type { Channel as OriginalChannel, User as OriginalUser } from "discord-types/general"; + +import type { TAGS } from "./tag"; + +export type Channel = { + isForumPost(): boolean; + isGroupDM(): boolean; +} & OriginalChannel; + +export type User = { + isClyde(): boolean; + isVerifiedBot(): boolean; +} & OriginalUser; + +export type CSSHex = `#${string}`; + +type ColoredTagName = keyof Omit; +type CustomColoredTagName = Exclude; + +export type ColoredTag = (typeof TAGS)[ColoredTagName]; +export type CustomColoredTag = (typeof TAGS)[CustomColoredTagName]; + +export type TagColors = Record; diff --git a/src/plugins/enhancedUserTags/util/hex.ts b/src/plugins/enhancedUserTags/util/hex.ts new file mode 100644 index 000000000..7c9e9b0a5 --- /dev/null +++ b/src/plugins/enhancedUserTags/util/hex.ts @@ -0,0 +1,11 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { CSSHex } from "../types"; + +export const hex2number = (hex: CSSHex): number => parseInt(hex.slice(1), 16); + +export const number2hex = (num: number): CSSHex => `#${num.toString(16).padStart(6, "0")}`; diff --git a/src/plugins/enhancedUserTags/util/permissions.ts b/src/plugins/enhancedUserTags/util/permissions.ts new file mode 100644 index 000000000..958a583e3 --- /dev/null +++ b/src/plugins/enhancedUserTags/util/permissions.ts @@ -0,0 +1,37 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { findByCodeLazy } from "@webpack"; +import { Guild } from "discord-types/general"; + +import { Channel, User } from "../types"; + +// can't use `PermissionsBits` from `@webpack/common` due to lazy find => throws error in `MODERATOR_PERMISSIONS_BITS` +export const PERMISSIONS_BITS = { + ADMINISTRATOR: 1n << 3n, + MANAGE_GUILD: 1n << 5n, + MANAGE_CHANNELS: 1n << 4n, + MANAGE_ROLES: 1n << 28n, + BAN_MEMBERS: 1n << 2n, + MANAGE_MESSAGES: 1n << 13n, + KICK_MEMBERS: 1n << 1n, + MODERATE_MEMBERS: 1n << 40n, +}; + +export const MODERATOR_PERMISSIONS_BITS = + PERMISSIONS_BITS.MANAGE_GUILD + | PERMISSIONS_BITS.MANAGE_CHANNELS + | PERMISSIONS_BITS.MANAGE_ROLES + | PERMISSIONS_BITS.MANAGE_MESSAGES + | PERMISSIONS_BITS.BAN_MEMBERS + | PERMISSIONS_BITS.KICK_MEMBERS + | PERMISSIONS_BITS.MODERATE_MEMBERS; + +export const computePermissions: (options: { + user?: User | string | null; + context?: Guild | Channel | null; + overwrites?: Channel["permissionOverwrites"] | null; +}) => bigint = findByCodeLazy(".getCurrentUser()", ".computeLurkerPermissionsAllowList()"); diff --git a/src/plugins/enhancedUserTags/util/system.ts b/src/plugins/enhancedUserTags/util/system.ts new file mode 100644 index 000000000..95ac82e4a --- /dev/null +++ b/src/plugins/enhancedUserTags/util/system.ts @@ -0,0 +1,27 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2024 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { findByCodeLazy } from "@webpack"; +import { Message } from "discord-types/general"; + +export const isSystemMessage = findByCodeLazy(".messageReference.guild_id===", ".author.id"); + +// 24 - `Activity Alerts Enabled`/`safety alert`/`has blocked a message in`/ +// 36 - `enabled security actions` +// 37 - `disabled security actions` +// 39 - `resolved an Activity Alert` +// I found no more automod message types but mb they're exists +export const SECURITY_ACTION_MESSAGE_TYPES = [24, 36, 37, 39]; + +export const isAutoModMessage = (msg: Message) => { + return SECURITY_ACTION_MESSAGE_TYPES.includes(msg.type); +}; + +const AUTO_MODERATION_EMBED_TYPE = "auto_moderation_message"; + +export const isAutoContentModerationMessage = (msg: Message) => { + return msg.embeds.some(({ type }) => type === AUTO_MODERATION_EMBED_TYPE); +};