Files
arindOS/dotfiles/.config/BetterDiscord/plugins/VoiceActivity.plugin.js
T

892 lines
34 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @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@*/