Plugin
AllCallTimers
Add call timer to all users in a server voice channel.
1
/*2
* FiveCord — a Discord client mod3
* Copyright (c) 2025 FiveCord4
* SPDX-License-Identifier: GPL-3.0-or-later5
*/6
7
import { definePluginSettings } from "@api/Settings";8
import ErrorBoundary from "@components/ErrorBoundary";9
import { Devs } from "@utils/constants";10
import definePlugin, { OptionType } from "@utils/types";11
import { FluxDispatcher, GuildStore, UserStore } from "@webpack/common";12
import { PassiveUpdateState, VoiceState } from "@webpack/types";13
14
import { Timer } from "./Timer";15
16
export const settings = definePluginSettings({17
showWithoutHover: {18
type: OptionType.BOOLEAN,19
description: "Always show the timer without needing to hover",20
restartNeeded: false,21
default: true22
},23
showRoleColor: {24
type: OptionType.BOOLEAN,25
description: "Show the user039;s role color (if this plugin in enabled)",26
restartNeeded: false,27
default: true28
},29
trackSelf: {30
type: OptionType.BOOLEAN,31
description: "Also track yourself",32
restartNeeded: false,33
default: true34
},35
showSeconds: {36
type: OptionType.BOOLEAN,37
description: "Show seconds in the timer",38
restartNeeded: false,39
default: true40
},41
format: {42
type: OptionType.SELECT,43
description: "Compact or human readable format:",44
options: [45
{46
label: "30:23:00:42",47
value: "stopwatch",48
default: true49
},50
{51
label: "30d 23h 00m 42s",52
value: "human"53
}54
]55
},56
watchLargeGuilds: {57
type: OptionType.BOOLEAN,58
description: "Track users in large guilds. This may cause lag if you039;re in a lot of large guilds with active voice users. Tested with up to 2000 active voice users with no issues.",59
restartNeeded: true,60
default: false61
}62
});63
64
65
// Save the join time of all users in a Map66
type userJoinData = { channelId: string, time: number; guildId: string; };67
const userJoinTimes = new Map<string, userJoinData>();68
69
/**70
* The function `addUserJoinTime` stores the join time of a user in a specific channel within a guild.71
* @param {string} userId - The `userId` parameter is a string that represents the unique identifier of72
* the user who is joining a channel in a guild.73
* @param {string} channelId - The `channelId` parameter represents the unique identifier of the74
* channel where the user joined.75
* @param {string} guildId - The `guildId` parameter in the `addUserJoinTime` function represents the76
* unique identifier of the guild (server) to which the user belongs. It is used to associate the77
* user's join time with a specific guild within the application or platform.78
*/79
function addUserJoinTime(userId: string, channelId: string, guildId: string) {80
// create a random number81
userJoinTimes.set(userId, { channelId, time: Date.now(), guildId });82
}83
84
/**85
* The function `removeUserJoinTime` removes the join time of a user identified by their user ID.86
* @param {string} userId - The `userId` parameter is a string that represents the unique identifier of87
* a user whose join time needs to be removed.88
*/89
function removeUserJoinTime(userId: string) {90
userJoinTimes.delete(userId);91
}92
93
// For every user, channelId and oldChannelId will differ when moving channel.94
// Only for the local user, channelId and oldChannelId will be the same when moving channel,95
// for some ungodly reason96
let myLastChannelId: string | undefined;97
98
// Allow user updates on discord first load99
let runOneTime = true;100
101
export default definePlugin({102
name: "AllCallTimers",103
description: "Add call timer to all users in a server voice channel.",104
authors: [Devs.FiveCord],105
106
settings,107
108
patches: [109
{110
find: "renderPrioritySpeaker",111
replacement: [112
{113
match: /(render\(\)\{.+\}\),children:)\[(.+renderName\(\),)/,114
replace: "$&,$self.showClockInjection(this),"115
}116
]117
},118
{119
find: "renderPrioritySpeaker",120
replacement: [121
{122
match: /(renderName\(\)\{.+:"")/,123
replace: "$&,$self.showTextInjection(this),"124
}125
]126
}127
],128
129
flux: {130
VOICE_STATE_UPDATES({ voiceStates }: { voiceStates: VoiceState[]; }) {131
const myId = UserStore.getCurrentUser().id;132
133
for (const state of voiceStates) {134
const { userId, channelId, guildId } = state;135
const isMe = userId === myId;136
137
if (!guildId) {138
// guildId is never undefined here139
continue;140
}141
142
// check if the state does not actually has a `oldChannelId` property143
if (!("oldChannelId" in state) && !runOneTime && !settings.store.watchLargeGuilds) {144
// batch update triggered. This is ignored because it145
// is caused by opening a previously unopened guild146
continue;147
}148
149
let { oldChannelId } = state;150
if (isMe && channelId !== myLastChannelId) {151
oldChannelId = myLastChannelId;152
myLastChannelId = channelId;153
}154
155
if (channelId !== oldChannelId) {156
if (channelId) {157
// move or join158
addUserJoinTime(userId, channelId, guildId);159
} else if (oldChannelId) {160
// leave161
removeUserJoinTime(userId);162
}163
}164
}165
runOneTime = false;166
},167
PASSIVE_UPDATE_V1(passiveUpdate: PassiveUpdateState) {168
if (settings.store.watchLargeGuilds) {169
return;170
}171
172
const { voiceStates } = passiveUpdate;173
if (!voiceStates) {174
// if there are no users in a voice call175
return;176
}177
178
// find all users that have the same guildId and if that user is not in the voiceStates, remove them from the map179
const { guildId } = passiveUpdate;180
181
// check the guildId in the userJoinTimes map182
for (const [userId, data] of userJoinTimes) {183
if (data.guildId === guildId) {184
// check if the user is in the voiceStates185
const userInVoiceStates = voiceStates.find(state => state.userId === userId);186
if (!userInVoiceStates) {187
// remove the user from the map188
removeUserJoinTime(userId);189
}190
}191
}192
193
// since we were gifted this data let's use it to update our join times194
for (const state of voiceStates) {195
const { userId, channelId } = state;196
197
if (!channelId) {198
// channelId is never undefined here199
continue;200
}201
202
// check if the user is in the map203
if (userJoinTimes.has(userId)) {204
// check if the user is in a channel205
if (channelId !== userJoinTimes.get(userId)?.channelId) {206
// update the user's join time207
addUserJoinTime(userId, channelId, guildId);208
}209
} else {210
// user wasn't previously tracked, add the user to the map211
addUserJoinTime(userId, channelId, guildId);212
}213
}214
},215
},216
217
subscribeToAllGuilds() {218
// we need to subscribe to all guilds' events because otherwise we would miss updates on large guilds219
const guilds = Object.values(GuildStore.getGuilds()).map(guild => guild.id);220
const subscriptions = guilds.reduce((acc, id) => ({ ...acc, [id]: { typing: true } }), {});221
FluxDispatcher.dispatch({ type: "GUILD_SUBSCRIPTIONS_FLUSH", subscriptions });222
},223
224
start() {225
if (settings.store.watchLargeGuilds) {226
this.subscribeToAllGuilds();227
}228
},229
230
showClockInjection(property: { props: { user: { id: string; }; }; }) {231
if (settings.store.showWithoutHover) {232
return "";233
}234
return this.showInjection(property);235
},236
237
showTextInjection(property: { props: { user: { id: string; }; }; }) {238
if (!settings.store.showWithoutHover) {239
return "";240
}241
return this.showInjection(property);242
},243
244
showInjection(property: { props: { user: { id: string; }; }; }) {245
const userId = property.props.user.id;246
return this.renderTimer(userId);247
},248
249
renderTimer(userId: string) {250
// get the user join time from the users object251
const joinTime = userJoinTimes.get(userId);252
if (!joinTime?.time) {253
// join time is unknown254
return;255
}256
if (userId === UserStore.getCurrentUser().id && !settings.store.trackSelf) {257
// don't show for self258
return;259
}260
261
return (262
<ErrorBoundary>263
<Timer time={joinTime.time} />264
</ErrorBoundary>265
);266
},267
});268