From 2613ec170ef7000a95e165bab807084b864c475f Mon Sep 17 00:00:00 2001 From: thororen1234 <78185467+thororen1234@users.noreply.github.com> Date: Fri, 2 May 2025 21:00:56 -0400 Subject: [PATCH] MoreUserTags Chat --- README.md | 3 +- src/api/NicknameIcons.tsx | 40 +++ src/api/index.ts | 6 + src/equicordplugins/moreUserTags/consts.ts | 63 ++++ src/equicordplugins/moreUserTags/index.tsx | 183 ++++++++++ src/equicordplugins/moreUserTags/settings.tsx | 81 +++++ src/equicordplugins/moreUserTags/styles.css | 20 ++ src/equicordplugins/moreUserTags/types.ts | 32 ++ .../components/NotificationComponent.tsx | 9 +- src/plugins/_api/nicknameIcons.ts | 23 ++ src/plugins/index.ts | 10 +- src/plugins/platformIndicators/index.tsx | 325 +++++++++--------- src/plugins/platformIndicators/style.css | 18 +- src/plugins/userVoiceShow/components.tsx | 40 ++- src/plugins/userVoiceShow/index.tsx | 37 +- src/utils/constants.ts | 4 + src/utils/types.ts | 2 + 17 files changed, 668 insertions(+), 228 deletions(-) create mode 100644 src/api/NicknameIcons.tsx create mode 100644 src/equicordplugins/moreUserTags/consts.ts create mode 100644 src/equicordplugins/moreUserTags/index.tsx create mode 100644 src/equicordplugins/moreUserTags/settings.tsx create mode 100644 src/equicordplugins/moreUserTags/styles.css create mode 100644 src/equicordplugins/moreUserTags/types.ts create mode 100644 src/plugins/_api/nicknameIcons.ts diff --git a/README.md b/README.md index 946a646e..5e77a949 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch ### Extra included plugins
-170 additional plugins +171 additional plugins ### All Platforms @@ -108,6 +108,7 @@ You can join our [discord server](https://discord.gg/5Xh2W87egW) for commits, ch - MessageTranslate by Samwich - ModalFade by Kyuuhachi - MoreStickers by Leko & Arjix +- MoreUserTags by Cyn, TheSun, RyanCaoDev, LordElias, AutumnVN, hen - Morse by zyqunix - NeverPausePreviews by vappstar - NewPluginsManager by Sqaaakoi diff --git a/src/api/NicknameIcons.tsx b/src/api/NicknameIcons.tsx new file mode 100644 index 00000000..8b0fbc20 --- /dev/null +++ b/src/api/NicknameIcons.tsx @@ -0,0 +1,40 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import ErrorBoundary from "@components/ErrorBoundary"; +import { Logger } from "@utils/Logger"; +import { ReactNode } from "react"; + +export interface NicknameIconProps { + userId: string; +} + +export type NicknameIconFactory = (props: NicknameIconProps) => ReactNode | Promise; + +export interface NicknameIcon { + priority: number; + factory: NicknameIconFactory; +} + +const nicknameIcons = new Map(); +const logger = new Logger("NicknameIcons"); + +export function addNicknameIcon(id: string, factory: NicknameIconFactory, priority = 0) { + return nicknameIcons.set(id, { + priority, + factory: ErrorBoundary.wrap(factory, { noop: true, onError: error => logger.error(`Failed to render ${id}`, error) }) + }); +} + +export function removeNicknameIcon(id: string) { + return nicknameIcons.delete(id); +} + +export function _renderIcons(props: NicknameIconProps) { + return Array.from(nicknameIcons) + .sort((a, b) => b[1].priority - a[1].priority) + .map(([id, { factory: NicknameIcon }]) => ); +} diff --git a/src/api/index.ts b/src/api/index.ts index f0538f52..8af6a49f 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -27,6 +27,7 @@ import * as $MessageDecorations from "./MessageDecorations"; import * as $MessageEventsAPI from "./MessageEvents"; import * as $MessagePopover from "./MessagePopover"; import * as $MessageUpdater from "./MessageUpdater"; +import * as $NicknameIcons from "./NicknameIcons"; import * as $Notices from "./Notices"; import * as $Notifications from "./Notifications"; import * as $ServerList from "./ServerList"; @@ -123,6 +124,11 @@ export const MessageUpdater = $MessageUpdater; */ export const UserSettings = $UserSettings; +/** + * An API allowing you to add icons to the nickname, in profiles + */ +export const NicknameIcons = $NicknameIcons; + /** * Just used to identify if user is on Equicord as Vencord doesnt have this */ diff --git a/src/equicordplugins/moreUserTags/consts.ts b/src/equicordplugins/moreUserTags/consts.ts new file mode 100644 index 00000000..14ea3dd9 --- /dev/null +++ b/src/equicordplugins/moreUserTags/consts.ts @@ -0,0 +1,63 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { findByCodeLazy, findLazy } from "@webpack"; +import { GuildStore } from "@webpack/common"; +import { RC } from "@webpack/types"; +import { Channel, Guild, Message, User } from "discord-types/general"; + +import type { ITag } from "./types"; + +export const isWebhook = (message: Message, user: User) => !!message?.webhookId && user.isNonUserBot(); +export const tags = [ + { + name: "WEBHOOK", + displayName: "Webhook", + description: "Messages sent by webhooks", + condition: isWebhook + }, { + name: "OWNER", + displayName: "Owner", + description: "Owns the server", + condition: (_, user, channel) => GuildStore.getGuild(channel?.guild_id)?.ownerId === user.id + }, { + name: "ADMINISTRATOR", + displayName: "Admin", + description: "Has the administrator permission", + permissions: ["ADMINISTRATOR"] + }, { + name: "MODERATOR_STAFF", + displayName: "Staff", + description: "Can manage the server, channels or roles", + permissions: ["MANAGE_GUILD", "MANAGE_CHANNELS", "MANAGE_ROLES"] + }, { + name: "MODERATOR", + displayName: "Mod", + description: "Can manage messages or kick/ban people", + permissions: ["MANAGE_MESSAGES", "KICK_MEMBERS", "BAN_MEMBERS"] + }, { + name: "VOICE_MODERATOR", + displayName: "VC Mod", + description: "Can manage voice chats", + permissions: ["MOVE_MEMBERS", "MUTE_MEMBERS", "DEAFEN_MEMBERS"] + }, { + name: "CHAT_MODERATOR", + displayName: "Chat Mod", + description: "Can timeout people", + permissions: ["MODERATE_MEMBERS"] + } +] as const satisfies ITag[]; + +export const Tag = findLazy(m => m.Types?.[0] === "BOT") as RC<{ type?: number | null, className?: string, useRemSizes?: boolean; }> & { Types: Record; }; + +// PermissionStore.computePermissions will not work here since it only gets permissions for the current user +export const computePermissions: (options: { + user?: { id: string; } | string | null; + context?: Guild | Channel | null; + overwrites?: Channel["permissionOverwrites"] | null; + checkElevated?: boolean /* = true */; + excludeGuildPermissions?: boolean /* = false */; +}) => bigint = findByCodeLazy(".getCurrentUser()", ".computeLurkerPermissionsAllowList()"); diff --git a/src/equicordplugins/moreUserTags/index.tsx b/src/equicordplugins/moreUserTags/index.tsx new file mode 100644 index 00000000..e85cd027 --- /dev/null +++ b/src/equicordplugins/moreUserTags/index.tsx @@ -0,0 +1,183 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import "./styles.css"; + +import { classNameFactory } from "@api/Styles"; +import { Devs } from "@utils/constants"; +import { getCurrentChannel, getIntlMessage } from "@utils/discord"; +import definePlugin from "@utils/types"; +import { ChannelStore, GuildStore, PermissionsBits, SelectedChannelStore, UserStore } from "@webpack/common"; +import { Channel, Message, User } from "discord-types/general"; + +import { computePermissions, Tag, tags } from "./consts"; +import { settings } from "./settings"; +import { TagSettings } from "./types"; + +const cl = classNameFactory("vc-mut-"); + +const genTagTypes = () => { + let i = 100; + const obj = {}; + + for (const { name } of tags) { + obj[name] = ++i; + obj[i] = name; + } + + return obj; +}; + +export default definePlugin({ + name: "MoreUserTags", + description: "Adds tags for webhooks and moderative roles (owner, admin, etc.)", + authors: [Devs.Cyn, Devs.TheSun, Devs.RyanCaoDev, Devs.LordElias, Devs.AutumnVN, Devs.hen], + dependencies: ["MemberListDecoratorsAPI", "NicknameIconsAPI", "MessageDecorationsAPI"], + settings, + patches: [ + // Make discord actually use our tags + { + find: ".STAFF_ONLY_DM:", + replacement: [{ + match: /(?<=type:(\i).{10,1000}.REMIX.{10,100})default:(\i)=/, + replace: "default:$2=$self.getTagText($self.localTags[$1]);", + }, { + match: /(?<=type:(\i).{10,1000}.REMIX.{10,100})\.BOT:(?=default:)/, + replace: "$&return null;", + predicate: () => settings.store.dontShowBotTag + }, + ], + } + ], + start() { + const tagSettings = settings.store.tagSettings || {} as TagSettings; + for (const tag of Object.values(tags)) { + tagSettings[tag.name] ??= { + showInChat: true, + showInNotChat: true, + text: tag.displayName + }; + } + + settings.store.tagSettings = tagSettings; + }, + localTags: genTagTypes(), + getChannelId() { + return SelectedChannelStore.getChannelId(); + }, + renderNicknameIcon(props) { + const tagId = this.getTag({ + user: UserStore.getUser(props.userId), + channel: ChannelStore.getChannel(this.getChannelId()), + channelId: this.getChannelId(), + isChat: false + }); + + return tagId && + ; + + }, + renderMessageDecoration(props) { + const tagId = this.getTag({ + message: props.message, + user: UserStore.getUser(props.message.author.id), + channelId: props.message.channel_id, + isChat: false + }); + + return tagId && + ; + }, + renderMemberListDecorator(props) { + const tagId = this.getTag({ + user: props.user, + channel: getCurrentChannel(), + channelId: this.getChannelId(), + isChat: false + }); + + return tagId && + ; + }, + + getTagText(tagName: string) { + if (!tagName) return getIntlMessage("APP_TAG"); + const tag = tags.find(({ name }) => tagName === name); + if (!tag) return tagName || getIntlMessage("APP_TAG"); + + return settings.store.tagSettings?.[tag.name]?.text || tag.displayName; + }, + + getTag({ + message, user, channelId, isChat, channel + }: { + message?: Message, + user?: User & { isClyde(): boolean; }, + channel?: Channel & { isForumPost(): boolean; isMediaPost(): boolean; }, + channelId?: string; + isChat?: boolean; + }): number | null { + const settings = this.settings.store; + + if (!user) return null; + if (isChat && user.id === "1") return null; + if (user.isClyde()) return null; + if (user.bot && settings.dontShowForBots) return null; + + channel ??= ChannelStore.getChannel(channelId!) as any; + if (!channel) return null; + + const perms = this.getPermissions(user, channel); + + for (const tag of tags) { + if (isChat && !settings.tagSettings[tag.name].showInChat) + continue; + if (!isChat && !settings.tagSettings[tag.name].showInNotChat) + continue; + + // If the owner tag is disabled, and the user is the owner of the guild, + // avoid adding other tags because the owner will always match the condition for them + if ( + (tag.name !== "OWNER" && + GuildStore.getGuild(channel?.guild_id)?.ownerId === + user.id && + isChat && + !settings.tagSettings.OWNER.showInChat) || + (!isChat && + !settings.tagSettings.OWNER.showInNotChat) + ) + continue; + + if ("permissions" in tag ? + tag.permissions.some(perm => perms.includes(perm)) : + tag.condition(message!, user, channel)) { + + return this.localTags[tag.name]; + } + } + + return null; + }, + getPermissions(user: User, channel: Channel): string[] { + const guild = GuildStore.getGuild(channel?.guild_id); + if (!guild) return []; + + const permissions = computePermissions({ user, context: guild, overwrites: channel.permissionOverwrites }); + return Object.entries(PermissionsBits) + .map(([perm, permInt]) => + permissions & permInt ? perm : "" + ) + .filter(Boolean); + }, +}); diff --git a/src/equicordplugins/moreUserTags/settings.tsx b/src/equicordplugins/moreUserTags/settings.tsx new file mode 100644 index 00000000..f48634e1 --- /dev/null +++ b/src/equicordplugins/moreUserTags/settings.tsx @@ -0,0 +1,81 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { definePluginSettings } from "@api/Settings"; +import { Margins } from "@utils/margins"; +import { OptionType } from "@utils/types"; +import { Card, Flex, Forms, Switch, TextInput, Tooltip } from "@webpack/common"; + +import { Tag, tags } from "./consts"; +import { TagSettings } from "./types"; + +function SettingsComponent() { + const tagSettings = settings.store.tagSettings as TagSettings; + + return ( + + {tags.map(t => ( + + + + {({ onMouseEnter, onMouseLeave }) => ( +
+ {t.displayName} Tag +
+ )} +
+
+ + tagSettings[t.name].text = v} + className={Margins.bottom16} + /> + + tagSettings[t.name].showInChat = v} + hideBorder + > + Show in messages + + + tagSettings[t.name].showInNotChat = v} + hideBorder + > + Show in member list and profiles + +
+ ))} +
+ ); +} + +export const settings = definePluginSettings({ + dontShowForBots: { + description: "Don't show extra tags for bots (excluding webhooks)", + type: OptionType.BOOLEAN, + default: false + }, + dontShowBotTag: { + description: "Only show extra tags for bots / Hide [APP] text", + type: OptionType.BOOLEAN, + default: false, + restartNeeded: true + }, + tagSettings: { + type: OptionType.COMPONENT, + component: SettingsComponent, + description: "fill me" + } +}); diff --git a/src/equicordplugins/moreUserTags/styles.css b/src/equicordplugins/moreUserTags/styles.css new file mode 100644 index 00000000..41160325 --- /dev/null +++ b/src/equicordplugins/moreUserTags/styles.css @@ -0,0 +1,20 @@ +.vc-mut-message-tag { + /* Remove default margin from tags in messages */ + margin-top: unset !important; + + /* Align with Discord default tags in messages */ + position: relative; + bottom: 0.01em; +} + +.vc-mut-message-verified { + height: 1rem !important; +} + +span[class*="botTagCozy"][data-moreTags-darkFg="true"]>svg>path { + fill: #000; +} + +span[class*="botTagCozy"][data-moreTags-darkFg="false"]>svg>path { + fill: #fff; +} diff --git a/src/equicordplugins/moreUserTags/types.ts b/src/equicordplugins/moreUserTags/types.ts new file mode 100644 index 00000000..40d8ac7d --- /dev/null +++ b/src/equicordplugins/moreUserTags/types.ts @@ -0,0 +1,32 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import type { Permissions } from "@webpack/types"; +import type { Channel, Message, User } from "discord-types/general"; + +import { tags } from "./consts"; + +export type ITag = { + // name used for identifying, must be alphanumeric + underscores + name: string; + // name shown on the tag itself, can be anything probably; automatically uppercase'd + displayName: string; + description: string; +} & ({ + permissions: Permissions[]; +} | { + condition?(message: Message | null, user: User, channel: Channel): boolean; +}); + +export interface TagSetting { + text: string; + showInChat: boolean; + showInNotChat: boolean; +} + +export type TagSettings = { + [k in typeof tags[number]["name"]]: TagSetting; +}; diff --git a/src/equicordplugins/toastNotifications/components/NotificationComponent.tsx b/src/equicordplugins/toastNotifications/components/NotificationComponent.tsx index 17e5c4ad..0f6adb75 100644 --- a/src/equicordplugins/toastNotifications/components/NotificationComponent.tsx +++ b/src/equicordplugins/toastNotifications/components/NotificationComponent.tsx @@ -125,7 +125,14 @@ export default ErrorBoundary.wrap(function NotificationComponent({
- {renderBody ? richBody ??

{body}

: null} + {renderBody ? ( + richBody ?? ( +

+ {body.length > 500 ? body.slice(0, 500) + "..." : body} +

+ ) + ) : null} + {PluginSettings.store.renderImages && image && ToastNotification Image} {footer &&

{`${attachments} attachment${attachments > 1 ? "s" : ""} ${attachments > 1 ? "were" : "was"} sent.`}

}
diff --git a/src/plugins/_api/nicknameIcons.ts b/src/plugins/_api/nicknameIcons.ts new file mode 100644 index 00000000..be3763cc --- /dev/null +++ b/src/plugins/_api/nicknameIcons.ts @@ -0,0 +1,23 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +import { Devs } from "@utils/constants"; +import definePlugin from "@utils/types"; + +export default definePlugin({ + name: "NicknameIconsAPI", + description: "API to add icons to the nickname, in profiles", + authors: [Devs.Nuckyz], + patches: [ + { + find: "#{intl::USER_PROFILE_LOAD_ERROR}", + replacement: { + match: /(\.fetchError.+?\?)null/, + replace: (_, rest) => `${rest}Vencord.Api.NicknameIcons._renderIcons(arguments[0])` + } + } + ] +}); diff --git a/src/plugins/index.ts b/src/plugins/index.ts index e5594e99..c1ca6023 100644 --- a/src/plugins/index.ts +++ b/src/plugins/index.ts @@ -25,6 +25,7 @@ import { addMessageAccessory, removeMessageAccessory } from "@api/MessageAccesso import { addMessageDecoration, removeMessageDecoration } from "@api/MessageDecorations"; import { addMessageClickListener, addMessagePreEditListener, addMessagePreSendListener, removeMessageClickListener, removeMessagePreEditListener, removeMessagePreSendListener } from "@api/MessageEvents"; import { addMessagePopoverButton, removeMessagePopoverButton } from "@api/MessagePopover"; +import { addNicknameIcon, removeNicknameIcon } from "@api/NicknameIcons"; import { Settings, SettingsStore } from "@api/Settings"; import { disableStyle, enableStyle } from "@api/Styles"; import { Logger } from "@utils/Logger"; @@ -96,7 +97,7 @@ function isReporterTestable(p: Plugin, part: ReporterTestable) { const pluginKeysToBind: Array = [ "onBeforeMessageEdit", "onBeforeMessageSend", "onMessageClick", - "renderChatBarButton", "renderMemberListDecorator", "renderMessageAccessory", "renderMessageDecoration", "renderMessagePopoverButton" + "renderChatBarButton", "renderMemberListDecorator", "renderNicknameIcon", "renderMessageAccessory", "renderMessageDecoration", "renderMessagePopoverButton" ]; const neededApiPlugins = new Set(); @@ -128,6 +129,7 @@ for (const p of pluginsValues) if (isPluginEnabled(p.name)) { if (p.onBeforeMessageEdit || p.onBeforeMessageSend || p.onMessageClick) neededApiPlugins.add("MessageEventsAPI"); if (p.renderChatBarButton) neededApiPlugins.add("ChatInputButtonAPI"); if (p.renderMemberListDecorator) neededApiPlugins.add("MemberListDecoratorsAPI"); + if (p.renderNicknameIcon) neededApiPlugins.add("NicknameIconsAPI"); if (p.renderMessageAccessory) neededApiPlugins.add("MessageAccessoriesAPI"); if (p.renderMessageDecoration) neededApiPlugins.add("MessageDecorationsAPI"); if (p.renderMessagePopoverButton) neededApiPlugins.add("MessagePopoverAPI"); @@ -261,7 +263,7 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p: const { name, commands, contextMenus, managedStyle, userProfileBadge, onBeforeMessageEdit, onBeforeMessageSend, onMessageClick, - renderChatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton + renderChatBarButton, renderMemberListDecorator, renderNicknameIcon, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton } = p; if (p.start) { @@ -313,6 +315,7 @@ export const startPlugin = traceFunction("startPlugin", function startPlugin(p: if (renderChatBarButton) addChatBarButton(name, renderChatBarButton); if (renderMemberListDecorator) addMemberListDecorator(name, renderMemberListDecorator); + if (renderNicknameIcon) addNicknameIcon(name, renderNicknameIcon); if (renderMessageDecoration) addMessageDecoration(name, renderMessageDecoration); if (renderMessageAccessory) addMessageAccessory(name, renderMessageAccessory); if (renderMessagePopoverButton) addMessagePopoverButton(name, renderMessagePopoverButton); @@ -324,7 +327,7 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu const { name, commands, contextMenus, managedStyle, userProfileBadge, onBeforeMessageEdit, onBeforeMessageSend, onMessageClick, - renderChatBarButton, renderMemberListDecorator, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton + renderChatBarButton, renderMemberListDecorator, renderNicknameIcon, renderMessageAccessory, renderMessageDecoration, renderMessagePopoverButton } = p; if (p.stop) { @@ -374,6 +377,7 @@ export const stopPlugin = traceFunction("stopPlugin", function stopPlugin(p: Plu if (renderChatBarButton) removeChatBarButton(name); if (renderMemberListDecorator) removeMemberListDecorator(name); + if (renderNicknameIcon) removeNicknameIcon(name); if (renderMessageDecoration) removeMessageDecoration(name); if (renderMessageAccessory) removeMessageAccessory(name); if (renderMessagePopoverButton) removeMessagePopoverButton(name); diff --git a/src/plugins/platformIndicators/index.tsx b/src/plugins/platformIndicators/index.tsx index f6550e65..0188959a 100644 --- a/src/plugins/platformIndicators/index.tsx +++ b/src/plugins/platformIndicators/index.tsx @@ -18,15 +18,15 @@ import "./style.css"; -import { addProfileBadge, BadgePosition, BadgeUserArgs, ProfileBadge, removeProfileBadge } from "@api/Badges"; import { addMemberListDecorator, removeMemberListDecorator } from "@api/MemberListDecorators"; import { addMessageDecoration, removeMessageDecoration } from "@api/MessageDecorations"; -import { Settings } from "@api/Settings"; -import ErrorBoundary from "@components/ErrorBoundary"; +import { addNicknameIcon, removeNicknameIcon } from "@api/NicknameIcons"; +import { definePluginSettings, migratePluginSetting } from "@api/Settings"; import { Devs } from "@utils/constants"; +import { classes } from "@utils/misc"; import definePlugin, { OptionType } from "@utils/types"; import { filters, findStoreLazy, mapMangledModuleLazy } from "@webpack"; -import { PresenceStore, Tooltip, UserStore } from "@webpack/common"; +import { PresenceStore, Tooltip, UserStore, useStateFromStores } from "@webpack/common"; import { User } from "discord-types/general"; export interface Session { @@ -44,10 +44,26 @@ const SessionsStore = findStoreLazy("SessionsStore") as { getSessions(): Record; }; -function Icon(path: string, opts?: { viewBox?: string; width?: number; height?: number; }) { - return ({ color, tooltip, small }: { color: string; tooltip: string; small: boolean; }) => ( +const { useStatusFillColor } = mapMangledModuleLazy(".concat(.5625*", { + useStatusFillColor: filters.byCode(".hex") +}); + +interface IconFactoryOpts { + viewBox?: string; + width?: number; + height?: number; +} + +interface IconProps { + color: string; + tooltip: string; + small?: boolean; +} + +function Icon(path: string, opts?: IconFactoryOpts) { + return ({ color, tooltip, small }: IconProps) => ( - {(tooltipProps: any) => ( + {tooltipProps => ( { +const PlatformIcon = ({ platform, status, small }: PlatformIconProps) => { const tooltip = platform === "embedded" ? "Console" : platform[0].toUpperCase() + platform.slice(1); let Icon = Icons[platform] ?? Icons.desktop; - const { ConsoleIcon } = Settings.plugins.PlatformIndicators; + const { ConsoleIcon } = settings.store; if (platform === "embedded" && ConsoleIcon === "vencord") { Icon = Icons.vencord; } @@ -92,159 +112,166 @@ const PlatformIcon = ({ platform, status, small }: { platform: Platform, status: return ; }; -function ensureOwnStatus(user: User) { - if (user.id === UserStore.getCurrentUser().id) { - const sessions = SessionsStore.getSessions(); - if (typeof sessions !== "object") return null; - const sortedSessions = Object.values(sessions).sort(({ status: a }, { status: b }) => { - if (a === b) return 0; - if (a === "online") return 1; - if (b === "online") return -1; - if (a === "idle") return 1; - if (b === "idle") return -1; - return 0; - }); - - const ownStatus = Object.values(sortedSessions).reduce((acc, curr) => { - if (curr.clientInfo.client !== "unknown") - acc[curr.clientInfo.client] = curr.status; - return acc; - }, {}); - - const { clientStatuses } = PresenceStore.getState(); - clientStatuses[UserStore.getCurrentUser().id] = ownStatus; +function useEnsureOwnStatus(user: User) { + if (user.id !== UserStore.getCurrentUser()?.id) { + return; } + + const sessions = useStateFromStores([SessionsStore], () => SessionsStore.getSessions()); + if (typeof sessions !== "object") return null; + const sortedSessions = Object.values(sessions).sort(({ status: a }, { status: b }) => { + if (a === b) return 0; + if (a === "online") return 1; + if (b === "online") return -1; + if (a === "idle") return 1; + if (b === "idle") return -1; + return 0; + }); + + const ownStatus = Object.values(sortedSessions).reduce((acc, curr) => { + if (curr.clientInfo.client !== "unknown") + acc[curr.clientInfo.client] = curr.status; + return acc; + }, {}); + + const { clientStatuses } = PresenceStore.getState(); + clientStatuses[UserStore.getCurrentUser().id] = ownStatus; } -function getBadges({ userId }: BadgeUserArgs): ProfileBadge[] { - const user = UserStore.getUser(userId); - - if (!user || (user.bot && !Settings.plugins.PlatformIndicators.showBots)) return []; - - ensureOwnStatus(user); - - const status = PresenceStore.getState()?.clientStatuses?.[user.id] as Record; - if (!status) return []; - - return Object.entries(status).map(([platform, status]) => ({ - component: () => ( - - - - ), - key: `vc-platform-indicator-${platform}` - })); +interface PlatformIndicatorProps { + user: User; + isProfile?: boolean; + isMessage?: boolean; + isMemberList?: boolean; } -const PlatformIndicator = ({ user, small = false }: { user: User; small?: boolean; }) => { - if (!user || (user.bot && !Settings.plugins.PlatformIndicators.showBots)) return null; +const PlatformIndicator = ({ user, isProfile, isMessage, isMemberList }: PlatformIndicatorProps) => { + if (user == null || user.bot) return null; + useEnsureOwnStatus(user); - ensureOwnStatus(user); + const status: Record | undefined = useStateFromStores([PresenceStore], () => PresenceStore.getState()?.clientStatuses?.[user.id]); + if (status == null) { + return null; + } - const status = PresenceStore.getState()?.clientStatuses?.[user.id] as Record; - if (!status) return null; - - const icons = Object.entries(status).map(([platform, status]) => ( + const icons = Array.from(Object.entries(status), ([platform, status]) => ( )); - if (!icons.length) return null; + if (!icons.length) { + return null; + } return ( - {icons} - + ); }; -const badge: ProfileBadge = { - getBadges, - position: BadgePosition.START, -}; +function toggleMemberListDecorators(enabled: boolean) { + if (enabled) { + addMemberListDecorator("PlatformIndicators", props => ); + } else { + removeMemberListDecorator("PlatformIndicators"); + } +} -const indicatorLocations = { +function toggleNicknameIcons(enabled: boolean) { + if (enabled) { + addNicknameIcon("PlatformIndicators", props => , 1); + } else { + removeNicknameIcon("PlatformIndicators"); + } +} + +function toggleMessageDecorators(enabled: boolean) { + if (enabled) { + addMessageDecoration("PlatformIndicators", props => ); + } else { + removeMessageDecoration("PlatformIndicators"); + } +} + +migratePluginSetting("PlatformIndicators", "badges", "profiles"); +const settings = definePluginSettings({ list: { - description: "In the member list", - onEnable: () => addMemberListDecorator("platform-indicator", props => - - - - ), - onDisable: () => removeMemberListDecorator("platform-indicator") + type: OptionType.BOOLEAN, + description: "Show indicators in the member list", + default: true, + onChange: toggleMemberListDecorators }, - badges: { - description: "In user profiles, as badges", - onEnable: () => addProfileBadge(badge), - onDisable: () => removeProfileBadge(badge) + profiles: { + type: OptionType.BOOLEAN, + description: "Show indicators in user profiles", + default: true, + onChange: toggleNicknameIcons }, messages: { - description: "Inside messages", - onEnable: () => addMessageDecoration("platform-indicator", props => - - - - ), - onDisable: () => removeMessageDecoration("platform-indicator") + type: OptionType.BOOLEAN, + description: "Show indicators inside messages", + default: true, + onChange: toggleMessageDecorators + }, + colorMobileIndicator: { + type: OptionType.BOOLEAN, + description: "Whether to make the mobile indicator match the color of the user status.", + default: true, + restartNeeded: true + }, + ConsoleIcon: { + type: OptionType.SELECT, + description: "What console icon to use", + restartNeeded: true, + options: [ + { + label: "Equicord", + value: "equicord", + default: true + }, + { + label: "Suncord", + value: "suncord", + }, + { + label: "Vencord", + value: "vencord", + }, + ], } -}; - -function addAllIndicators() { - const settings = Settings.plugins.PlatformIndicators; - const { displayMode } = settings; - - // transfer settings from the old ones, which had a select menu instead of booleans - if (displayMode) { - if (displayMode !== "both") settings[displayMode] = true; - else { - settings.list = true; - settings.badges = true; - } - settings.messages = true; - delete settings.displayMode; - } - - Object.entries(indicatorLocations).forEach(([key, value]) => { - if (settings[key]) value.onEnable(); - }); -} - -function deleteAllIndicators() { - Object.entries(indicatorLocations).forEach(([_, value]) => { - value.onDisable(); - }); -} +}); export default definePlugin({ name: "PlatformIndicators", description: "Adds platform indicators (Desktop, Mobile, Web...) to users", authors: [Devs.kemo, Devs.TheSun, Devs.Nuckyz, Devs.Ven], - dependencies: ["MessageDecorationsAPI", "MemberListDecoratorsAPI"], + dependencies: ["MemberListDecoratorsAPI", "NicknameIconsAPI", "MessageDecorationsAPI"], + settings, start() { - addAllIndicators(); + if (settings.store.list) toggleMemberListDecorators(true); + if (settings.store.profiles) toggleNicknameIcons(true); + if (settings.store.messages) toggleMessageDecorators(true); }, stop() { - deleteAllIndicators(); + if (settings.store.list) toggleMemberListDecorators(false); + if (settings.store.profiles) toggleNicknameIcons; + if (settings.store.messages) toggleMessageDecorators(false); }, patches: [ { find: ".Masks.STATUS_ONLINE_MOBILE", - predicate: () => Settings.plugins.PlatformIndicators.colorMobileIndicator, + predicate: () => settings.store.colorMobileIndicator, replacement: [ { // Return the STATUS_ONLINE_MOBILE mask if the user is on mobile, no matter the status @@ -260,7 +287,7 @@ export default definePlugin({ }, { find: ".AVATAR_STATUS_MOBILE_16;", - predicate: () => Settings.plugins.PlatformIndicators.colorMobileIndicator, + predicate: () => settings.store.colorMobileIndicator, replacement: [ { // Return the AVATAR_STATUS_MOBILE size mask if the user is on mobile, no matter the status @@ -281,58 +308,12 @@ export default definePlugin({ }, { find: "}isMobileOnline(", - predicate: () => Settings.plugins.PlatformIndicators.colorMobileIndicator, + predicate: () => settings.store.colorMobileIndicator, replacement: { // Make isMobileOnline return true no matter what is the user status match: /(?<=\i\[\i\.\i\.MOBILE\])===\i\.\i\.ONLINE/, replace: "!= null" } } - ], - - options: { - ...Object.fromEntries( - Object.entries(indicatorLocations).map(([key, value]) => { - return [key, { - type: OptionType.BOOLEAN, - description: `Show indicators ${value.description.toLowerCase()}`, - // onChange doesn't give any way to know which setting was changed, so restart required - restartNeeded: true, - default: true - }]; - }) - ), - colorMobileIndicator: { - type: OptionType.BOOLEAN, - description: "Whether to make the mobile indicator match the color of the user status.", - default: true, - restartNeeded: true - }, - showBots: { - type: OptionType.BOOLEAN, - description: "Whether to show platform indicators on bots", - default: false, - restartNeeded: false - }, - ConsoleIcon: { - type: OptionType.SELECT, - description: "What console icon to use", - restartNeeded: true, - options: [ - { - label: "Equicord", - value: "equicord", - default: true - }, - { - label: "Suncord", - value: "suncord", - }, - { - label: "Vencord", - value: "vencord", - }, - ], - }, - } + ] }); diff --git a/src/plugins/platformIndicators/style.css b/src/plugins/platformIndicators/style.css index 38ea5ef4..a5566fdd 100644 --- a/src/plugins/platformIndicators/style.css +++ b/src/plugins/platformIndicators/style.css @@ -2,6 +2,20 @@ display: inline-flex; justify-content: center; align-items: center; - vertical-align: top; - position: relative; + gap: 2px; +} + +.vc-platform-indicator-profile { + background: rgb(var(--bg-overlay-color) / var(--bg-overlay-opacity-6)); + border: 1px solid var(--border-faint); + border-radius: var(--radius-xs); + border-color: var(--profile-body-border-color); + margin: 0 1px; + padding: 0 1px; +} + +.vc-platform-indicator-message { + position: relative; + vertical-align: top; + top: 2px; } diff --git a/src/plugins/userVoiceShow/components.tsx b/src/plugins/userVoiceShow/components.tsx index 675588ed..136febd6 100644 --- a/src/plugins/userVoiceShow/components.tsx +++ b/src/plugins/userVoiceShow/components.tsx @@ -111,32 +111,32 @@ function VoiceChannelTooltip({ channel, isLocked }: VoiceChannelTooltipProps) { {guild.name} )} -
+
{channelIcon} {channelName} -
- + {users.length < 10 ? `0${users.length}` : `${users.length}`} - {channel.userLimit < 10 ? `0${channel.userLimit}` : `${channel.userLimit}`} @@ -156,8 +156,10 @@ function VoiceChannelTooltip({ channel, isLocked }: VoiceChannelTooltipProps) { ); } -export interface VoiceChannelIndicatorProps { +interface VoiceChannelIndicatorProps { userId: string; + isMessageIndicator?: boolean; + isProfile?: boolean; isActionButton?: boolean; shouldHighlight?: boolean; } diff --git a/src/plugins/userVoiceShow/index.tsx b/src/plugins/userVoiceShow/index.tsx index 67e5e022..5f1a05e5 100644 --- a/src/plugins/userVoiceShow/index.tsx +++ b/src/plugins/userVoiceShow/index.tsx @@ -20,6 +20,7 @@ import "./style.css"; import { addMemberListDecorator, removeMemberListDecorator } from "@api/MemberListDecorators"; import { addMessageDecoration, removeMessageDecoration } from "@api/MessageDecorations"; +import { addNicknameIcon, removeNicknameIcon } from "@api/NicknameIcons"; import { definePluginSettings } from "@api/Settings"; import { Devs, EquicordDevs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; @@ -51,38 +52,10 @@ export default definePlugin({ name: "UserVoiceShow", description: "Shows an indicator when a user is in a Voice Channel", authors: [Devs.Nuckyz, Devs.LordElias, EquicordDevs.omaw], - dependencies: ["MemberListDecoratorsAPI", "MessageDecorationsAPI"], + dependencies: ["NicknameIconsAPI", "MemberListDecoratorsAPI", "MessageDecorationsAPI"], settings, patches: [ - // User Popout, Full Size Profile, Direct Messages Side Profile - { - find: "#{intl::USER_PROFILE_LOAD_ERROR}", - replacement: { - match: /(\.fetchError.+?\?)null/, - replace: (_, rest) => `${rest}$self.VoiceChannelIndicator({userId:arguments[0]?.userId})` - }, - predicate: () => settings.store.showInUserProfileModal - }, - // To use without the MemberList decorator API - /* // Guild Members List - { - find: ".lostPermission)", - replacement: { - match: /\.lostPermission\).+?(?=avatar:)/, - replace: "$&children:[$self.VoiceChannelIndicator({userId:arguments[0]?.user?.id})]," - }, - predicate: () => settings.store.showVoiceChannelIndicator - }, - // Direct Messages List - { - find: "PrivateChannel.renderAvatar", - replacement: { - match: /#{intl::CLOSE_DM}.+?}\)(?=])/, - replace: "$&,$self.VoiceChannelIndicator({userId:arguments[0]?.user?.id})" - }, - predicate: () => settings.store.showVoiceChannelIndicator - }, */ // Friends List { find: "null!=this.peopleListItemRef.current", @@ -95,15 +68,19 @@ export default definePlugin({ ], start() { + if (settings.store.showInUserProfileModal) { + addNicknameIcon("UserVoiceShow", ({ userId }) => ); + } if (settings.store.showInMemberList) { addMemberListDecorator("UserVoiceShow", ({ user }) => user == null ? null : ); } if (settings.store.showInMessages) { - addMessageDecoration("UserVoiceShow", ({ message }) => message?.author == null ? null : ); + addMessageDecoration("UserVoiceShow", ({ message }) => message?.author == null ? null : ); } }, stop() { + removeNicknameIcon("UserVoiceShow"); removeMemberListDecorator("UserVoiceShow"); removeMessageDecoration("UserVoiceShow"); }, diff --git a/src/utils/constants.ts b/src/utils/constants.ts index b798c88e..5a230a6a 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -606,6 +606,10 @@ export const Devs = /* #__PURE__*/ Object.freeze({ name: "samsam", id: 836452332387565589n, }, + hen: { + id: 279266228151779329n, + name: "Hen" + }, } satisfies Record); export const EquicordDevs = Object.freeze({ diff --git a/src/utils/types.ts b/src/utils/types.ts index 270a7a09..b2435c6f 100644 --- a/src/utils/types.ts +++ b/src/utils/types.ts @@ -25,6 +25,7 @@ import { MessageAccessoryFactory } from "@api/MessageAccessories"; import { MessageDecorationFactory } from "@api/MessageDecorations"; import { MessageClickListener, MessageEditListener, MessageSendListener } from "@api/MessageEvents"; import { MessagePopoverButtonFactory } from "@api/MessagePopover"; +import { NicknameIconFactory } from "@api/NicknameIcons"; import { FluxEvents } from "@webpack/types"; import { ReactNode } from "react"; import { Promisable } from "type-fest"; @@ -187,6 +188,7 @@ export interface PluginDef { renderMessageDecoration?: MessageDecorationFactory; renderMemberListDecorator?: MemberListDecoratorFactory; + renderNicknameIcon?: NicknameIconFactory; renderChatBarButton?: ChatBarButtonFactory; }