From f680c4db1476053d00d1afd333cea76a36500f84 Mon Sep 17 00:00:00 2001 From: Luca <45944543+AfoxDesignz@users.noreply.github.com> Date: Mon, 13 Jul 2026 01:22:02 +0200 Subject: [PATCH] feat(showRolesInChat): add plugin (#1252) * feat(showRolesInChat): add plugin * chore(showRolesInChat): add tags * fix(showRolesInChat): correct devs object --- src/equicordplugins/showRolesInChat/index.tsx | 156 ++++++++++++++++++ .../showRolesInChat/styles.css | 28 ++++ src/utils/constants.ts | 4 + 3 files changed, 188 insertions(+) create mode 100644 src/equicordplugins/showRolesInChat/index.tsx create mode 100644 src/equicordplugins/showRolesInChat/styles.css diff --git a/src/equicordplugins/showRolesInChat/index.tsx b/src/equicordplugins/showRolesInChat/index.tsx new file mode 100644 index 000000000..b7520178a --- /dev/null +++ b/src/equicordplugins/showRolesInChat/index.tsx @@ -0,0 +1,156 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2026 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./styles.css"; + +import { definePluginSettings } from "@api/Settings"; +import ErrorBoundary from "@components/ErrorBoundary"; +import { EyeIcon } from "@components/Icons"; +import { EquicordDevs } from "@utils/constants"; +import { classNameFactory } from "@utils/css"; +import { getCurrentGuild } from "@utils/discord"; +import definePlugin, { OptionType } from "@utils/types"; +import { User } from "@vencord/discord-types"; +import { ChannelStore, ContextMenuApi, GuildMemberStore, GuildRoleStore, Menu, useStateFromStores } from "@webpack/common"; +import type React from "react"; + +const cl = classNameFactory("vc-sric-"); + +const SETTINGS_KEYS: (keyof typeof settings.store)[] = ["showBots", "useRoleColor", "excludedRoles"]; + +const settings = definePluginSettings({ + showBots: { + type: OptionType.BOOLEAN, + description: "Whether to show the highest role on bots.", + default: false + }, + useRoleColor: { + type: OptionType.BOOLEAN, + description: "Use the role's color for the icon.", + default: true + } +}).withPrivateSettings<{ excludedRoles?: string[]; }>(); + +function getHighestRole(guildId: string, userId: string, excludedRoles?: string[]) { + const roles = GuildMemberStore.getMember(guildId, userId)?.roles; + if (!roles?.length) return null; + + return GuildRoleStore.getSortedRoles(guildId).find(r => roles.includes(r.id) && !excludedRoles?.includes(r.id)); +} + +function toggleRole(roleId: string) { + const excluded = settings.store.excludedRoles ?? []; + settings.store.excludedRoles = excluded.includes(roleId) ? excluded.filter(id => id !== roleId) : [...excluded, roleId]; +} + +function makeToggleRoleItem(roleId: string) { + const isExcluded = settings.store.excludedRoles?.includes(roleId); + return ( + toggleRole(roleId)} + /> + ); +} + +function EyeSlashIcon() { + return ( + + + + + + ); +} + +function ShieldUserIcon({ color }: { color: string; }) { + return ( + + ); +} + +const HighestRoleIndicator = ErrorBoundary.wrap(({ user, channelId }: { user: User; channelId: string; }) => { + const { showBots, useRoleColor, excludedRoles } = settings.use(SETTINGS_KEYS); + + const guildId = (!user.bot || showBots) ? ChannelStore.getChannel(channelId)?.guild_id : null; + const role = useStateFromStores([GuildMemberStore, GuildRoleStore], () => guildId ? getHighestRole(guildId, user.id, excludedRoles) : null, [guildId, user.id, excludedRoles]); + + if (!guildId || !role) return null; + + const handleContextMenu = (e: React.MouseEvent) => { + ContextMenuApi.openContextMenu(e, () => { + const userRoles = GuildMemberStore.getMember(guildId, user.id)?.roles ?? []; + const excluded = excludedRoles ?? []; + const hiddenRoles = GuildRoleStore.getSortedRoles(guildId).filter(r => excluded.includes(r.id) && userRoles.includes(r.id)); + + return ( + + + {makeToggleRoleItem(role.id)} + + {hiddenRoles.length > 0 && ( + + {hiddenRoles.map(r => ( + toggleRole(r.id)} + /> + ))} + + )} + + + ); + }); + }; + + return ( + + + + {role.name} + + + ); +}, { noop: true }); + +export default definePlugin({ + name: "ShowRolesInChat", + description: "Shows a user's highest role next to their name in chat messages. Hide/show specific roles in their context menu (right-click).", + tags: ["Appearance", "Chat", "Roles", "Servers"], + authors: [EquicordDevs.lucabeyer], + settings, + contextMenus: { + "dev-context"(children, { id }: { id: string; }) { + if (GuildRoleStore.getRole(getCurrentGuild()?.id ?? "", id)) { + children.push( + + {makeToggleRoleItem(id)} + + ); + } + }, + "guild-settings-role-context"(children, props: { role: { id: string; }; }) { + children.push( + + {makeToggleRoleItem(props.role.id)} + + ); + } + }, + renderMessageDecoration: ({ message }) => ( + + ) +}); diff --git a/src/equicordplugins/showRolesInChat/styles.css b/src/equicordplugins/showRolesInChat/styles.css new file mode 100644 index 000000000..016d7361c --- /dev/null +++ b/src/equicordplugins/showRolesInChat/styles.css @@ -0,0 +1,28 @@ +.vc-sric-indicator { + display: inline-flex; + align-items: center; + box-sizing: border-box; + height: 16px; + padding: 0 6px 0 4px; + background: var(--background-mod-strong); + border-radius: 4px; + font-size: .72rem; + font-weight: 600; + line-height: 16px; + color: var(--text-default); + cursor: default; +} + +.vc-sric-icon { + width: 14px; + height: 14px; + margin-right: 4px; + flex-shrink: 0; +} + +.vc-sric-name { + white-space: nowrap; + max-width: 120px; + overflow: hidden; + text-overflow: ellipsis; +} \ No newline at end of file diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 17d82cf64..f1b28f5f5 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1390,6 +1390,10 @@ export const EquicordDevs = Object.freeze({ name: "bastih18", id: 273797150880563201n }, + lucabeyer: { + name: "luca.beyer", + id: 405090676771127317n + }, } satisfies Record); // iife so #__PURE__ works correctly