From b6e96a4d3b631c5a2d236ae28179ee0879ae00fe Mon Sep 17 00:00:00 2001 From: Gleb P <63789651+ultard@users.noreply.github.com> Date: Fri, 5 Sep 2025 04:37:13 +0300 Subject: [PATCH 1/4] MemberCount: also show members in voice (#2937) Co-authored-by: Vendicated --- .../src/stores/VoiceStateStore.d.ts | 42 +++++++++++++++++++ packages/discord-types/src/stores/index.d.ts | 1 + src/plugins/memberCount/MemberCount.tsx | 42 ++++++++++++++++--- src/plugins/memberCount/VoiceIcon.tsx | 14 +++++++ src/plugins/memberCount/index.tsx | 17 +++++--- src/plugins/memberCount/style.css | 21 +++++++++- src/plugins/userVoiceShow/components.tsx | 9 ++-- src/plugins/vcNarrator/index.tsx | 15 +++---- src/utils/constants.ts | 4 ++ src/webpack/common/stores.ts | 2 + 10 files changed, 140 insertions(+), 27 deletions(-) create mode 100644 packages/discord-types/src/stores/VoiceStateStore.d.ts create mode 100644 src/plugins/memberCount/VoiceIcon.tsx diff --git a/packages/discord-types/src/stores/VoiceStateStore.d.ts b/packages/discord-types/src/stores/VoiceStateStore.d.ts new file mode 100644 index 000000000..84540d996 --- /dev/null +++ b/packages/discord-types/src/stores/VoiceStateStore.d.ts @@ -0,0 +1,42 @@ +import { DiscordRecord } from "../common"; +import { FluxStore } from "./FluxStore"; + +export type UserVoiceStateRecords = Record; +export type VoiceStates = Record; + +export interface VoiceState extends DiscordRecord { + userId: string; + channelId: string | null | undefined; + sessionId: string | null | undefined; + mute: boolean; + deaf: boolean; + selfMute: boolean; + selfDeaf: boolean; + selfVideo: boolean; + selfStream: boolean | undefined; + suppress: boolean; + requestToSpeakTimestamp: string | null | undefined; + discoverable: boolean; + + isVoiceMuted(): boolean; + isVoiceDeafened(): boolean; +} + +export class VoiceStateStore extends FluxStore { + getAllVoiceStates(): VoiceStates; + + getVoiceStates(guildId?: string | null): UserVoiceStateRecords; + getVoiceStatesForChannel(channelId: string): UserVoiceStateRecords; + getVideoVoiceStatesForChannel(channelId: string): UserVoiceStateRecords; + + getVoiceState(guildId: string | null, userId: string): VoiceState | undefined; + getUserVoiceChannelId(guildId: string | null, userId: string): string | undefined; + getVoiceStateForChannel(channelId: string, userId?: string): VoiceState | undefined; + getVoiceStateForUser(userId: string): VoiceState | undefined; + + getCurrentClientVoiceChannelId(guildId: string | null): string | undefined; + isCurrentClientInVoiceChannel(): boolean; + + isInChannel(channelId: string, userId?: string): boolean; + hasVideo(channelId: string): boolean; +} diff --git a/packages/discord-types/src/stores/index.d.ts b/packages/discord-types/src/stores/index.d.ts index 7a435ff5d..5906262ab 100644 --- a/packages/discord-types/src/stores/index.d.ts +++ b/packages/discord-types/src/stores/index.d.ts @@ -16,6 +16,7 @@ export * from "./ThemeStore"; export * from "./TypingStore"; export * from "./UserProfileStore"; export * from "./UserStore"; +export * from "./VoiceStateStore"; export * from "./WindowStore"; /** diff --git a/src/plugins/memberCount/MemberCount.tsx b/src/plugins/memberCount/MemberCount.tsx index 0a3f5e620..020338861 100644 --- a/src/plugins/memberCount/MemberCount.tsx +++ b/src/plugins/memberCount/MemberCount.tsx @@ -6,16 +6,37 @@ import { getCurrentChannel } from "@utils/discord"; import { isObjectEmpty } from "@utils/misc"; -import { SelectedChannelStore, Tooltip, useEffect, useStateFromStores } from "@webpack/common"; +import { ChannelStore, PermissionsBits, PermissionStore, SelectedChannelStore, Tooltip, useEffect, useStateFromStores, VoiceStateStore } from "@webpack/common"; -import { ChannelMemberStore, cl, GuildMemberCountStore, numberFormat, ThreadMemberListStore } from "."; +import { ChannelMemberStore, cl, GuildMemberCountStore, numberFormat, settings, ThreadMemberListStore } from "."; import { OnlineMemberCountStore } from "./OnlineMemberCountStore"; +import { VoiceIcon } from "./VoiceIcon"; export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; tooltipGuildId?: string; }) { - const currentChannel = useStateFromStores([SelectedChannelStore], () => getCurrentChannel()); + const { voiceActivity } = settings.use(["voiceActivity"]); + const includeVoice = voiceActivity && !isTooltip; + const currentChannel = useStateFromStores([SelectedChannelStore], () => getCurrentChannel()); const guildId = isTooltip ? tooltipGuildId! : currentChannel?.guild_id; + const voiceActivityCount = useStateFromStores( + [VoiceStateStore], + () => { + if (!includeVoice) return 0; + + const voiceStates = VoiceStateStore.getVoiceStates(guildId); + if (!voiceStates) return 0; + + return Object.values(voiceStates) + .filter(({ channelId }) => { + if (!channelId) return false; + const channel = ChannelStore.getChannel(channelId); + return channel && PermissionStore.can(PermissionsBits.VIEW_CHANNEL, channel); + }) + .length; + } + ); + const totalCount = useStateFromStores( [GuildMemberCountStore], () => GuildMemberCountStore.getMemberCount(guildId) @@ -51,13 +72,14 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t if (totalCount == null) return null; + const formattedVoiceCount = numberFormat(voiceActivityCount ?? 0); const formattedOnlineCount = onlineCount != null ? numberFormat(onlineCount) : "?"; return (
{props => ( -
+
{formattedOnlineCount}
@@ -65,12 +87,22 @@ export function MemberCount({ isTooltip, tooltipGuildId }: { isTooltip?: true; t {props => ( -
+
{numberFormat(totalCount)}
)} + {includeVoice && voiceActivityCount > 0 && + + {props => ( +
+ + {formattedVoiceCount} +
+ )} +
+ }
); } diff --git a/src/plugins/memberCount/VoiceIcon.tsx b/src/plugins/memberCount/VoiceIcon.tsx new file mode 100644 index 000000000..e76f7151b --- /dev/null +++ b/src/plugins/memberCount/VoiceIcon.tsx @@ -0,0 +1,14 @@ +/* + * Vencord, a Discord client mod + * Copyright (c) 2025 Vendicated and contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +export function VoiceIcon({ className }: { className?: string; }) { + return ( + + + + + ); +} diff --git a/src/plugins/memberCount/index.tsx b/src/plugins/memberCount/index.tsx index 7fa6fbcde..bb48eedbf 100644 --- a/src/plugins/memberCount/index.tsx +++ b/src/plugins/memberCount/index.tsx @@ -37,18 +37,23 @@ export const ThreadMemberListStore = findStoreLazy("ThreadMemberListStore") as F }; -const settings = definePluginSettings({ +export const settings = definePluginSettings({ toolTip: { type: OptionType.BOOLEAN, - description: "If the member count should be displayed on the server tooltip", + description: "Show member count on the server tooltip", default: true, restartNeeded: true }, memberList: { type: OptionType.BOOLEAN, - description: "If the member count should be displayed on the member list", + description: "Show member count in the member list", default: true, restartNeeded: true + }, + voiceActivity: { + type: OptionType.BOOLEAN, + description: "Show voice activity with member count in the member list", + default: true } }); @@ -58,8 +63,8 @@ export const cl = classNameFactory("vc-membercount-"); export default definePlugin({ name: "MemberCount", - description: "Shows the amount of online & total members in the server member list and tooltip", - authors: [Devs.Ven, Devs.Commandtechno], + description: "Shows the number of online members, total members, and users in voice channels on the server — in the member list and tooltip.", + authors: [Devs.Ven, Devs.Commandtechno, Devs.Apexo], settings, patches: [ @@ -82,6 +87,6 @@ export default definePlugin({ predicate: () => settings.store.toolTip } ], - render: ErrorBoundary.wrap(MemberCount, { noop: true }), + render: ErrorBoundary.wrap(() => , { noop: true }), renderTooltip: ErrorBoundary.wrap(guild => , { noop: true }) }); diff --git a/src/plugins/memberCount/style.css b/src/plugins/memberCount/style.css index f43bff830..2b0e4400f 100644 --- a/src/plugins/memberCount/style.css +++ b/src/plugins/memberCount/style.css @@ -1,9 +1,11 @@ .vc-membercount-widget { + gap: 0.85em; display: flex; align-content: center; --color-online: var(--green-360); --color-total: var(--primary-400); + --color-voice: var(--primary-400); } .vc-membercount-tooltip { @@ -13,10 +15,17 @@ .vc-membercount-member-list { justify-content: center; + flex-wrap: wrap; margin-top: 1em; padding-inline: 1em; } +.vc-membercount-container { + display: flex; + align-items: center; + gap: 0.5em; +} + .vc-membercount-online { color: var(--color-online); } @@ -25,13 +34,16 @@ color: var(--color-total); } +.vc-membercount-voice { + color: var(--color-voice); +} + .vc-membercount-online-dot { background-color: var(--color-online); display: inline-block; width: 12px; height: 12px; border-radius: 50%; - margin-right: 0.5em; } .vc-membercount-total-dot { @@ -40,5 +52,10 @@ height: 6px; border-radius: 50%; border: 3px solid var(--color-total); - margin: 0 0.5em 0 1em; } + +.vc-membercount-voice-icon { + color: var(--color-voice); + width: 15px; + height: 15px; +} \ No newline at end of file diff --git a/src/plugins/userVoiceShow/components.tsx b/src/plugins/userVoiceShow/components.tsx index 9d1c7211b..b7907978d 100644 --- a/src/plugins/userVoiceShow/components.tsx +++ b/src/plugins/userVoiceShow/components.tsx @@ -8,8 +8,8 @@ import { classNameFactory } from "@api/Styles"; import ErrorBoundary from "@components/ErrorBoundary"; import { classes } from "@utils/misc"; import { Channel } from "@vencord/discord-types"; -import { filters, findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, findStoreLazy, mapMangledModuleLazy } from "@webpack"; -import { ChannelRouter, ChannelStore, GuildStore, IconUtils, match, P, PermissionsBits, PermissionStore, React, showToast, Text, Toasts, Tooltip, useMemo, UserStore, UserSummaryItem, useStateFromStores } from "@webpack/common"; +import { filters, findByCodeLazy, findByPropsLazy, findComponentByCodeLazy, mapMangledModuleLazy } from "@webpack"; +import { ChannelRouter, ChannelStore, GuildStore, IconUtils, match, P, PermissionsBits, PermissionStore, React, showToast, Text, Toasts, Tooltip, useMemo, UserStore, UserSummaryItem, useStateFromStores, VoiceStateStore } from "@webpack/common"; const cl = classNameFactory("vc-uvs-"); @@ -18,7 +18,6 @@ const { useChannelName } = mapMangledModuleLazy("#{intl::GROUP_DM_ALONE}", { useChannelName: filters.byCode("()=>null==") }); const getDMChannelIcon = findByCodeLazy(".getChannelIconURL({"); -const VoiceStateStore = findStoreLazy("VoiceStateStore"); const Avatar = findComponentByCodeLazy(".status)/2):0"); const GroupDMAvatars = findComponentByCodeLazy("frontSrc:", "getAvatarURL"); @@ -84,7 +83,7 @@ function VoiceChannelTooltip({ channel, isLocked }: VoiceChannelTooltipProps) { const voiceStates = useStateFromStores([VoiceStateStore], () => VoiceStateStore.getVoiceStatesForChannel(channel.id)); const users = useMemo( - () => Object.values(voiceStates).map(voiceState => UserStore.getUser(voiceState.userId)).filter(user => user != null), + () => Object.values(voiceStates).map(voiceState => UserStore.getUser(voiceState.userId)).filter(user => user != null), [voiceStates] ); @@ -139,7 +138,7 @@ export interface VoiceChannelIndicatorProps { const clickTimers = {} as Record; export const VoiceChannelIndicator = ErrorBoundary.wrap(({ userId, isProfile, isActionButton, shouldHighlight }: VoiceChannelIndicatorProps) => { - const channelId = useStateFromStores([VoiceStateStore], () => VoiceStateStore.getVoiceStateForUser(userId)?.channelId as string | undefined); + const channelId = useStateFromStores([VoiceStateStore], () => VoiceStateStore.getVoiceStateForUser(userId)?.channelId); const channel = channelId == null ? undefined : ChannelStore.getChannel(channelId); if (channel == null) return null; diff --git a/src/plugins/vcNarrator/index.tsx b/src/plugins/vcNarrator/index.tsx index 8931efe3d..5b7b3f82a 100644 --- a/src/plugins/vcNarrator/index.tsx +++ b/src/plugins/vcNarrator/index.tsx @@ -22,13 +22,12 @@ import { Logger } from "@utils/Logger"; import { Margins } from "@utils/margins"; import { wordsToTitle } from "@utils/text"; import definePlugin, { ReporterTestable } from "@utils/types"; -import { findByPropsLazy } from "@webpack"; -import { Button, ChannelStore, Forms, GuildMemberStore, SelectedChannelStore, SelectedGuildStore, useMemo, UserStore } from "@webpack/common"; +import { Button, ChannelStore, Forms, GuildMemberStore, SelectedChannelStore, SelectedGuildStore, useMemo, UserStore, VoiceStateStore } from "@webpack/common"; import { ReactElement } from "react"; import { getCurrentVoice, settings } from "./settings"; -interface VoiceState { +interface VoiceStateChangeEvent { userId: string; channelId?: string; oldChannelId?: string; @@ -38,8 +37,6 @@ interface VoiceState { selfMute: boolean; } -const VoiceStateStore = findByPropsLazy("getVoiceStatesForChannel", "getCurrentClientVoiceChannelId"); - // Mute/Deaf for other people than you is commented out, because otherwise someone can spam it and it will be annoying // Filtering out events is not as simple as just dropping duplicates, as otherwise mute, unmute, mute would // not say the second mute, which would lead you to believe they're unmuted @@ -88,7 +85,7 @@ let StatusMap = {} as Record WindowStore = m); waitForStore("EmojiStore", m => EmojiStore = m); waitForStore("StickersStore", m => StickersStore = m); waitForStore("TypingStore", m => TypingStore = m); +waitForStore("VoiceStateStore", m => VoiceStateStore = m); waitForStore("ThemeStore", m => { ThemeStore = m; // Importing this directly can easily cause circular imports. For this reason, use a non import access here. From e857f6806fa181596c160ce867b6b90e8a3895eb Mon Sep 17 00:00:00 2001 From: Vendicated Date: Fri, 5 Sep 2025 03:55:38 +0200 Subject: [PATCH 2/4] ShowMeYourName: respect streamer mode --- .../discord-types/src/stores/StreamerModeStore.d.ts | 11 +++++++++++ packages/discord-types/src/stores/index.d.ts | 1 + src/plugins/showMeYourName/index.tsx | 6 ++++-- src/webpack/common/stores.ts | 2 ++ 4 files changed, 18 insertions(+), 2 deletions(-) create mode 100644 packages/discord-types/src/stores/StreamerModeStore.d.ts diff --git a/packages/discord-types/src/stores/StreamerModeStore.d.ts b/packages/discord-types/src/stores/StreamerModeStore.d.ts new file mode 100644 index 000000000..efa6cfc9e --- /dev/null +++ b/packages/discord-types/src/stores/StreamerModeStore.d.ts @@ -0,0 +1,11 @@ +import { FluxStore } from "@vencord/discord-types"; + +export class StreamerModeStore extends FluxStore { + get autoToggle(): boolean; + get disableNotifications(): boolean; + get disableSounds(): boolean; + get enableContentProtection(): boolean; + get enabled(): boolean; + get hideInstantInvites(): boolean; + get hidePersonalInformation(): boolean; +} diff --git a/packages/discord-types/src/stores/index.d.ts b/packages/discord-types/src/stores/index.d.ts index 5906262ab..a843445bc 100644 --- a/packages/discord-types/src/stores/index.d.ts +++ b/packages/discord-types/src/stores/index.d.ts @@ -12,6 +12,7 @@ export * from "./RelationshipStore"; export * from "./SelectedChannelStore"; export * from "./SelectedGuildStore"; export * from "./StickersStore"; +export * from "./StreamerModeStore"; export * from "./ThemeStore"; export * from "./TypingStore"; export * from "./UserProfileStore"; diff --git a/src/plugins/showMeYourName/index.tsx b/src/plugins/showMeYourName/index.tsx index 623c70ca0..24a1121dc 100644 --- a/src/plugins/showMeYourName/index.tsx +++ b/src/plugins/showMeYourName/index.tsx @@ -11,7 +11,7 @@ import ErrorBoundary from "@components/ErrorBoundary"; import { Devs } from "@utils/constants"; import definePlugin, { OptionType } from "@utils/types"; import { Channel, Message, User } from "@vencord/discord-types"; -import { RelationshipStore } from "@webpack/common"; +import { RelationshipStore, StreamerModeStore } from "@webpack/common"; interface UsernameProps { author: { nick: string; authorId: string; }; @@ -74,7 +74,9 @@ export default definePlugin({ const { mode, friendNicknames, displayNames, inReplies } = settings.store; const user = userOverride ?? message.author; - let { username } = user; + let username = StreamerModeStore.enabled + ? user.username[0] + "…" + : user.username; if (displayNames) username = user.globalName || username; diff --git a/src/webpack/common/stores.ts b/src/webpack/common/stores.ts index b1877b083..1e3e5797e 100644 --- a/src/webpack/common/stores.ts +++ b/src/webpack/common/stores.ts @@ -54,6 +54,7 @@ export let StickersStore: t.StickersStore; export let ThemeStore: t.ThemeStore; export let WindowStore: t.WindowStore; export let DraftStore: t.DraftStore; +export let StreamerModeStore: t.StreamerModeStore; /** * @see jsdoc of {@link t.useStateFromStores} @@ -81,6 +82,7 @@ waitForStore("EmojiStore", m => EmojiStore = m); waitForStore("StickersStore", m => StickersStore = m); waitForStore("TypingStore", m => TypingStore = m); waitForStore("VoiceStateStore", m => VoiceStateStore = m); +waitForStore("StreamerModeStore", m => StreamerModeStore = m); waitForStore("ThemeStore", m => { ThemeStore = m; // Importing this directly can easily cause circular imports. For this reason, use a non import access here. From 26074b7f185013745da0656adb500b3fbe95fa4f Mon Sep 17 00:00:00 2001 From: u32 Date: Fri, 5 Sep 2025 03:04:08 +0100 Subject: [PATCH 3/4] MessageLatency: fix bot check (#3523) --- src/plugins/messageLatency/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/messageLatency/index.tsx b/src/plugins/messageLatency/index.tsx index cd331b9d7..b81c55cf7 100644 --- a/src/plugins/messageLatency/index.tsx +++ b/src/plugins/messageLatency/index.tsx @@ -98,7 +98,7 @@ export default definePlugin({ if (!isNonNullish(nonce)) return null; // Bots basically never send a nonce, and if someone does do it then it's usually not a snowflake - if (message.bot) return null; + if (message.author.bot) return null; let isDiscordKotlin = false; let delta = SnowflakeUtils.extractTimestamp(id) - SnowflakeUtils.extractTimestamp(nonce); // milliseconds From 1cfc3fb8f808c5770c0175a2471c7eb5525800fd Mon Sep 17 00:00:00 2001 From: thororen <78185467+thororen1234@users.noreply.github.com> Date: Sat, 6 Sep 2025 12:27:29 -0400 Subject: [PATCH 4/4] fix OnePingPerDM (#3648) also removes a now obsolete patch from FakeNitro --- src/plugins/fakeNitro/index.tsx | 3 +-- src/plugins/onePingPerDM/index.ts | 24 ++++++++++++++---------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/plugins/fakeNitro/index.tsx b/src/plugins/fakeNitro/index.tsx index 8dcb2300e..2988f8fb6 100644 --- a/src/plugins/fakeNitro/index.tsx +++ b/src/plugins/fakeNitro/index.tsx @@ -159,7 +159,6 @@ function makeBypassPatches(): Omit { { func: "canUseHighVideoUploadQuality", predicate: () => settings.store.enableStreamQualityBypass }, { func: "canStreamQuality", predicate: () => settings.store.enableStreamQualityBypass }, { func: "canUseClientThemes" }, - { func: "canUseCustomNotificationSounds" }, { func: "canUsePremiumAppIcons" } ]; @@ -176,7 +175,7 @@ function makeBypassPatches(): Omit { export default definePlugin({ name: "FakeNitro", authors: [Devs.Arjix, Devs.D3SOX, Devs.Ven, Devs.fawn, Devs.captain, Devs.Nuckyz, Devs.AutumnVN], - description: "Allows you to stream in nitro quality, send fake emojis/stickers, use client themes and custom Discord notifications.", + description: "Allows you to stream in nitro quality, send fake emojis/stickers, and use client themes.", dependencies: ["MessageEventsAPI"], settings, diff --git a/src/plugins/onePingPerDM/index.ts b/src/plugins/onePingPerDM/index.ts index b6192fb64..fae787a84 100644 --- a/src/plugins/onePingPerDM/index.ts +++ b/src/plugins/onePingPerDM/index.ts @@ -38,17 +38,21 @@ export default definePlugin({ description: "If unread messages are sent by a user in DMs multiple times, you'll only receive one audio ping. Read the messages to reset the limit", authors: [Devs.ProffDea], settings, - patches: [{ - find: ".getDesktopType()===", - replacement: [{ - match: /(\i\.\i\.getDesktopType\(\)===\i\.\i\.NEVER)\)/, - replace: "$&if(!$self.isPrivateChannelRead(arguments[0]?.message))return;else " - }, + patches: [ { - match: /sound:(\i\?\i:void 0,soundpack:\i,volume:\i,onClick)/, - replace: "sound:!$self.isPrivateChannelRead(arguments[0]?.message)?undefined:$1" - }] - }], + find: ".getDesktopType()===", + replacement: [ + { + match: /(\i\.\i\.getDesktopType\(\)===\i\.\i\.NEVER)\)/, + replace: "$&if(!$self.isPrivateChannelRead(arguments[0]?.message))return;else " + }, + { + match: /sound:(\i\?\i:void 0,volume:\i,onClick)/, + replace: "sound:!$self.isPrivateChannelRead(arguments[0]?.message)?undefined:$1" + } + ] + } + ], isPrivateChannelRead(message: MessageJSON) { const channelType = ChannelStore.getChannel(message.channel_id)?.type; if (