892 lines
34 KiB
JavaScript
892 lines
34 KiB
JavaScript
/**
|
||
* @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@*/ |