From 1a507b086825707b09a5ad2f4ea0c8fc57e26b68 Mon Sep 17 00:00:00 2001 From: Key Date: Sun, 28 Dec 2025 21:56:29 +0000 Subject: [PATCH] Merge AllCallTimers into CallTimer (#612) --- src/equicordplugins/allCallTimers/index.tsx | 242 ---------------- .../callTimer}/Timer.tsx | 0 .../callTimer}/TimerIcon.tsx | 0 src/plugins/callTimer/alignedChatInputFix.css | 2 +- src/plugins/callTimer/index.tsx | 269 +++++++++++++++--- .../callTimer}/timerText.tsx | 0 6 files changed, 236 insertions(+), 277 deletions(-) delete mode 100644 src/equicordplugins/allCallTimers/index.tsx rename src/{equicordplugins/allCallTimers => plugins/callTimer}/Timer.tsx (100%) rename src/{equicordplugins/allCallTimers => plugins/callTimer}/TimerIcon.tsx (100%) rename src/{equicordplugins/allCallTimers => plugins/callTimer}/timerText.tsx (100%) diff --git a/src/equicordplugins/allCallTimers/index.tsx b/src/equicordplugins/allCallTimers/index.tsx deleted file mode 100644 index 01d95f0a0..000000000 --- a/src/equicordplugins/allCallTimers/index.tsx +++ /dev/null @@ -1,242 +0,0 @@ -/* - * 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 ErrorBoundary from "@components/ErrorBoundary"; -import { Devs, EquicordDevs } from "@utils/constants"; -import definePlugin, { OptionType } from "@utils/types"; -import { PassiveUpdateState, VoiceState } from "@vencord/discord-types"; -import { FluxDispatcher, GuildStore, UserStore } from "@webpack/common"; - -import { Timer } from "./Timer"; - -export const settings = definePluginSettings({ - showWithoutHover: { - type: OptionType.BOOLEAN, - description: "Always show the timer without needing to hover", - restartNeeded: true, - default: true - }, - showRoleColor: { - type: OptionType.BOOLEAN, - description: "Show the user's role color (if this plugin in enabled)", - restartNeeded: false, - default: true - }, - trackSelf: { - type: OptionType.BOOLEAN, - description: "Also track yourself", - restartNeeded: false, - default: true - }, - showSeconds: { - type: OptionType.BOOLEAN, - description: "Show seconds in the timer", - restartNeeded: false, - default: true - }, - format: { - type: OptionType.SELECT, - description: "Compact or human readable format:", - options: [ - { - label: "30:23:00:42", - value: "stopwatch", - default: true - }, - { - label: "30d 23h 00m 42s", - value: "human" - } - ] - }, - watchLargeGuilds: { - type: OptionType.BOOLEAN, - description: "Track users in large guilds. This may cause lag if you're in a lot of large guilds with active voice users. Tested with up to 2000 active voice users with no issues.", - restartNeeded: true, - default: false - } -}); - -// Save the join time of all users in a Map -type userJoinData = { channelId: string, time: number; guildId: string; }; -const userJoinTimes = new Map(); - -/** - * The function `addUserJoinTime` stores the join time of a user in a specific channel within a guild. - * @param {string} userId - The `userId` parameter is a string that represents the unique identifier of - * the user who is joining a channel in a guild. - * @param {string} channelId - The `channelId` parameter represents the unique identifier of the - * channel where the user joined. - * @param {string} guildId - The `guildId` parameter in the `addUserJoinTime` function represents the - * unique identifier of the guild (server) to which the user belongs. It is used to associate the - * user's join time with a specific guild within the application or platform. - */ -function addUserJoinTime(userId: string, channelId: string, guildId: string) { - // create a random number - userJoinTimes.set(userId, { channelId, time: Date.now(), guildId }); -} - -/** - * The function `removeUserJoinTime` removes the join time of a user identified by their user ID. - * @param {string} userId - The `userId` parameter is a string that represents the unique identifier of - * a user whose join time needs to be removed. - */ -function removeUserJoinTime(userId: string) { - userJoinTimes.delete(userId); -} - -// For every user, channelId and oldChannelId will differ when moving channel. -// Only for the local user, channelId and oldChannelId will be the same when moving channel, -// for some ungodly reason -let myLastChannelId: string | undefined; - -// Allow user updates on discord first load -let runOneTime = true; - -export default definePlugin({ - name: "AllCallTimers", - description: "Add call timer to all users in a server voice channel.", - authors: [EquicordDevs.MaxHerbold, Devs.D3SOX], - settings, - patches: [ - { - find: ".usernameSpeaking]", - replacement: [ - { - match: /(?<=user:(\i).*?)iconGroup,.{0,200}children:\[/, - replace: "$&$self.renderTimer($1.id),", - predicate: () => !settings.store.showWithoutHover, - }, - { - match: /function \i\(\)\{.+:""(?=.*?userId:(\i))/, - replace: "$&,$self.renderTimer($1.id),", - predicate: () => settings.store.showWithoutHover, - } - ] - } - ], - - flux: { - VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) { - const myId = UserStore.getCurrentUser().id; - - for (const state of voiceStates) { - const { userId, channelId, guildId } = state; - const isMe = userId === myId; - - if (!guildId) { - // guildId is never undefined here - continue; - } - - // check if the state does not actually has a `oldChannelId` property - if (!("oldChannelId" in state) && !runOneTime && !settings.store.watchLargeGuilds) { - // batch update triggered. This is ignored because it - // is caused by opening a previously unopened guild - continue; - } - - let { oldChannelId } = state; - if (isMe && channelId !== myLastChannelId) { - oldChannelId = myLastChannelId; - myLastChannelId = channelId ?? undefined; - } - - if (channelId !== oldChannelId) { - if (channelId) { - // move or join - addUserJoinTime(userId, channelId, guildId); - } else if (oldChannelId) { - // leave - removeUserJoinTime(userId); - } - } - } - runOneTime = false; - }, - PASSIVE_UPDATE_V1(passiveUpdate: PassiveUpdateState) { - if (settings.store.watchLargeGuilds) { - return; - } - - const { voiceStates } = passiveUpdate; - if (!voiceStates) { - // if there are no users in a voice call - return; - } - - // find all users that have the same guildId and if that user is not in the voiceStates, remove them from the map - const { guildId } = passiveUpdate; - - // check the guildId in the userJoinTimes map - for (const [userId, data] of userJoinTimes) { - if (data.guildId === guildId) { - // check if the user is in the voiceStates - const userInVoiceStates = voiceStates.find(state => state.userId === userId); - if (!userInVoiceStates) { - // remove the user from the map - removeUserJoinTime(userId); - } - } - } - - // since we were gifted this data let's use it to update our join times - for (const state of voiceStates) { - const { userId, channelId } = state; - - if (!channelId) { - // channelId is never undefined here - continue; - } - - // check if the user is in the map - if (userJoinTimes.has(userId)) { - // check if the user is in a channel - if (channelId !== userJoinTimes.get(userId)?.channelId) { - // update the user's join time - addUserJoinTime(userId, channelId, guildId); - } - } else { - // user wasn't previously tracked, add the user to the map - addUserJoinTime(userId, channelId, guildId); - } - } - }, - }, - - subscribeToAllGuilds() { - // we need to subscribe to all guilds' events because otherwise we would miss updates on large guilds - const guilds = Object.values(GuildStore.getGuilds()).map(guild => guild.id); - const subscriptions = guilds.reduce((acc, id) => ({ ...acc, [id]: { typing: true } }), {}); - FluxDispatcher.dispatch({ type: "GUILD_SUBSCRIPTIONS_FLUSH", subscriptions }); - }, - - start() { - if (settings.store.watchLargeGuilds) { - this.subscribeToAllGuilds(); - } - }, - - renderTimer(userId: string) { - // get the user join time from the users object - const joinTime = userJoinTimes.get(userId); - if (!joinTime?.time) { - // join time is unknown - return; - } - if (userId === UserStore.getCurrentUser().id && !settings.store.trackSelf) { - // don't show for self - return; - } - - return ( - - - - ); - }, -}); diff --git a/src/equicordplugins/allCallTimers/Timer.tsx b/src/plugins/callTimer/Timer.tsx similarity index 100% rename from src/equicordplugins/allCallTimers/Timer.tsx rename to src/plugins/callTimer/Timer.tsx diff --git a/src/equicordplugins/allCallTimers/TimerIcon.tsx b/src/plugins/callTimer/TimerIcon.tsx similarity index 100% rename from src/equicordplugins/allCallTimers/TimerIcon.tsx rename to src/plugins/callTimer/TimerIcon.tsx diff --git a/src/plugins/callTimer/alignedChatInputFix.css b/src/plugins/callTimer/alignedChatInputFix.css index 6ec48b39f..4374d53ab 100644 --- a/src/plugins/callTimer/alignedChatInputFix.css +++ b/src/plugins/callTimer/alignedChatInputFix.css @@ -1,4 +1,4 @@ [class*="panels"] [class*="inner"], [class*="rtcConnectionStatus"] { height: fit-content !important; -} \ No newline at end of file +} diff --git a/src/plugins/callTimer/index.tsx b/src/plugins/callTimer/index.tsx index a51bdc0c2..423646292 100644 --- a/src/plugins/callTimer/index.tsx +++ b/src/plugins/callTimer/index.tsx @@ -1,38 +1,28 @@ /* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ + * 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 { definePluginSettings, migratePluginToSettings } from "@api/Settings"; import ErrorBoundary from "@components/ErrorBoundary"; -import { Devs } from "@utils/constants"; +import { Devs, EquicordDevs } from "@utils/constants"; import { useTimer } from "@utils/react"; import { formatDurationMs } from "@utils/text"; import definePlugin, { OptionType } from "@utils/types"; -import { React } from "@webpack/common"; +import { PassiveUpdateState, VoiceState } from "@vencord/discord-types"; +import { FluxDispatcher, GuildStore, React, UserStore } from "@webpack/common"; import alignedChatInputFix from "./alignedChatInputFix.css?managed"; +import { Timer } from "./Timer"; -const settings = definePluginSettings({ +export const settings = definePluginSettings({ format: { type: OptionType.SELECT, - description: "The timer format. This can be any valid moment.js format", + description: "Compact or human readable format:", options: [ { - label: "30d 23:00:42", + label: "30:23:00:42", value: "stopwatch", default: true }, @@ -42,34 +32,245 @@ const settings = definePluginSettings({ } ] }, + allCallTimers: { + type: OptionType.BOOLEAN, + description: "Add call timer to all users in a server voice channel", + restartNeeded: true, + default: false + }, + showWithoutHover: { + type: OptionType.BOOLEAN, + description: "Always show the timer without needing to hover", + restartNeeded: true, + default: false + }, + showRoleColor: { + type: OptionType.BOOLEAN, + description: "Show the user's role color (if this plugin in enabled)", + restartNeeded: false, + default: false + }, + trackSelf: { + type: OptionType.BOOLEAN, + description: "Also track yourself", + restartNeeded: false, + default: false + }, + showSeconds: { + type: OptionType.BOOLEAN, + description: "Show seconds in the timer", + restartNeeded: false, + default: false + }, + watchLargeGuilds: { + type: OptionType.BOOLEAN, + description: "Track users in large guilds. This may cause lag if you're in a lot of large guilds with active voice users. Tested with up to 2000 active voice users with no issues.", + restartNeeded: true, + default: false + } }); +// Save the join time of all users in a Map +type userJoinData = { channelId: string, time: number; guildId: string; }; +const userJoinTimes = new Map(); + +/** + * The function `addUserJoinTime` stores the join time of a user in a specific channel within a guild. + * @param {string} userId - The `userId` parameter is a string that represents the unique identifier of + * the user who is joining a channel in a guild. + * @param {string} channelId - The `channelId` parameter represents the unique identifier of the + * channel where the user joined. + * @param {string} guildId - The `guildId` parameter in the `addUserJoinTime` function represents the + * unique identifier of the guild (server) to which the user belongs. It is used to associate the + * user's join time with a specific guild within the application or platform. + */ +function addUserJoinTime(userId: string, channelId: string, guildId: string) { + // create a random number + userJoinTimes.set(userId, { channelId, time: Date.now(), guildId }); +} + +/** + * The function `removeUserJoinTime` removes the join time of a user identified by their user ID. + * @param {string} userId - The `userId` parameter is a string that represents the unique identifier of + * a user whose join time needs to be removed. + */ +function removeUserJoinTime(userId: string) { + userJoinTimes.delete(userId); +} + +// For every user, channelId and oldChannelId will differ when moving channel. +// Only for the local user, channelId and oldChannelId will be the same when moving channel, +// for some ungodly reason +let myLastChannelId: string | undefined; + +// Allow user updates on discord first load +let runOneTime = true; + +migratePluginToSettings("CallTimer", "AllCallTimers", "allCallTimers"); export default definePlugin({ name: "CallTimer", - description: "Adds a timer to vcs", - authors: [Devs.Ven], + description: "Add call timers for all users in voice channels and in the connection status.", + authors: [EquicordDevs.MaxHerbold, Devs.D3SOX], managedStyle: alignedChatInputFix, settings, + isModified: true, startTime: 0, interval: void 0 as NodeJS.Timeout | undefined, - patches: [{ - find: "renderConnectionStatus(){", - replacement: { - // in renderConnectionStatus() - match: /(lineClamp:1,children:)(\i)(?=,|}\))/, - replace: "$1[$2,$self.renderTimer(this.props.channel.id)]" + patches: [ + { + find: ".usernameSpeaking]", + predicate: () => settings.store.allCallTimers, + replacement: [ + { + match: /(?<=user:(\i).*?)iconGroup,.{0,200}children:\[/, + replace: "$&$self.renderTimer($1.id),", + predicate: () => !settings.store.showWithoutHover, + }, + { + match: /function \i\(\)\{.+:""(?=.*?userId:(\i))/, + replace: "$&,$self.renderTimer($1.id),", + predicate: () => settings.store.showWithoutHover, + } + ] + }, + { + find: "renderConnectionStatus(){", + replacement: { + match: /(lineClamp:1,children:)(\i)(?=,|}\))/, + replace: "$1[$2,$self.renderConnectionTimer(this.props.channel.id)]" + } } - }], + ], - renderTimer(channelId: string) { + flux: { + VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) { + const myId = UserStore.getCurrentUser().id; + + for (const state of voiceStates) { + const { userId, channelId, guildId } = state; + const isMe = userId === myId; + + if (!guildId) { + // guildId is never undefined here + continue; + } + + // check if the state does not actually has a `oldChannelId` property + if (!("oldChannelId" in state) && !runOneTime && !settings.store.watchLargeGuilds) { + // batch update triggered. This is ignored because it + // is caused by opening a previously unopened guild + continue; + } + + let { oldChannelId } = state; + if (isMe && channelId !== myLastChannelId) { + oldChannelId = myLastChannelId; + myLastChannelId = channelId ?? undefined; + } + + if (channelId !== oldChannelId) { + if (channelId) { + // move or join + addUserJoinTime(userId, channelId, guildId); + } else if (oldChannelId) { + // leave + removeUserJoinTime(userId); + } + } + } + runOneTime = false; + }, + PASSIVE_UPDATE_V1(passiveUpdate: PassiveUpdateState) { + if (settings.store.watchLargeGuilds) { + return; + } + + const { voiceStates } = passiveUpdate; + if (!voiceStates) { + // if there are no users in a voice call + return; + } + + // find all users that have the same guildId and if that user is not in the voiceStates, remove them from the map + const { guildId } = passiveUpdate; + + // check the guildId in the userJoinTimes map + for (const [userId, data] of userJoinTimes) { + if (data.guildId === guildId) { + // check if the user is in the voiceStates + const userInVoiceStates = voiceStates.find(state => state.userId === userId); + if (!userInVoiceStates) { + // remove the user from the map + removeUserJoinTime(userId); + } + } + } + + // since we were gifted this data let's use it to update our join times + for (const state of voiceStates) { + const { userId, channelId } = state; + + if (!channelId) { + // channelId is never undefined here + continue; + } + + // check if the user is in the map + if (userJoinTimes.has(userId)) { + // check if the user is in a channel + if (channelId !== userJoinTimes.get(userId)?.channelId) { + // update the user's join time + addUserJoinTime(userId, channelId, guildId); + } + } else { + // user wasn't previously tracked, add the user to the map + addUserJoinTime(userId, channelId, guildId); + } + } + }, + }, + + subscribeToAllGuilds() { + // we need to subscribe to all guilds' events because otherwise we would miss updates on large guilds + const guilds = Object.values(GuildStore.getGuilds()).map(guild => guild.id); + const subscriptions = guilds.reduce((acc, id) => ({ ...acc, [id]: { typing: true } }), {}); + FluxDispatcher.dispatch({ type: "GUILD_SUBSCRIPTIONS_FLUSH", subscriptions }); + }, + + start() { + if (settings.store.watchLargeGuilds) { + this.subscribeToAllGuilds(); + } + }, + + renderTimer(userId: string) { + // get the user join time from the users object + const joinTime = userJoinTimes.get(userId); + if (!joinTime?.time) { + // join time is unknown + return; + } + if (userId === UserStore.getCurrentUser().id && !settings.store.trackSelf) { + // don't show for self + return; + } + + return ( + + + + ); + }, + + renderConnectionTimer(channelId: string) { return - + ; }, - Timer({ channelId }: { channelId: string; }) { + ConnectionTimer({ channelId }: { channelId: string; }) { const time = useTimer({ deps: [channelId] }); diff --git a/src/equicordplugins/allCallTimers/timerText.tsx b/src/plugins/callTimer/timerText.tsx similarity index 100% rename from src/equicordplugins/allCallTimers/timerText.tsx rename to src/plugins/callTimer/timerText.tsx