Add BetterDiscord themes and plugin configurations for customization

This commit is contained in:
2026-02-21 14:17:51 +01:00
parent ff0be9b328
commit 19f4a500dc
87 changed files with 68305 additions and 35 deletions
@@ -0,0 +1,892 @@
/**
* @name VoiceActivity
* @author Neodymium
* @version 1.12.1
* @description Shows icons and info in popouts, the member list, and more when someone is in a voice channel.
* @source https://github.com/Neodymium7/BetterDiscordStuff/blob/main/VoiceActivity/VoiceActivity.plugin.js
* @invite fRbsqH87Av
*/
/*@cc_on
@if (@_jscript)
// Offer to self-install for clueless users that try to run this directly.
var shell = WScript.CreateObject("WScript.Shell");
var fs = new ActiveXObject("Scripting.FileSystemObject");
var pathPlugins = shell.ExpandEnvironmentStrings("%APPDATA%\BetterDiscord\plugins");
var pathSelf = WScript.ScriptFullName;
// Put the user at ease by addressing them in the first person
shell.Popup("It looks like you've mistakenly tried to run me directly. \n(Don't do that!)", 0, "I'm a plugin for BetterDiscord", 0x30);
if (fs.GetParentFolderName(pathSelf) === fs.GetAbsolutePathName(pathPlugins)) {
shell.Popup("I'm in the correct folder already.", 0, "I'm already installed", 0x40);
} else if (!fs.FolderExists(pathPlugins)) {
shell.Popup("I can't find the BetterDiscord plugins folder.\nAre you sure it's even installed?", 0, "Can't install myself", 0x10);
} else if (shell.Popup("Should I copy myself to BetterDiscord's plugins folder for you?", 0, "Do you need some help?", 0x34) === 6) {
fs.CopyFile(pathSelf, fs.BuildPath(pathPlugins, fs.GetFileName(pathSelf)), true);
// Show the user where to put plugins in the future
shell.Exec("explorer " + pathPlugins);
shell.Popup("I'm installed!", 0, "Successfully installed", 0x40);
}
WScript.Quit();
@else@*/
'use strict';
const betterdiscord = new BdApi("VoiceActivity");
const react = BdApi.React;
// styles
let _styles = "";
function _loadStyle(path, css) {
_styles += "/*" + path + "*/\n" + css + "\n";
}
function styles() {
return _styles;
}
// @lib/settings.ts
class SettingsManager {
settings = betterdiscord.Data.load("settings");
listeners = new Set();
constructor(defaultSettings) {
if (!this.settings) {
betterdiscord.Data.save("settings", defaultSettings);
this.settings = defaultSettings;
return;
}
if (Object.keys(this.settings) !== Object.keys(defaultSettings)) {
this.settings = { ...defaultSettings, ...this.settings };
let changed = false;
for (const key in this.settings) {
if (!(key in defaultSettings)) {
delete this.settings[key];
changed = true;
}
}
if (changed) betterdiscord.Data.save("settings", this.settings);
}
}
addListener(listener) {
this.listeners.add(listener);
return () => {
this.listeners.delete(listener);
};
}
clearListeners() {
this.listeners.clear();
}
useSettingsState(...keys) {
let initialState = this.settings;
if (keys.length) initialState = Object.fromEntries(keys.map((key) => [key, initialState[key]]));
const [state, setState] = react.useState(initialState);
react.useEffect(() => {
return this.addListener((key, value) => {
if (!keys.length || keys.includes(key)) setState((state2) => ({ ...state2, [key]: value }));
});
}, []);
return state;
}
get(key) {
return this.settings[key];
}
set(key, value) {
this.settings[key] = value;
betterdiscord.Data.save("settings", this.settings);
for (const listener of this.listeners) listener(key, value);
}
}
function buildSettingsPanel(settingsManager, settings) {
for (const setting of settings) {
setting.value = settingsManager.get(setting.id);
}
return betterdiscord.UI.buildSettingsPanel({
settings,
onChange: (_, id, value) => settingsManager.set(id, value)
});
}
// @lib/strings.ts
const LocaleStore = betterdiscord.Webpack.getStore("LocaleStore");
class StringsManager {
locales;
defaultLocale;
strings;
constructor(locales, defaultLocale) {
this.locales = locales;
this.defaultLocale = defaultLocale;
this.strings = locales[defaultLocale];
}
setLocale = () => {
this.strings = this.locales[LocaleStore.locale] || this.locales[this.defaultLocale];
};
subscribe() {
this.setLocale();
LocaleStore.addReactChangeListener(this.setLocale);
}
unsubscribe() {
LocaleStore.removeReactChangeListener(this.setLocale);
}
get(key) {
return this.strings[key] || this.locales[this.defaultLocale][key];
}
}
// @lib/changelog.ts
function showChangelog(changes, meta) {
if (!changes || changes.length == 0) return;
const changelogVersion = betterdiscord.Data.load("changelogVersion");
if (meta.version === changelogVersion) return;
betterdiscord.UI.showChangelogModal({
title: meta.name,
subtitle: meta.version,
changes
});
betterdiscord.Data.save("changelogVersion", meta.version);
}
// @lib/utils/webpack.ts
function getClasses(...classes) {
return betterdiscord.Webpack.getModule((m) => betterdiscord.Webpack.Filters.byKeys(...classes)(m) && typeof m[classes[0]] == "string");
}
function getSelectors(...classes) {
const module = getClasses(...classes);
if (!module) return void 0;
return classes.reduce((obj, className) => {
obj[className] = "." + module[className].replaceAll(" ", ".");
return obj;
}, {});
}
function getIcon(searchString) {
const filter = (m) => betterdiscord.Webpack.Filters.byStrings(searchString, '"svg"')(m) && typeof m === "function";
return betterdiscord.Webpack.getModule(filter, {
searchExports: true
});
}
function expect(object, options) {
if (object) return object;
const fallbackMessage = !options.fatal && options.fallback ? " Using fallback value instead." : "";
const errorMessage = `Module ${options.name} not found.${fallbackMessage}\n\nContact the plugin developer to inform them of this error.`;
betterdiscord.Logger.error(errorMessage);
options.onError?.();
if (options.fatal) throw new Error(errorMessage);
return options.fallback;
}
function expectModule(options) {
return expect(betterdiscord.Webpack.getModule(options.filter, options), options);
}
function expectWithKey(options) {
const [module, key] = betterdiscord.Webpack.getWithKey(options.filter, options);
if (module) return [module, key];
const fallback = expect(module, options);
if (fallback) {
const key2 = "__key";
return [{ [key2]: fallback }, key2];
}
return void 0;
}
function expectSelectors(name, classes) {
return expect(getSelectors(...classes), {
name
});
}
function expectIcon(name, searchString) {
return expect(getIcon(searchString), {
name,
fallback: (_props) => null
});
}
function byType(type) {
return (e) => typeof e === type;
}
// manifest.json
const changelog = [
{
title: "Fixed",
type: "fixed",
items: [
"Fixed member list icons."
]
}
];
// @discord/stores.ts
const UserStore = betterdiscord.Webpack.getStore("UserStore");
const VoiceStateStore = betterdiscord.Webpack.getStore("VoiceStateStore");
const GuildStore = betterdiscord.Webpack.getStore("GuildStore");
const ChannelStore = betterdiscord.Webpack.getStore("ChannelStore");
const PermissionStore = betterdiscord.Webpack.getStore("PermissionStore");
const useStateFromStores = expectModule({
filter: betterdiscord.Webpack.Filters.byStrings("useStateFromStores"),
name: "Flux",
fallback(stores, callback) {
return callback();
},
searchExports: true
});
// modules/discordmodules.tsx
function useUserVoiceStateFallback({ userId }) {
const voiceState = useStateFromStores(
[VoiceStateStore],
() => userId && VoiceStateStore.getDiscoverableVoiceStateForUser(userId)
);
const channel = useStateFromStores([ChannelStore], () => {
if (voiceState?.channelId) return ChannelStore.getChannel(voiceState?.channelId);
});
const visible = useStateFromStores(
[PermissionStore],
() => channel?.isPrivate() || PermissionStore.can(Permissions.VIEW_CHANNEL, channel)
);
if (visible) {
return {
voiceState,
voiceChannel: channel
};
} else return {};
}
const MemberListItem = expectModule({
filter: (m) => m.type?.toString?.().includes("MemberListItem"),
name: "MemberListItem",
searchExports: true
});
const PrivateChannel = expectWithKey({
filter: betterdiscord.Webpack.Filters.byStrings("PrivateChannel", "getTypingUsers"),
name: "PrivateChannel",
defaultExport: false
});
const PeopleListItem = expectModule({
filter: (m) => m?.prototype?.render && betterdiscord.Webpack.Filters.byStrings("this.peopleListItemRef")(m),
name: "PeopleListItem"
});
const Permissions = expectModule({
filter: betterdiscord.Webpack.Filters.byKeys("VIEW_CREATOR_MONETIZATION_ANALYTICS"),
searchExports: true,
name: "Permissions",
fallback: {
VIEW_CHANNEL: 1024n
}
});
const memberSelectors = expectSelectors("Children Class", ["avatar", "children", "layout"]);
const useUserVoiceState = expectModule({
filter: betterdiscord.Webpack.Filters.byStrings("getDiscoverableVoiceState", "getDiscoverableVoiceStateForUser"),
name: "useUserVoiceState",
fallback: useUserVoiceStateFallback
});
// locales.json
const el = {
SETTINGS_PROFILE: "Τομέας Προφίλ",
SETTINGS_PROFILE_NOTE: "Εμφανίζει τον τομέα προφίλ για την τρέχουσα δραστηριότητα φωνής στα αναδυόμενα χρήστη και στην πλευρικές μπάρες προφίλ των Άμεσων Μηνυμάτων.",
SETTINGS_ICONS: "Εικονίδια Λίστας Μελών",
SETTINGS_ICONS_NOTE: "Εμφανίζει εικονίδια στη λίστα μελών όταν κάποιος είναι σε κανάλι φωνής.",
SETTINGS_DM_ICONS: "Εικονίδια Άμεσων Μηνυμάτων",
SETTINGS_DM_ICONS_NOTE: "Εμφανίζει εικονίδια στη λίστα Άμεσων Μηνυμάτων όταν κάποιος είναι σε κανάλι φωνής.",
SETTINGS_PEOPLE_ICONS: "Εικονίδια Λίστας Φίλων",
SETTINGS_PEOPLE_ICONS_NOTE: "Εμφανίζει εικονίδια στη λίστα φίλων όταν κάποιος είναι σε κανάλι φωνής.",
SETTINGS_GUILD_ICONS: "Εικονίδια Συντεχνίας",
SETTINGS_GUILD_ICONS_NOTE: "Εμφανίζει εικονίδια στις συντεχνίες ακόμα και αν δεν συμμετέχετε.",
SETTINGS_COLOR: "Λίστα Μελών - Χρώμα Εικονιδίου Τρέχοντος Καναλιού",
SETTINGS_COLOR_NOTE: "Αλλάζει τα εικονίδια της Λίστας Μελών σε πράσινα όταν ο χρήστης είναι στο δικό σας τρέχον κανάλι φωνής.",
SETTINGS_STATUS: "Λίστα Μελών - Εμφάνιση Εικονιδίων Κατάστασης",
SETTINGS_STATUS_NOTE: "Αλλάζει τα εικονίδια της Λίστας Μελών όταν ο χρήστης είναι σε Σίγαση, Κώφωση ή έχε ενεργοποιημένο Βίντεο.",
SETTINGS_IGNORE: "Αγνόηση",
SETTINGS_IGNORE_NOTE: "Προσθέτει μια επιλογή στο Κανάλι Φωνής και στα μενού περιεχομένου Συντεχνίας για αγνόηση αυτού του καναλιού/συντεχνίας στα Εικονίδια Λϊστας Μελών και στα Αναδυόμενα Χρήστη.",
CONTEXT_IGNORE: "Αγνόηση στη Δραστηριότητα Φωνής",
VOICE_CALL: "Φωνητική Κλήση",
PRIVATE_CALL: "Ιδιωτική Κλήση",
GROUP_CALL: "Ομαδική Κλήση",
LIVE: "Ζωντανά",
HEADER: "Σε ένα Κανάλι Φωνής",
HEADER_VOICE: "Σε μια Κλήση Φωνής",
HEADER_PRIVATE: "Σε μια Ιδιωτική Κλήση",
HEADER_GROUP: "Σε μια Ομαδική Κλήση",
HEADER_STAGE: "Σε ένα Κανάλι Σταδίου",
VIEW: "Προβολή Καναλιού",
VIEW_CALL: "Προβολή Κλήσης",
JOIN: "Συμμετοχή σε Κανάλι",
JOIN_CALL: "Συμμετοχή σε Κλήση",
JOIN_DISABLED: "Ήδη σε Κανάλι",
JOIN_DISABLED_CALL: "Ήδη σε Κλήση",
JOIN_VIDEO: "Συμμετοχή με Βίντεο"
};
const ru = {
SETTINGS_PROFILE: "Раздел в Профиле",
SETTINGS_PROFILE_NOTE: "Показывает раздел для текущей голосовой активности в профиле, всплывающих окнах и боковых панелях в ЛС.",
SETTINGS_ICONS: "Иконки в Списке Участников",
SETTINGS_ICONS_NOTE: "Показывает иконки в списке участников когда пользователь в голосовом канале.",
SETTINGS_DM_ICONS: "Иконки в ЛС",
SETTINGS_DM_ICONS_NOTE: "Показывает иконки в списке участников ЛС когда пользователь в голосовом канале.",
SETTINGS_PEOPLE_ICONS: "Иконки в Списке Друзей",
SETTINGS_PEOPLE_ICONS_NOTE: "Показывает иконки в списке друзей когда пользователь в голосовом канале.",
SETTINGS_GUILD_ICONS: "Иконки Сервера",
SETTINGS_GUILD_ICONS_NOTE: "Показывает голосовые иконки на серверах даже когда вы не участвуете.",
SETTINGS_COLOR: "Цвет Иконки Голосовой Активности",
SETTINGS_COLOR_NOTE: "Делает иконки в Списке Участников зелёными когда пользователь в голосовом канале вместе с вами.",
SETTINGS_STATUS: "Показывать Иконки Статуса",
SETTINGS_STATUS_NOTE: "Меняет иконки в Списке Участников когда пользователь Выключил Микрофон/Звук или Начал трансляцию.",
SETTINGS_IGNORE: "Ингор",
SETTINGS_IGNORE_NOTE: "Добавляет возможность в контекстных меню Голосовых Каналов и Серверов игнорировать этот канал/сервер в Списке Участников и Профилях Пользователей.",
CONTEXT_IGNORE: "Игнор (Голосовая Активность)",
VOICE_CALL: "Голосовой звонок",
PRIVATE_CALL: "Приватный звонок",
GROUP_CALL: "Групповой звонок",
LIVE: "В ЭФИРЕ",
HEADER: "В Голосовом Канале",
HEADER_VOICE: "В Голосовом Звонке",
HEADER_PRIVATE: "В Приватном Звонке",
HEADER_GROUP: "В Групповом Звонке",
HEADER_STAGE: "В Канале Трибуны",
VIEW: "Просмотреть Канал",
VIEW_CALL: "Просмотреть Звонок",
JOIN: "Присоединиться к Каналу",
JOIN_CALL: "Присоединиться к Звонку",
JOIN_DISABLED: "Уже в Канале",
JOIN_DISABLED_CALL: "Уже в Звонке",
JOIN_VIDEO: "Присоединиться с Видео"
};
const de = {
SETTINGS_PROFILE: "Profilbereich",
SETTINGS_PROFILE_NOTE: "Zeigt den Profilbereich für die aktuelle Sprachaktivität in Benutzer-Popouts und DM-Profilseitenleisten an.",
SETTINGS_ICONS: "Symbole in der Mitgliederliste",
SETTINGS_ICONS_NOTE: "Zeigt Symbole in der Mitgliederliste an, wenn sich jemand in einem Sprachkanal befindet.",
SETTINGS_DM_ICONS: "DM-Symbole",
SETTINGS_DM_ICONS_NOTE: "Zeigt Symbole in der DM-Liste an, wenn sich jemand in einem Sprachkanal befindet.",
SETTINGS_PEOPLE_ICONS: "Symbole in der Freundesliste",
SETTINGS_PEOPLE_ICONS_NOTE: "Zeigt Symbole in der Freundesliste an, wenn sich jemand in einem Sprachkanal befindet.",
SETTINGS_GUILD_ICONS: "Gildensymbole",
SETTINGS_GUILD_ICONS_NOTE: "Zeigt Symbole für Gilden an, auch wenn du nicht teilnimmst.",
SETTINGS_COLOR: "Mitgliederliste - Farbe des aktuellen Kanalsymbols",
SETTINGS_COLOR_NOTE: "Die Symbole in der Mitgliederliste werden grün, wenn sich der Benutzer in Ihrem aktuellen Sprachkanal befindet.",
SETTINGS_STATUS: "Mitgliederliste - Statussymbole anzeigen",
SETTINGS_STATUS_NOTE: "Ändert die Symbole in der Mitgliederliste, wenn ein Benutzer stummgeschaltet oder mit aktiviertem Video ist.",
SETTINGS_IGNORE: "Ignorieren",
SETTINGS_IGNORE_NOTE: "Fügt eine Option in die Kontextmenüs von Sprachkanälen und Gilden hinzu, um diesen Kanal/diese Gilde in der Mitgliederliste und Benutzer-Popouts zu ignorieren.",
CONTEXT_IGNORE: "Bei Sprachaktivität ignorieren",
VOICE_CALL: "Sprachanruf",
PRIVATE_CALL: "Privatanruf",
GROUP_CALL: "Gruppenanruf",
LIVE: "Live",
HEADER: "In einem Sprachkanal",
HEADER_VOICE: "In einem Sprachanruf",
HEADER_PRIVATE: "In einem privaten Anruf",
HEADER_GROUP: "In einem Gruppenruf",
HEADER_STAGE: "In einem Stage Kanal",
VIEW: "Kanal anzeigen",
VIEW_CALL: "Anruf anzeigen",
JOIN: "Kanal beitreten",
JOIN_CALL: "Anruf beitreten",
JOIN_DISABLED: "Bereits im Kanal",
JOIN_DISABLED_CALL: "Bereits im Aufruf",
JOIN_VIDEO: "Beitreten mit Video"
};
const fr = {
SETTINGS_PROFILE: "Section de profil",
SETTINGS_PROFILE_NOTE: "Affiche une section de profile pour l'activité vocale actuelle dans les popouts utilisateur et les profiles latéraux de MP.",
SETTINGS_ICONS: "Icônes de la liste de membre",
SETTINGS_ICONS_NOTE: "Affiche des icônes dans la liste de membre quand quelqu'un est dans un salon vocal.",
SETTINGS_DM_ICONS: "Icônes de MP",
SETTINGS_DM_ICONS_NOTE: "Affiche des icônes dans la liste de MP quand quelqu'un est dans un salon vocal.",
SETTINGS_PEOPLE_ICONS: "Icônes de la liste d'amis",
SETTINGS_PEOPLE_ICONS_NOTE: "Affiche des icônes dans la liste d'amis quand quelqu'un est dans un salon vocal.",
SETTINGS_GUILD_ICONS: "Icônes de serveur",
SETTINGS_GUILD_ICONS_NOTE: "Affiche des icônes de vocal sur les serveurs même quand vous ne participez pas.",
SETTINGS_COLOR: "Liste de membre - Couleur d'icône du salon actuel",
SETTINGS_COLOR_NOTE: "Rends les icônes de la liste de membre vertes quand un utilisateur est dans le salon vocal actuel.",
SETTINGS_STATUS: "Liste de membre - Afficher les icônes de status",
SETTINGS_STATUS_NOTE: "Change les icônes de la liste de membre quand un utilisateur est muet, mis en sourdine, ou a la vidéo activée.",
SETTINGS_IGNORE: "Ignorer",
SETTINGS_IGNORE_NOTE: "Ajoute une option dans les menus contextuels des salons vocaux et serveurs pour ignorer le dit salon/serveur dans les icônes de liste de membre et popouts utilisateur.",
CONTEXT_IGNORE: "Ignorer dans Activité Vocale",
VOICE_CALL: "Appel vocal",
PRIVATE_CALL: "Appel privé",
GROUP_CALL: "Appel de groupe",
LIVE: "Live",
HEADER: "Dans un salon vocal",
HEADER_VOICE: "Dans un appel vocal",
HEADER_PRIVATE: "Dans un appel privé",
HEADER_GROUP: "Dans un appel de groupe",
HEADER_STAGE: "Dans un salon de conférence",
VIEW: "Voir le salon",
VIEW_CALL: "Voir l'appel",
JOIN: "Rejoindre le salon",
JOIN_CALL: "Rejoindre l'appel",
JOIN_DISABLED: "Déjà dans le salon",
JOIN_DISABLED_CALL: "Déjà en appel",
JOIN_VIDEO: "Rejoidre avec vidéo",
MEMBER: "Membre",
MEMBERS: "Membres"
};
const locales = {
"en-US": {
SETTINGS_PROFILE: "Profile Section",
SETTINGS_PROFILE_NOTE: "Shows profile section for current voice activity in user popouts and DM profile sidebars.",
SETTINGS_ICONS: "Member List Icons",
SETTINGS_ICONS_NOTE: "Shows icons on the member list when someone is in a voice channel.",
SETTINGS_DM_ICONS: "DM Icons",
SETTINGS_DM_ICONS_NOTE: "Shows icons on the DM list when someone is in a voice channel.",
SETTINGS_PEOPLE_ICONS: "Friends List Icons",
SETTINGS_PEOPLE_ICONS_NOTE: "Shows icons on the friends list when someone is in a voice channel.",
SETTINGS_GUILD_ICONS: "Server Icons",
SETTINGS_GUILD_ICONS_NOTE: "Shows voice icons on servers even when you're not participating.",
SETTINGS_COLOR: "Member List - Current Channel Icon Color",
SETTINGS_COLOR_NOTE: "Makes the Member List icons green when the user is in your current voice channel.",
SETTINGS_STATUS: "Member List - Show Status Icons",
SETTINGS_STATUS_NOTE: "Changes the Member List icons when a user is Muted, Deafened, or has Video enabled.",
SETTINGS_CURRENT_USER: "Current User Icon",
SETTINGS_CURRENT_USER_NOTE: "Toggles displaying a voice channel icon for the current user.",
SETTINGS_IGNORE: "Ignore",
SETTINGS_IGNORE_NOTE: "Adds an option on Voice Channel and Server context menus to ignore that channel/server in Member List Icons and User Popouts.",
CONTEXT_IGNORE: "Ignore in Voice Activity",
VOICE_CALL: "Voice Call",
PRIVATE_CALL: "Private Call",
GROUP_CALL: "Group Call",
LIVE: "Live",
HEADER: "In a Voice Channel",
HEADER_VOICE: "In a Voice Call",
HEADER_PRIVATE: "In a Private Call",
HEADER_GROUP: "In a Group Call",
HEADER_STAGE: "In a Stage Channel",
VIEW: "View Channel",
VIEW_CALL: "View Call",
JOIN: "Join Channel",
JOIN_CALL: "Join Call",
JOIN_DISABLED: "Already in Channel",
JOIN_DISABLED_CALL: "Already in Call",
JOIN_VIDEO: "Join With Video",
MEMBER: "Member",
MEMBERS: "Members"
},
el: el,
ru: ru,
de: de,
fr: fr
};
// modules/utils.ts
const Settings = new SettingsManager({
showMemberListIcons: true,
showDMListIcons: true,
showPeopleListIcons: true,
currentChannelColor: true,
showStatusIcons: true,
currentUserIcon: true,
ignoreEnabled: false,
ignoredChannels: [],
ignoredGuilds: []
});
const Strings = new StringsManager(locales, "en-US");
function groupDMName(members) {
if (members.length === 1) {
return UserStore.getUser(members[0]).globalName;
} else if (members.length > 1) {
let name = "";
for (let i = 0; i < members.length; i++) {
if (i === members.length - 1) name += UserStore.getUser(members[i]).globalName;
else name += UserStore.getUser(members[i]).globalName + ", ";
}
return name;
}
return "Unnamed";
}
// styles/voiceicon.module.css
const css = `
.VoiceActivity-voiceicon-icon {
height: 20px;
width: 20px;
min-width: 20px;
border-radius: 50%;
background-color: var(--background-accent);
cursor: pointer;
svg {
padding: 3px;
color: #fff;
}
}
.VoiceActivity-voiceicon-iconCurrentCall {
background-color: var(--status-positive);
}
.VoiceActivity-voiceicon-iconLive {
height: 16px;
border-radius: 16px;
background-color: var(--red-400);
color: #fff;
font-size: 12px;
line-height: 16px;
font-weight: 600;
font-family: var(--font-display);
text-transform: uppercase;
& > div {
padding: 0 6px;
}
}
.VoiceActivity-voiceicon-tooltip {
.VoiceActivity-voiceicon-header {
display: block;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.VoiceActivity-voiceicon-subtext {
display: flex;
flex-direction: row;
margin-top: 3px;
& > div {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
}
.VoiceActivity-voiceicon-tooltipIcon {
min-width: 16px;
margin-right: 3px;
color: var(--interactive-normal);
}
}
.VoiceActivity-voiceicon-iconContainer {
margin-left: auto;
}
.VoiceActivity-voiceicon-peopleListIcon {
margin-right: 16px;
}`;
_loadStyle("voiceicon.module.css", css);
const modules_1af761ba = {
"icon": "VoiceActivity-voiceicon-icon",
"iconCurrentCall": "VoiceActivity-voiceicon-iconCurrentCall",
"iconLive": "VoiceActivity-voiceicon-iconLive",
"tooltip": "VoiceActivity-voiceicon-tooltip",
"header": "VoiceActivity-voiceicon-header",
"subtext": "VoiceActivity-voiceicon-subtext",
"tooltipIcon": "VoiceActivity-voiceicon-tooltipIcon",
"iconContainer": "VoiceActivity-voiceicon-iconContainer",
"peopleListIcon": "VoiceActivity-voiceicon-peopleListIcon"
};
const iconStyles = modules_1af761ba;
// @discord/icons.tsx
const Speaker = expectIcon("Speaker", "M12 3a1 1 0 0 0-1-1h-.06a1 1 0 0 0-.74.32L5.92 7H3a1 1");
const Muted = expectIcon(
"Muted",
"m2.7 22.7 20-20a1 1 0 0 0-1.4-1.4l-20 20a1 1 0 1 0 1.4 1.4ZM10.8 17.32c-.21.21-.1.58.2.62V20H9a1"
);
const ServerMuted = expectIcon(
"ServerMuted",
"M21.76.83a5.02 5.02 0 0 1 .78 7.7 5 5 0 0 1-7.07 0 5.02 5.02 0 0 1 0-7.07"
);
const Deafened = expectIcon(
"Deafened",
"M22.7 2.7a1 1 0 0 0-1.4-1.4l-20 20a1 1 0 1 0 1.4 1.4l20-20ZM17.06"
);
const ServerDeafened = expectIcon(
"ServerDeafened",
"M12.38 1c.38.02.58.45.4.78-.15.3-.3.62-.4.95A.4.4 0 0 1 12 3a9 9 0 0 0-8.95 10h1.87a5"
);
const Video = expectIcon("Video", "M4 4a3 3 0 0 0-3 3v10a3 3 0 0 0 3 3h11a3 3");
const ChannelIcon = expectModule({
filter: betterdiscord.Webpack.Filters.combine(
betterdiscord.Webpack.Filters.byStrings("isGuildStageVoice", "isGroupDM", "isPrivate"),
(m) => !m.toString?.().includes("intl")
),
name: "ChannelIcon",
fallback: (_props) => null
});
// @discord/modules.ts
const transitionTo = expectModule({
filter: betterdiscord.Webpack.Filters.combine(
betterdiscord.Webpack.Filters.byStrings("transitionTo -"),
byType("function")
),
searchExports: true,
name: "transitionTo"
});
// components/VoiceIcon.tsx
function VoiceIcon(props) {
const settingsState = Settings.useSettingsState(
"showMemberListIcons",
"showDMListIcons",
"showPeopleListIcons",
"ignoreEnabled",
"ignoredChannels",
"ignoredGuilds",
"currentChannelColor",
"showStatusIcons",
"currentUserIcon"
);
const currentUser = UserStore.getCurrentUser();
const { voiceState, voiceChannel: channel } = useUserVoiceState({ userId: props.userId });
const { voiceState: currentUserVoiceState } = useUserVoiceState({ userId: currentUser?.id });
if (props.context === "memberlist" && !settingsState.showMemberListIcons) return null;
if (props.context === "dmlist" && !settingsState.showDMListIcons) return null;
if (props.context === "peoplelist" && !settingsState.showPeopleListIcons) return null;
if (props.userId === currentUser?.id && !settingsState.currentUserIcon) return null;
if (!voiceState) return null;
const guild = GuildStore.getGuild(channel.guild_id);
const ignored = settingsState.ignoredChannels.includes(channel.id) || settingsState.ignoredGuilds.includes(guild?.id);
if (settingsState.ignoreEnabled && ignored) return null;
let text;
let subtext;
let channelPath;
let className = iconStyles.icon;
if (settingsState.currentChannelColor && channel.id === currentUserVoiceState?.channelId)
className = `${iconStyles.icon} ${iconStyles.iconCurrentCall}`;
if (voiceState.selfStream) className = iconStyles.iconLive;
if (guild) {
text = guild.name;
subtext = channel.name;
channelPath = `/channels/${guild.id}/${channel.id}`;
} else {
text = channel.name;
subtext = Strings.get("VOICE_CALL");
channelPath = `/channels/@me/${channel.id}`;
}
switch (channel.type) {
case 1:
text = UserStore.getUser(channel.recipients[0]).globalName;
subtext = Strings.get("PRIVATE_CALL");
break;
case 3:
text = channel.name || groupDMName(channel.recipients);
subtext = Strings.get("GROUP_CALL");
break;
}
let Icon = Speaker;
if (settingsState.showStatusIcons) {
if (voiceState.selfVideo) Icon = Video;
else if (voiceState.deaf) Icon = ServerDeafened;
else if (voiceState.selfDeaf) Icon = Deafened;
else if (voiceState.mute) Icon = ServerMuted;
else if (voiceState.selfMute) Icon = Muted;
}
return BdApi.React.createElement(
"div",
{
className,
onClick: (e) => {
e.stopPropagation();
e.preventDefault();
if (channelPath) transitionTo?.(channelPath);
}
},
BdApi.React.createElement(
betterdiscord.Components.Tooltip,
{
text: BdApi.React.createElement("div", { className: iconStyles.tooltip }, BdApi.React.createElement("div", { className: iconStyles.header, style: { fontWeight: "600" } }, text), BdApi.React.createElement("div", { className: iconStyles.subtext }, BdApi.React.createElement(
ChannelIcon,
{
className: iconStyles.tooltipIcon,
size: "16",
width: "16",
height: "16",
color: "currentColor",
channel
}
), BdApi.React.createElement("div", { style: { fontWeight: "400" } }, subtext)))
},
(props2) => BdApi.React.createElement("div", { ...props2 }, !voiceState.selfStream ? BdApi.React.createElement(Icon, { size: "14", width: "14", height: "14", color: "currentColor" }) : Strings.get("LIVE"))
)
);
}
// index.tsx
class VoiceActivity {
meta;
contextMenuUnpatches = new Set();
constructor(meta) {
this.meta = meta;
}
start() {
showChangelog(changelog, this.meta);
betterdiscord.DOM.addStyle(
styles() + `${memberSelectors?.children}:empty { margin-left: 0; } ${memberSelectors?.children} { display: flex; gap: 8px; } ${memberSelectors?.layout} { width: 100%; }`
);
Strings.subscribe();
this.patchPeopleListItem();
this.patchMemberListItem();
this.patchPrivateChannel();
this.patchChannelContextMenu();
this.patchGuildContextMenu();
betterdiscord.Patcher.instead(VoiceStateStore, "getDiscoverableVoiceState", (_, [guildId, userId]) => {
return VoiceStateStore.getDiscoverableVoiceStateForUser(userId);
});
}
patchMemberListItem() {
if (!MemberListItem) return;
betterdiscord.Patcher.after(MemberListItem, "type", (_, [props], ret) => {
if (!props.user) return ret;
const children = ret.props.children;
ret.props.children = (childrenProps) => {
const childrenRet = children(childrenProps);
const target = betterdiscord.Utils.findInTree(childrenRet, (x) => x.props?.avatar && x.props?.decorators, {
walkable: ["props", "children"]
});
const icon = BdApi.React.createElement(VoiceIcon, { userId: props.user.id, context: "memberlist" });
Array.isArray(target.props.children) ? target.props.children.unshift(icon) : target.props.children = [icon];
return childrenRet;
};
});
}
patchPrivateChannel() {
if (!PrivateChannel) return;
const patchType = (props, ret) => {
if (props.channel.type !== 1) return;
const target = betterdiscord.Utils.findInTree(ret, (e) => typeof e?.props?.children !== "function", {
walkable: ["children", "props"]
})?.props?.children ?? ret;
if (!target) return;
const children = target.props.children;
target.props.children = (childrenProps) => {
const childrenRet = children(childrenProps);
const privateChannel = betterdiscord.Utils.findInTree(childrenRet, (e) => e?.children?.props?.avatar, {
walkable: ["children", "props"]
});
privateChannel.children = [
privateChannel.children,
BdApi.React.createElement("div", { className: iconStyles.iconContainer }, BdApi.React.createElement(VoiceIcon, { userId: props.user.id, context: "dmlist" }))
];
return childrenRet;
};
};
let patchedType;
betterdiscord.Patcher.after(...PrivateChannel, (_, __, containerRet) => {
let target = containerRet.children || containerRet;
if (patchedType) {
target.type = patchedType;
return containerRet;
}
const original = target.type;
patchedType = (props) => {
const ret = original(props);
patchType(props, ret);
return ret;
};
target.type = patchedType;
});
}
patchPeopleListItem() {
if (!PeopleListItem) return;
betterdiscord.Patcher.after(PeopleListItem.prototype, "render", (that, _, ret) => {
if (!that.props.user) return;
const children = ret.props.children;
ret.props.children = (childrenProps) => {
const childrenRet = children(childrenProps);
betterdiscord.Utils.findInTree(childrenRet, (i) => Array.isArray(i), { walkable: ["props", "children"] }).splice(
1,
0,
BdApi.React.createElement("div", { className: `${iconStyles.iconContainer} ${iconStyles.peopleListIcon}` }, BdApi.React.createElement(VoiceIcon, { userId: that.props.user.id, context: "peoplelist" }))
);
return childrenRet;
};
});
}
patchChannelContextMenu() {
const unpatch = betterdiscord.ContextMenu.patch("channel-context", (ret, props) => {
if (!Settings.get("ignoreEnabled")) return ret;
if (props.channel.type !== 2 && props.channel.type !== 13) return ret;
const { ignoredChannels } = Settings.useSettingsState("ignoredChannels");
const ignored = ignoredChannels.includes(props.channel.id);
const menuItem = betterdiscord.ContextMenu.buildItem({
type: "toggle",
label: Strings.get("CONTEXT_IGNORE"),
id: "voiceactivity-ignore",
checked: ignored,
action: () => {
if (ignored) {
const newIgnoredChannels = ignoredChannels.filter((id) => id !== props.channel.id);
Settings.set("ignoredChannels", newIgnoredChannels);
} else {
const newIgnoredChannels = [...ignoredChannels, props.channel.id];
Settings.set("ignoredChannels", newIgnoredChannels);
}
}
});
ret.props.children[3].props.children.splice(4, 0, menuItem);
});
this.contextMenuUnpatches.add(unpatch);
}
patchGuildContextMenu() {
const unpatch = betterdiscord.ContextMenu.patch("guild-context", (ret, props) => {
if (!props.guild) return ret;
if (!Settings.get("ignoreEnabled")) return ret;
const { ignoredGuilds } = Settings.useSettingsState("ignoredGuilds");
const ignored = ignoredGuilds.includes(props.guild.id);
const menuItem = betterdiscord.ContextMenu.buildItem({
type: "toggle",
label: Strings.get("CONTEXT_IGNORE"),
id: "voiceactivity-ignore",
checked: ignored,
action: () => {
if (ignored) {
const newIgnoredGuilds = ignoredGuilds.filter((id) => id !== props.guild.id);
Settings.set("ignoredGuilds", newIgnoredGuilds);
} else {
const newIgnoredGuilds = [...ignoredGuilds, props.guild.id];
Settings.set("ignoredGuilds", newIgnoredGuilds);
}
}
});
ret.props.children[2].props.children.push(menuItem);
});
this.contextMenuUnpatches.add(unpatch);
}
stop() {
betterdiscord.DOM.removeStyle();
betterdiscord.Patcher.unpatchAll();
this.contextMenuUnpatches.forEach((unpatch) => unpatch());
this.contextMenuUnpatches.clear();
Strings.unsubscribe();
}
getSettingsPanel() {
return buildSettingsPanel(Settings, [
{
id: "showMemberListIcons",
type: "switch",
name: Strings.get("SETTINGS_ICONS"),
note: Strings.get("SETTINGS_ICONS_NOTE")
},
{
id: "showDMListIcons",
type: "switch",
name: Strings.get("SETTINGS_DM_ICONS"),
note: Strings.get("SETTINGS_DM_ICONS_NOTE")
},
{
id: "showPeopleListIcons",
type: "switch",
name: Strings.get("SETTINGS_PEOPLE_ICONS"),
note: Strings.get("SETTINGS_PEOPLE_ICONS_NOTE")
},
{
id: "currentChannelColor",
type: "switch",
name: Strings.get("SETTINGS_COLOR"),
note: Strings.get("SETTINGS_COLOR_NOTE")
},
{
id: "showStatusIcons",
type: "switch",
name: Strings.get("SETTINGS_STATUS"),
note: Strings.get("SETTINGS_STATUS_NOTE")
},
{
id: "currentUserIcon",
type: "switch",
name: Strings.get("SETTINGS_CURRENT_USER"),
note: Strings.get("SETTINGS_CURRENT_USER_NOTE")
},
{
id: "ignoreEnabled",
type: "switch",
name: Strings.get("SETTINGS_IGNORE"),
note: Strings.get("SETTINGS_IGNORE_NOTE")
}
]);
}
}
module.exports = VoiceActivity;
/*@end@*/