Merge pull request #191 from samuelthomas2774/discord-api

DiscordAPI, Toasts, Normalizer, Bugfixes, etc.
This commit is contained in:
Zack 2018-08-05 15:08:53 -04:00 committed by GitHub
commit 520366c4ac
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
134 changed files with 6975 additions and 3190 deletions

View File

@ -3,8 +3,7 @@
["env", {
"targets": {
"node": "6.7.0"
},
"debug": true
}
}]
]
}

View File

@ -1,6 +1,8 @@
language: node_js
node_js:
- stable
branches:
only:
- master

View File

@ -8,11 +8,12 @@
* LICENSE file in the root directory of this source tree.
*/
import { Events, Settings, Globals, WebpackModules, ReactComponents, MonkeyPatch } from 'modules';
import { DOM, VueInjector, Reflection } from 'ui';
import { Settings, Globals, WebpackModules, ReactComponents, MonkeyPatch } from 'modules';
import { VueInjector, Reflection } from 'ui';
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
import path from 'path';
import EmoteComponent from './EmoteComponent.vue';
import Autocomplete from '../ui/components/common/Autocomplete.vue';
const enforceWrapperFrom = (new Date('2018-05-01')).valueOf();
@ -24,26 +25,34 @@ export default new class EmoteModule {
}
async init() {
const dataPath = Globals.getPath('data');
this.enabledSetting = Settings.getSetting('emotes', 'default', 'enable');
this.enabledSetting.on('setting-updated', event => {
this.enabledSetting.on('setting-updated', async event => {
// Load emotes if we haven't already
if (event.value && !this.emotes.size) await this.load(path.join(dataPath, 'emotes.json'));
// Rerender all messages (or if we're disabling emotes, those that have emotes)
for (const message of document.querySelectorAll(event.value ? '.message' : '.bd-emote-outer')) {
for (const message of document.querySelectorAll(event.value ? '.message' : '.bd-emotewrapper')) {
Reflection(event.value ? message : message.closest('.message')).forceUpdate();
}
});
const dataPath = Globals.getPath('data');
try {
await this.load(path.join(dataPath, 'emotes.json'));
if (this.enabledSetting.value) await this.load(path.join(dataPath, 'emotes.json'));
else Logger.info('EmoteModule', ['Not loading emotes as they\'re disabled.']);
} catch (err) {
Logger.err('EmoteModule', [`Failed to load emote data. Make sure you've downloaded the emote data and placed it in ${dataPath}:`, err]);
return;
}
try {
await this.observe();
await Promise.all([
this.patchMessageContent(),
this.patchChannelTextArea()
]);
} catch (err) {
Logger.err('EmoteModule', ['Error patching Message', err]);
Logger.err('EmoteModule', ['Error patching Message / ChannelTextArea', err]);
}
}
@ -54,8 +63,12 @@ export default new class EmoteModule {
if ((index % 10000) === 0)
await Utils.wait();
const uri = emote.type === 2 ? 'https://cdn.betterttv.net/emote/:id/1x' : emote.type === 1 ? 'https://cdn.frankerfacez.com/emoticon/:id/1' : 'https://static-cdn.jtvnw.net/emoticons/v1/:id/1.0';
emote.name = emote.id;
const uri = emote.type === 2 ? 'https://cdn.betterttv.net/emote/:id/1x'
: emote.type === 1 ? 'https://cdn.frankerfacez.com/emoticon/:id/1'
: 'https://static-cdn.jtvnw.net/emoticons/v1/:id/1.0';
// emote.id is the emote's name
// emote.src is the emote's URL
emote.src = uri.replace(':id', emote.value.id || emote.value);
this.emotes.set(emote.id, emote);
}
@ -92,17 +105,7 @@ export default new class EmoteModule {
return this._searchCache || (this._searchCache = {});
}
get React() {
return WebpackModules.getModuleByName('React');
}
get ReactDOM() {
return WebpackModules.getModuleByName('ReactDOM');
}
processMarkup(markup, timestamp) {
if (!this.enabledSetting.value) return markup;
timestamp = timestamp.valueOf();
const allowNoWrapper = timestamp < enforceWrapperFrom;
@ -126,12 +129,13 @@ export default new class EmoteModule {
newMarkup.push(text);
text = null;
}
newMarkup.push(this.React.createElement('span', {
className: 'bd-emote-outer',
'data-bdemote-name': emote.name,
'data-bdemote-src': emote.src,
'data-has-wrapper': /;[\w]+;/gmi.test(word)
newMarkup.push(VueInjector.createReactElement(EmoteComponent, {
src: emote.src,
name: emote.id,
hasWrapper: /;[\w]+;/gmi.test(word)
}));
continue;
}
if (text === null) {
@ -151,16 +155,6 @@ export default new class EmoteModule {
return !/;[\w]+;/gmi.test(word);
}
injectAll() {
if (!this.enabledSetting.value) return;
const all = document.getElementsByClassName('bd-emote-outer');
for (const ec of all) {
if (ec.children.length) continue;
this.injectEmote(ec);
}
}
findByProp(obj, what, value) {
if (obj.hasOwnProperty(what) && obj[what] === value) return obj;
if (obj.props && !obj.children) return this.findByProp(obj.props, what, value);
@ -173,56 +167,6 @@ export default new class EmoteModule {
return null;
}
async observe() {
const Message = await ReactComponents.getComponent('Message');
this.unpatchRender = MonkeyPatch('BD:EmoteModule', Message.component.prototype).after('render', (component, args, retVal) => {
try {
// First child has all the actual text content, second is the edited timestamp
const markup = this.findByProp(retVal, 'className', 'markup');
if (!markup) return;
markup.children[0] = this.processMarkup(markup.children[0], component.props.message.editedTimestamp || component.props.message.timestamp);
} catch (err) {
Logger.err('EmoteModule', err);
}
});
for (const message of document.querySelectorAll('.message')) {
Reflection(message).forceUpdate();
}
this.injectAll();
this.unpatchMount = MonkeyPatch('BD:EmoteModule', Message.component.prototype).after('componentDidMount', component => {
const element = this.ReactDOM.findDOMNode(component);
if (!element) return;
this.injectEmotes(element);
});
this.unpatchUpdate = MonkeyPatch('BD:EmoteModule', Message.component.prototype).after('componentDidUpdate', component => {
const element = this.ReactDOM.findDOMNode(component);
if (!element) return;
this.injectEmotes(element);
});
}
injectEmote(root) {
if (!this.enabledSetting.value) return;
while (root.firstChild) {
root.removeChild(root.firstChild);
}
const { bdemoteName, bdemoteSrc, hasWrapper } = root.dataset;
if (!bdemoteName || !bdemoteSrc) return;
VueInjector.inject(root, {
components: { EmoteComponent },
data: { src: bdemoteSrc, name: bdemoteName, hasWrapper },
template: '<EmoteComponent :src="src" :name="name" :hasWrapper="hasWrapper" />'
}, DOM.createElement('span'));
root.classList.add('bd-is-emote');
}
injectEmotes(element) {
if (!this.enabledSetting.value || !element) return;
for (const beo of element.getElementsByClassName('bd-emote-outer')) this.injectEmote(beo);
}
getEmote(word) {
const name = word.replace(/;/g, '');
return this.emotes.get(name);
@ -250,4 +194,35 @@ export default new class EmoteModule {
return matching;
}
async patchMessageContent() {
const selector = '.' + WebpackModules.getClassName('container', 'containerCozy', 'containerCompact', 'edited');
const MessageContent = await ReactComponents.getComponent('MessageContent', {selector});
this.unpatchRender = MonkeyPatch('BD:EmoteModule', MessageContent.component.prototype).after('render', (component, args, retVal) => {
try {
// First child has all the actual text content, second is the edited timestamp
const markup = retVal.props.children[1].props;
if (!markup || !markup.children || !this.enabledSetting.value) return;
markup.children[1] = this.processMarkup(markup.children[1], component.props.message.editedTimestamp || component.props.message.timestamp);
} catch (err) {
Logger.err('EmoteModule', err);
}
});
MessageContent.forceUpdateAll();
}
async patchChannelTextArea() {
const selector = '.' + WebpackModules.getClassName('channelTextArea', 'emojiButton');
const ChannelTextArea = await ReactComponents.getComponent('ChannelTextArea', {selector});
this.unpatchChannelTextArea = MonkeyPatch('BD:EmoteModule', ChannelTextArea.component.prototype).after('render', (component, args, retVal) => {
if (!(retVal.props.children instanceof Array)) retVal.props.children = [retVal.props.children];
retVal.props.children.splice(0, 0, VueInjector.createReactElement(Autocomplete, {}, true));
});
ChannelTextArea.forceUpdateAll();
}
}

View File

@ -65,6 +65,18 @@
"value": false
}
]
},
{
"id": "window-preferences",
"name": "Window Preferences",
"type": "drawer",
"settings": [
{
"id": "window-preferences",
"type": "custom",
"component": "WindowPreferences"
}
]
}
]
},

View File

@ -8,9 +8,9 @@
* LICENSE file in the root directory of this source tree.
*/
import { DOM, BdUI, BdMenu, Modals, Reflection } from 'ui';
import { DOM, BdUI, BdMenu, Modals, Reflection, Toasts } from 'ui';
import BdCss from './styles/index.scss';
import { Events, CssEditor, Globals, Settings, Database, Updater, ModuleManager, PluginManager, ThemeManager, ExtModuleManager, Vendor, WebpackModules, Patcher, MonkeyPatch, ReactComponents, ReactAutoPatcher, DiscordApi } from 'modules';
import { Events, CssEditor, Globals, Settings, Database, Updater, ModuleManager, PluginManager, ThemeManager, ExtModuleManager, Vendor, WebpackModules, Patcher, MonkeyPatch, ReactComponents, ReactHelpers, ReactAutoPatcher, DiscordApi } from 'modules';
import { ClientLogger as Logger, ClientIPC, Utils } from 'common';
import { EmoteModule } from 'builtin';
import electron from 'electron';
@ -23,19 +23,30 @@ class BetterDiscord {
constructor() {
Logger.file = tests ? path.resolve(__dirname, '..', '..', 'tests', 'log.txt') : path.join(__dirname, 'log.txt');
Logger.trimLogFile();
Logger.log('main', 'BetterDiscord starting');
this._bd = {
DOM, BdUI, BdMenu, Modals, Reflection,
DOM, BdUI, BdMenu, Modals, Reflection, Toasts,
Events, CssEditor, Globals, Settings, Database, Updater,
ModuleManager, PluginManager, ThemeManager, ExtModuleManager,
Vendor,
WebpackModules, Patcher, MonkeyPatch, ReactComponents, DiscordApi,
WebpackModules, Patcher, MonkeyPatch, ReactComponents, ReactHelpers, ReactAutoPatcher, DiscordApi,
EmoteModule,
Logger, ClientIPC, Utils
Logger, ClientIPC, Utils,
plugins: PluginManager.localContent,
themes: ThemeManager.localContent,
extmodules: ExtModuleManager.localContent,
__filename, __dirname,
module: Globals.require.cache[__filename],
require: Globals.require,
webpack_require: __webpack_require__, // eslint-disable-line no-undef
get discord_require() { return WebpackModules.require }
};
const developermode = Settings.getSetting('core', 'advanced', 'developer-mode');

View File

@ -8,7 +8,7 @@
* LICENSE file in the root directory of this source tree.
*/
import { Utils, FileUtils, ClientLogger as Logger, AsyncEventEmitter } from 'common';
import { Utils, ClientLogger as Logger, AsyncEventEmitter } from 'common';
import { Modals } from 'ui';
import Database from './database';

View File

@ -12,7 +12,6 @@ import Content from './content';
import Globals from './globals';
import Database from './database';
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
import { Events } from 'modules';
import { SettingsSet, ErrorEvent } from 'structs';
import { Modals } from 'ui';
import path from 'path';
@ -240,7 +239,7 @@ export default class {
mainPath
};
const content = await this.loadContent(paths, configs, readConfig.info, readConfig.main, readConfig.dependencies, readConfig.permissions);
const content = await this.loadContent(paths, configs, readConfig.info, readConfig.main, readConfig.dependencies, readConfig.permissions, readConfig.mainExport);
if (!content) return undefined;
if (!reload && this.getContentById(content.id))
throw {message: `A ${this.contentType} with the ID ${content.id} already exists.`};
@ -256,20 +255,26 @@ export default class {
/**
* Unload content.
* @param {Content|String} content Content to unload
* @param {Boolean} force If true the content will be unloaded even if an exception is thrown when disabling/unloading
* @param {Boolean} reload Whether to reload the content after
* @return {Content}
*/
static async unloadContent(content, reload) {
static async unloadContent(content, force, reload) {
content = this.findContent(content);
if (!content) throw {message: `Could not find a ${this.contentType} from ${content}.`};
try {
await content.disable(false);
await content.emit('unload', reload);
const disablePromise = content.disable(false);
const unloadPromise = content.emit('unload', reload);
if (!force) {
await disablePromise;
await unloadPromise;
}
const index = this.getContentIndex(content);
delete window.require.cache[window.require.resolve(content.paths.mainPath)];
delete Globals.require.cache[Globals.require.resolve(content.paths.mainPath)];
if (reload) {
const newcontent = await this.preloadContent(content.dirName, true, index);
@ -288,10 +293,11 @@ export default class {
/**
* Reload content.
* @param {Content|String} content Content to reload
* @param {Boolean} force If true the content will be unloaded even if an exception is thrown when disabling/unloading
* @return {Content}
*/
static reloadContent(content) {
return this.unloadContent(content, true);
static reloadContent(content, force) {
return this.unloadContent(content, force, true);
}
/**
@ -335,18 +341,10 @@ export default class {
/**
* Wait for content to load
* @param {String} content_id
* @return {Promise}
* @return {Promise => Content}
*/
static waitForContent(content_id) {
return new Promise((resolve, reject) => {
const check = () => {
const content = this.getContentById(content_id);
if (content) return resolve(content);
setTimeout(check, 100);
};
check();
});
return Utils.until(() => this.getContentById(content_id), 100);
}
}

View File

@ -8,12 +8,12 @@
* LICENSE file in the root directory of this source tree.
*/
import { FileUtils, ClientLogger as Logger, ClientIPC } from 'common';
import Settings from './settings';
import { DOM } from 'ui';
import filewatcher from 'filewatcher';
import { FileUtils, ClientLogger as Logger, ClientIPC } from 'common';
import path from 'path';
import electron from 'electron';
import filewatcher from 'filewatcher';
import Settings from './settings';
/**
* Custom css editor communications

View File

@ -1,47 +1,25 @@
/**
* BetterDiscord Discord API
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { List } from 'structs';
import { User, Channel, Guild, Message } from 'discordstructs';
import { WebpackModules } from './webpackmodules';
import { $ } from 'vendor';
class List extends Array {
constructor() {
super(...arguments);
}
get(...filters) {
return this.find(item => {
for (let filter of filters) {
for (let key in filter) {
if (filter.hasOwnProperty(key)) {
if (item[key] !== filter[key]) return false;
}
}
}
return true;
});
}
}
class PermissionsError extends Error {
constructor(message) {
super(message);
this.name = 'PermissionsError';
}
}
class InsufficientPermissions extends PermissionsError {
constructor(message) {
super(`Missing Permission — ${message}`)
this.name = 'InsufficientPermissions';
}
}
const Modules = {
export const Modules = {
_getModule(name) {
const foundModule = WebpackModules.getModuleByName(name);
if (!foundModule) return null;
delete this[name];
return this[name] = foundModule;
},
get ChannelSelector() { return this._getModule('ChannelSelector'); },
get MessageActions() { return this._getModule('MessageActions'); },
get MessageParser() { return this._getModule('MessageParser'); },
@ -64,356 +42,258 @@ const Modules = {
get UserStore() { return this._getModule('UserStore'); },
get RelationshipStore() { return this._getModule('RelationshipStore'); },
get RelationshipManager() { return this._getModule('RelationshipManager'); },
get ChangeNicknameModal() { return this._getModule('ChangeNicknameModal'); },
get UserSettingsStore() { return this._getModule('UserSettingsStore'); },
get UserSettingsWindow() { return this._getModule('UserSettingsWindow'); },
get UserStatusStore() { return this._getModule('UserStatusStore'); },
get ChannelSettingsWindow() { return this._getModule('ChannelSettingsWindow'); },
get GuildSettingsWindow() { return this._getModule('GuildSettingsWindow'); },
get CreateChannelModal() { return this._getModule('CreateChannelModal'); },
get PruneMembersModal() { return this._getModule('PruneMembersModal'); },
get NotificationSettingsModal() { return this._getModule('NotificationSettingsModal'); },
get PrivacySettingsModal() { return this._getModule('PrivacySettingsModal'); },
get UserProfileModal() { return this._getModule('UserProfileModal'); },
get APIModule() { return this._getModule('APIModule'); },
get UserNoteStore() { return this._getModule('UserNoteStore'); },
get DiscordPermissions() { return this.DiscordConstants.Permissions; }
};
class User {
constructor(data) {
for (let key in data)
if (data.hasOwnProperty(key))
this[key] = data[key];
this.discordObject = data;
}
static fromId(id) {
return new User(Modules.UserStore.getUser(id));
}
async sendMessage(content, parse = true) {
const id = await Modules.PrivateChannelActions.ensurePrivateChannel(DiscordApi.currentUser.id, this.id);
const channel = new PrivateChannel(Modules.ChannelStore.getChannel(id));
channel.sendMessage(content, parse);
}
get isFriend() {
return Modules.RelationshipStore.isFriend(this.id);
}
get isBlocked() {
return Modules.RelationshipStore.isBlocked(this.id);
}
addFriend() {
Modules.RelationshipManager.addRelationship(this.id, {location: 'Context Menu'});
}
removeFriend() {
Modules.RelationshipManager.removeRelationship(this.id, {location: 'Context Menu'});
}
block() {
Modules.RelationshipManager.addRelationship(this.id, {location: 'Context Menu'}, Modules.DiscordConstants.RelationshipTypes.BLOCKED);
}
unblock() {
Modules.RelationshipManager.removeRelationship(this.id, {location: 'Context Menu'});
}
}
class Member extends User {
constructor(data, guild) {
super(data);
const userData = Modules.UserStore.getUser(data.userId);
for (let key in userData)
if (userData.hasOwnProperty(key))
this[key] = userData[key];
this.guild_id = guild;
}
checkPermissions(perms) {
return Modules.PermissionUtils.can(perms, DiscordApi.currentUser, Modules.GuildStore.getGuild(this.guild_id));
}
kick(reason = '') {
if (!this.checkPermissions(Modules.DiscordPermissions.KICK_MEMBERS)) throw new InsufficientPermissions('KICK_MEMBERS');
Modules.GuildActions.kickUser(this.guild_id, this.id, reason);
}
ban(daysToDelete = '1', reason = '') {
if (!this.checkPermissions(Modules.DiscordPermissions.BAN_MEMBERS)) throw new InsufficientPermissions('BAN_MEMBERS');
Modules.GuildActions.banUser(this.guild_id, this.id, daysToDelete, reason);
}
unban() {
if (!this.checkPermissions(Modules.DiscordPermissions.BAN_MEMBERS)) throw new InsufficientPermissions('BAN_MEMBERS');
Modules.GuildActions.unbanUser(this.guild_id, this.id);
}
move(channel_id) {
if (!this.checkPermissions(Modules.DiscordPermissions.MOVE_MEMBERS)) throw new InsufficientPermissions('MOVE_MEMBERS');
Modules.GuildActions.setChannel(this.guild_id, this.id, channel_id);
}
mute(active = true) {
if (!this.checkPermissions(Modules.DiscordPermissions.MUTE_MEMBERS)) throw new InsufficientPermissions('MUTE_MEMBERS');
Modules.GuildActions.setServerMute(this.guild_id, this.id, active);
}
unmute(active = true) {
this.mute(false);
}
deafen(active = true) {
if (!this.checkPermissions(Modules.DiscordPermissions.DEAFEN_MEMBERS)) throw new InsufficientPermissions('DEAFEN_MEMBERS');
Modules.GuildActions.setServerDeaf(this.guild_id, this.id, active);
}
undeafen(active = true) {
this.deafen(false);
}
}
class Guild {
constructor(data) {
for (let key in data)
if (data.hasOwnProperty(key))
this[key] = data[key];
this.discordObject = data;
}
get channels() {
const channels = Modules.GuildChannelsStore.getChannels(this.id);
const returnChannels = new List();
for (const category in channels) {
if (channels.hasOwnProperty(category)) {
if (!Array.isArray(channels[category])) continue;
const channelList = channels[category];
for (const channel of channelList) {
returnChannels.push(new GuildChannel(channel.channel));
}
}
}
return returnChannels;
}
get defaultChannel() {
return new GuildChannel(Modules.GuildChannelsStore.getDefaultChannel(this.id));
}
get members() {
const members = Modules.GuildMemberStore.getMembers(this.id);
const returnMembers = new List();
for (const member of members) returnMembers.push(new Member(member, this.id));
return returnMembers;
}
get memberCount() {
return Modules.MemberCountStore.getMemberCount(this.id);
}
get emojis() {
return Modules.EmojiUtils.getGuildEmoji(this.id);
}
get permissions() {
return Modules.GuildPermissions.getGuildPermissions(this.id);
}
getMember(userId) {
return Modules.GuildMemberStore.getMember(this.id, userId);
}
isMember(userId) {
return Modules.GuildMemberStore.isMember(this.id, userId);
}
markAsRead() {
Modules.GuildActions.markGuildAsRead(this.id);
}
select() {
Modules.GuildActions.selectGuild(this.id);
}
nsfwAgree() {
Modules.GuildActions.nsfwAgree(this.id);
}
nsfwDisagree() {
Modules.GuildActions.nsfwDisagree(this.id);
}
changeSortLocation(index) {
Modules.GuildActions.move(DiscordApi.guildPositions.indexOf(this.id), index);
}
}
class Channel {
constructor(data) {
for (let key in data)
if (data.hasOwnProperty(key))
this[key] = data[key];
this.discordObject = data;
}
checkPermissions(perms) {
return Modules.PermissionUtils.can(perms, DiscordApi.currentUser, this.discordObject) || this.isPrivate();
}
async sendMessage(content, parse = true) {
if (!this.checkPermissions(Modules.DiscordPermissions.VIEW_CHANNEL | Modules.DiscordPermissions.SEND_MESSAGES)) throw new InsufficientPermissions('SEND_MESSAGES');
let response = {};
if (parse) response = await Modules.MessageActions._sendMessage(this.id, Modules.MessageParser.parse(this.discordObject, content));
else response = await Modules.MessageActions._sendMessage(this.id, {content});
return new Message(Modules.MessageStore.getMessage(this.id, response.body.id));
}
get messages() {
const messages = Modules.MessageStore.getMessages(this.id).toArray();
for (let i in messages)
if (messages.hasOwnProperty(i))
messages[i] = new Message(messages[i]);
return new List(...messages);
}
jumpToPresent() {
if (!this.checkPermissions(Modules.DiscordPermissions.VIEW_CHANNEL)) throw new InsufficientPermissions('VIEW_CHANNEL');
if (this.hasMoreAfter) Modules.MessageActions.jumpToPresent(this.id, Modules.DiscordConstants.MAX_MESSAGES_PER_CHANNEL);
else this.messages[this.messages.length - 1].jumpTo(false);
}
get hasMoreAfter() {
return Modules.MessageStore.getMessages(this.id).hasMoreAfter;
}
sendInvite(inviteId) {
if (!this.checkPermissions(Modules.DiscordPermissions.VIEW_CHANNEL | Modules.DiscordPermissions.SEND_MESSAGES)) throw new InsufficientPermissions('SEND_MESSAGES');
Modules.MessageActions.sendInvite(this.id, inviteId);
}
select() {
if (!this.checkPermissions(Modules.DiscordPermissions.VIEW_CHANNEL)) throw new InsufficientPermissions('VIEW_CHANNEL');
Modules.NavigationUtils.transitionToGuild(this.guild_id ? this.guild_id : Modules.DiscordConstants.ME, this.id);
}
}
class GuildChannel extends Channel {
constructor(data) {
super(data);
}
get permissions() {
return Modules.GuildPermissions.getChannelPermissions(this.id);
}
get guild() {
return new Guild(Modules.GuildStore.getGuild(this.guild_id));
}
isDefaultChannel() {
return Modules.GuildChannelsStore.getDefaultChannel(this.guild_id).id === this.id;
}
}
class PrivateChannel extends Channel {
constructor(data) {
super(data);
}
}
class Message {
constructor(data) {
for (let key in data)
if (data.hasOwnProperty(key))
this[key] = data[key];
this.discordObject = data;
}
delete() {
Modules.MessageActions.deleteMessage(this.channel_id, this.id);
}
// programmatically update the content
edit(content, parse = false) {
if (this.author.id !== DiscordApi.currentUser.id) return;
if (parse) Modules.MessageActions.editMessage(this.channel_id, this.id, Modules.MessageParser.parse(this.discordObject, content));
else Modules.MessageActions.editMessage(this.channel_id, this.id, {content});
}
// start the editing mode of GUI
startEdit() {
if (this.author.id !== DiscordApi.currentUser.id) return;
Modules.MessageActions.startEditMessage(this.channel_id, this.id, this.content);
}
// end editing mode of GUI
endEdit() {
Modules.MessageActions.endEditMessage();
}
jumpTo(flash = true) {
Modules.MessageActions.jumpToMessage(this.channel_id, this.id, flash);
}
}
export default class DiscordApi {
static get channels() {
const channels = Modules.ChannelStore.getChannels();
const returnChannels = new List();
for (const [key, value] of Object.entries(channels)) {
returnChannels.push(value.isPrivate() ? new PrivateChannel(value) : new GuildChannel(value));
}
return returnChannels;
}
static get modules() { return Modules }
static get User() { return User }
static get Channel() { return Channel }
static get Guild() { return Guild }
static get Message() { return Message }
/**
* A list of loaded guilds.
*/
static get guilds() {
const guilds = Modules.GuildStore.getGuilds();
const returnGuilds = new List();
for (const [key, value] of Object.entries(guilds)) {
returnGuilds.push(new Guild(value));
}
return returnGuilds;
return List.from(Object.entries(guilds), ([i, g]) => Guild.from(g));
}
/**
* A list of loaded channels.
*/
static get channels() {
const channels = Modules.ChannelStore.getChannels();
return List.from(Object.entries(channels), ([i, c]) => Channel.from(c));
}
/**
* A list of loaded users.
*/
static get users() {
const users = Modules.UserStore.getUsers();
const returnUsers = new List();
for (const [key, value] of Object.entries(users)) {
returnUsers.push(new User(value));
}
return returnUsers;
return List.from(Object.entries(users), ([i, u]) => User.from(u));
}
/**
* An object mapping guild IDs to their member counts.
*/
static get memberCounts() {
return Modules.MemberCountStore.getMemberCounts();
}
/**
* A list of guilds in the order they appear in the server list.
*/
static get sortedGuilds() {
const guilds = Modules.SortedGuildStore.getSortedGuilds();
const returnGuilds = new List();
for (const guild of guilds) {
returnGuilds.push(new Guild(guild));
}
return returnGuilds;
return List.from(guilds, g => Guild.from(g));
}
/**
* An array of guild IDs in the order they appear in the server list.
*/
static get guildPositions() {
return Modules.SortedGuildStore.guildPositions;
}
/**
* The currently selected guild.
*/
static get currentGuild() {
return new Guild(Modules.GuildStore.getGuild(Modules.SelectedGuildStore.getGuildId()));
const guild = Modules.GuildStore.getGuild(Modules.SelectedGuildStore.getGuildId());
if (guild) return Guild.from(guild);
}
/**
* The currently selected channel.
*/
static get currentChannel() {
const channel = Modules.ChannelStore.getChannel(Modules.SelectedChannelStore.getChannelId());
if (channel) return channel.isPrivate() ? new PrivateChannel(channel) : new GuildChannel(channel);
if (channel) return Channel.from(channel);
}
/**
* The current user.
*/
static get currentUser() {
return Modules.UserStore.getCurrentUser();
const user = Modules.UserStore.getCurrentUser();
if (user) return User.from(user);
}
/**
* A list of the current user's friends.
*/
static get friends() {
const friends = Modules.RelationshipStore.getFriendIDs();
const returnUsers = new List();
for (const id of friends) returnUsers.push(User.fromId(id));
return returnUsers;
return List.from(friends, id => User.fromId(id));
}
static get UserSettings() {
return UserSettings;
}
}
export class UserSettings {
/**
* Opens Discord's settings UI.
*/
static open(section = 'ACCOUNT') {
Modules.UserSettingsWindow.setSection(section);
Modules.UserSettingsWindow.open();
}
/**
* The user's current status. Either "online", "idle", "dnd" or "invisible".
*/
static get status() { return Modules.UserSettingsStore.status }
/**
* The user's selected explicit content filter level.
* 0 == off, 1 == everyone except friends, 2 == everyone
* Configurable in the privacy and safety panel.
*/
static get explicitContentFilter() { return Modules.UserSettingsStore.explicitContentFilter }
/**
* Whether to disallow direct messages from server members by default.
*/
static get defaultGuildsRestricted() { return Modules.UserSettingsStore.defaultGuildsRestricted }
/**
* An array of guilds to disallow direct messages from their members.
* This is bypassed if the member is has another mutual guild with this disabled, or the member is friends with the current user.
* Configurable in each server's privacy settings.
*/
static get restrictedGuildIds() { return Modules.UserSettingsStore.restrictedGuilds }
static get restrictedGuilds() {
return List.from(this.restrictedGuildIds, id => Guild.fromId(id) || id);
}
/**
* An array of flags specifying who should be allowed to add the current user as a friend.
* If everyone is checked, this will only have one item, "all". Otherwise it has either "mutual_friends", "mutual_guilds", both or neither.
* Configurable in the privacy and safety panel.
*/
static get friendSourceFlags() { return Object.keys(Modules.UserSettingsStore.friendSourceFlags) }
static get friendSourceEveryone() { return this.friendSourceFlags.include('all') }
static get friendSourceMutual_friends() { return this.friendSourceFlags.include('all') || this.friendSourceFlags.include('mutual_friends') }
static get friendSourceMutual_guilds() { return this.friendSourceFlags.include('all') || this.friendSourceFlags.include('mutual_guilds') }
static get friendSourceAnyone() { return this.friendSourceFlags.length > 0 }
/**
* Whether to automatically add accounts from other platforms running on the user's computer.
* Configurable in the connections panel.
*/
static get detectPlatformAccounts() { return Modules.UserSettingsStore.detectPlatformAccounts }
/**
* The number of seconds Discord will wait for activity before sending mobile push notifications.
* Configurable in the notifications panel.
*/
static get afkTimeout() { return Modules.UserSettingsStore.afkTimeout }
/**
* Whether to display the currently running game as a status message.
* Configurable in the games panel.
*/
static get showCurrentGame() { return Modules.UserSettingsStore.showCurrentGame }
/**
* Whether to show images uploaded directly to Discord.
* Configurable in the text and images panel.
*/
static get inlineAttachmentMedia() { return Modules.UserSettingsStore.inlineAttachmentMedia }
/**
* Whether to show images linked in Discord.
* Configurable in the text and images panel.
*/
static get inlineEmbedMedia() { return Modules.UserSettingsStore.inlineEmbedMedia }
/**
* Whether to automatically play GIFs when the Discord window is active without having to hover the mouse over the image.
* Configurable in the text and images panel.
*/
static get autoplayGifs() { return Modules.UserSettingsStore.gifAutoPlay }
/**
* Whether to show content from HTTP[s] links as embeds.
* Configurable in the text and images panel.
*/
static get showEmbeds() { return Modules.UserSettingsStore.renderEmbeds }
/**
* Whether to show a message's reactions.
* Configurable in the text and images panel.
*/
static get showReactions() { return Modules.UserSettingsStore.renderReactions }
/**
* Whether to play animated emoji.
* Configurable in the text and images panel.
*/
static get animateEmoji() { return Modules.UserSettingsStore.animateEmoji }
/**
* Whether to convert ASCII emoticons to emoji.
* Configurable in the text and images panel.
*/
static get convertEmoticons() { return Modules.UserSettingsStore.convertEmoticons }
/**
* Whether to allow playing text-to-speech messages.
* Configurable in the text and images panel.
*/
static get allowTts() { return Modules.UserSettingsStore.enableTTSCommand }
/**
* The user's selected theme. Either "dark" or "light".
* Configurable in the appearance panel.
*/
static get theme() { return Modules.UserSettingsStore.theme }
/**
* Whether the user has enabled compact mode.
* `true` if compact mode is enabled, `false` if cozy mode is enabled.
* Configurable in the appearance panel.
*/
static get displayCompact() { return Modules.UserSettingsStore.messageDisplayCompact }
/**
* Whether the user has enabled developer mode.
* Currently only adds a "Copy ID" option to the context menu on users, guilds and channels.
* Configurable in the appearance panel.
*/
static get developerMode() { return Modules.UserSettingsStore.developerMode }
/**
* The user's selected language code.
* Configurable in the language panel.
*/
static get locale() { return Modules.UserSettingsStore.locale }
/**
* The user's timezone offset in hours.
* This is not configurable.
*/
static get timezoneOffset() { return Modules.UserSettingsStore.timezoneOffset }
}

View File

@ -8,7 +8,6 @@
* LICENSE file in the root directory of this source tree.
*/
import { Utils, ClientLogger as Logger } from 'common';
import { WebpackModules } from './webpackmodules';
import Events from './events';
import EventListener from './eventlistener';
@ -22,7 +21,6 @@ import * as SocketStructs from '../structs/socketstructs';
export default class extends EventListener {
init() {
Logger.log('EventHook', SocketStructs);
this.hook();
}
@ -38,18 +36,16 @@ export default class extends EventListener {
hook() {
const self = this;
const orig = this.eventsModule.prototype.emit;
this.eventsModule.prototype.emit = function (...args) {
const Events = WebpackModules.getModuleByName('Events');
const orig = Events.prototype.emit;
Events.prototype.emit = function (...args) {
orig.call(this, ...args);
self.wsc = this;
self.emit(...args);
};
}
get eventsModule() {
return WebpackModules.getModuleByName('Events');
}
/**
* Discord emit overload
* @param {any} event
@ -68,22 +64,29 @@ export default class extends EventListener {
* @param {any} event Event
* @param {any} data Event data
*/
dispatch(e, d) {
Events.emit('raw-event', { type: e, data: d });
if (e === this.actions.READY || e === this.actions.RESUMED) {
Events.emit(e, d);
dispatch(type, data) {
Events.emit('raw-event', { type, data });
if (type === this.actions.READY || type === this.actions.RESUMED) {
Events.emit(type, data);
return;
}
if (!Object.keys(SocketStructs).includes(e)) return;
const evt = new SocketStructs[e](d);
Events.emit(`discord:${e}`, evt);
if (!Object.keys(SocketStructs).includes(type)) return;
Events.emit(`discord:${type}`, new SocketStructs[type](data));
}
get SocketStructs() {
return SocketStructs;
}
/**
* All known socket actions
*/
get actions() {
return {
if (this._actions) return this._actions;
return this._actions = {
READY: 'READY', // Socket ready
RESUMED: 'RESUMED', // Socket resumed
TYPING_START: 'TYPING_START', // User typing start

View File

@ -40,13 +40,15 @@ export default class {
emitter.removeListener(event, callback);
}
static get removeListener() { return this.off }
/**
* Emits an event
* @param {String} event The event to emit
* @param {Any} ...data Data to pass to the event listeners
*/
static emit(...args) {
emitter.emit(...args);
static emit(event, ...data) {
emitter.emit(event, ...data);
}
}

View File

@ -14,6 +14,7 @@ export default class EventsWrapper {
constructor(eventemitter, bind) {
eventemitters.set(this, eventemitter);
this.bind = bind || this;
}
get eventSubs() {
@ -37,16 +38,16 @@ export default class EventsWrapper {
get off() { return this.unsubscribe }
unsubscribe(event, callback) {
for (let index of this.eventSubs) {
for (let index in this.eventSubs) {
if (this.eventSubs[index].event !== event || (callback && this.eventSubs[index].callback === callback)) continue;
eventemitters.get(this).off(event, this.eventSubs[index].boundCallback);
eventemitters.get(this).removeListener(event, this.eventSubs[index].boundCallback);
this.eventSubs.splice(index, 1);
}
}
unsubscribeAll() {
for (let event of this.eventSubs) {
eventemitters.get(this).off(event.event, event.boundCallback);
eventemitters.get(this).removeListener(event.event, event.boundCallback);
}
this.eventSubs.splice(0, this.eventSubs.length);
}

View File

@ -8,13 +8,14 @@
* LICENSE file in the root directory of this source tree.
*/
import Globals from './globals';
import Content from './content';
export default class ExtModule extends Content {
constructor(internals) {
super(internals);
this.__require = window.require(this.paths.mainPath);
this.__require = Globals.require(this.paths.mainPath);
}
get type() { return 'module' }

View File

@ -10,8 +10,6 @@
import ContentManager from './contentmanager';
import ExtModule from './extmodule';
import { ClientLogger as Logger } from 'common';
import { Events } from 'modules';
export default class extends ContentManager {

View File

@ -17,6 +17,10 @@ export default new class extends Module {
constructor(args) {
super(args);
// webpack replaces this with the normal require function
// eslint-disable-next-line no-undef
this.require = __non_webpack_require__;
}
initg() {

View File

@ -9,8 +9,8 @@
*/
import { ClientLogger as Logger } from 'common';
import { Events, SocketProxy, EventHook, CssEditor } from 'modules';
import { ProfileBadges } from 'ui';
import { SocketProxy, EventHook, CssEditor } from 'modules';
import { ProfileBadges, ClassNormaliser } from 'ui';
import Updater from './updater';
/**
@ -24,6 +24,7 @@ export default class {
static get modules() {
return this._modules ? this._modules : (this._modules = [
new ProfileBadges(),
new ClassNormaliser(),
new SocketProxy(),
new EventHook(),
CssEditor,

View File

@ -21,4 +21,4 @@ export { default as Module } from './module';
export { default as EventListener } from './eventlistener';
export { default as SocketProxy } from './socketproxy';
export { default as EventHook } from './eventhook';
export { default as DiscordApi } from './discordapi';
export { default as DiscordApi, Modules as DiscordApiModules } from './discordapi';

View File

@ -9,7 +9,7 @@
*/
import { WebpackModules } from './webpackmodules';
import { ClientLogger as Logger, Utils } from 'common';
import { ClientLogger as Logger } from 'common';
export class Patcher {
@ -37,9 +37,8 @@ export class Patcher {
}
static resolveModule(module) {
if (module instanceof Function || (module instanceof Object && !(module instanceof Array))) return module;
if (module instanceof Function || (module instanceof Object)) return module;
if (typeof module === 'string') return WebpackModules.getModuleByName(module);
if (module instanceof Array) return WebpackModules.getModuleByProps(module);
return null;
}
@ -70,7 +69,7 @@ export class Patcher {
for (const slavePatch of patch.children.filter(c => c.type === 'after')) {
try {
slavePatch.callback(this, arguments, retVal);
slavePatch.callback(this, arguments, retVal, r => retVal = r);
} catch (err) {
Logger.err(`Patcher:${patch.id}`, err);
}
@ -80,7 +79,9 @@ export class Patcher {
}
static rePatch(patch) {
patch.proxyFunction = patch.module[patch.functionName] = this.overrideFn(patch);
if (patch.module instanceof Array && typeof patch.functionName === 'number')
patch.module.splice(patch.functionName, 1, patch.proxyFunction = this.overrideFn(patch));
else patch.proxyFunction = patch.module[patch.functionName] = this.overrideFn(patch);
}
static pushPatch(caller, id, module, functionName) {

View File

@ -18,12 +18,12 @@ export default class Plugin extends Content {
get start() { return this.enable }
get stop() { return this.disable }
reload() {
return PluginManager.reloadPlugin(this);
reload(force) {
return PluginManager.reloadPlugin(this, force);
}
unload() {
return PluginManager.unloadPlugin(this);
unload(force) {
return PluginManager.unloadPlugin(this, force);
}
}

View File

@ -10,8 +10,9 @@
import { EmoteModule } from 'builtin';
import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs';
import { BdMenu, Modals, DOM, Reflection } from 'ui';
import { Utils, ClientLogger as Logger, ClientIPC, AsyncEventEmitter } from 'common';
import { BdMenu, Modals, DOM, DOMObserver, Reflection, VueInjector, Toasts } from 'ui';
import * as CommonComponents from 'commoncomponents';
import { Utils, Filters, ClientLogger as Logger, ClientIPC, AsyncEventEmitter } from 'common';
import Settings from './settings';
import ExtModuleManager from './extmodulemanager';
import PluginManager from './pluginmanager';
@ -20,7 +21,7 @@ import Events from './events';
import EventsWrapper from './eventswrapper';
import { WebpackModules } from './webpackmodules';
import DiscordApi from './discordapi';
import { ReactComponents } from './reactcomponents';
import { ReactComponents, ReactHelpers } from './reactcomponents';
import { Patcher, MonkeyPatch } from './patcher';
export default class PluginApi {
@ -58,6 +59,20 @@ export default class PluginApi {
get AsyncEventEmitter() { return AsyncEventEmitter }
get EventsWrapper() { return EventsWrapper }
get CommonComponents() { return CommonComponents }
get Filters() { return Filters }
get Discord() { return DiscordApi }
get DiscordApi() { return DiscordApi }
get ReactComponents() { return ReactComponents }
get ReactHelpers() { return ReactHelpers }
get Reflection() { return Reflection }
get DOM() { return DOM }
get VueInjector() { return VueInjector }
get observer() {
return this._observer || (this._observer = new DOMObserver());
}
/**
* Logger
*/
@ -216,7 +231,7 @@ export default class PluginApi {
this.injectedStyles.splice(this.injectedStyles.indexOf(styleid), 1);
DOM.deleteStyle(styleid);
}
deleteAllStyles(id, css) {
deleteAllStyles(id) {
for (let id of this.injectedStyles) {
this.deleteStyle(id);
}
@ -267,10 +282,10 @@ export default class PluginApi {
return this.modalStack[this.modalStack.length - 1].close(force);
}
basicModal(title, text) {
return this.addModal(Modals.basic(title, text));
return this.addModal(Modals.createBasicModal(title, text));
}
settingsModal(settingsset, headertext, options) {
return this.addModal(Modals.settings(settingsset, headertext, options));
return this.addModal(Modals.createSettingsModal(settingsset, headertext, options));
}
get Modals() {
return Object.defineProperties({
@ -290,6 +305,36 @@ export default class PluginApi {
});
}
/**
* Toasts
*/
showToast(message, options = {}) {
return Toasts.push(message, options);
}
showSuccessToast(message, options = {}) {
return Toasts.success(message, options);
}
showInfoToast(message, options = {}) {
return Toasts.info(message, options);
}
showErrorToast(message, options = {}) {
return Toasts.error(message, options);
}
showWarningToast(message, options = {}) {
return Toasts.warning(message, options);
}
get Toasts() {
return {
push: this.showToast.bind(this),
success: this.showSuccessToast.bind(this),
error: this.showErrorToast.bind(this),
info: this.showInfoToast.bind(this),
warning: this.showWarningToast.bind(this)
};
}
/**
* Emotes
*/
@ -403,6 +448,9 @@ export default class PluginApi {
getWebpackModuleByName(name, fallback) {
return WebpackModules.getModuleByName(name, fallback);
}
getWebpackModuleByDisplayName(name) {
return WebpackModules.getModuleByDisplayName(name);
}
getWebpackModuleByRegex(regex) {
return WebpackModules.getModuleByRegex(regex, true);
}
@ -421,38 +469,58 @@ export default class PluginApi {
getWebpackModulesByPrototypeFields(...props) {
return WebpackModules.getModuleByPrototypes(props, false);
}
waitForWebpackModule(filter) {
return WebpackModules.waitForModule(filter);
}
waitForWebpackModuleByName(name, fallback) {
return WebpackModules.waitForModuleByName(name, fallback);
}
waitForWebpackModuleByDisplayName(name) {
return WebpackModules.waitForModuleByDisplayName(name);
}
waitForWebpackModuleByRegex(regex) {
return WebpackModules.waitForModuleByRegex(regex);
}
waitForWebpackModuleByProperties(...props) {
return WebpackModules.waitForModuleByProps(props);
}
waitForWebpackModuleByPrototypeFields(...props) {
return WebpackModules.waitForModuleByPrototypes(props);
}
getWebpackClassName(...classes) {
return WebpackModules.getClassName(...classes);
}
waitForWebpackClassName(...classes) {
return WebpackModules.waitForClassName(...classes);
}
get WebpackModules() {
return Object.defineProperty({
return new Proxy({
getModule: this.getWebpackModule.bind(this),
getModuleByName: this.getWebpackModuleByName.bind(this),
getModuleByDisplayName: this.getWebpackModuleByName.bind(this),
getModuleByDisplayName: this.getWebpackModuleByDisplayName.bind(this),
getModuleByRegex: this.getWebpackModuleByRegex.bind(this),
getModulesByRegex: this.getWebpackModulesByRegex.bind(this),
getModuleByProperties: this.getWebpackModuleByProperties.bind(this),
getModuleByPrototypeFields: this.getWebpackModuleByPrototypeFields.bind(this),
getModulesByProperties: this.getWebpackModulesByProperties.bind(this),
getModulesByPrototypeFields: this.getWebpackModulesByPrototypeFields.bind(this)
}, 'require', {
get: () => this.webpackRequire
getModulesByPrototypeFields: this.getWebpackModulesByPrototypeFields.bind(this),
waitForModule: this.waitForWebpackModule.bind(this),
waitForModuleByName: this.waitForWebpackModuleByName.bind(this),
waitForModuleByDisplayName: this.waitForWebpackModuleByDisplayName.bind(this),
waitForModuleByRegex: this.waitForWebpackModuleByRegex.bind(this),
waitForModuleByProperties: this.waitForWebpackModuleByProperties.bind(this),
waitForModuleByPrototypeFields: this.waitForWebpackModuleByPrototypeFields.bind(this),
getClassName: this.getWebpackClassName.bind(this),
waitForClassName: this.waitForWebpackClassName.bind(this),
get KnownModules() { return WebpackModules.KnownModules },
get require() { return WebpackModules.require }
}, {
get(WebpackModules, property) {
return WebpackModules[property] || WebpackModules.getModuleByName(property);
}
});
}
/**
* DiscordApi
*/
get Discord() {
return DiscordApi;
}
get ReactComponents() {
return ReactComponents;
}
get Reflection() {
return Reflection;
}
/**
* Patcher
*/

View File

@ -8,15 +8,16 @@
* LICENSE file in the root directory of this source tree.
*/
import { Permissions } from 'modules';
import { Modals } from 'ui';
import { ErrorEvent } from 'structs';
import { ClientLogger as Logger } from 'common';
import Globals from './globals';
import ContentManager from './contentmanager';
import ExtModuleManager from './extmodulemanager';
import Plugin from './plugin';
import PluginApi from './pluginapi';
import Vendor from './vendor';
import { ClientLogger as Logger } from 'common';
import { Events, Permissions } from 'modules';
import { Modals } from 'ui';
import { ErrorEvent } from 'structs';
export default class extends ContentManager {
@ -73,7 +74,7 @@ export default class extends ContentManager {
static get refreshPlugins() { return this.refreshContent }
static get loadContent() { return this.loadPlugin }
static async loadPlugin(paths, configs, info, main, dependencies, permissions) {
static async loadPlugin(paths, configs, info, main, dependencies, permissions, mainExport) {
if (permissions && permissions.length > 0) {
for (let perm of permissions) {
Logger.log(this.moduleName, `Permission: ${Permissions.permissionText(perm).HEADER} - ${Permissions.permissionText(perm).BODY}`);
@ -85,7 +86,7 @@ export default class extends ContentManager {
}
}
const deps = [];
const deps = {};
if (dependencies) {
for (const [key, value] of Object.entries(dependencies)) {
const extModule = ExtModuleManager.findModule(key);
@ -96,8 +97,15 @@ export default class extends ContentManager {
}
}
const plugin = window.require(paths.mainPath)(Plugin, new PluginApi(info, paths.contentPath), Vendor, deps);
if (!(plugin.prototype instanceof Plugin))
const pluginExports = Globals.require(paths.mainPath);
const pluginFunction = mainExport ? pluginExports[mainExport]
: pluginExports.__esModule ? pluginExports.default : pluginExports;
if (typeof pluginFunction !== 'function')
throw {message: `Plugin ${info.name} did not export a function.`};
const plugin = pluginFunction.call(pluginExports, Plugin, new PluginApi(info, paths.contentPath), Vendor, deps);
if (!plugin || !(plugin.prototype instanceof Plugin))
throw {message: `Plugin ${info.name} did not return a class that extends Plugin.`};
const instance = new plugin({

View File

@ -1,6 +1,7 @@
/**
* BetterDiscord React Component Manipulations
* original concept and some code by samogot - https://github.com/samogot / https://github.com/samogot/betterdiscord-plugins/tree/master/v2/1Lib%20Discord%20Internals
* Original concept and some code by samogot - https://github.com/samogot / https://github.com/samogot/betterdiscord-plugins/tree/master/v2/1Lib%20Discord%20Internals
*
* Copyright (c) 2015-present JsSucks - https://github.com/JsSucks
* All rights reserved.
* https://github.com/JsSucks - https://betterdiscord.net
@ -9,11 +10,10 @@
* LICENSE file in the root directory of this source tree.
*/
import { EmoteModule } from 'builtin';
import { Reflection } from 'ui';
import { ClientLogger as Logger } from 'common';
import { MonkeyPatch, Patcher } from './patcher';
import { WebpackModules, Filters } from './webpackmodules';
import { DOM, Reflection } from 'ui';
import { Utils, Filters, ClientLogger as Logger } from 'common';
import { MonkeyPatch } from './patcher';
import { WebpackModules } from './webpackmodules';
import DiscordApi from './discordapi';
class Helpers {
@ -166,22 +166,18 @@ class Helpers {
export { Helpers as ReactHelpers };
class ReactComponent {
constructor(id, component, retVal) {
this._id = id;
this._component = component;
this._retVal = retVal;
constructor(id, component, retVal, important) {
this.id = id;
this.component = component;
this.retVal = retVal;
this.important = important;
}
get id() {
return this._id;
}
get component() {
return this._component;
}
get retVal() {
return this._retVal;
forceUpdateAll() {
if (!this.important || !this.important.selector) return;
for (let e of document.querySelectorAll(this.important.selector)) {
Reflection(e).forceUpdate(this);
}
}
}
@ -191,60 +187,85 @@ export class ReactComponents {
static get listeners() { return this._listeners || (this._listeners = []) }
static get nameSetters() { return this._nameSetters || (this._nameSetters = []) }
static push(component, retVal) {
static get ReactComponent() { return ReactComponent }
static push(component, retVal, important) {
if (!(component instanceof Function)) return null;
const { displayName } = component;
if (!displayName) {
return this.processUnknown(component, retVal);
}
const have = this.components.find(comp => comp.id === displayName);
if (have) return component;
const c = new ReactComponent(displayName, component, retVal);
this.components.push(c);
const listener = this.listeners.find(listener => listener.id === displayName);
if (!listener) return c;
for (const l of listener.listeners) {
l(c);
if (have) {
if (!have.important) have.important = important;
return component;
}
this.listeners.splice(this.listeners.findIndex(listener => listener.id === displayName), 1);
const c = new ReactComponent(displayName, component, retVal, important);
this.components.push(c);
const listener = this.listeners.find(listener => listener.id === displayName);
if (listener) {
for (const l of listener.listeners) l(c);
Utils.removeFromArray(this.listeners, listener);
}
return c;
}
static async getComponent(name, important) {
/**
* Finds a component from the components array or by waiting for it to be mounted.
* @param {String} name The component's name
* @param {Object} important An object containing a selector to look for
* @param {Function} filter A function to filter components if a single element is rendered by multiple components
* @return {Promise => ReactComponent}
*/
static async getComponent(name, important, filter) {
const have = this.components.find(c => c.id === name);
if (have) return have;
if (important) {
const importantInterval = setInterval(() => {
const callback = () => {
if (this.components.find(c => c.id === name)) {
Logger.info('ReactComponents', `Important component ${name} already found`);
clearInterval(importantInterval);
DOM.observer.unsubscribe(observerSubscription);
return;
}
const select = document.querySelector(important.selector);
if (!select) return;
const reflect = Reflection(select);
if (!reflect.component) {
clearInterval(importantInterval);
Logger.error('ReactComponents', [`FAILED TO GET IMPORTANT COMPONENT ${name} WITH REFLECTION FROM`, select]);
const element = document.querySelector(important.selector);
if (!element) return;
DOM.observer.unsubscribe(observerSubscription);
const reflect = Reflection(element);
const component = filter ? reflect.components.find(filter) : reflect.component;
if (!component) {
Logger.err('ReactComponents', [`FAILED TO GET IMPORTANT COMPONENT ${name} WITH REFLECTION FROM`, element]);
return;
}
if (!reflect.component.displayName) reflect.component.displayName = name;
if (!component.displayName) component.displayName = name;
Logger.info('ReactComponents', [`Found important component ${name} with reflection`, reflect]);
this.push(reflect.component);
clearInterval(importantInterval);
}, 50);
important.filter = filter;
this.push(component, undefined, important);
};
const observerSubscription = DOM.observer.subscribeToQuerySelector(callback, important.selector);
setTimeout(callback, 0);
}
const listener = this.listeners.find(l => l.id === name);
if (!listener) this.listeners.push({
let listener = this.listeners.find(l => l.id === name);
if (!listener) this.listeners.push(listener = {
id: name,
listeners: []
});
return new Promise(resolve => {
this.listeners.find(l => l.id === name).listeners.push(resolve);
listener.listeners.push(resolve);
});
}
static setName(name, filter, callback) {
static setName(name, filter) {
const have = this.components.find(c => c.id === name);
if (have) return have;
@ -262,7 +283,7 @@ export class ReactComponents {
const have = this.unknownComponents.find(c => c.component === component);
for (const [fi, filter] of this.nameSetters.entries()) {
if (filter.filter.filter(component)) {
console.log('filter match!');
Logger.log('ReactComponents', 'Filter match!');
component.displayName = filter.name;
this.nameSetters.splice(fi, 1);
return this.push(component, retVal);
@ -275,36 +296,42 @@ export class ReactComponents {
}
export class ReactAutoPatcher {
/**
* Wait for React to be loaded and patch it's createElement to store all unknown components.
* Also patches of some known components.
*/
static async autoPatch() {
await this.ensureReact();
this.React = {};
this.React.unpatchCreateElement = MonkeyPatch('BD:ReactComponents:createElement', 'React').before('createElement', (component, args) => {
ReactComponents.push(args[0]);
});
const React = await WebpackModules.waitForModuleByName('React');
this.unpatchCreateElement = MonkeyPatch('BD:ReactComponents:createElement', React).before('createElement', (component, args) => ReactComponents.push(args[0]));
this.patchComponents();
return 1;
}
static async ensureReact() {
while (!window.webpackJsonp || !WebpackModules.getModuleByName('React')) await new Promise(resolve => setTimeout(resolve, 10));
return 1;
}
static async patchComponents() {
await this.patchMessage();
await this.patchMessageGroup();
await this.patchChannelMember();
await this.patchGuild();
await this.patchChannel();
await this.patchChannelList();
this.forceUpdate();
/**
* Patches a few known components.
*/
static patchComponents() {
return Promise.all([
this.patchMessage(),
this.patchMessageGroup(),
this.patchChannelMember(),
this.patchGuild(),
this.patchChannel(),
this.patchChannelList(),
this.patchUserProfileModal(),
this.patchUserPopout()
]);
}
static async patchMessage() {
this.Message = await ReactComponents.getComponent('Message', { selector: '.message' });
const selector = '.' + WebpackModules.getClassName('message', 'messageCozy', 'messageCompact');
this.Message = await ReactComponents.getComponent('Message', {selector}, m => m.prototype && m.prototype.renderCozy);
this.unpatchMessageRender = MonkeyPatch('BD:ReactComponents', this.Message.component.prototype).after('render', (component, args, retVal) => {
const { message } = component.props;
const { message, jumpSequenceId, canFlash } = component.props;
const { id, colorString, bot, author, attachments, embeds } = message;
if (jumpSequenceId && canFlash) retVal = retVal.props.children;
retVal.props['data-message-id'] = id;
retVal.props['data-colourstring'] = colorString;
if (author && author.id) retVal.props['data-user-id'] = author.id;
@ -312,63 +339,128 @@ export class ReactAutoPatcher {
if (attachments && attachments.length) retVal.props.className += ' bd-hasAttachments';
if (embeds && embeds.length) retVal.props.className += ' bd-hasEmbeds';
if (author && author.id === DiscordApi.currentUser.id) retVal.props.className += ' bd-isCurrentUser';
const dapiMessage = DiscordApi.Message.from(message);
if (dapiMessage.guild && author.id === dapiMessage.guild.ownerId) retVal.props.className += ' bd-isGuildOwner';
if (dapiMessage.guild && dapiMessage.guild.isMember(author.id)) retVal.props.className += ' bd-isGuildMember';
});
this.Message.forceUpdateAll();
}
static async patchMessageGroup() {
this.MessageGroup = await ReactComponents.getComponent('MessageGroup', { selector: '.message-group' });
const selector = '.' + WebpackModules.getClassName('container', 'message', 'messageCozy');
this.MessageGroup = await ReactComponents.getComponent('MessageGroup', {selector});
this.unpatchMessageGroupRender = MonkeyPatch('BD:ReactComponents', this.MessageGroup.component.prototype).after('render', (component, args, retVal) => {
const { author, type } = component.props.messages[0];
retVal.props['data-author-id'] = author.id;
if (author.id === DiscordApi.currentUser.id) retVal.props.className += ' bd-isCurrentUser';
if (type !== 0) retVal.props.className += ' bd-isSystemMessage';
const dapiMessage = DiscordApi.Message.from(component.props.messages[0]);
if (dapiMessage.guild && author.id === dapiMessage.guild.ownerId) retVal.props.className += ' bd-isGuildOwner';
if (dapiMessage.guild && dapiMessage.guild.isMember(author.id)) retVal.props.className += ' bd-isGuildMember';
});
this.MessageGroup.forceUpdateAll();
}
static async patchChannelMember() {
this.ChannelMember = await ReactComponents.getComponent('ChannelMember', { selector: '.member-2FrNV0' });
const selector = '.' + WebpackModules.getClassName('member', 'memberInner', 'activity');
this.ChannelMember = await ReactComponents.getComponent('ChannelMember', {selector}, m => m.prototype.renderActivity);
this.unpatchChannelMemberRender = MonkeyPatch('BD:ReactComponents', this.ChannelMember.component.prototype).after('render', (component, args, retVal) => {
// Logger.log('ReactComponents', ['Rendering ChannelMember', component, args, retVal]);
if (!retVal.props || !retVal.props.children) return;
const user = Helpers.findProp(component, 'user');
if (!user) return;
retVal.props['data-user-id'] = user.id;
retVal.props['data-colourstring'] = component.props.colorString;
if (component.props.isOwner) retVal.props.className += ' bd-isGuildOwner';
});
this.ChannelMember.forceUpdateAll();
}
static async patchGuild() {
this.Guild = await ReactComponents.getComponent('Guild');
const selector = `div.${WebpackModules.getClassName('guild', 'guildsWrapper')}:not(:first-child)`;
this.Guild = await ReactComponents.getComponent('Guild', {selector}, m => m.prototype.renderBadge);
this.unpatchGuild = MonkeyPatch('BD:ReactComponents', this.Guild.component.prototype).after('render', (component, args, retVal) => {
const { guild } = component.props;
if (!guild) return;
retVal.props['data-guild-id'] = guild.id;
retVal.props['data-guild-name'] = guild.name;
});
this.Guild.forceUpdateAll();
}
static async patchChannel() {
this.Channel = await ReactComponents.getComponent('Channel');
const selector = '.chat';
this.Channel = await ReactComponents.getComponent('Channel', {selector});
this.unpatchChannel = MonkeyPatch('BD:ReactComponents', this.Channel.component.prototype).after('render', (component, args, retVal) => {
const channel = component.props.channel || component.state.channel;
if (!channel) return;
retVal.props['data-channel-id'] = channel.id;
retVal.props['data-channel-name'] = channel.name;
if ([0, 2, 4].includes(channel.type)) retVal.props.className += ' bd-isGuildChannel';
if ([1, 3].includes(channel.type)) retVal.props.className += ' bd-isPrivateChannel';
if (channel.type === 3) retVal.props.className += ' bd-isGroupChannel';
});
this.Channel.forceUpdateAll();
}
static async patchChannelList() {
this.GuildChannel = await ReactComponents.getComponent('GuildChannel', { selector: '.containerDefault-7RImuF' });
const selector = '.' + WebpackModules.getClassName('containerDefault', 'actionIcon');
this.GuildChannel = await ReactComponents.getComponent('GuildChannel', {selector});
this.unpatchGuildChannel = MonkeyPatch('BD:ReactComponents', this.GuildChannel.component.prototype).after('render', (component, args, retVal) => {
const { channel } = component.props;
if (!channel) return;
retVal.props['data-channel-id'] = channel.id;
retVal.props['data-channel-name'] = channel.name;
if ([0, 2, 4].includes(channel.type)) retVal.props.className += ' bd-isGuildChannel';
if ([1, 3].includes(channel.type)) retVal.props.className += ' bd-isPrivateChannel';
if (channel.type === 3) retVal.props.className += ' bd-isGroupChannel';
});
this.GuildChannel.forceUpdateAll();
}
static forceUpdate() {
for (const e of document.querySelectorAll('.message, .message-group, .guild, .containerDefault-7RImuF, .channel-members .member-2FrNV0')) {
Reflection(e).forceUpdate();
}
static async patchUserProfileModal() {
const selector = '.' + WebpackModules.getClassName('root', 'topSectionNormal');
this.UserProfileModal = await ReactComponents.getComponent('UserProfileModal', {selector}, Filters.byPrototypeFields(['renderHeader', 'renderBadges']));
this.unpatchUserProfileModal = MonkeyPatch('BD:ReactComponents', this.UserProfileModal.component.prototype).after('render', (component, args, retVal) => {
const { user } = component.props;
if (!user) return;
retVal.props['data-user-id'] = user.id;
if (user.bot) retVal.props.className += ' bd-isBot';
if (user.id === DiscordApi.currentUser.id) retVal.props.className += ' bd-isCurrentUser';
});
this.UserProfileModal.forceUpdateAll();
}
static async patchUserPopout() {
const selector = '.' + WebpackModules.getClassName('userPopout', 'headerNormal');
this.UserPopout = await ReactComponents.getComponent('UserPopout', {selector});
this.unpatchUserPopout = MonkeyPatch('BD:ReactComponents', this.UserPopout.component.prototype).after('render', (component, args, retVal) => {
const { user, guild, guildMember } = component.props;
if (!user) return;
retVal.props['data-user-id'] = user.id;
if (user.bot) retVal.props.className += ' bd-isBot';
if (user.id === DiscordApi.currentUser.id) retVal.props.className += ' bd-isCurrentUser';
if (guild) retVal.props['data-guild-id'] = guild.id;
if (guild && user.id === guild.ownerId) retVal.props.className += ' bd-isGuildOwner';
if (guild && guildMember) retVal.props.className += ' bd-isGuildMember';
if (guildMember && guildMember.roles.length) retVal.props.className += ' bd-hasRoles';
});
this.UserPopout.forceUpdateAll();
}
}

View File

@ -8,9 +8,10 @@
* LICENSE file in the root directory of this source tree.
*/
import { Toasts } from 'ui';
import { EmoteModule } from 'builtin';
import { SettingsSet, SettingUpdatedEvent } from 'structs';
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
import { SettingsSet } from 'structs';
import { FileUtils, ClientLogger as Logger } from 'common';
import path from 'path';
import Globals from './globals';
import CssEditor from './csseditor';
@ -28,6 +29,7 @@ export default new class Settings {
Logger.log('Settings', [`${set.id}/${category.id}/${setting.id} was changed from`, old_value, 'to', value]);
Events.emit('setting-updated', event);
Events.emit(`setting-updated-${set.id}_${category.id}_${setting.id}`, event);
Toasts.success(`${set.id}/${category.id}/${setting.id} was changed from ${old_value} to ${value}`); // Just for debugging purposes remove in prod
});
set.on('settings-updated', async event => {
@ -61,7 +63,7 @@ export default new class Settings {
CssEditor.setState(scss, css, css_editor_files, scss_error);
CssEditor.editor_bounds = css_editor_bounds || {};
EmoteModule.favourite_emotes = favourite_emotes;
EmoteModule.favourite_emotes = favourite_emotes || [];
} catch (err) {
// There was an error loading settings
// This probably means that the user doesn't have any settings yet
@ -94,7 +96,7 @@ export default new class Settings {
}
} catch (err) {
// There was an error saving settings
Logger.err('Settings', err);
Logger.err('Settings', ['Failed to save internal settings', err]);
throw err;
}
}

View File

@ -10,8 +10,6 @@
import ContentManager from './contentmanager';
import Theme from './theme';
import { FileUtils } from 'common';
import path from 'path';
export default class ThemeManager extends ContentManager {
@ -116,8 +114,7 @@ export default class ThemeManager extends ContentManager {
* @return {Promise}
*/
static async parseSetting(setting) {
const { type, id, value } = setting;
const name = id.replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-');
const name = setting.id.replace(/[^a-zA-Z0-9-]/g, '-').replace(/--/g, '-');
const scss = await setting.toSCSS();
if (scss) return [name, scss];

View File

@ -10,8 +10,8 @@
import Events from './events';
import Globals from './globals';
import { $ } from 'vendor';
import { ClientLogger as Logger } from 'common';
import request from 'request-promise-native';
export default new class {
@ -57,40 +57,32 @@ export default new class {
* Checks for updates.
* @return {Promise}
*/
checkForUpdates() {
return new Promise((resolve, reject) => {
if (this.updatesAvailable) return resolve(true);
Events.emit('update-check-start');
Logger.info('Updater', 'Checking for updates');
async checkForUpdates() {
if (this.updatesAvailable) return true;
Events.emit('update-check-start');
Logger.info('Updater', 'Checking for updates');
$.ajax({
type: 'GET',
url: 'https://rawgit.com/JsSucks/BetterDiscordApp/master/package.json',
cache: false,
success: e => {
try {
this.latestVersion = e.version;
Events.emit('update-check-end');
Logger.info('Updater', `Latest Version: ${e.version} - Current Version: ${Globals.version}`);
if (this.latestVersion !== Globals.version) {
this.updatesAvailable = true;
Events.emit('updates-available');
resolve(true);
}
resolve(false);
} catch (err) {
Events.emit('update-check-fail', err);
reject(err);
}
},
fail: err => {
Events.emit('update-check-fail', err);
reject(err);
}
try {
const response = await request({
uri: 'https://rawgit.com/JsSucks/BetterDiscordApp/master/package.json',
json: true
});
});
this.latestVersion = response.version;
Events.emit('update-check-end');
Logger.info('Updater', `Latest Version: ${response.version} - Current Version: ${Globals.version}`);
if (this.latestVersion !== Globals.version) {
this.updatesAvailable = true;
Events.emit('updates-available');
return true;
}
return false;
} catch (err) {
Events.emit('update-check-fail', err);
throw err;
}
}
}

View File

@ -8,12 +8,18 @@
* LICENSE file in the root directory of this source tree.
*/
import { WebpackModules } from './webpackmodules';
import jQuery from 'jquery';
import lodash from 'lodash';
import Vue from 'vue';
export { jQuery as $ };
import request from 'request-promise-native';
import Combokeys from 'combokeys';
import filetype from 'file-type';
import filewatcher from 'filewatcher';
import VTooltip from 'v-tooltip';
export { jQuery as $, request };
export default class {
@ -29,18 +35,16 @@ export default class {
static get lodash() { return lodash }
static get _() { return this.lodash }
/**
* Moment
*/
static get moment() {
return WebpackModules.getModuleByName('Moment');
}
/**
* Vue
*/
static get Vue() {
return Vue;
}
static get Vue() { return Vue }
static get request() { return request }
static get Combokeys() { return Combokeys }
static get filetype() { return filetype }
static get filewatcher() { return filewatcher }
static get VTooltip() { return VTooltip }
}

View File

@ -8,44 +8,8 @@
* LICENSE file in the root directory of this source tree.
*/
export class Filters {
static byProperties(props, selector = m => m) {
return module => {
const component = selector(module);
if (!component) return false;
return props.every(property => component[property] !== undefined);
};
}
static byPrototypeFields(fields, selector = m => m) {
return module => {
const component = selector(module);
if (!component) return false;
if (!component.prototype) return false;
return fields.every(field => component.prototype[field] !== undefined);
};
}
static byCode(search, selector = m => m) {
return module => {
const method = selector(module);
if (!method) return false;
return method.toString().search(search) !== -1;
};
}
static byDisplayName(name) {
return module => {
return module && module.displayName === name;
};
}
static combine(...filters) {
return module => {
return filters.every(filter => filter(module));
};
}
}
import { Utils, Filters } from 'common';
import Events from './events';
const KnownModules = {
React: Filters.byProperties(['createElement', 'cloneElement']),
@ -90,6 +54,8 @@ const KnownModules = {
UserTypingStore: Filters.byProperties(['isTyping']),
UserActivityStore: Filters.byProperties(['getActivity']),
UserNameResolver: Filters.byProperties(['getName']),
UserNoteStore: Filters.byProperties(['getNote']),
UserNoteActions: Filters.byProperties(['updateNote']),
/* Emoji Store and Utils */
EmojiInfo: Filters.byProperties(['isEmojiDisabled']),
@ -136,7 +102,7 @@ const KnownModules = {
DNDSources: Filters.byProperties(["addTarget"]),
DNDObjects: Filters.byProperties(["DragSource"]),
/* Electron & Other Internals with Utils*/
/* Electron & Other Internals with Utils */
ElectronModule: Filters.byProperties(["_getMainWindow"]),
Dispatcher: Filters.byProperties(['dirtyDispatch']),
PathUtils: Filters.byProperties(["hasBasename"]),
@ -162,7 +128,6 @@ const KnownModules = {
WindowInfo: Filters.byProperties(['isFocused', 'windowSize']),
TagInfo: Filters.byProperties(['VALID_TAG_NAMES']),
DOMInfo: Filters.byProperties(['canUseDOM']),
HTMLUtils: Filters.byProperties(['htmlFor', 'sanitizeUrl']),
/* Locale/Location and Time */
LocaleManager: Filters.byProperties(['setLocale']),
@ -179,15 +144,27 @@ const KnownModules = {
URLParser: Filters.byProperties(['Url', 'parse']),
ExtraURLs: Filters.byProperties(['getArticleURL']),
/* Text Processing */
hljs: Filters.byProperties(['highlight', 'highlightBlock']),
SimpleMarkdown: Filters.byProperties(['parseBlock', 'parseInline', 'defaultOutput']),
/* DOM/React Components */
/* ==================== */
UserSettingsWindow: Filters.byProperties(['open', 'updateAccount']),
LayerManager: Filters.byProperties(['popLayer', 'pushLayer']),
UserSettingsWindow: Filters.byProperties(['open', 'updateAccount']),
ChannelSettingsWindow: Filters.byProperties(['open', 'updateChannel']),
GuildSettingsWindow: Filters.byProperties(['open', 'updateGuild']),
/* Modals */
ModalStack: Filters.byProperties(['push', 'update', 'pop', 'popWithKey']),
UserProfileModals: Filters.byProperties(['fetchMutualFriends', 'setSection']),
ConfirmModal: Filters.byPrototypeFields(['handleCancel', 'handleSubmit', 'handleMinorConfirm']),
UserProfileModal: Filters.byProperties(['fetchMutualFriends', 'setSection']),
ChangeNicknameModal: Filters.byProperties(['open', 'changeNickname']),
CreateChannelModal: Filters.byProperties(['open', 'createChannel']),
PruneMembersModal: Filters.byProperties(['open', 'prune']),
NotificationSettingsModal: Filters.byProperties(['open', 'updateNotificationSettings']),
PrivacySettingsModal: Filters.byCode(/PRIVACY_SETTINGS_MODAL_OPEN/, m => m.open),
CreateInviteModal: Filters.byProperties(['open', 'createInvite']),
/* Popouts */
PopoutStack: Filters.byProperties(['open', 'close', 'closeAll']),
@ -203,16 +180,17 @@ const KnownModules = {
ExternalLink: Filters.byCode(/\.trusted\b/)
};
export class WebpackModules {
class WebpackModules {
/**
* Finds a module using a filter function.
* @param {Function} filter A function to use to filter modules
* @param {Boolean} first Whether to return only the first matching module
* @param {Array} modules An array of modules to search in
* @return {Any}
*/
static getModule(filter, first = true) {
const modules = this.getAllModules();
static getModule(filter, first = true, _modules) {
const modules = _modules || this.getAllModules();
const rm = [];
for (let index in modules) {
if (!modules.hasOwnProperty(index)) continue;
@ -227,7 +205,7 @@ export class WebpackModules {
if (first) return foundModule;
rm.push(foundModule);
}
return first || rm.length == 0 ? undefined : rm;
return first ? undefined : rm;
}
/**
@ -288,15 +266,139 @@ export class WebpackModules {
*/
static get require() {
if (this._require) return this._require;
const id = 'bd-webpackmodules';
const __webpack_require__ = window['webpackJsonp']([], {
[id]: (module, exports, __webpack_require__) => exports.default = __webpack_require__
}, [id]).default;
delete __webpack_require__.m[id];
delete __webpack_require__.c[id];
const __webpack_require__ = this.getWebpackRequire();
if (!__webpack_require__) return;
this.hookWebpackRequireCache(__webpack_require__);
return this._require = __webpack_require__;
}
static getWebpackRequire() {
const id = 'bd-webpackmodules';
if (typeof window.webpackJsonp === 'function') {
const __webpack_require__ = window['webpackJsonp']([], {
[id]: (module, exports, __webpack_require__) => exports.default = __webpack_require__
}, [id]).default;
delete __webpack_require__.m[id];
delete __webpack_require__.c[id];
return __webpack_require__;
} else if (window.webpackJsonp && window.webpackJsonp.push) {
const __webpack_require__ = window['webpackJsonp'].push([[], {
[id]: (module, exports, req) => exports.default = req
}, [[id]]]).default;
window['webpackJsonp'].pop();
delete __webpack_require__.m[id];
delete __webpack_require__.c[id];
return __webpack_require__;
}
}
static hookWebpackRequireCache(__webpack_require__) {
__webpack_require__.c = new Proxy(__webpack_require__.c, {
set(module_cache, module_id, module) {
// Add it to our emitter cache and emit a module-loading event
this.moduleLoading(module_id, module);
Events.emit('module-loading', module);
// Add the module to the cache as normal
module_cache[module_id] = module;
}
});
}
static moduleLoading(module_id, module) {
if (this.require.c[module_id]) return;
if (!this.moduleLoadedEventTimeout) {
this.moduleLoadedEventTimeout = setTimeout(() => {
this.moduleLoadedEventTimeout = undefined;
// Emit a module-loaded event for every module
for (let module of this.modulesLoadingCache) {
Events.emit('module-loaded', module);
}
// Emit a modules-loaded event
Events.emit('modules-loaded', this.modulesLoadingCache);
this.modulesLoadedCache = [];
}, 0);
}
// Add this to our own cache
if (!this.modulesLoadingCache) this.modulesLoadingCache = [];
this.modulesLoadingCache.push(module);
}
static waitForWebpackRequire() {
return Utils.until(() => this.require, 10);
}
/**
* Waits for a module to load.
* This only returns a single module, as it can't guarentee there are no more modules that could
* match the filter, which is pretty much what that would be asking for.
* @param {Function} filter The name of a known module or a filter function
* @return {Any}
*/
static async waitForModule(filter) {
const module = this.getModule(filter);
if (module) return module;
while (this.require.m.length > this.require.c.length) {
const additionalModules = await Events.once('modules-loaded');
const module = this.getModule(filter, true, additionalModules);
if (module) return module;
}
throw new Error('All modules have now been loaded. None match the passed filter.');
}
/**
* Finds a module by it's name.
* @param {String} name The name of the module
* @param {Function} fallback A function to use to filter modules if not finding a known module
* @return {Any}
*/
static async waitForModuleByName(name, fallback) {
if (Cache.hasOwnProperty(name)) return Cache[name];
if (KnownModules.hasOwnProperty(name)) fallback = KnownModules[name];
if (!fallback) return undefined;
const module = await this.waitForModule(fallback, true);
return module ? Cache[name] = module : undefined;
}
static waitForModuleByDisplayName(props) {
return this.waitForModule(Filters.byDisplayName(props));
}
static waitForModuleByRegex(props) {
return this.waitForModule(Filters.byCode(props));
}
static waitForModuleByProps(props) {
return this.waitForModule(Filters.byProperties(props));
}
static waitForModuleByPrototypes(props) {
return this.waitForModule(Filters.byPrototypeFields(props));
}
/**
* Searches for a class module and returns a class from it.
* @param {String} base The first part of the class to find
* @param {String} ...additional_classes Additional classes to look for to filter duplicate class modules
* @return {String}
*/
static getClassName(base, ...additional_classes) {
const class_module = this.getModuleByProps([base, ...additional_classes]);
if (class_module && class_module[base]) return class_module[base].split(' ')[0];
}
static async waitForClassName(base, ...additional_classes) {
const class_module = await this.waitForModuleByProps([base, ...additional_classes]);
if (class_module && class_module[base]) return class_module[base].split(' ')[0];
}
/**
* Returns all loaded modules.
* @return {Array}
@ -313,4 +415,14 @@ export class WebpackModules {
return Object.keys(KnownModules);
}
static get KnownModules() { return KnownModules }
}
const WebpackModulesProxy = new Proxy(WebpackModules, {
get(WebpackModules, property) {
return WebpackModules[property] || WebpackModules.getModuleByName(property);
}
});
export { WebpackModulesProxy as WebpackModules };

View File

@ -0,0 +1,436 @@
/**
* BetterDiscord Channel Struct
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { DiscordApi, DiscordApiModules as Modules } from 'modules';
import { List, InsufficientPermissions } from 'structs';
import { Guild } from './guild';
import { Message } from './message';
import { User, GuildMember } from './user';
const channels = new WeakMap();
export class Channel {
constructor(data) {
if (channels.has(data)) return channels.get(data);
channels.set(data, this);
this.discordObject = data;
}
static from(channel) {
switch (channel.type) {
default: return new Channel(channel);
case 0: return new GuildTextChannel(channel);
case 1: return new DirectMessageChannel(channel);
case 2: return new GuildVoiceChannel(channel);
case 3: return new GroupChannel(channel);
case 4: return new ChannelCategory(channel);
}
}
static fromId(id) {
const channel = Modules.ChannelStore.getChannel(id);
if (channel) return Channel.from(channel);
}
static get GuildChannel() { return GuildChannel }
static get GuildTextChannel() { return GuildTextChannel }
static get GuildVoiceChannel() { return GuildVoiceChannel }
static get ChannelCategory() { return ChannelCategory }
static get PrivateChannel() { return PrivateChannel }
static get DirectMessageChannel() { return DirectMessageChannel }
static get GroupChannel() { return GroupChannel }
get id() { return this.discordObject.id }
get applicationId() { return this.discordObject.application_id }
get type() { return this.discordObject.type }
get name() { return this.discordObject.name }
/**
* Send a message in this channel.
* @param {String} content The new message's content
* @param {Boolean} parse Whether to parse the message or send it as it is
* @return {Promise => Message}
*/
async sendMessage(content, parse = false) {
if (this.assertPermissions) this.assertPermissions('SEND_MESSAGES', Modules.DiscordPermissions.VIEW_CHANNEL | Modules.DiscordPermissions.SEND_MESSAGES);
this.select();
if (parse) content = Modules.MessageParser.parse(this.discordObject, content);
else content = {content};
const response = await Modules.MessageActions._sendMessage(this.id, content);
return Message.from(Modules.MessageStore.getMessage(this.id, response.body.id));
}
/**
* Send a bot message in this channel that only the current user can see.
* @param {String} content The new message's content
* @return {Message}
*/
sendBotMessage(content) {
this.select();
const message = Modules.MessageParser.createBotMessage(this.id, content);
Modules.MessageActions.receiveMessage(this.id, message);
return Message.from(Modules.MessageStore.getMessage(this.id, message.id));
}
/**
* A list of messages in this channel.
*/
get messages() {
const messages = Modules.MessageStore.getMessages(this.id).toArray();
return List.from(messages, m => Message.from(m));
}
/**
* Jumps to the latest message in this channel.
*/
jumpToPresent() {
if (this.assertPermissions) this.assertPermissions('VIEW_CHANNEL', Modules.DiscordPermissions.VIEW_CHANNEL);
if (this.hasMoreAfter) Modules.MessageActions.jumpToPresent(this.id, Modules.DiscordConstants.MAX_MESSAGES_PER_CHANNEL);
else this.messages[this.messages.length - 1].jumpTo(false);
}
get hasMoreAfter() {
return Modules.MessageStore.getMessages(this.id).hasMoreAfter;
}
/**
* Sends an invite in this channel.
* @param {String} code The invite code
* @return {Promise => Messaage}
*/
async sendInvite(code) {
if (this.assertPermissions) this.assertPermissions('SEND_MESSAGES', Modules.DiscordPermissions.VIEW_CHANNEL | Modules.DiscordPermissions.SEND_MESSAGES);
const response = Modules.MessageActions.sendInvite(this.id, code);
return Message.from(Modules.MessageStore.getMessage(this.id, response.body.id));
}
/**
* Opens this channel in the UI.
*/
select() {
if (this.assertPermissions) this.assertPermissions('VIEW_CHANNEL', Modules.DiscordPermissions.VIEW_CHANNEL);
Modules.NavigationUtils.transitionToGuild(this.guildId ? this.guildId : Modules.DiscordConstants.ME, this.id);
}
/**
* Whether this channel is currently selected.
*/
get isSelected() {
return DiscordApi.currentChannel === this;
}
/**
* Updates this channel.
* @return {Promise}
*/
async updateChannel(body) {
if (this.assertPermissions) this.assertPermissions('MANAGE_CHANNELS', Modules.DiscordPermissions.MANAGE_CHANNELS);
const response = await Modules.APIModule.patch({
url: `${Modules.DiscordConstants.Endpoints.CHANNELS}/${this.id}`,
body
});
this.discordObject = Modules.ChannelStore.getChannel(this.id);
channels.set(this.discordObject, this);
}
}
export class PermissionOverwrite {
constructor(data, channel_id) {
this.discordObject = data;
this.channelId = channel_id;
}
static from(data, channel_id) {
switch (data.type) {
default: return new PermissionOverwrite(data, channel_id);
case 'role': return new RolePermissionOverwrite(data, channel_id);
case 'member': return new MemberPermissionOverwrite(data, channel_id);
}
}
static get RolePermissionOverwrite() { return RolePermissionOverwrite }
static get MemberPermissionOverwrite() { return MemberPermissionOverwrite }
get type() { return this.discordObject.type }
get allow() { return this.discordObject.allow }
get deny() { return this.discordObject.deny }
get channel() {
return Channel.fromId(this.channelId);
}
get guild() {
if (this.channel) return this.channel.guild;
}
}
export class RolePermissionOverwrite extends PermissionOverwrite {
get roleId() { return this.discordObject.id }
get role() {
if (this.guild) return this.guild.roles.find(r => r.id === this.roleId);
}
}
export class MemberPermissionOverwrite extends PermissionOverwrite {
get memberId() { return this.discordObject.id }
get member() {
return GuildMember.fromId(this.memberId);
}
}
export class GuildChannel extends Channel {
static get PermissionOverwrite() { return PermissionOverwrite }
get guildId() { return this.discordObject.guild_id }
get parentId() { return this.discordObject.parent_id } // Channel category
get position() { return this.discordObject.position }
get nicks() { return this.discordObject.nicks }
checkPermissions(perms) {
return Modules.PermissionUtils.can(perms, DiscordApi.currentUser, this.discordObject);
}
assertPermissions(name, perms) {
if (!this.checkPermissions(perms)) throw new InsufficientPermissions(name);
}
get category() {
return Channel.fromId(this.parentId);
}
/**
* The current user's permissions on this channel.
*/
get permissions() {
return Modules.GuildPermissions.getChannelPermissions(this.id);
}
get permissionOverwrites() {
return List.from(Object.entries(this.discordObject.permissionOverwrites), ([i, p]) => PermissionOverwrite.from(p, this.id));
}
get guild() {
return Guild.fromId(this.guildId);
}
/**
* Whether this channel is the guild's default channel.
*/
get isDefaultChannel() {
return Modules.GuildChannelsStore.getDefaultChannel(this.guildId).id === this.id;
}
/**
* Opens this channel's settings window.
* @param {String} section The section to open (see DiscordConstants.ChannelSettingsSections)
*/
openSettings(section = 'OVERVIEW') {
Modules.ChannelSettingsWindow.setSection(section);
Modules.ChannelSettingsWindow.open(this.id);
}
/**
* Updates this channel's name.
* @param {String} name The channel's new name
* @return {Promise}
*/
updateName(name) {
return this.updateChannel({ name });
}
/**
* Changes the channel's position.
* @param {Number} position The channel's new position
* @return {Promise}
*/
changeSortLocation(position = 0) {
if (position instanceof GuildChannel) position = position.position;
return this.updateChannel({ position });
}
/**
* Updates this channel's permission overwrites.
* @param {Array} permissionOverwrites An array of permission overwrites
* @return {Promise}
*/
updatePermissionOverwrites(permission_overwrites) {
return this.updateChannel({ permission_overwrites });
}
/**
* Updates this channel's category.
* @param {ChannelCategory} category The new channel category
* @return {Promise}
*/
updateCategory(category) {
return this.updateChannel({ parent_id: category.id || category });
}
}
// Type 0 - GUILD_TEXT
export class GuildTextChannel extends GuildChannel {
get type() { return 'GUILD_TEXT' }
get topic() { return this.discordObject.topic }
get nsfw() { return this.discordObject.nsfw }
/**
* Updates this channel's topic.
* @param {String} topc The new channel topic
* @return {Promise}
*/
updateTopic(topic) {
return this.updateChannel({ topic });
}
/**
* Updates this channel's not-safe-for-work flag.
* @param {Boolean} nsfw Whether the channel should be marked as NSFW
* @return {Promise}
*/
setNsfw(nsfw = true) {
return this.updateChannel({ nsfw });
}
setNotNsfw() {
return this.setNsfw(false);
}
}
// Type 2 - GUILD_VOICE
export class GuildVoiceChannel extends GuildChannel {
get type() { return 'GUILD_VOICE' }
get userLimit() { return this.discordObject.userLimit }
get bitrate() { return this.discordObject.bitrate }
sendMessage() { throw new Error('Cannot send messages in a voice channel.'); }
get messages() { return new List(); }
jumpToPresent() { throw new Error('Cannot select a voice channel.'); }
get hasMoreAfter() { return false; }
sendInvite() { throw new Error('Cannot invite someone to a voice channel.'); }
select() { throw new Error('Cannot select a voice channel.'); }
/**
* Updates this channel's bitrate.
* @param {Number} bitrate The new bitrate
* @return {Promise}
*/
updateBitrate(bitrate) {
return this.updateChannel({ bitrate });
}
/**
* Updates this channel's user limit.
* @param {Number} userLimit The new user limit
* @return {Promise}
*/
updateUserLimit(user_limit) {
return this.updateChannel({ user_limit });
}
}
// Type 4 - GUILD_CATEGORY
export class ChannelCategory extends GuildChannel {
get type() { return 'GUILD_CATEGORY' }
get parentId() { return undefined }
get category() { return undefined }
sendMessage() { throw new Error('Cannot send messages in a channel category.'); }
get messages() { return new List(); }
jumpToPresent() { throw new Error('Cannot select a channel category.'); }
get hasMoreAfter() { return false; }
sendInvite() { throw new Error('Cannot invite someone to a channel category.'); }
select() { throw new Error('Cannot select a channel category.'); }
updateCategory() { throw new Error('Cannot set a channel category on another channel category.'); }
/**
* A list of channels in this category.
*/
get channels() {
return List.from(this.guild.channels, c => c.parentId === this.id);
}
/**
* Opens the create channel modal for this guild.
* @param {Number} type The type of channel to create - either 0 (text), 2 (voice) or 4 (category)
* @param {GuildChannel} clone A channel to clone permissions of
*/
openCreateChannelModal(type, category, clone) {
this.guild.openCreateChannelModal(type, this.id, this, clone);
}
/**
* Creates a channel in this category.
* @param {Number} type The type of channel to create - either 0 (text) or 2 (voice)
* @param {String} name A name for the new channel
* @param {Array} permission_overwrites An array of PermissionOverwrite-like objects - leave to use the permissions of the category
* @return {Promise => GuildChannel}
*/
createChannel(type, name, permission_overwrites) {
return this.guild.createChannel(type, name, this, permission_overwrites);
}
}
export class PrivateChannel extends Channel {
get userLimit() { return this.discordObject.userLimit }
get bitrate() { return this.discordObject.bitrate }
}
// Type 1 - DM
export class DirectMessageChannel extends PrivateChannel {
get type() { return 'DM' }
get recipientId() { return this.discordObject.recipients[0] }
/**
* The other user of this direct message channel.
*/
get recipient() {
return User.fromId(this.recipientId);
}
}
// Type 3 - GROUP_DM
export class GroupChannel extends PrivateChannel {
get ownerId() { return this.discordObject.ownerId }
get type() { return 'GROUP_DM' }
get name() { return this.discordObject.name }
get icon() { return this.discordObject.icon }
/**
* A list of the other members of this group direct message channel.
*/
get members() {
return List.from(this.discordObject.recipients, id => User.fromId(id));
}
/**
* The owner of this group direct message channel. This is usually the person who created it.
*/
get owner() {
return User.fromId(this.ownerId);
}
/**
* Updates this channel's name.
* @param {String} name The channel's new name
* @return {Promise}
*/
updateName(name) {
return this.updateChannel({ name });
}
}

View File

@ -0,0 +1,483 @@
/**
* BetterDiscord Guild Struct
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { DiscordApi, DiscordApiModules as Modules } from 'modules';
import { List, InsufficientPermissions } from 'structs';
import { FileUtils } from 'common';
import { Channel } from './channel';
import { GuildMember } from './user';
const roles = new WeakMap();
export class Role {
constructor(data, guild_id) {
if (roles.has(data)) return roles.get(data);
roles.set(data, this);
this.discordObject = data;
this.guildId = guild_id;
}
get id() { return this.discordObject.id }
get name() { return this.discordObject.name }
get position() { return this.discordObject.position }
get originalPosition() { return this.discordObject.originalPosition }
get permissions() { return this.discordObject.permissions }
get managed() { return this.discordObject.managed }
get mentionable() { return this.discordObject.mentionable }
get hoist() { return this.discordObject.hoist }
get colour() { return this.discordObject.color }
get colourString() { return this.discordObject.colorString }
get guild() {
return Guild.fromId(this.guildId);
}
get members() {
return this.guild.members.filter(m => m.roles.includes(this));
}
}
const emojis = new WeakMap();
export class Emoji {
constructor(data) {
if (emojis.has(data)) return emojis.get(data);
emojis.set(data, this);
this.discordObject = data;
}
get id() { return this.discordObject.id }
get guildId() { return this.discordObject.guild_id }
get name() { return this.discordObject.name }
get managed() { return this.discordObject.managed }
get animated() { return this.discordObject.animated }
get allNamesString() { return this.discordObject.allNamesString }
get requireColons() { return this.discordObject.require_colons }
get url() { return this.discordObject.url }
get roles() { return this.discordObject.roles }
get guild() {
return Guild.fromId(this.guildId);
}
}
const guilds = new WeakMap();
export class Guild {
constructor(data) {
if (guilds.has(data)) return guilds.get(data);
guilds.set(data, this);
this.discordObject = data;
}
static from(data) {
return new Guild(data);
}
static fromId(id) {
const guild = Modules.GuildStore.getGuild(id);
if (guild) return Guild.from(guild);
}
static get Role() { return Role }
static get Emoji() { return Emoji }
get id() { return this.discordObject.id }
get ownerId() { return this.discordObject.ownerId }
get applicationId() { return this.discordObject.application_id }
get systemChannelId() { return this.discordObject.systemChannelId }
get name() { return this.discordObject.name }
get acronym() { return this.discordObject.acronym }
get icon() { return this.discordObject.icon }
get joinedAt() { return this.discordObject.joinedAt }
get verificationLevel() { return this.discordObject.verificationLevel }
get mfaLevel() { return this.discordObject.mfaLevel }
get large() { return this.discordObject.large }
get lazy() { return this.discordObject.lazy }
get voiceRegion() { return this.discordObject.region }
get afkChannelId() { return this.discordObject.afkChannelId }
get afkTimeout() { return this.discordObject.afkTimeout }
get explicitContentFilter() { return this.discordObject.explicitContentFilter }
get defaultMessageNotifications() { return this.discordObject.defaultMessageNotifications }
get splash() { return this.discordObject.splash }
get features() { return this.discordObject.features }
get owner() {
return this.members.find(m => m.userId === this.ownerId);
}
get roles() {
return List.from(Object.entries(this.discordObject.roles), ([i, r]) => new Role(r, this.id))
.sort((r1, r2) => r1.position === r2.position ? 0 : r1.position > r2.position ? 1 : -1);
}
get channels() {
const channels = Modules.GuildChannelsStore.getChannels(this.id);
const returnChannels = new List();
for (const category in channels) {
if (channels.hasOwnProperty(category)) {
if (!Array.isArray(channels[category])) continue;
const channelList = channels[category];
for (const channel of channelList) {
// For some reason Discord adds a new category with the ID "null" and name "Uncategorized"
if (channel.channel.id === 'null') continue;
returnChannels.push(Channel.from(channel.channel));
}
}
}
return returnChannels;
}
/**
* Channels that don't have a parent. (Channel categories and any text/voice channel not in one.)
*/
get mainChannels() {
return this.channels.filter(c => !c.parentId);
}
/**
* The guild's default channel. (Usually the first in the list.)
*/
get defaultChannel() {
return Channel.from(Modules.GuildChannelsStore.getDefaultChannel(this.id));
}
/**
* The guild's AFK channel.
*/
get afkChannel() {
if (this.afkChannelId) return Channel.fromId(this.afkChannelId);
}
/**
* The channel system messages are sent to.
*/
get systemChannel() {
if (this.systemChannelId) return Channel.fromId(this.systemChannelId);
}
/**
* A list of GuildMember objects.
*/
get members() {
const members = Modules.GuildMemberStore.getMembers(this.id);
return List.from(members, m => new GuildMember(m, this.id));
}
/**
* The current user as a GuildMember of this guild.
*/
get currentUser() {
return this.members.find(m => m.user === DiscordApi.currentUser);
}
/**
* The total number of members in the guild.
*/
get memberCount() {
return Modules.MemberCountStore.getMemberCount(this.id);
}
/**
* An array of the guild's custom emojis.
*/
get emojis() {
return List.from(Modules.EmojiUtils.getGuildEmoji(this.id), e => new Emoji(e, this.id));
}
checkPermissions(perms) {
return Modules.PermissionUtils.can(perms, DiscordApi.currentUser, this.discordObject);
}
assertPermissions(name, perms) {
if (!this.checkPermissions(perms)) throw new InsufficientPermissions(name);
}
/**
* The current user's permissions on this guild.
*/
get permissions() {
return Modules.GuildPermissions.getGuildPermissions(this.id);
}
/**
* Returns the GuildMember object for a user.
* @param {User|GuildMember|Number} user A User or GuildMember object or a user ID
* @return {GuildMember}
*/
getMember(user) {
const member = Modules.GuildMemberStore.getMember(this.id, user.userId || user.id || user);
if (member) return new GuildMember(member, this.id);
}
/**
* Checks if a user is a member of this guild.
* @param {User|GuildMember|Number} user A User or GuildMember object or a user ID
* @return {Boolean}
*/
isMember(user) {
return Modules.GuildMemberStore.isMember(this.id, user.userId || user.id || user);
}
/**
* Whether the user has not restricted direct messages from members of this guild.
*/
get allowPrivateMessages() {
return !DiscordApi.UserSettings.restrictedGuildIds.includes(this.id);
}
/**
* Marks all messages in the guild as read.
*/
markAsRead() {
Modules.GuildActions.markGuildAsRead(this.id);
}
/**
* Selects the guild in the UI.
*/
select() {
Modules.GuildActions.selectGuild(this.id);
}
/**
* Whether this guild is currently selected.
*/
get isSelected() {
return DiscordApi.currentGuild === this;
}
/**
* Opens this guild's settings window.
* @param {String} section The section to open (see DiscordConstants.GuildSettingsSections)
*/
openSettings(section = 'OVERVIEW') {
Modules.GuildSettingsWindow.setSection(section);
Modules.GuildSettingsWindow.open(this.id);
}
/**
* Kicks members who don't have any roles and haven't been seen in the number of days passed.
* @param {Number} days
*/
pruneMembers(days) {
this.assertPermissions('KICK_MEMBERS', Modules.DiscordPermissions.KICK_MEMBERS);
Modules.PruneMembersModal.prune(this.id, days);
}
openPruneMumbersModal() {
this.assertPermissions('KICK_MEMBERS', Modules.DiscordPermissions.KICK_MEMBERS);
Modules.PruneMembersModal.open(this.id);
}
/**
* Opens the create channel modal for this guild.
* @param {Number} type The type of channel to create - either 0 (text), 2 (voice) or 4 (category)
* @param {ChannelCategory} category The category to create the channel in
* @param {GuildChannel} clone A channel to clone permissions, topic, bitrate and user limit of
*/
openCreateChannelModal(type, category, clone) {
this.assertPermissions('MANAGE_CHANNELS', Modules.DiscordPermissions.MANAGE_CHANNELS);
Modules.CreateChannelModal.open(type, this.id, category ? category.id : undefined, clone ? clone.id : undefined);
}
/**
* Creates a channel in this guild.
* @param {Number} type The type of channel to create - either 0 (text), 2 (voice) or 4 (category)
* @param {String} name A name for the new channel
* @param {ChannelCategory} category The category to create the channel in
* @param {Array} permission_overwrites An array of PermissionOverwrite-like objects - leave to use the permissions of the category
* @return {Promise => GuildChannel}
*/
async createChannel(type, name, category, permission_overwrites) {
this.assertPermissions('MANAGE_CHANNELS', Modules.DiscordPermissions.MANAGE_CHANNELS);
const response = await Modules.APIModule.post({
url: Modules.DiscordConstants.Endpoints.GUILD_CHANNELS(this.id),
body: {
type, name,
parent_id: category ? category.id : undefined,
permission_overwrites: permission_overwrites ? permission_overwrites.map(p => ({
type: p.type,
id: (p.type === 'user' ? p.userId : p.roleId) || p.id,
allow: p.allow,
deny: p.deny
})) : undefined
}
});
return Channel.fromId(response.body.id);
}
openNotificationSettingsModal() {
Modules.NotificationSettingsModal.open(this.id);
}
openPrivacySettingsModal() {
Modules.PrivacySettingsModal.open(this.id);
}
nsfwAgree() {
Modules.GuildActions.nsfwAgree(this.id);
}
nsfwDisagree() {
Modules.GuildActions.nsfwDisagree(this.id);
}
/**
* Changes the guild's position in the list.
* @param {Number} index The new position
*/
changeSortLocation(index) {
Modules.GuildActions.move(DiscordApi.guildPositions.indexOf(this.id), index);
}
/**
* Updates this guild.
* @return {Promise}
*/
async updateGuild(body) {
this.assertPermissions('MANAGE_GUILD', Modules.DiscordPermissions.MANAGE_GUILD);
const response = await Modules.APIModule.patch({
url: Modules.DiscordConstants.Endpoints.GUILD(this.id),
body
});
this.discordObject = Modules.GuildStore.getGuild(this.id);
guilds.set(this.discordObject, this);
}
/**
* Updates this guild's name.
* @param {String} name The new name
* @return {Promise}
*/
updateName(name) {
return this.updateGuild({ name });
}
/**
* Updates this guild's voice region.
* @param {String} region The ID of the new voice region (obtainable via the API - see https://discordapp.com/developers/docs/resources/voice#list-voice-regions)
* @return {Promise}
*/
updateVoiceRegion(region) {
return this.updateGuild({ region });
}
/**
* Updates this guild's verification level.
* @param {Number} verificationLevel The new verification level (see https://discordapp.com/developers/docs/resources/guild#guild-object-verification-level)
* @return {Promise}
*/
updateVerificationLevel(verification_level) {
return this.updateGuild({ verification_level });
}
/**
* Updates this guild's default message notification level.
* @param {Number} defaultMessageNotifications The new default notification level (0: all messages, 1: only mentions)
* @return {Promise}
*/
updateDefaultMessageNotifications(default_message_notifications) {
return this.updateGuild({ default_message_notifications });
}
/**
* Updates this guild's explicit content filter level.
* @param {Number} explicitContentFilter The new explicit content filter level (0: disabled, 1: members without roles, 2: everyone)
* @return {Promise}
*/
updateExplicitContentFilter(explicit_content_filter) {
return this.updateGuild({ explicit_content_filter });
}
/**
* Updates this guild's AFK channel.
* @param {GuildVoiceChannel} afkChannel The new AFK channel
* @return {Promise}
*/
updateAfkChannel(afk_channel) {
return this.updateGuild({ afk_channel_id: afk_channel.id || afk_channel });
}
/**
* Updates this guild's AFK timeout.
* @param {Number} afkTimeout The new AFK timeout
* @return {Promise}
*/
updateAfkTimeout(afk_timeout) {
return this.updateGuild({ afk_timeout });
}
/**
* Updates this guild's icon.
* @param {Buffer|String} icon A buffer/base 64 encoded 128x128 JPEG image
* @return {Promise}
*/
updateIcon(icon) {
return this.updateGuild({ icon: typeof icon === 'string' ? icon : icon.toString('base64') });
}
/**
* Updates this guild's icon using a local file.
* TODO
* @param {String} icon_path The path to the new icon
* @return {Promise}
*/
async updateIconFromFile(icon_path) {
const buffer = await FileUtils.readFileBuffer(icon_path);
return this.updateIcon(buffer);
}
/**
* Updates this guild's owner. (Should plugins really ever need to do this?)
* @param {User|GuildMember} owner The user/guild member to transfer ownership to
* @return {Promise}
*/
updateOwner(owner) {
return this.updateGuild({ owner_id: owner.user ? owner.user.id : owner.id || owner });
}
/**
* Updates this guild's splash image.
* (I don't know what this is actually used for. The API documentation says it's VIP-only.)
* @param {Buffer|String} icon A buffer/base 64 encoded 128x128 JPEG image
* @return {Promise}
*/
updateSplash(splash) {
return this.updateGuild({ splash: typeof splash === 'string' ? splash : splash.toString('base64') });
}
/**
* Updates this guild's splash image using a local file.
* TODO
* @param {String} splash_path The path to the new splash
* @return {Promise}
*/
async updateSplashFromFile(splash_path) {
const buffer = await FileUtils.readFileBuffer(splash_path);
return this.updateSplash(buffer);
}
/**
* Updates this guild's system channel.
* @param {GuildTextChannel} systemChannel The new system channel
* @return {Promise}
*/
updateSystemChannel(system_channel) {
return this.updateGuild({ system_channel_id: system_channel.id || system_channel });
}
}

View File

@ -0,0 +1,306 @@
/**
* BetterDiscord Message Struct
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { DiscordApi, DiscordApiModules as Modules } from 'modules';
import { List, InsufficientPermissions } from 'structs';
import { Channel } from './channel';
import { User } from './user';
const reactions = new WeakMap();
export class Reaction {
constructor(data, message_id, channel_id) {
if (reactions.has(data)) return reactions.get(data);
reactions.set(data, this);
this.discordObject = data;
this.messageId = message_id;
this.channelId = channel_id;
}
get emoji() {
const id = this.discordObject.emoji.id;
if (!id || !this.guild) return this.discordObject.emoji;
return this.guild.emojis.find(e => e.id === id);
}
get count() { return this.discordObject.count }
get me() { return this.discordObject.me }
get channel() {
return Channel.fromId(this.channel_id);
}
get message() {
if (this.channel) return this.channel.messages.find(m => m.id === this.messageId);
}
get guild() {
if (this.channel) return this.channel.guild;
}
}
const embeds = new WeakMap();
export class Embed {
constructor(data, message_id, channel_id) {
if (embeds.has(data)) return embeds.get(data);
embeds.set(data, this);
this.discordObject = data;
this.messageId = message_id;
this.channelId = channel_id;
}
get title() { return this.discordObject.title }
get type() { return this.discordObject.type }
get description() { return this.discordObject.description }
get url() { return this.discordObject.url }
get timestamp() { return this.discordObject.timestamp }
get colour() { return this.discordObject.color }
get footer() { return this.discordObject.footer }
get image() { return this.discordObject.image }
get thumbnail() { return this.discordObject.thumbnail }
get video() { return this.discordObject.video }
get provider() { return this.discordObject.provider }
get author() { return this.discordObject.author }
get fields() { return this.discordObject.fields }
get channel() {
return Channel.fromId(this.channelId);
}
get message() {
if (this.channel) return this.channel.messages.find(m => m.id === this.messageId);
}
get guild() {
if (this.channel) return this.channel.guild;
}
}
const messages = new WeakMap();
export class Message {
constructor(data) {
if (messages.has(data)) return messages.get(data);
messages.set(data, this);
this.discordObject = data;
}
static from(data) {
switch (data.type) {
default: return new Message(data);
case 0: return new DefaultMessage(data);
case 1: return new RecipientAddMessage(data);
case 2: return new RecipientRemoveMessage(data);
case 3: return new CallMessage(data);
case 4: return new GroupChannelNameChangeMessage(data);
case 5: return new GroupChannelIconChangeMessage(data);
case 6: return new MessagePinnedMessage(data);
case 7: return new GuildMemberJoinMessage(data);
}
}
static get DefaultMessage() { return DefaultMessage }
static get RecipientAddMessage() { return RecipientAddMessage }
static get RecipientRemoveMessage() { return RecipientRemoveMessage }
static get CallMessage() { return CallMessage }
static get GroupChannelNameChangeMessage() { return GroupChannelNameChangeMessage }
static get GroupChannelIconChangeMessage() { return GroupChannelIconChangeMessage }
static get MessagePinnedMessage() { return MessagePinnedMessage }
static get GuildMemberJoinMessage() { return GuildMemberJoinMessage }
static get Reaction() { return Reaction }
static get Embed() { return Embed }
get id() { return this.discordObject.id }
get channelId() { return this.discordObject.channel_id }
get nonce() { return this.discordObject.nonce }
get type() { return this.discordObject.type }
get timestamp() { return this.discordObject.timestamp }
get state() { return this.discordObject.state }
get nick() { return this.discordObject.nick }
get colourString() { return this.discordObject.colorString }
get author() {
if (this.discordObject.author && !this.webhookId) return User.from(this.discordObject.author);
}
get channel() {
return Channel.fromId(this.channelId);
}
get guild() {
if (this.channel) return this.channel.guild;
}
/**
* Deletes the message.
* @return {Promise}
*/
delete() {
if (!this.isDeletable) throw new Error(`Message type ${this.type} is not deletable.`);
if (this.author === DiscordApi.currentUser) {}
else if (this.channel.assertPermissions) this.channel.assertPermissions('MANAGE_MESSAGES', Modules.DiscordPermissions.MANAGE_MESSAGES);
else if (!this.channel.owner === DiscordApi.currentUser) throw new InsufficientPermissions('MANAGE_MESSAGES');
return Modules.APIModule.delete(`${Modules.DiscordConstants.Endpoints.MESSAGES(this.channelId)}/${this.id}`);
}
get isDeletable() {
return this.type === 'DEFAULT' || this.type === 'CHANNEL_PINNED_MESSAGE' || this.type === 'GUILD_MEMBER_JOIN';
}
/**
* Jumps to the message.
*/
jumpTo(flash = true) {
Modules.MessageActions.jumpToMessage(this.channelId, this.id, flash);
}
}
export class DefaultMessage extends Message {
get webhookId() { return this.discordObject.webhookId }
get type() { return 'DEFAULT' }
get content() { return this.discordObject.content }
get contentParsed() { return this.discordObject.contentParsed }
get inviteCodes() { return this.discordObject.invites }
get attachments() { return this.discordObject.attachments }
get mentionIds() { return this.discordObject.mentions }
get mentionRoleIds() { return this.discordObject.mentionRoles }
get mentionEveryone() { return this.discordObject.mentionEveryone }
get editedTimestamp() { return this.discordObject.editedTimestamp }
get tts() { return this.discordObject.tts }
get mentioned() { return this.discordObject.mentioned }
get bot() { return this.discordObject.bot }
get blocked() { return this.discordObject.blocked }
get pinned() { return this.discordObject.pinned }
get activity() { return this.discordObject.activity }
get application() { return this.discordObject.application }
get webhook() {
if (this.webhookId) return this.discordObject.author;
}
get mentions() {
return List.from(this.mentionIds, id => User.fromId(id));
}
get mention_roles() {
return List.from(this.mentionRoleIds, id => this.guild.roles.find(r => r.id === id));
}
get embeds() {
return List.from(this.discordObject.embeds, r => new Embed(r, this.id, this.channelId));
}
get reactions() {
return List.from(this.discordObject.reactions, r => new Reaction(r, this.id, this.channelId));
}
get edited() {
return !!this.editedTimestamp;
}
/**
* Programmatically update the message's content.
* @param {String} content The message's new content
* @param {Boolean} parse Whether to parse the message or update it as it is
* @return {Promise}
*/
async edit(content, parse = false) {
if (this.author !== DiscordApi.currentUser) throw new Error('Cannot edit messages sent by other users.');
if (parse) content = Modules.MessageParser.parse(this.discordObject, content);
else content = {content};
const response = await Modules.APIModule.patch({
url: `${Modules.DiscordConstants.Endpoints.MESSAGES(this.channelId)}/${this.id}`,
body: content
});
this.discordObject = Modules.MessageStore.getMessage(this.id, response.body.id);
messages.set(this.discordObject, this);
}
/**
* Start the edit mode of the UI.
* @param {String} content A string to show in the message text area - if empty the message's current content will be used
*/
startEdit(content) {
if (this.author !== DiscordApi.currentUser) throw new Error('Cannot edit messages sent by other users.');
Modules.MessageActions.startEditMessage(this.channelId, this.id, content || this.content);
}
/**
* Exit the edit mode of the UI.
*/
endEdit() {
Modules.MessageActions.endEditMessage();
}
}
export class RecipientAddMessage extends Message {
get type() { return 'RECIPIENT_ADD' }
get addedUserId() { return this.discordObject.mentions[0] }
get addedUser() {
return User.fromId(this.addedUserId);
}
}
export class RecipientRemoveMessage extends Message {
get type() { return 'RECIPIENT_REMOVE' }
get removedUserId() { return this.discordObject.mentions[0] }
get removedUser() {
return User.fromId(this.removedUserId);
}
get userLeft() {
return this.author === this.removedUser;
}
}
export class CallMessage extends Message {
get type() { return 'CALL' }
get mentionIds() { return this.discordObject.mentions }
get call() { return this.discordObject.call }
get endedTimestamp() { return this.call.endedTimestamp }
get mentions() {
return List.from(this.mentionIds, id => User.fromId(id));
}
get participants() {
return List.from(this.call.participants, id => User.fromId(id));
}
}
export class GroupChannelNameChangeMessage extends Message {
get type() { return 'CHANNEL_NAME_CHANGE' }
get newName() { return this.discordObject.content }
}
export class GroupChannelIconChangeMessage extends Message {
get type() { return 'CHANNEL_ICON_CHANGE' }
}
export class MessagePinnedMessage extends Message {
get type() { return 'CHANNEL_PINNED_MESSAGE' }
}
export class GuildMemberJoinMessage extends Message {
get type() { return 'GUILD_MEMBER_JOIN' }
}

View File

@ -0,0 +1,330 @@
/**
* BetterDiscord User Struct
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { DiscordApi, DiscordApiModules as Modules } from 'modules';
import { List, InsufficientPermissions } from 'structs';
import { Utils } from 'common';
import { Guild } from './guild';
import { Channel } from './channel';
const users = new WeakMap();
export class User {
constructor(data) {
if (users.has(data)) return users.get(data);
users.set(data, this);
this.discordObject = data;
}
static from(data) {
return new User(data);
}
static fromId(id) {
const user = Modules.UserStore.getUser(id);
if (user) return User.from(user);
}
static get GuildMember() { return GuildMember }
get id() { return this.discordObject.id }
get username() { return this.discordObject.username }
get usernameLowerCase() { return this.discordObject.usernameLowerCase }
get discriminator() { return this.discordObject.discriminator }
get avatar() { return this.discordObject.avatar }
get email() { return undefined }
get phone() { return undefined }
get flags() { return this.discordObject.flags }
get isBot() { return this.discordObject.bot }
get premium() { return this.discordObject.premium }
get verified() { return this.discordObject.verified }
get mfaEnabled() { return this.discordObject.mfaEnabled }
get mobile() { return this.discordObject.mobile }
get tag() { return this.discordObject.tag }
get avatarUrl() { return this.discordObject.avatarURL }
get createdAt() { return this.discordObject.createdAt }
get isClamied() { return this.discordObject.isClaimed() }
get isLocalBot() { return this.discordObject.isLocalBot() }
get isPhoneVerified() { return this.discordObject.isPhoneVerified() }
get guilds() {
return DiscordApi.guilds.filter(g => g.members.find(m => m.user === this));
}
get status() {
return Modules.UserStatusStore.getStatus(this.id);
}
get activity() {
// type can be either 0 (normal/rich presence game), 1 (streaming) or 2 (listening to Spotify)
// (3 appears as watching but is undocumented)
return Modules.UserStatusStore.getActivity(this.id);
}
get note() {
const note = Modules.UserNoteStore.getNote(this.id);
if (note) return note;
}
/**
* Updates the note for this user.
* @param {String} note The new note
* @return {Promise}
*/
updateNote(note) {
return Modules.APIModule.put({
url: `${Modules.DiscordConstants.Endpoints.NOTES}/${this.id}`,
body: { note }
});
}
get privateChannel() {
return DiscordApi.channels.find(c => c.type === 'DM' && c.recipientId === this.id);
}
async ensurePrivateChannel() {
if (DiscordApi.currentUser === this)
throw new Error('Cannot create a direct message channel to the current user.');
return Channel.fromId(await Modules.PrivateChannelActions.ensurePrivateChannel(DiscordApi.currentUser.id, this.id));
}
async sendMessage(content, parse = true) {
const channel = await this.ensurePrivateChannel();
return channel.sendMessage(content, parse);
}
get isFriend() {
return Modules.RelationshipStore.isFriend(this.id);
}
get isBlocked() {
return Modules.RelationshipStore.isBlocked(this.id);
}
addFriend() {
Modules.RelationshipManager.addRelationship(this.id, {location: 'Context Menu'});
}
removeFriend() {
Modules.RelationshipManager.removeRelationship(this.id, {location: 'Context Menu'});
}
block() {
Modules.RelationshipManager.addRelationship(this.id, {location: 'Context Menu'}, Modules.DiscordConstants.RelationshipTypes.BLOCKED);
}
unblock() {
Modules.RelationshipManager.removeRelationship(this.id, {location: 'Context Menu'});
}
/**
* Opens the profile modal for this user.
* @param {String} section The section to open (see DiscordConstants.UserProfileSections)
*/
openUserProfileModal(section = 'USER_INFO') {
Modules.UserProfileModal.open(this.id);
Modules.UserProfileModal.setSection(section);
}
}
const guild_members = new WeakMap();
export class GuildMember {
constructor(data, guild_id) {
if (guild_members.has(data)) return guild_members.get(data);
guild_members.set(data, this);
this.discordObject = data;
this.guildId = guild_id;
}
get userId() { return this.discordObject.userId }
get nickname() { return this.discordObject.nick }
get colourString() { return this.discordObject.colorString }
get hoistRoleId() { return this.discordObject.hoistRoleId }
get roleIds() { return this.discordObject.roles }
get user() {
return User.fromId(this.userId);
}
get name() {
return this.nickname || this.user.username;
}
get guild() {
return Guild.fromId(this.guildId);
}
get roles() {
return List.from(this.roleIds, id => this.guild.roles.find(r => r.id === id))
.sort((r1, r2) => r1.position === r2.position ? 0 : r1.position > r2.position ? 1 : -1);
}
get hoistRole() {
return this.guild.roles.find(r => r.id === this.hoistRoleId);
}
checkPermissions(perms) {
return Modules.PermissionUtils.can(perms, DiscordApi.currentUser.discordObject, this.guild.discordObject);
}
assertPermissions(name, perms) {
if (!this.checkPermissions(perms)) throw new InsufficientPermissions(name);
}
/**
* Opens the modal to change this user's nickname.
*/
openChangeNicknameModal() {
if (DiscordApi.currentUser === this.user)
this.assertPermissions('CHANGE_NICKNAME', Modules.DiscordPermissions.CHANGE_NICKNAME);
else this.assertPermissions('MANAGE_NICKNAMES', Modules.DiscordPermissions.MANAGE_NICKNAMES);
Modules.ChangeNicknameModal.open(this.guildId, this.userId);
}
/**
* Changes the user's nickname on this guild.
* @param {String} nickname The user's new nickname
* @return {Promise}
*/
changeNickname(nick) {
if (DiscordApi.currentUser === this.user)
this.assertPermissions('CHANGE_NICKNAME', Modules.DiscordPermissions.CHANGE_NICKNAME);
else this.assertPermissions('MANAGE_NICKNAMES', Modules.DiscordPermissions.MANAGE_NICKNAMES);
return Modules.APIModule.patch({
url: `${Modules.DiscordConstants.Endpoints.GUILD_MEMBERS(this.guild_id)}/${DiscordApi.currentUser === this.user ? '@me/nick' : this.userId}`,
body: { nick }
});
}
/**
* Kicks this user from the guild.
* @param {String} reason A reason to attach to the audit log entry
* @return {Promise}
*/
kick(reason = '') {
this.assertPermissions('KICK_MEMBERS', Modules.DiscordPermissions.KICK_MEMBERS);
return Modules.GuildActions.kickUser(this.guildId, this.userId, reason);
}
/**
* Bans this user from the guild.
* @param {Number} daysToDelete The number of days of the user's recent message history to delete
* @param {String} reason A reason to attach to the audit log entry
* @return {Promise}
*/
ban(daysToDelete = 1, reason = '') {
this.assertPermissions('BAN_MEMBERS', Modules.DiscordPermissions.BAN_MEMBERS);
return Modules.GuildActions.banUser(this.guildId, this.userId, daysToDelete, reason);
}
/**
* Removes the ban for this user.
* @return {Promise}
*/
unban() {
this.assertPermissions('BAN_MEMBERS', Modules.DiscordPermissions.BAN_MEMBERS);
return Modules.GuildActions.unbanUser(this.guildId, this.userId);
}
/**
* Moves this user to another voice channel.
* @param {GuildVoiceChannel} channel The channel to move this user to
*/
move(channel) {
this.assertPermissions('MOVE_MEMBERS', Modules.DiscordPermissions.MOVE_MEMBERS);
Modules.GuildActions.setChannel(this.guildId, this.userId, channel.id);
}
/**
* Mutes this user for everyone in the guild.
*/
mute(active = true) {
this.assertPermissions('MUTE_MEMBERS', Modules.DiscordPermissions.MUTE_MEMBERS);
Modules.GuildActions.setServerMute(this.guildId, this.userId, active);
}
/**
* Unmutes this user.
*/
unmute() {
this.mute(false);
}
/**
* Deafens this user.
*/
deafen(active = true) {
this.assertPermissions('DEAFEN_MEMBERS', Modules.DiscordPermissions.DEAFEN_MEMBERS);
Modules.GuildActions.setServerDeaf(this.guildId, this.userId, active);
}
/**
* Undeafens this user.
*/
undeafen() {
this.deafen(false);
}
/**
* Gives this user a role.
* @param {Role} role The role to add
* @return {Promise}
*/
addRole(...roles) {
const newRoles = this.roleIds.concat([]);
let changed = false;
for (let role of roles) {
if (newRoles.includes(role.id || role)) continue;
newRoles.push(role.id || role);
changed = true;
}
if (!changed) return;
return this.updateRoles(newRoles);
}
/**
* Removes a role from this user.
* @param {Role} role The role to remove
* @return {Promise}
*/
removeRole(...roles) {
const newRoles = this.roleIds.concat([]);
let changed = false;
for (let role of roles) {
if (!newRoles.includes(role.id || role)) continue;
Utils.removeFromArray(newRoles, role.id || role);
changed = true;
}
if (!changed) return;
return this.updateRoles(newRoles);
}
/**
* Updates this user's roles.
* @param {Array} roles An array of Role objects or role IDs
* @return {Promise}
*/
updateRoles(roles) {
roles = roles.map(r => r.id || r);
return Modules.APIModule.patch({
url: `${Modules.DiscordConstants.Endpoints.GUILD_MEMBERS(this.guildId)}/${this.userId}`,
body: { roles }
});
}
}

View File

@ -0,0 +1,4 @@
export * from './discord/user';
export * from './discord/guild';
export * from './discord/channel';
export * from './discord/message';

View File

@ -1,3 +1,4 @@
export { default as SettingUpdatedEvent } from './settingupdated';
export { default as SettingsUpdatedEvent } from './settingsupdated';
export { default as ErrorEvent } from './error';
export { PermissionsError, InsufficientPermissions } from './permissionserror';

View File

@ -0,0 +1,25 @@
/**
* BetterDiscord Permissions Error Struct
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import ErrorEvent from './error';
export class PermissionsError extends ErrorEvent {
constructor(message) {
super(message);
this.name = 'PermissionsError';
}
}
export class InsufficientPermissions extends PermissionsError {
constructor(message) {
super(`Missing Permission — ${message}`);
this.name = 'InsufficientPermissions';
}
}

View File

@ -0,0 +1,30 @@
/**
* BetterDiscord List
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
export default class List extends Array {
constructor() {
super(...arguments);
}
get(...filters) {
return this.find(item => {
for (let filter of filters) {
for (let key in filter) {
if (filter.hasOwnProperty(key)) {
if (item[key] !== filter[key]) return false;
}
}
}
return true;
});
}
}

View File

@ -8,8 +8,6 @@
* LICENSE file in the root directory of this source tree.
*/
import { Utils } from 'common';
export default class MultipleChoiceOption {
constructor(args) {

View File

@ -8,8 +8,6 @@
* LICENSE file in the root directory of this source tree.
*/
import { Utils } from 'common';
import BoolSetting from './types/bool';
import StringSetting from './types/text';
import NumberSetting from './types/number';

View File

@ -8,10 +8,11 @@
* LICENSE file in the root directory of this source tree.
*/
import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs';
import { ClientLogger as Logger, AsyncEventEmitter } from 'common';
import Setting from './setting';
import BaseSetting from './types/basesetting';
import { ClientLogger as Logger, AsyncEventEmitter } from 'common';
import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs';
import SettingsProxy from './settingsproxy';
export default class SettingsCategory extends AsyncEventEmitter {
@ -173,6 +174,14 @@ export default class SettingsCategory extends AsyncEventEmitter {
await this.emit('removed-category', event);
}
/**
* Returns a proxy which can be used to access the category's values like a normal object.
* @return {SettingsCategoryProxy}
*/
get proxy() {
return this._proxy || (this._proxy = SettingsProxy.createProxy(this));
}
/**
* Returns the first setting where calling {function} returns true.
* @param {Function} function A function to call to filter settings

View File

@ -0,0 +1,109 @@
/**
* BetterDiscord Settings Proxy
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import SettingsSet from './settingsset';
import SettingsCategory from './settingscategory';
const setHandler = {
get({ set }, category_id) {
const category = set.getCategory(category_id);
if (category) return SettingsProxy.createProxy(category);
const setting = set.getSetting(category_id);
if (setting) return setting.value;
},
set({ set }, category_id, values) {
const category = set.getCategory(category_id);
if (category) return category.merge(values);
const setting = set.getSetting(category_id);
if (setting) return setting.value = values;
},
has({ set }, category_id) {
const category = set.getCategory(category_id);
if (category) return true;
const setting = set.getSetting(category_id);
if (setting) return true;
},
getPrototypeOf({ set }) {
return SettingsSetProxy.prototype;
},
setPrototypeOf({ set }) {},
isExtensible({ set }) {
return false;
},
preventExtensions({ set }) {},
getOwnPropertyDescriptor({ set }, category_id) {
return {
value: setHandler.get({ set }, category_id),
writable: true,
enumerable: true,
configurable: true
};
},
defineProperty({ set }) {},
deleteProperty({ set }) {},
ownKeys({ set }) {
return set.categories.map(c => c.id);
}
};
const categoryHandler = {
get({ category }, setting_id) {
const setting = category.getSetting(setting_id);
if (setting) return setting.value;
},
set({ category }, setting_id, value) {
const setting = category.getSetting(setting_id);
if (setting) return setting.value = value;
},
has({ category }, setting_id) {
const setting = category.getSetting(setting_id);
if (setting) return true;
},
getPrototypeOf({ category }) {
return SettingsCategoryProxy.prototype;
},
setPrototypeOf({ category }) {},
isExtensible({ category }) {
return false;
},
preventExtensions({ category }) {},
getOwnPropertyDescriptor({ category }, setting_id) {
return {
value: categoryHandler.get({ category }, setting_id),
writable: true,
enumerable: true,
configurable: true
};
},
defineProperty({ category }) {},
deleteProperty({ category }) {},
ownKeys({ category }) {
return category.settings.map(s => s.id);
}
};
export default class SettingsProxy {
constructor(args) {
Object.assign(this, args);
}
static createProxy(set) {
if (set instanceof SettingsSet) return new Proxy(new SettingsSetProxy({ set }), setHandler);
if (set instanceof SettingsCategory) return new Proxy(new SettingsCategoryProxy({ category: set }), categoryHandler);
}
}
export class SettingsSetProxy extends SettingsProxy {}
export class SettingsCategoryProxy extends SettingsProxy {}

View File

@ -8,11 +8,12 @@
* LICENSE file in the root directory of this source tree.
*/
import SettingsCategory from './settingscategory';
import SettingsScheme from './settingsscheme';
import { ClientLogger as Logger, AsyncEventEmitter } from 'common';
import { SettingUpdatedEvent, SettingsUpdatedEvent } from 'structs';
import { Modals } from 'ui';
import { ClientLogger as Logger, AsyncEventEmitter } from 'common';
import SettingsCategory from './settingscategory';
import SettingsScheme from './settingsscheme';
import SettingsProxy from './settingsproxy';
export default class SettingsSet extends AsyncEventEmitter {
@ -245,6 +246,14 @@ export default class SettingsSet extends AsyncEventEmitter {
});
}
/**
* Returns a proxy which can be used to access the set's categories like a normal object.
* @return {SettingsSetProxy}
*/
get proxy() {
return this._proxy || (this._proxy = SettingsProxy.createProxy(this));
}
/**
* Returns the first category where calling {function} returns true.
* @param {Function} function A function to call to filter categories
@ -356,7 +365,7 @@ export default class SettingsSet extends AsyncEventEmitter {
* Merges a set into this set without emitting events (and therefore synchronously).
* This only exists for use by the constructor.
*/
_merge(newSet, emit_multi = true) {
_merge(newSet) {
let updatedSettings = [];
// const categories = newSet instanceof Array ? newSet : newSet.settings;
const categories = newSet && newSet.args ? newSet.args.settings : newSet ? newSet.settings : newSet;

View File

@ -8,10 +8,9 @@
* LICENSE file in the root directory of this source tree.
*/
import Setting from './basesetting';
import SettingsCategory from '../settingscategory';
import SettingsScheme from '../settingsscheme';
import { Globals } from 'modules';
import path from 'path';
import Setting from './basesetting';
export default class CustomSetting extends Setting {
@ -68,7 +67,7 @@ export default class CustomSetting extends Setting {
* @param {String} classExport The name of a property of the file's exports that will be used (optional)
*/
setClass(class_file, class_export) {
const component = window.require(path.join(this.path, class_file));
const component = Globals.require(path.join(this.path, class_file));
const setting_class = class_export ? component[class_export](CustomSetting) : component.default ? component.default(CustomSetting) : component(CustomSetting);
if (!(setting_class.prototype instanceof CustomSetting))

View File

@ -8,9 +8,11 @@
* LICENSE file in the root directory of this source tree.
*/
import Setting from './basesetting';
import Combokeys from 'combokeys';
import CombokeysGlobalBind from 'combokeys/plugins/global-bind';
import Setting from './basesetting';
const instances = new Set();
let keybindsPaused = false;
export default class KeybindSetting extends Setting {
@ -18,10 +20,23 @@ export default class KeybindSetting extends Setting {
constructor(args, ...merge) {
super(args, ...merge);
// When adding a keybind-activated listener, add the keybind setting to the set of active keybind settings
// This creates a reference to the keybind setting, which may cause memory leaks
this.on('newListener', ({event: [event, listener]}) => {
if (event === 'keybind-activated') instances.add(this);
});
// When there are no more keybind-activated listeners, remove the keybind setting from the set of active keybind settings
// Always remember to unbind keybind-activated listeners!
this.on('removeListener', ({event: [event, listener]}) => {
if (!this.listenerCount('keybind-activated')) instances.delete(this);
});
this.__keybind_activated = this.__keybind_activated.bind(this);
this.combokeys = new Combokeys(document);
this.combokeys.bind(this.value, this.__keybind_activated);
this.combokeys = new Combokeys(this);
CombokeysGlobalBind(this.combokeys);
this.combokeys.bindGlobal(this.value, this.__keybind_activated);
}
/**
@ -33,7 +48,7 @@ export default class KeybindSetting extends Setting {
setValueHook() {
this.combokeys.reset();
this.combokeys.bind(this.value, this.__keybind_activated);
this.combokeys.bindGlobal(this.value, this.__keybind_activated);
}
__keybind_activated(event) {
@ -41,6 +56,22 @@ export default class KeybindSetting extends Setting {
this.emit('keybind-activated', event);
}
// Event function aliases for Combokeys
get addEventListener() { return this.on }
get removeEventListener() { return this.removeListener }
static _init() {
document.addEventListener('keydown', this.__event_handler.bind(this, 'keydown'));
document.addEventListener('keyup', this.__event_handler.bind(this, 'keyup'));
document.addEventListener('keypress', this.__event_handler.bind(this, 'keypress'));
}
static __event_handler(event, data) {
for (let keybindSetting of instances) {
keybindSetting.emit(event, data);
}
}
static get paused() {
return keybindsPaused;
}
@ -50,3 +81,5 @@ export default class KeybindSetting extends Setting {
}
}
KeybindSetting._init();

View File

@ -52,6 +52,28 @@ export default class RadioSetting extends Setting {
this.args.value = selected_option.id;
}
/**
* Whether the user should be allowed to choose multiple options.
*/
get multi() {
return this.args.multi;
}
/**
* The minimum number of options the user may select if a multi select group.
* This only restricts deselecting options when there is less or equal options selected than this, and does not ensure that this number of options are actually selected.
*/
get min() {
return this.multi ? this.args.min || 0 : 1;
}
/**
* The maximum number of options the user may select if a multi select group.
*/
get max() {
return this.multi ? this.args.max || 0 : 1;
}
/**
* Returns a representation of this setting's value in SCSS.
* @return {String}

View File

@ -1,2 +1,4 @@
export { default as List } from './list';
export * from './events/index';
export * from './settings/index';

View File

@ -1 +1 @@
@import './partials/index.scss';
@import './partials/index.scss';

View File

@ -49,6 +49,30 @@
}
}
@keyframes bd-toast-up {
0% {
transform: translateY(10px);
opacity: 0;
}
100% {
transform: translateY(0%);
opacity: 1;
}
}
@keyframes bd-toast-down {
0% {
transform: translateY(0%);
opacity: 1;
}
100% {
transform: translateY(10px);
opacity: 0;
}
}
@keyframes bd-fade-out {
0% {
opacity: 1;

View File

@ -18,50 +18,49 @@
background-size: cover;
cursor: pointer;
height: 16px;
width: 16px;
margin-right: 6px;
}
.bd-profile-badge-developer,
.bd-profile-badge-contributor,
.bd-message-badge-developer,
.bd-message-badge-contributor {
.bd-profile-badge-webdev,
.bd-profile-badge-contributor {
background-image: $logoSmallBw;
width: 16px;
filter: brightness(10);
cursor: pointer;
.theme-light [class*="topSectionNormal-"] .bd-profile-badges-profile-modal > &,
.theme-light :not(.bd-profile-badges-profile-modal) > & {
background-image: $logoSmallLight;
filter: none;
}
}
.theme-light [class*="topSectionNormal-"] .bd-profile-badge-developer,
.theme-light [class*="topSectionNormal-"] .bd-profile-badge-contributor,
.theme-light .bd-message-badge-developer,
.theme-light .bd-message-badge-contributor {
background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2FscXVlXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjAwMCAyMDAwIiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAyMDAwIDIwMDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxnPjxwYXRoIGZpbGw9IiMzRTgyRTUiIGQ9Ik0xNDAyLjIsNjMxLjdjLTkuNy0zNTMuNC0yODYuMi00OTYtNjQyLjYtNDk2SDY4LjR2NzE0LjFsNDQyLDM5OFY0OTAuN2gyNTdjMjc0LjUsMCwyNzQuNSwzNDQuOSwwLDM0NC45SDU5Ny42djMyOS41aDE2OS44YzI3NC41LDAsMjc0LjUsMzQ0LjgsMCwzNDQuOGgtNjk5djM1NC45aDY5MS4yYzM1Ni4zLDAsNjMyLjgtMTQyLjYsNjQyLjYtNDk2YzAtMTYyLjYtNDQuNS0yODQuMS0xMjIuOS0zNjguNkMxMzU3LjcsOTE1LjgsMTQwMi4yLDc5NC4zLDE0MDIuMiw2MzEuN3oiLz48cGF0aCBmaWxsPSIjYmJiYmJiIiBkPSJNMTI2Mi41LDEzNS4yTDEyNjIuNSwxMzUuMmwtNzYuOCwwYzI2LjYsMTMuMyw1MS43LDI4LjEsNzUsNDQuM2M3MC43LDQ5LjEsMTI2LjEsMTExLjUsMTY0LjYsMTg1LjNjMzkuOSw3Ni42LDYxLjUsMTY1LjYsNjQuMywyNjQuNmwwLDEuMnYxLjJjMCwxNDEuMSwwLDU5Ni4xLDAsNzM3LjF2MS4ybDAsMS4yYy0yLjcsOTktMjQuMywxODgtNjQuMywyNjQuNmMtMzguNSw3My44LTkzLjgsMTM2LjItMTY0LjYsMTg1LjNjLTIyLjYsMTUuNy00Ni45LDMwLjEtNzIuNiw0My4xaDcyLjVjMzQ2LjIsMS45LDY3MS0xNzEuMiw2NzEtNTY3LjlWNzE2LjdDMTkzMy41LDMxMi4yLDE2MDguNywxMzUuMiwxMjYyLjUsMTM1LjJ6Ii8+PC9nPjwvc3ZnPg==');
filter: none;
}
.bd-message-badges-wrap {
.bd-profile-badges.bd-profile-badges-nametag {
display: inline-block;
margin-left: 6px;
height: 11px;
.bd-message-badge-developer,
.bd-message-badge-contributor {
.bd-profile-badge {
width: 12px;
height: 12px;
&:last-child {
margin-right: 0;
}
}
}
.member-username .bd-message-badges-wrap {
display: inline-block;
height: 17px;
width: 14px;
.bd-message-badge-developer,
.bd-message-badge-contributor {
width: 14px;
height: 16px;
background-position: center;
background-size: 12px 12px;
background-repeat: no-repeat;
}
}
// .member-username .bd-profile-badges {
// display: inline-block;
// height: 17px;
// width: 14px;
//
// .bd-badge,
// .bd-badge-c {
// width: 14px;
// height: 16px;
// background-position: center;
// background-size: 12px 12px;
// background-repeat: no-repeat;
// }
// }

View File

@ -19,6 +19,7 @@
.bd-card-icon {
width: 30px;
height: 30px;
background-size: cover;
}
> span {

View File

@ -5,3 +5,4 @@
@import './tooltips.scss';
@import './settings-schemes.scss';
@import './updater.scss';
@import './window-preferences';

View File

@ -0,0 +1,5 @@
.bd-window-preferences {
.bd-window-preferences-disabled p {
color: #f6f6f7;
}
}

View File

@ -1,4 +1,4 @@
.guilds-wrapper {
[class*="guildsWrapper-"] {
padding-top: 49px !important;
.platform-osx & {
@ -6,11 +6,11 @@
}
}
[class*="guilds-wrapper"] + [class*="flex"] {
[class*="guildsWrapper-"] + [class*="flex"] {
border-radius: 0 0 0 5px;
}
.unread-mentions-indicator-top {
[class*="unreadMentionsIndicatorTop-"] {
top: 49px;
.platform-osx & {
@ -19,6 +19,10 @@
}
// Any layers need to be above the main layer (where the BD button is placed)
.layer-kosS71 + .layer-kosS71 {
[class*="layers-"] > * + * {
z-index: 900;
}
.bd-settings-wrapper.platform-linux {
transform: none;
}

View File

@ -9,7 +9,7 @@
}
.bd-emotewrapper {
display: flex;
display: inline-flex;
max-height: 32px;
img {

View File

@ -61,13 +61,14 @@
.bd-button,
.bd-material-button {
&:first-of-type {
border-radius: 6px 0 0 6px;
border-top-left-radius: 6px;
border-bottom-left-radius: 6px;
}
&:last-of-type {
border-radius: 0 6px 6px 0;
border-top-right-radius: 6px;
border-bottom-right-radius: 6px;
}
&:not(:last-of-type) {

View File

@ -33,7 +33,6 @@
}
.bd-drawer-contents-wrap {
overflow: hidden;
min-height: 5px;
}
@ -50,7 +49,8 @@
}
}
&.bd-animating {
&.bd-animating,
&:not(.bd-drawer-open) {
> .bd-drawer-contents-wrap {
overflow: hidden;
}

View File

@ -13,3 +13,4 @@
@import './helpers.scss';
@import './misc.scss';
@import './emotes.scss';
@import './toasts.scss';

View File

@ -0,0 +1,68 @@
.bd-toasts {
display: flex;
position: fixed;
top: 0;
width: 700px;
left: 50%;
transform: translateX(-50%);
bottom: 100px;
flex-direction: column;
align-items: center;
justify-content: flex-end;
pointer-events: none;
z-index: 4000;
.bd-toast {
position: relative;
animation: bd-toast-up 300ms ease;
background: #36393F;
padding: 10px;
border-radius: 5px;
box-shadow: 0 0 0 1px rgba(32,34,37,.6), 0 2px 10px 0 rgba(0,0,0,.2);
font-weight: 500;
color: #fff;
user-select: text;
font-size: 14px;
margin-top: 10px;
&.bd-toast-error {
background: #f04747;
}
&.bd-toast-info {
background: #4a90e2;
}
&.bd-toast-warning {
background: #FFA600;
}
&.bd-toast-success {
background: #43b581;
}
&.bd-toast-has-icon {
padding-left: 30px;
}
}
.bd-toast-icon {
position: absolute;
left: 5px;
top: 50%;
transform: translateY(-50%);
bottom: 0;
height: 20px;
width: 20px;
border-radius: 50%;
overflow: hidden;
svg {
fill: white;
}
}
.bd-toast.bd-toast-closing {
animation: bd-toast-down 300ms ease;
}
}

View File

@ -1,3 +1,5 @@
$logoSmallBw: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2FscXVlXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjAwMCAyMDAwIiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAyMDAwIDIwMDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxnPjxwYXRoIGZpbGw9IiMzRTgyRTUiIGQ9Ik0xNDAyLjIsNjMxLjdjLTkuNy0zNTMuNC0yODYuMi00OTYtNjQyLjYtNDk2SDY4LjR2NzE0LjFsNDQyLDM5OFY0OTAuN2gyNTdjMjc0LjUsMCwyNzQuNSwzNDQuOSwwLDM0NC45SDU5Ny42djMyOS41aDE2OS44YzI3NC41LDAsMjc0LjUsMzQ0LjgsMCwzNDQuOGgtNjk5djM1NC45aDY5MS4yYzM1Ni4zLDAsNjMyLjgtMTQyLjYsNjQyLjYtNDk2YzAtMTYyLjYtNDQuNS0yODQuMS0xMjIuOS0zNjguNkMxMzU3LjcsOTE1LjgsMTQwMi4yLDc5NC4zLDE0MDIuMiw2MzEuN3oiLz48cGF0aCBmaWxsPSIjRkZGRkZGIiBkPSJNMTI2Mi41LDEzNS4yTDEyNjIuNSwxMzUuMmwtNzYuOCwwYzI2LjYsMTMuMyw1MS43LDI4LjEsNzUsNDQuM2M3MC43LDQ5LjEsMTI2LjEsMTExLjUsMTY0LjYsMTg1LjNjMzkuOSw3Ni42LDYxLjUsMTY1LjYsNjQuMywyNjQuNmwwLDEuMnYxLjJjMCwxNDEuMSwwLDU5Ni4xLDAsNzM3LjF2MS4ybDAsMS4yYy0yLjcsOTktMjQuMywxODgtNjQuMywyNjQuNmMtMzguNSw3My44LTkzLjgsMTM2LjItMTY0LjYsMTg1LjNjLTIyLjYsMTUuNy00Ni45LDMwLjEtNzIuNiw0My4xaDcyLjVjMzQ2LjIsMS45LDY3MS0xNzEuMiw2NzEtNTY3LjlWNzE2LjdDMTkzMy41LDMxMi4yLDE2MDguNywxMzUuMiwxMjYyLjUsMTM1LjJ6Ii8+PC9nPjwvc3ZnPg==);
$logoSmallLight: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2FscXVlXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMjAwMCAyMDAwIiBlbmFibGUtYmFja2dyb3VuZD0ibmV3IDAgMCAyMDAwIDIwMDAiIHhtbDpzcGFjZT0icHJlc2VydmUiPjxnPjxwYXRoIGZpbGw9IiMzRTgyRTUiIGQ9Ik0xNDAyLjIsNjMxLjdjLTkuNy0zNTMuNC0yODYuMi00OTYtNjQyLjYtNDk2SDY4LjR2NzE0LjFsNDQyLDM5OFY0OTAuN2gyNTdjMjc0LjUsMCwyNzQuNSwzNDQuOSwwLDM0NC45SDU5Ny42djMyOS41aDE2OS44YzI3NC41LDAsMjc0LjUsMzQ0LjgsMCwzNDQuOGgtNjk5djM1NC45aDY5MS4yYzM1Ni4zLDAsNjMyLjgtMTQyLjYsNjQyLjYtNDk2YzAtMTYyLjYtNDQuNS0yODQuMS0xMjIuOS0zNjguNkMxMzU3LjcsOTE1LjgsMTQwMi4yLDc5NC4zLDE0MDIuMiw2MzEuN3oiLz48cGF0aCBmaWxsPSIjYmJiYmJiIiBkPSJNMTI2Mi41LDEzNS4yTDEyNjIuNSwxMzUuMmwtNzYuOCwwYzI2LjYsMTMuMyw1MS43LDI4LjEsNzUsNDQuM2M3MC43LDQ5LjEsMTI2LjEsMTExLjUsMTY0LjYsMTg1LjNjMzkuOSw3Ni42LDYxLjUsMTY1LjYsNjQuMywyNjQuNmwwLDEuMnYxLjJjMCwxNDEuMSwwLDU5Ni4xLDAsNzM3LjF2MS4ybDAsMS4yYy0yLjcsOTktMjQuMywxODgtNjQuMywyNjQuNmMtMzguNSw3My44LTkzLjgsMTM2LjItMTY0LjYsMTg1LjNjLTIyLjYsMTUuNy00Ni45LDMwLjEtNzIuNiw0My4xaDcyLjVjMzQ2LjIsMS45LDY3MS0xNzEuMiw2NzEtNTY3LjlWNzE2LjdDMTkzMy41LDMxMi4yLDE2MDguNywxMzUuMiwxMjYyLjUsMTM1LjJ6Ii8+PC9nPjwvc3ZnPg==');
$logoBigBw: url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz48IURPQ1RZUEUgc3ZnIFBVQkxJQyAiLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4iICJodHRwOi8vd3d3LnczLm9yZy9HcmFwaGljcy9TVkcvMS4xL0RURC9zdmcxMS5kdGQiPjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iQ2FscXVlXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4IiB2aWV3Qm94PSIwIDAgMTk0MCA2NDAiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDE5NDAgNjQwIiB4bWw6c3BhY2U9InByZXNlcnZlIj48Zz48cGF0aCBmaWxsPSIjRkZGRkZGIiBkPSJNMTU2LjEsMjczLjlIMjl2MTQyLjdsODQuNiw3Ni4yVjM1NC4yaDQ1LjJjMjguNywwLDQyLjksMTMuOCw0Mi45LDM2djEwNmMwLDIyLjItMTMuNCwzNy4xLTQyLjksMzcuMUgyOC42djgwLjdoMTI3YzY4LjEsMC40LDEzMi0zMy43LDEzMi0xMTEuN3YtMTE0QzI4OC4xLDMwOC43LDIyNC4yLDI3My45LDE1Ni4xLDI3My45TDE1Ni4xLDI3My45eiIvPjxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik04MjEuOSw1MDIuM1YzODUuMmMwLTQyLjEsNzUuOC01MS43LDk4LjctOS42bDcwLTI4LjNjLTI3LjYtNjAuNS03Ny43LTc4LjEtMTE5LjQtNzguMWMtNjguMSwwLTEzNS41LDM5LjQtMTM1LjUsMTE1Ljl2MTE3LjFjMCw3Ny4zLDY3LjMsMTE1LjksMTMzLjksMTE1LjljNDIuOSwwLDk0LjEtMjEsMTIyLjUtNzYuMmwtNzUtMzQuNEM4OTguOCw1NTQuOCw4MjEuOSw1NDMuMyw4MjEuOSw1MDIuM0w4MjEuOSw1MDIuM3oiLz48cGF0aCBmaWxsPSIjRkZGRkZGIiBkPSJNNTkwLjQsNDAxLjNjLTI2LjQtNS43LTQ0LTE1LjMtNDUuMi0zMS44YzEuNS0zOS40LDYyLjQtNDAuOSw5OC0zLjFsNTYuMy00My4yYy0zNS4yLTQyLjktNzUtNTQuMy0xMTUuOS01NC4zYy02Mi40LDAtMTIyLjgsMzUuMi0xMjIuOCwxMDEuOGMwLDY0LjcsNDkuNyw5OS41LDEwNC41LDEwNy45YzI3LjksMy44LDU4LjksMTQuOSw1OC4yLDM0LjFjLTIuMywzNi40LTc3LjMsMzQuNC0xMTEuNC02LjlsLTU0LjMsNTAuOWMzMS44LDQwLjksNzUsNjEuNiwxMTUuNiw2MS42YzYyLjQsMCwxMzEuNi0zNiwxMzQuMy0xMDEuOEM3MTEuMyw0MzMuNSw2NTAuOSw0MTIuNCw1OTAuNCw0MDEuM0w1OTAuNCw0MDEuM3oiLz48cmVjdCB4PSIzMzQiIHk9IjI3My45IiBmaWxsPSIjRkZGRkZGIiB3aWR0aD0iODUuNyIgaGVpZ2h0PSIzMzkuOCIvPjxwYXRoIGZpbGw9IiNGRkZGRkYiIGQ9Ik0xNzc5LjMsMjczLjloLTEyN3YxNDIuN2w4NC42LDc2LjJWMzU0LjJoNDUuMmMyOC43LDAsNDIuOSwxMy44LDQyLjksMzZ2MTA2YzAsMjIuMi0xMy40LDM3LjEtNDIuOSwzNy4xaC0xMzAuMXY4MC43aDEyNy40YzY4LjEsMC40LDEzMi0zMy43LDEzMi0xMTEuN3YtMTE0QzE5MTEuNCwzMDguNywxODQ3LjUsMjczLjksMTc3OS4zLDI3My45TDE3NzkuMywyNzMuOXoiLz48cGF0aCBmaWxsPSIjRkZGRkZGIiBkPSJNMTE1NiwyNjkuM2MtNzAuNCwwLTE0MC40LDM4LjMtMTQwLjQsMTE2LjdWNTAyYzAsNzcuNyw3MC40LDExNi43LDE0MS4yLDExNi43YzcwLjQsMCwxNDAuNC0zOSwxNDAuNC0xMTYuN1YzODZDMTI5Ny4yLDMwNy45LDEyMjYuNCwyNjkuMywxMTU2LDI2OS4zeiBNMTIxMS4xLDUwMmMwLDI0LjUtMjcuNiwzNy4xLTU0LjcsMzcuMWMtMjcuNiwwLTU1LjEtMTEuOS01NS4xLTM3LjFWMzg2YzAtMjQuOSwyNi44LTM4LjMsNTMuNi0zOC4zYzI3LjksMCw1Ni4zLDExLjksNTYuMywzOC4zVjUwMnoiLz48cGF0aCBmaWxsPSIjRkZGRkZGIiBkPSJNMTUzOC4zLDQ5Ny40YzQwLjktMTMsNjYuNi00OC42LDY2LjYtMTExLjRjLTEuOS03OS42LTU2LjMtMTExLjctMTI2LjMtMTExLjdoLTEzNS44djMzOS44aDg2LjlWNTA2LjJoMTUuM2w3OC44LDEwNy45aDEwNy4xTDE1MzguMyw0OTcuNHogTTE0ODAuMSw0MzEuOWgtNTAuNXYtNzcuN2g1MC41QzE1MzQuMSwzNTQuMiwxNTM0LjEsNDMxLjksMTQ4MC4xLDQzMS45eiIvPjwvZz48Zz48cGF0aCBmaWxsPSIjM0U4MkU1IiBkPSJNMTAzLjMsNDMuM0gyOC42djc3LjFsNDcuNyw0M1Y4MS43aDI3LjhjMjkuNiwwLDI5LjYsMzcuMiwwLDM3LjJIODUuOHYzNS42aDE4LjNjMjkuNiwwLDI5LjYsMzcuMiwwLDM3LjJIMjguNlYyMzBoNzQuNmMzOC41LDAsNjguMy0xNS40LDY5LjQtNTMuNmMwLTE3LjYtNC44LTMwLjctMTMuMy0zOS44YzguNS05LjEsMTMuMy0yMi4yLDEzLjMtMzkuOEMxNzEuNiw1OC43LDE0MS44LDQzLjMsMTAzLjMsNDMuM3oiLz48cG9seWdvbiBmaWxsPSIjM0U4MkU1IiBwb2ludHM9IjM1OC43LDQzLjMgMzU4LjcsODEuNyA0MDYuOCw4MS43IDQwNi44LDIzMCA0NTQuNSwyMzAgNDU0LjUsODEuNyA1MjkuOCw4MS43IDUyOS44LDIzMCA1NzcuNiwyMzAgNTc3LjYsODEuNyA2MjUuNyw4MS43IDYyNS43LDQzLjMgIi8+PHBvbHlnb24gZmlsbD0iIzNFODJFNSIgcG9pbnRzPSIxOTcuNiw0My4zIDE5Ny42LDIzMCAzNDEuNywyMzAgMzQxLjcsMTkxLjcgMjQ1LjQsMTkxLjcgMjQ1LjQsMTYwLjUgMjk2LjUsMTYwLjUgMjk2LjUsMTEyLjggMjQ1LjQsMTEyLjggMjQ1LjQsODEuNyAzNDEuNyw4MS43IDM0MS43LDQzLjMgIi8+PHBvbHlnb24gZmlsbD0iIzNFODJFNSIgcG9pbnRzPSI2NDIuNCw0My4zIDY0Mi40LDIzMCA3ODYuNCwyMzAgNzg2LjQsMTkxLjcgNjkwLjEsMTkxLjcgNjkwLjEsMTYwLjUgNzQxLjMsMTYwLjUgNzQxLjMsMTEyLjggNjkwLjEsMTEyLjggNjkwLjEsODEuNyA3ODYuNCw4MS43IDc4Ni40LDQzLjMgIi8+PHBhdGggZmlsbD0iIzNFODJFNSIgZD0iTTkxOC42LDE2NmMyMi41LTcuMSwzNi41LTI2LjYsMzYuNS02MS4xYy0xLTQzLjYtMzAuOC02MS43LTY5LjItNjEuN2gtNzQuNVYyMzBIODU5di01OS4yaDguNGw0My4yLDU5LjJoNTguN0w5MTguNiwxNjZ6IE04ODYuNywxMzAuMkg4NTlWODEuN2gyNy43QzkxNi4zLDgxLjcsOTE2LjMsMTMwLjIsODg2LjcsMTMwLjJ6Ii8+PC9nPjwvc3ZnPg==);

View File

@ -1,196 +0,0 @@
/**
* BetterDiscord Automated DOM Manipulations
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { Events, WebpackModules, EventListener, DiscordApi, ReactComponents, Renderer } from 'modules';
import { ClientLogger as Logger } from 'common';
import Reflection from './reflection';
import DOM from './dom';
import VueInjector from './vueinjector';
import EditedTimeStamp from './components/common/EditedTimestamp.vue';
import Autocomplete from './components/common/Autocomplete.vue';
export default class extends EventListener {
constructor(args) {
super(args);
}
bindings() {
this.manipAll = this.manipAll.bind(this);
this.markupInjector = this.markupInjector.bind(this);
this.setIds = this.setIds.bind(this);
this.setMessageIds = this.setMessageIds.bind(this);
this.setUserIds = this.setUserIds.bind(this);
}
get eventBindings() {
return [
// { id: 'server-switch', callback: this.manipAll },
// { id: 'channel-switch', callback: this.manipAll },
// { id: 'discord:MESSAGE_CREATE', callback: this.markupInjector },
// { id: 'discord:MESSAGE_UPDATE', callback: this.markupInjector },
{ id: 'gkh:keyup', callback: this.injectAutocomplete }
];
}
manipAll() {
try {
this.appMount.setAttribute('guild-id', DiscordApi.currentGuild.id);
this.appMount.setAttribute('channel-id', DiscordApi.currentChannel.id);
this.setIds();
this.makeMutable();
} catch (err) {
Logger.err('AutoManip', err);
}
}
markupInjector(e) {
if (!e.element) return;
this.setId(e.element);
const markup = e.element.querySelector('.markup:not(.mutable)');
if (markup) this.injectMarkup(markup, this.cloneMarkup(markup), false);
}
getEts(node) {
try {
const reh = Object.keys(node).find(k => k.startsWith('__reactInternalInstance'));
return node[reh].memoizedProps.children[node[reh].memoizedProps.children.length - 1].props.text;
} catch (err) {
return null;
}
}
makeMutable() {
for (const el of document.querySelectorAll('.markup:not(.mutable)')) {
this.injectMarkup(el, this.cloneMarkup(el), false);
}
}
cloneMarkup(node) {
const childNodes = [...node.childNodes];
const clone = document.createElement('div');
clone.className = 'markup mutable';
const ets = this.getEts(node);
for (const [cni, cn] of childNodes.entries()) {
if (cn.nodeType !== Node.TEXT_NODE) {
if (cn.className.includes('edited')) continue;
}
clone.appendChild(cn.cloneNode(true));
}
return { clone, ets }
}
injectMarkup(sibling, markup, reinject) {
if (sibling.className && sibling.className.includes('mutable')) return; // Ignore trying to make mutable again
let cc = null;
for (const cn of sibling.parentElement.childNodes) {
if (cn.className && cn.className.includes('mutable')) cc = cn;
}
if (cc) sibling.parentElement.removeChild(cc);
if (markup === true) markup = this.cloneMarkup(sibling);
sibling.parentElement.insertBefore(markup.clone, sibling);
sibling.classList.add('shadow');
sibling.style.display = 'none';
if (markup.ets) {
const etsRoot = document.createElement('span');
markup.clone.appendChild(etsRoot);
VueInjector.inject(etsRoot, {
components: { EditedTimeStamp },
data: { ets: markup.ets },
template: '<EditedTimeStamp :ets="ets" />'
});
}
Events.emit('ui:mutable:.markup', markup.clone);
}
setIds() {
this.setMessageIds();
this.setUserIds();
this.setChannelIds();
}
setMessageIds() {
for (let msg of document.querySelectorAll('.message')) {
this.setId(msg);
}
}
setUserIds() {
for (let user of document.querySelectorAll('.channel-members-wrap .member, .channel-members-wrap .member-2FrNV0')) {
this.setUserId(user);
}
}
setChannelIds() {
for (let channel of document.querySelectorAll('[class*=channels] [class*=containerDefault]')) {
this.setChannelId(channel);
}
}
setId(msg) {
if (msg.hasAttribute('message-id')) return;
const messageid = Reflection(msg).prop('message.id');
const authorid = Reflection(msg).prop('message.author.id');
if (!messageid || !authorid) {
const msgGroup = msg.closest('.message-group');
if (!msgGroup) return;
const userTest = Reflection(msgGroup).prop('user');
if (!userTest) return;
msgGroup.setAttribute('data-author-id', userTest.id);
if (userTest.id === DiscordApi.currentUserId) msgGroup.setAttribute('data-currentuser', true);
return;
}
msg.setAttribute('data-message-id', messageid);
const msgGroup = msg.closest('.message-group');
if (!msgGroup) return;
msgGroup.setAttribute('data-author-id', authorid);
if (authorid === DiscordApi.currentUser.id) msgGroup.setAttribute('data-currentuser', true);
}
setUserId(user) {
if (user.hasAttribute('data-user-id')) return;
const userid = Reflection(user).prop('user.id');
if (!userid) return;
user.setAttribute('data-user-id', userid);
const currentUser = userid === DiscordApi.currentUser.id;
if (currentUser) user.setAttribute('data-currentuser', true);
Events.emit('ui:useridset', user);
}
setChannelId(channel) {
if (channel.hasAttribute('data-channel-id')) return;
const channelObj = Reflection(channel).prop('channel');
if (!channelObj) return;
channel.setAttribute('data-channel-id', channelObj.id);
if (channelObj.nsfw) channel.setAttribute('data-channel-nsfw', true);
if (channelObj.type && channelObj.type === 2) channel.setAttribute('data-channel-voice', true);
}
get appMount() {
return document.getElementById('app-mount');
}
injectAutocomplete(e) {
if (document.querySelector('.bd-autocomplete')) return;
if (!e.target.closest('[class*=channelTextArea]')) return;
const root = document.createElement('span');
const parent = document.querySelector('[class*="channelTextArea"] > [class*="inner"]');
if (!parent) return;
parent.parentElement.insertBefore(root, parent);
VueInjector.inject(root, {
components: { Autocomplete },
data: { initial: e.target.value },
template: '<Autocomplete :initial="initial" />'
});
}
}

View File

@ -31,8 +31,6 @@ let items = 0;
export const BdMenuItems = new class {
constructor() {
window.bdmenu = this;
this.items = [];
const updater = this.add({category: 'Updates', contentid: 'updater', text: 'Updates available!', hidden: true});

View File

@ -8,13 +8,11 @@
* LICENSE file in the root directory of this source tree.
*/
import { Events, WebpackModules, DiscordApi, MonkeyPatch } from 'modules';
import { Utils } from 'common';
import { Events, DiscordApi } from 'modules';
import { remote } from 'electron';
import DOM from './dom';
import Vue from './vue';
import AutoManip from './automanip';
import { BdSettingsWrapper, BdModals } from './components';
import { BdSettingsWrapper, BdModals, BdToasts } from './components';
export default class {
@ -25,50 +23,35 @@ export default class {
channel: DiscordApi.currentChannel
};
window.addEventListener('keyup', e => Events.emit('gkh:keyup', e));
this.autoManip = new AutoManip();
remote.getCurrentWindow().webContents.on('did-navigate-in-page', (e, url, isMainFrame) => {
const { currentGuild, currentChannel } = DiscordApi;
const ehookInterval = setInterval(() => {
if (!remote.BrowserWindow.getFocusedWindow()) return;
clearInterval(ehookInterval);
remote.BrowserWindow.getFocusedWindow().webContents.on('did-navigate-in-page', (e, url, isMainFrame) => {
const { currentGuild, currentChannel } = DiscordApi;
if (!this.pathCache.server)
Events.emit('server-switch', { server: currentGuild, channel: currentChannel });
else if (!this.pathCache.channel)
Events.emit('channel-switch', currentChannel);
else if (currentGuild && currentGuild.id && this.pathCache.server && this.pathCache.server.id !== currentGuild.id)
Events.emit('server-switch', { server: currentGuild, channel: currentChannel });
else if (currentChannel && currentChannel.id && this.pathCache.channel && this.pathCache.channel.id !== currentChannel.id)
Events.emit('channel-switch', currentChannel);
if (!this.pathCache.server) {
Events.emit('server-switch', { server: currentGuild, channel: currentChannel });
this.pathCache.server = currentGuild;
this.pathCache.channel = currentChannel;
return;
}
if (!this.pathCache.channel) {
Events.emit('channel-switch', currentChannel);
this.pathCache.server = currentGuild;
this.pathCache.channel = currentChannel;
return;
}
if (currentGuild && currentGuild.id && this.pathCache.server && this.pathCache.server.id !== currentGuild.id) {
Events.emit('server-switch', { server: currentGuild, channel: currentChannel });
this.pathCache.server = currentGuild;
this.pathCache.channel = currentChannel;
return;
}
if (currentChannel && currentChannel.id && this.pathCache.channel && this.pathCache.channel.id !== currentChannel.id)
Events.emit('channel-switch', currentChannel);
this.pathCache.server = currentGuild;
this.pathCache.channel = currentChannel;
});
}, 100);
this.pathCache.server = currentGuild;
this.pathCache.channel = currentChannel;
});
}
static injectUi() {
DOM.createElement('div', null, 'bd-settings').appendTo(DOM.bdBody);
DOM.createElement('div', null, 'bd-modals').appendTo(DOM.bdModals);
DOM.createElement('div', null, 'bd-toasts').appendTo(DOM.bdToasts);
DOM.createElement('bd-tooltips').appendTo(DOM.bdBody);
this.toasts = new Vue({
el: '#bd-toasts',
components: { BdToasts },
template: '<BdToasts />'
});
this.modals = new Vue({
el: '#bd-modals',
components: { BdModals },

View File

@ -0,0 +1,47 @@
/**
* BetterDiscord Class Normaliser
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import { Module, WebpackModules } from 'modules';
export default class ClassNormaliser extends Module {
init() {
this.patchClassModules(WebpackModules.getModule(this.moduleFilter, false));
}
patchClassModules(modules) {
for (let module of modules) {
this.patchClassModule('da', module);
}
}
moduleFilter(module) {
if (typeof module !== 'object' || Array.isArray(module)) return false;
if (Array.isArray(module)) return false;
if (module.__esModule) return false;
if (!Object.keys(module).length) return false;
for (let baseClassName in module) {
if (typeof module[baseClassName] !== 'string') return false;
if (module[baseClassName].split('-').length === 1) return false;
const alphaNumeric = module[baseClassName].split(/-(.+)/)[1].split(' ')[0];
if (alphaNumeric.length !== 6) return false;
}
return true;
}
patchClassModule(componentName, classNames) {
for (let baseClassName in classNames) {
const normalised = baseClassName.split('-')[0].replace(/[A-Z]/g, m => `-${m}`).toLowerCase();
classNames[baseClassName] += ` ${componentName}-${normalised}`;
}
}
}

View File

@ -0,0 +1,10 @@
export { ReactComponent } from './vue';
export * from './components/common';
export { default as SettingsWrapper } from './components/bd/SettingsWrapper.vue';
export { default as SettingsPanel } from './components/bd/SettingsPanel.vue';
export { default as Setting } from './components/bd/setting/Setting.vue';
export { default as Card } from './components/bd/Card.vue';
export { default as ContentAuthor } from './components/bd/ContentAuthor.vue';
export { default as BdBadge } from './components/bd/BdBadge.vue';

View File

@ -47,9 +47,12 @@
Events.on('bd-refresh-modals', this.eventListener = () => {
this.$forceUpdate();
});
window.addEventListener('keyup', this.keyupListener);
},
destroyed() {
if (this.eventListener) Events.off('bd-refresh-modals', this.eventListener);
window.removeEventListener('keyup', this.keyupListener);
},
methods: {
closeModal(modal) {
@ -57,6 +60,10 @@
},
downscale(index, times) {
return 1 - ((this.modals.stack.filter(m => !m.closing).length - index) * times);
},
keyupListener(e) {
if (this.modals.stack.length && e.which === 27)
this.modals.closeLast(e.shiftKey);
}
}
}

View File

@ -75,7 +75,8 @@
first: true,
Settings,
timeout: null,
SettingsWrapper
SettingsWrapper,
openMenuHandler: null
};
},
props: ['active'],
@ -101,7 +102,8 @@
methods: {
itemOnClick(id) {
if (this.animating || id === this.activeIndex) return;
if (this.activeIndex >= 0) this.sidebarItems.find(item => item.id === this.activeIndex).active = false;
const activeItem = this.sidebarItems.find(item => item.id === this.activeIndex);
if (activeItem) activeItem.active = false;
this.sidebarItems.find(item => item.id === id).active = true;
this.animating = true;
this.lastActiveIndex = this.activeIndex;
@ -153,7 +155,10 @@
}
},
created() {
Events.on('bd-open-menu', item => item && this.itemOnClick(this.sidebarItems.find(i => i === item || i.id === item || i.contentid === item || i.set === item).id));
Events.on('bd-open-menu', this.openMenuHandler = item => item && this.itemOnClick(this.sidebarItems.find(i => i === item || i.id === item || i.contentid === item || i.set === item).id));
},
destroyed() {
if (this.openMenuHandler) Events.off('bd-open-menu', this.openMenuHandler);
}
}
</script>

View File

@ -23,6 +23,7 @@
// Imports
import { Events, Settings } from 'modules';
import { Modals } from 'ui';
import process from 'process';
import BdSettings from './BdSettings.vue';
export default {
@ -33,7 +34,9 @@
active: false,
animating: false,
timeout: null,
platform: global.process.platform
platform: process.platform,
eventHandlers: {},
keybindHandler: null
};
},
components: {
@ -65,21 +68,29 @@
}
},
created() {
Events.on('ready', e => this.loaded = true);
Events.on('bd-open-menu', item => this.active = true);
Events.on('bd-close-menu', () => this.active = false);
Events.on('update-check-start', e => this.updating = 0);
Events.on('update-check-end', e => this.updating = 1);
Events.on('updates-available', e => this.updating = 2);
Events.on('ready', this.eventHandlers.ready = e => this.loaded = true);
Events.on('bd-open-menu', this.eventHandlers['bd-open-menu'] = item => this.active = true);
Events.on('bd-close-menu', this.eventHandlers['bd-close-menu'] = () => this.active = false);
Events.on('update-check-start', this.eventHandlers['update-check-start'] = e => this.updating = 0);
Events.on('update-check-end', this.eventHandlers['update-check-end'] = e => this.updating = 1);
Events.on('updates-available', this.eventHandlers['updates-available'] = e => this.updating = 2);
window.addEventListener('keyup', this.keyupListener);
window.addEventListener('keydown', this.prevent, true);
const menuKeybind = Settings.getSetting('core', 'default', 'menu-keybind');
menuKeybind.on('keybind-activated', () => this.active = !this.active);
menuKeybind.on('keybind-activated', this.keybindHandler = () => this.active = !this.active);
},
destroyed() {
for (let event in this.eventHandlers) Events.off(event, this.eventHandlers[event]);
window.removeEventListener('keyup', this.keyupListener);
window.removeEventListener('keydown', this.prevent);
if (this.keybindHandler) {
const menuKeybind = Settings.getSetting('core', 'default', 'menu-keybind');
menuKeybind.removeListener('keybind-activated', this.keybindHandler = () => this.active = !this.active);
}
}
}
</script>

View File

@ -0,0 +1,32 @@
/**
* BetterDiscord Toasts Component
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
<template>
<div class="bd-toasts">
<Toast v-for="(toast, index) in toasts" :message="toast.message" :type="toast.type" :icon="toast.icon" :class="toast.additionalClasses" :closing="toast.closing" :key="toast.id"></Toast>
</div>
</template>
<script>
// Imports
import { Toasts } from 'ui';
import { Toast } from './common';
export default {
components: {
Toast
},
data() {
return {
toasts: Toasts.stack
};
}
}
</script>

View File

@ -9,12 +9,10 @@
*/
<template>
<div class="bd-profile-badges-wrap">
<div class="bd-profile-badges">
<div v-if="developer" v-tooltip="'BetterDiscord Developer'" class="bd-profile-badge bd-profile-badge-developer" @click="click"></div>
<div v-else-if="webdev" v-tooltip="'BetterDiscord Web Developer'" class="bd-profile-badge bd-profile-badge-developer" @click="click"></div>
<div v-else-if="contributor" v-tooltip="'BetterDiscord Contributor'" class="bd-profile-badge bd-profile-badge-contributor" @click="click"></div>
</div>
<div class="bd-profile-badges" :class="[`bd-profile-badges-${type}`]" @click.stop>
<div v-if="contributor.developer" v-tooltip="'BetterDiscord Developer'" class="bd-profile-badge bd-profile-badge-developer" @click="click"></div>
<div v-else-if="contributor.webdev" v-tooltip="'BetterDiscord Web Developer'" class="bd-profile-badge bd-profile-badge-developer" @click="click"></div>
<div v-else-if="contributor.contributor" v-tooltip="'BetterDiscord Contributor'" class="bd-profile-badge bd-profile-badge-contributor" @click="click"></div>
</div>
</template>
@ -23,12 +21,12 @@
import { shell } from 'electron';
export default {
props: ['webdev', 'developer', 'contributor'],
props: ['contributor', 'type'],
methods: {
click() {
if (this.developer) return shell.openExternal('https://github.com/JsSucks/BetterDiscordApp');
if (this.webdev) return shell.openExternal('https://betterdiscord.net');
if (this.contributor) return shell.openExternal('https://github.com/JsSucks/BetterDiscordApp/graphs/contributors');
if (this.contributor.developer) return shell.openExternal('https://github.com/JsSucks/BetterDiscordApp');
if (this.contributor.webdev) return shell.openExternal('https://betterdiscord.net');
if (this.contributor.contributor) return shell.openExternal('https://github.com/JsSucks/BetterDiscordApp/graphs/contributors');
}
}
}

View File

@ -1,33 +0,0 @@
/**
* BetterDiscord BD Message Badge Component
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
<template>
<div class="bd-message-badges-wrap" @click.stop>
<div v-if="developer" v-tooltip="'BetterDiscord Developer'" class="bd-message-badge bd-message-badge-developer" @click="click"></div>
<div v-else-if="webdev" v-tooltip="'BetterDiscord Web Developer'" class="bd-message-badge bd-message-badge-developer" @click="click"></div>
<div v-else-if="contributor" v-tooltip="'BetterDiscord Contributor'" class="bd-message-badge bd-message-badge-contributor" @click="click"></div>
</div>
</template>
<script>
// Imports
import { shell } from 'electron';
export default {
props: ['webdev', 'developer', 'contributor', 'hasBadges'],
methods: {
click() {
if (this.developer) return shell.openExternal('https://github.com/JsSucks/BetterDiscordApp');
if (this.webdev) return shell.openExternal('https://betterdiscord.net');
if (this.contributor) return shell.openExternal('https://github.com/JsSucks/BetterDiscordApp/graphs/contributors');
}
}
}
</script>

View File

@ -11,7 +11,7 @@
<template>
<div class="bd-card">
<div class="bd-card-header">
<div class="bd-card-icon" :style="{backgroundImage: item.icon ? `url(${item.icon})` : null}">
<div class="bd-card-icon" :style="{backgroundImage: iconURL}">
<MiExtension v-if="!item.icon" :size="30" />
</div>
<span>{{item.name}}</span>
@ -37,14 +37,41 @@
<script>
// Imports
import { FileUtils, ClientLogger as Logger } from 'common';
import path from 'path';
import { MiExtension } from '../common';
import ContentAuthor from './ContentAuthor.vue';
export default {
props: ['item'],
data() {
return {
iconURL: undefined
};
},
components: {
ContentAuthor,
MiExtension
},
methods: {
async getIconURL() {
if (!this.item.icon) return;
try {
if (this.item.icon.substr(0, 5) === 'data:') {
return `url(${this.item.icon})`;
}
const iconPath = path.join(this.item.contentPath, this.item.icon);
const iconURL = await FileUtils.toDataURI(iconPath, this.item.info.icon_type);
return `url(${iconURL})`;
} catch (err) {
Logger.err('ContentCard', ['Invalid icon URL', this.item]);
}
}
},
async created() {
this.iconURL = await this.getIconURL();
}
}
</script>

View File

@ -10,12 +10,6 @@
<template>
<v-popover class="bd-content-author bd-inline" popoverClass="bd-popover bd-content-author-links" trigger="click" placement="top" :disabled="!hasLinks">
<!-- <template v-if="typeof author === 'string'">{{ author }}</template>
<a v-else-if="author.url" :href="author.url" @click="openLink">{{ author.name }}</a>
<a v-else-if="author.github_username" :href="'https://github.com/' + author.github_username" @click="openLink">{{ author.name }}</a>
<span v-else-if="author.discord_id" @click="openUserProfileModal(author.discord_id)">{{ author.name }}</span>
<template v-else>{{ author.name }}</template> -->
<span :class="{'bd-content-author-link': hasLinks}">{{ author.name || author }}</span><span v-text="after" @click.stop></span>
<template slot="popover">
@ -50,8 +44,7 @@
e.preventDefault();
},
openUserProfileModal(discord_id) {
const UserProfileModal = WebpackModules.getModuleByProps(['fetchMutualFriends', 'setSection']);
UserProfileModal.open(discord_id);
WebpackModules.getModuleByName('UserProfileModal').open(discord_id);
BdMenu.close();
},
openGitHub() {

View File

@ -5,4 +5,3 @@ export { default as PluginsView } from './PluginsView.vue';
export { default as ThemesView } from './ThemesView.vue';
export { default as UpdaterView } from './UpdaterView.vue';
export { default as BdBadge } from './BdBadge.vue';
export { default as BdMessageBadge } from './BdMessageBadge.vue';

View File

@ -0,0 +1,193 @@
/**
* BetterDiscord Window Preferences Component
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
<template>
<div class="bd-window-preferences">
<div v-if="disabled" class="bd-form-item bd-window-preferences-disabled">
<p>You can't edit your window preferences here as you are using a JavaScript file to set window preferences.</p>
<FormButton @click="openFile">Open</FormButton>
<p class="bd-hint">This will open {{ filePath }} in your system's default editor.</p>
</div>
<div v-else-if="settingsSet" class="bd-form-item">
<SettingsPanel :settings="settingsSet" />
<p class="bd-hint">You must fully restart Discord for changes here to take effect.</p>
<FormButton @click="restart">Restart</FormButton>
<FormButton @click="openFile">Open</FormButton>
<p class="bd-hint">This will open {{ filePath }} in your system's default editor.</p>
</div>
<div v-else>
<p>Loading...</p>
</div>
</div>
</template>
<script>
import { Globals } from 'modules';
import { SettingsSet } from 'structs';
import { Utils, FileUtils, ClientLogger as Logger } from 'common';
import path from 'path';
import { remote, shell } from 'electron';
import SettingsPanel from '../SettingsPanel.vue';
import { FormButton } from '../../common';
export default {
props: ['setting'],
data() {
return {
saved: {},
settingsSet: null,
disabled: false
};
},
components: {
// SettingsPanel,
FormButton
},
computed: {
filePath() {
return Globals.require.resolve(path.join(Globals.getPath('data'), 'window'));
}
},
methods: {
openFile() {
return shell.openExternal('file://' + this.filePath);
},
restart() {
remote.app.relaunch();
remote.app.quit();
},
async listener(event) {
const newPreferences = Utils.deepclone(this.saved);
if (event.category.id === 'default' && event.setting.id === 'transparent') {
newPreferences.transparent = event.value;
if (event.value) delete newPreferences.backgroundColor;
}
if (event.category.id === 'default' && event.setting.id === 'background-colour') {
newPreferences.backgroundColor = event.value;
}
if (event.category.id === 'default' && event.setting.id === 'frame') {
newPreferences.frame = event.value;
}
if (event.category.id === 'advanced' && event.setting.id === 'experimental-features') {
if (!newPreferences.webPreferences) newPreferences.webPreferences = {};
newPreferences.webPreferences.experimentalFeatures = event.value;
}
if (event.category.id === 'advanced' && event.setting.id === 'preload') {
if (!newPreferences.webPreferences) newPreferences.webPreferences = {};
newPreferences.webPreferences.preload = event.value;
}
if (event.category.id === 'advanced' && event.setting.id === 'webview-tag') {
if (!newPreferences.webPreferences) newPreferences.webPreferences = {};
newPreferences.webPreferences.webviewTag = event.value;
}
try {
await FileUtils.writeJsonToFile(this.filePath, newPreferences, true);
await this.update();
} catch (err) {
Logger.err('WindowPreferences', ['Failed to update window preferences:', err]);
}
},
async update() {
this.saved = await FileUtils.readJsonFromFile(this.filePath) || {};
this.settingsSet.getSetting('default', 'transparent').value = this.saved.transparent;
this.settingsSet.getSetting('default', 'background-colour').value = this.saved.backgroundColor;
this.settingsSet.getSetting('default', 'frame').value = this.saved.frame;
this.settingsSet.getSetting('advanced', 'experimental-features').value = this.saved.webPreferences && this.saved.webPreferences.experimentalFeatures;
this.settingsSet.getSetting('advanced', 'preload').value = this.saved.webPreferences && this.saved.webPreferences.preload;
this.settingsSet.getSetting('advanced', 'webview-tag').value = this.saved.webPreferences && this.saved.webPreferences.webviewTag;
this.settingsSet.setSaved();
}
},
beforeCreate() {
// https://vuejs.org/v2/guide/components.html#Circular-References-Between-Components
this.$options.components.SettingsPanel = SettingsPanel;
},
async created() {
if (this.filePath !== path.join(Globals.getPath('data'), 'window.json')) {
this.disabled = true;
return;
}
this.settingsSet = new SettingsSet({});
const category = await this.settingsSet.addCategory({id: 'default'});
await category.addSetting({
id: 'transparent',
type: 'bool',
text: 'Transparent',
hint: 'Removes the window background. This requires the background colour option to be disabled, and a theme to remove any higher backgrounds.'
});
await category.addSetting({
id: 'background-colour',
type: 'colour',
text: 'Background colour',
hint: 'Sets the background colour under any elements.'
});
await category.addSetting({
id: 'frame',
type: 'bool',
text: 'Frame',
hint: 'Shows the desktop environment\'s default window frame.'
});
const advanced = await this.settingsSet.addCategory({
id: 'advanced',
name: 'Advanced',
type: 'drawer'
});
await advanced.addSetting({
id: 'experimental-features',
type: 'bool',
text: 'Experimental features',
hint: 'Enables Chromium\'s experimental features.'
});
await advanced.addSetting({
id: 'preload',
type: 'text',
text: 'Preload script',
hint: 'The path of a JavaScript file relative to the BetterDiscord data directory to run before the window is loaded.'
});
await advanced.addSetting({
id: 'webview-tag',
type: 'bool',
text: 'Webview tag',
hint: 'Enables the webview tag. If you enable this you should use a preload script to restrict how the webview tag can be used.'
});
try {
await this.update();
} catch (err) {
Logger.err('WindowPreferences', ['Failed to read window preferences:', err]);
}
this.settingsSet.on('setting-updated', this.listener);
},
unmounted() {
if (this.settingsSet) this.settingsSet.removeListener('setting-updated', this.listener);
}
}
</script>

View File

@ -0,0 +1 @@
export { default as WindowPreferences } from './WindowPreferences.vue';

View File

@ -1,6 +1,6 @@
<template>
<Modal :headerText="modal.event.header" @close="modal.close"
:class="[{'bd-err': modal.event.type && modal.event.type === 'err'}, {'bd-modal-out': modal.closing}]">
:class="{'bd-err': modal.event.type && modal.event.type === 'err', 'bd-modal-out': modal.closing}">
<MiError v-if="modal.event.type === 'err'" slot="icon" size="20"/>
<div slot="body">
<div v-for="(content, index) in modal.event.content">
@ -28,11 +28,10 @@
<script>
// Imports
import process from 'process';
import { Modal } from '../../common';
import { MiError } from '../../common/MaterialIcon';
const process = window.require('process');
export default {
props: ['modal'],
components: {

View File

@ -37,19 +37,18 @@
import { Permissions } from 'modules';
export default {
data() {
return { permissions: [] }
},
props: ['modal'],
components: {
Modal
},
beforeMount() {
this.permissions = this.modal.perms.map(perm => {
const getPerm = Permissions.permissionText(perm);
getPerm.BODY = getPerm.BODY.replace(':NAME:', this.modal.name);
return getPerm;
});
computed: {
permissions() {
return this.modal.perms.map(perm => {
const getPerm = Permissions.permissionText(perm);
getPerm.BODY = getPerm.BODY.replace(':NAME:', this.modal.name);
return getPerm;
});
}
}
}
</script>

View File

@ -37,7 +37,8 @@
warnclose: false,
settings: null,
closing: false,
saving: false
saving: false,
closeHandler: null
}
},
components: {
@ -70,7 +71,7 @@
}
},
created() {
this.modal.on('close', force => {
this.modal.on('close', this.closeHandler = force => {
if (this.changed && !force) {
this.warnclose = true;
setTimeout(() => this.warnclose = false, 400);
@ -82,6 +83,7 @@
this.cloneSettings();
},
destroyed() {
if (this.closeHandler) this.modal.removeListener('close', this.closeHandler);
this.modal.settings.off('settings-updated', this.cloneSettings);
}
}

View File

@ -27,10 +27,11 @@
</template>
<script>
import { PluginManager } from 'modules';
import { Globals, PluginManager } from 'modules';
import SettingsPanel from '../SettingsPanel.vue';
import Drawer from '../../common/Drawer.vue';
import Button from '../../common/Button.vue';
import * as InternalSettings from '../internal-settings';
import path from 'path';
export default {
@ -41,12 +42,12 @@
},
computed: {
component() {
if (typeof this.setting.file === 'string') {
const component = window.require(path.join(this.setting.path, this.setting.file));
if (this.setting.path && typeof this.setting.file === 'string') {
const component = Globals.require(path.join(this.setting.path, this.setting.file));
return this.setting.component ? component[this.setting.component] : component.default ? component.default : component;
}
if (typeof this.setting.function === 'string') {
if (this.setting.path && typeof this.setting.function === 'string') {
const plugin = PluginManager.getPluginByPath(this.setting.path);
if (!plugin) return;
const component = plugin[this.setting.function](this.setting, this.change);
@ -55,16 +56,18 @@
return component;
}
if (typeof this.setting.component === 'string') {
if (this.setting.path && typeof this.setting.component === 'string') {
const plugin = PluginManager.getPluginByPath(this.setting.path);
if (!plugin) return;
const component = plugin[this.setting.component];
return component;
if (plugin && plugin[this.setting.component]) return plugin[this.setting.component];
}
if (typeof this.setting.component === 'object') {
return this.setting.component;
}
if (typeof this.setting.component === 'string' && InternalSettings[this.setting.component]) {
return InternalSettings[this.setting.component];
}
}
},
methods: {

View File

@ -20,8 +20,8 @@
<!-- Maybe add a preview here later? -->
<!-- For now just show the selected file path -->
<span class="bd-file-path">{{ file_path }}</span>
<span class="bd-file-open" @click="() => openItem(file_path)"><MiOpenInNew /></span>
<span class="bd-file-remove" :class="{'bd-disabled': setting.disabled}" @click="() => removeItem(file_path)"><MiMinus /></span>
<span class="bd-file-open" @click="openItem(file_path)"><MiOpenInNew /></span>
<span class="bd-file-remove" :class="{'bd-disabled': setting.disabled}" @click="removeItem(file_path)"><MiMinus /></span>
</div>
</div>
</div>
@ -29,7 +29,7 @@
<script>
import { shell } from 'electron';
import { ClientIPC } from 'common';
import { Utils, ClientIPC } from 'common';
import { MiOpenInNew, MiMinus } from '../../common';
import path from 'path';
@ -51,7 +51,7 @@
},
removeItem(file_path) {
if (this.setting.disabled) return;
this.setting = this.setting.value.filter(f => f !== file_path);
this.setting.value = Utils.removeFromArray(this.setting.value, file_path);
}
}
}

View File

@ -30,8 +30,12 @@
textarea.style.height = textarea.scrollHeight + 2 + 'px';
}
},
watch: {
'setting.value'() {
this.recalculateHeight();
}
},
mounted() {
this.$watch('setting.value', this.recalculateHeight);
this.recalculateHeight();
}
}

View File

@ -16,7 +16,7 @@
</div>
<div class="bd-hint">{{setting.hint}}</div>
</div>
<RadioGroup :options="setting.options" v-model="setting.value" :disabled="setting.disabled" />
<RadioGroup :options="setting.options" v-model="setting.value" :multi="setting.multi" :min="setting.min" :max="setting.max" :disabled="setting.disabled" />
</div>
</template>

View File

@ -37,7 +37,7 @@
<script>
import { EmoteModule } from 'builtin';
import { Events, Settings } from 'modules';
import { DOM } from 'ui';
import { DOMManip as Manip } from 'ui';
import { MiStar } from './MaterialIcon';
export default {
@ -53,25 +53,26 @@
selected: '',
open: false,
selectedIndex: 0,
sterm: ''
sterm: '',
settingUpdatedHandler: null
};
},
props: ['initial'],
beforeMount() {
// this.emotes = EmoteModule.filter(new RegExp(this.initial, 'i'), 10);
// this.open = this.emotes.length;
},
created() {
const enabled = Settings.getSetting('emotes', 'default', 'enable');
enabled.on('setting-updated', event => {
const enabledSetting = Settings.getSetting('emotes', 'default', 'enable');
enabledSetting.on('setting-updated', this.settingUpdatedHandler = event => {
if (event.value) return this.addEventListeners();
this.removeEventListeners();
this.reset();
});
if (enabled.value) this.addEventListeners();
if (enabledSetting.value) this.addEventListeners();
},
destroyed() {
if (this.settingUpdatedHandler) {
const enabledSetting = Settings.getSetting('emotes', 'default', 'enable');
enabledSetting.removeListener('setting-updated', this.settingUpdatedHandler);
}
this.removeEventListeners();
},
methods: {
@ -134,8 +135,8 @@
this.reset();
return;
}
this.title = this.sterm;
this.emotes = EmoteModule.filter(new RegExp(this.sterm.substr(1), ''), 10);
this.title = this.sterm.substr(1);
this.emotes = EmoteModule.filter(new RegExp(this.sterm.substr(1), 'i'), 10);
this.open = this.emotes.length;
},
traverse(e) {
@ -167,7 +168,7 @@
let substr = value.substr(0, selectionEnd);
substr = substr.replace(new RegExp(this.sterm + '$'), en);
DOM.manip.setText(substr + value.substr(selectionEnd, value.length), false);
Manip.setText(substr + value.substr(selectionEnd, value.length), false);
ta.selectionEnd = ta.selectionStart = selectionEnd + en.length - this.sterm.length;
this.reset();
}

View File

@ -27,7 +27,8 @@
props: ['options', 'value', 'disabled'],
data() {
return {
active: false
active: false,
clickHandler: null
};
},
methods: {
@ -41,12 +42,15 @@
}
},
mounted() {
document.addEventListener('click', e => {
document.addEventListener('click', this.clickHandler = e => {
let options = this.$refs.options;
if (options && !options.contains(e.target) && options !== e.target) {
this.active = false;
}
});
},
beforeDestroy() {
if (this.clickHandler) document.removeEventListener('click', this.clickHandler);
}
}
</script>

View File

@ -1,8 +0,0 @@
<template>
<span class="edited" v-tooltip="ets">(edited)</span>
</template>
<script>
export default {
props: ['ets']
}
</script>

View File

@ -14,3 +14,6 @@ export { default as MiExtension } from './materialicons/Extension.vue';
export { default as MiError } from './materialicons/Error.vue';
export { default as MiDiscord } from './materialicons/Discord.vue';
export { default as MiStar } from './materialicons/Star.vue';
export { default as MiInfo } from './materialicons/Info.vue';
export { default as MiWarning } from './materialicons/Warning.vue';
export { default as MiSuccess } from './materialicons/Success.vue';

View File

@ -47,18 +47,6 @@
return {
scrolled: false
};
},
beforeMount() {
window.addEventListener('keyup', this.keyupListener);
},
destroyed() {
window.removeEventListener('keyup', this.keyupListener);
},
methods: {
keyupListener(e) {
if (e.which === 27)
this.$emit('close', false, e);
}
}
}
</script>

View File

@ -10,7 +10,7 @@
<template>
<div class="bd-radio-group" :class="{'bd-disabled': disabled}">
<label class="bd-radio" v-for="option in options" :class="{'bd-radio-selected': value === option.value}" @click="$emit('input', option.value)">
<label class="bd-radio" v-for="option in options" :class="{'bd-radio-selected': isSelected(option.value)}" @click="toggleOption(option.value)">
<div class="bd-radio-control-wrap">
<svg class="bd-radio-control" name="Checkmark" width="18" height="18" viewBox="0 0 18 18" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd"><polyline stroke="#3e82e5" stroke-width="2" points="3.5 9.5 7 13 15 5"></polyline></g>
@ -22,7 +22,32 @@
</template>
<script>
import { Utils } from 'common';
export default {
props: ['options', 'value', 'disabled']
props: ['options', 'value', 'multi', 'min', 'max', 'disabled'],
methods: {
toggleOption(value) {
if (!this.multi)
return this.$emit('input', value);
const values = this.value instanceof Array ? this.value : [this.value];
if (values.find(v => Utils.compare(v, value))) {
if (this.min && (values.length - 1) <= this.min) return;
this.$emit('input', values.filter(v => !Utils.compare(v, value)));
} else {
if (this.max && values.length > this.max) return;
this.$emit('input', values.concat([value]));
}
},
isSelected(value) {
if (!this.multi)
return Utils.compare(this.value, value);
const values = this.value instanceof Array ? this.value : [this.value];
return values.find(v => Utils.compare(v, value));
}
}
}
</script>

View File

@ -0,0 +1,46 @@
/**
* BetterDiscord Toast Component
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
<template>
<div :class="['bd-toast', 'bd-toast-' + type, {'bd-toast-has-icon': type !== 'basic' || icon, 'bd-toast-closing': closing}]">
<div class="bd-toast-icon" v-if="type !== 'basic' || icon">
<img v-if="icon" :src="icon" width="20" height="20" />
<MiSuccess v-else-if="type === 'success'" size="20" />
<MiWarning v-else-if="type === 'warning'" size="20" />
<MiInfo v-else-if="type === 'info'" size="20" />
<MiError v-else-if="type === 'error'" size="20" />
</div>
<div class="bd-toast-text">
{{message}}
</div>
</div>
</template>
<script>
// Imports
import { MiSuccess, MiWarning, MiError, MiInfo } from './MaterialIcon';
export default {
props: {
message: String,
icon: String,
type: {
default: 'basic',
validator(value) {
return ['success', 'warning', 'error', 'info', 'basic'].indexOf(value) !== -1
}
},
closing: {type: Boolean, default: false}
},
components: {
MiSuccess, MiWarning, MiError, MiInfo
}
}
</script>

View File

@ -4,4 +4,10 @@ export { default as FormButton } from './FormButton.vue';
export { default as ButtonGroup } from './ButtonGroup.vue';
export { default as Button } from './Button.vue';
export { default as Modal } from './Modal.vue';
export { default as Toast } from './Toast.vue';
export * from './MaterialIcon';
export { default as RefreshBtn } from './RefreshBtn.vue';
export { default as RadioGroup } from './RadioGroup.vue';
export { default as Dropdown } from './Dropdown.vue';
export { default as Drawer } from './Drawer.vue';

View File

@ -16,7 +16,7 @@
<template>
<span class="bd-material-design-icon">
<svg :width="size || 24" :height="size || 24" viewBox="0 0 24 24">
<path d="M 22,24L 16.75,19L 17.375,21L 4.5,21C 3.11929,21 2,19.8807 2,18.5L 2,3.5C 2,2.11929 3.11929,1 4.5,1L 19.5,1C 20.8807,1 22,2.11929 22,3.5L 22,24 Z M 12,6.8C 9.31508,6.8 7.4401,7.95052 7.4401,7.95052C 8.47135,7.02864 10.2682,6.48177 10.2682,6.48177L 10.0964,6.32552C 8.40885,6.35677 6.8776,7.52864 6.8776,7.52864C 5.15885,11.1224 5.26823,14.2161 5.26823,14.2161C 6.67448,16.0286 8.7526,15.9036 8.7526,15.9036L 9.45573,14.9974C 8.20572,14.7318 7.42448,13.6224 7.42448,13.6224C 7.42448,13.6224 9.29946,14.9 12,14.9C 14.7005,14.9 16.5755,13.6224 16.5755,13.6224C 16.5755,13.6224 15.7943,14.7318 14.5443,14.9974L 15.2474,15.9036C 15.2474,15.9036 17.3255,16.0286 18.7318,14.2161C 18.7318,14.2161 18.8411,11.1224 17.1224,7.52865C 17.1224,7.52865 15.5911,6.35677 13.9036,6.32552L 13.7318,6.48177C 13.7318,6.48177 15.5286,7.02865 16.5599,7.95052C 16.5599,7.95052 14.6849,6.80001 12,6.8 Z M 9.93143,10.5886C 10.5829,10.5886 11.1086,11.16 11.0971,11.8571C 11.0971,12.5543 10.5829,13.1257 9.93143,13.1257C 9.29143,13.1257 8.76571,12.5543 8.76571,11.8571C 8.76571,11.16 9.28,10.5886 9.93143,10.5886 Z M 14.1028,10.5886C 14.7543,10.5886 15.2686,11.16 15.2686,11.8572C 15.2686,12.5543 14.7543,13.1257 14.1028,13.1257C 13.4628,13.1257 12.9371,12.5543 12.9371,11.8572C 12.9371,11.16 13.4514,10.5886 14.1028,10.5886 Z " />
<path d="M 22,24L 16.75,19L 17.375,21L 4.5,21C 3.11929,21 2,19.8807 2,18.5L 2,3.5C 2,2.11929 3.11929,1 4.5,1L 19.5,1C 20.8807,1 22,2.11929 22,3.5L 22,24 Z M 12,6.8C 9.31508,6.8 7.4401,7.95052 7.4401,7.95052C 8.47135,7.02864 10.2682,6.48177 10.2682,6.48177L 10.0964,6.32552C 8.40885,6.35677 6.8776,7.52864 6.8776,7.52864C 5.15885,11.1224 5.26823,14.2161 5.26823,14.2161C 6.67448,16.0286 8.7526,15.9036 8.7526,15.9036L 9.45573,14.9974C 8.20572,14.7318 7.42448,13.6224 7.42448,13.6224C 7.42448,13.6224 9.29946,14.9 12,14.9C 14.7005,14.9 16.5755,13.6224 16.5755,13.6224C 16.5755,13.6224 15.7943,14.7318 14.5443,14.9974L 15.2474,15.9036C 15.2474,15.9036 17.3255,16.0286 18.7318,14.2161C 18.7318,14.2161 18.8411,11.1224 17.1224,7.52865C 17.1224,7.52865 15.5911,6.35677 13.9036,6.32552L 13.7318,6.48177C 13.7318,6.48177 15.5286,7.02865 16.5599,7.95052C 16.5599,7.95052 14.6849,6.80001 12,6.8 Z M 9.93143,10.5886C 10.5829,10.5886 11.1086,11.16 11.0971,11.8571C 11.0971,12.5543 10.5829,13.1257 9.93143,13.1257C 9.29143,13.1257 8.76571,12.5543 8.76571,11.8571C 8.76571,11.16 9.28,10.5886 9.93143,10.5886 Z M 14.1028,10.5886C 14.7543,10.5886 15.2686,11.16 15.2686,11.8572C 15.2686,12.5543 14.7543,13.1257 14.1028,13.1257C 13.4628,13.1257 12.9371,12.5543 12.9371,11.8572C 12.9371,11.16 13.4514,10.5886 14.1028,10.5886 Z " />
</svg>
</span>
</template>

View File

@ -0,0 +1,28 @@
/**
* BetterDiscord Material Design Icon
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* Material Design Icons
* Copyright (c) 2014 Google
* Apache 2.0 LICENSE
* https://www.apache.org/licenses/LICENSE-2.0.txt
*/
<template>
<span class="bd-material-design-icon">
<svg :width="size || 24" :height="size || 24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z"/>
</svg>
</span>
</template>
<script>
export default {
props: ['size']
}
</script>

View File

@ -16,7 +16,7 @@
<template>
<span class="bd-material-design-icon">
<svg :width="size || 24" :height="size || 24" viewBox="0 0 24 24">
<path d="M 11.9994,17.2708L 18.1794,20.9978L 16.5444,13.9688L 21.9994,9.24277L 14.8084,8.62477L 11.9994,1.99777L 9.1904,8.62477L 1.9994,9.24277L 7.4544,13.9688L 5.8194,20.9978L 11.9994,17.2708 Z " />
<path d="M 11.9994,17.2708L 18.1794,20.9978L 16.5444,13.9688L 21.9994,9.24277L 14.8084,8.62477L 11.9994,1.99777L 9.1904,8.62477L 1.9994,9.24277L 7.4544,13.9688L 5.8194,20.9978L 11.9994,17.2708 Z " />
</svg>
</span>
</template>

View File

@ -0,0 +1,28 @@
/**
* BetterDiscord Material Design Icon
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* Material Design Icons
* Copyright (c) 2014 Google
* Apache 2.0 LICENSE
* https://www.apache.org/licenses/LICENSE-2.0.txt
*/
<template>
<span class="bd-material-design-icon">
<svg :width="size || 24" :height="size || 24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>
</span>
</template>
<script>
export default {
props: ['size']
}
</script>

View File

@ -0,0 +1,28 @@
/**
* BetterDiscord Material Design Icon
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* Material Design Icons
* Copyright (c) 2014 Google
* Apache 2.0 LICENSE
* https://www.apache.org/licenses/LICENSE-2.0.txt
*/
<template>
<span class="bd-material-design-icon">
<svg :width="size || 24" :height="size || 24" viewBox="0 0 24 24">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2v-4h2v4z"/>
</svg>
</span>
</template>
<script>
export default {
props: ['size']
}
</script>

View File

@ -1,3 +1,4 @@
export { default as BdSettingsWrapper } from './BdSettingsWrapper.vue';
export { default as BdSettings } from './BdSettings.vue';
export { default as BdModals } from './BdModals.vue';
export { default as BdToasts } from './BdToasts.vue';

View File

@ -8,6 +8,8 @@
* LICENSE file in the root directory of this source tree.
*/
import { Utils, ClientLogger as Logger } from 'common';
class BdNode {
constructor(tag, className, id) {
this.element = document.createElement(tag);
@ -30,12 +32,16 @@ class BdNode {
}
}
class DOMObserver {
constructor() {
export class DOMObserver {
constructor(root, options) {
this.observe = this.observe.bind(this);
this.subscribe = this.subscribe.bind(this);
this.observerCallback = this.observerCallback.bind(this);
this.active = false;
this.root = root || document.getElementById('app-mount');
this.options = options || { attributes: true, childList: true, subtree: true };
this.observer = new MutationObserver(this.observerCallback);
this.observe();
}
@ -43,46 +49,103 @@ class DOMObserver {
observerCallback(mutations) {
for (let sub of this.subscriptions) {
try {
const f = sub.type && sub.type === 'filter' ? mutations.filter(sub.filter) : mutations.find(sub.filter);
if (!f) continue;
if (sub.type && sub.type === 'filter' && !f.length) continue;
sub.callback(f);
} catch(err) {}
const filteredMutations = sub.filter ? mutations.filter(sub.filter) : mutations;
if (sub.group) {
if (!filteredMutations.length) continue;
sub.callback.call(sub.bind || sub, filteredMutations);
} else {
for (let mutation of filteredMutations) sub.callback.call(sub.bind || sub, mutation);
}
} catch (err) {
Logger.warn('DOMObserver', [`Error in observer callback`, err]);
}
}
}
/**
* Starts observing the element. This will be called when attaching a callback.
* You don't need to call this manually.
*/
observe() {
if (this.active) return;
this.observer.observe(this.root, this.options);
this.active = true;
}
get root() {
return document.getElementById('app-mount');
/**
* Disconnects this observer. This stops callbacks being called, but does not unbind them.
* You probably want to use observer.unsubscribeAll instead.
*/
disconnect() {
if (!this.active) return;
this.observer.disconnect();
this.active = false;
}
get options() {
return { attributes: true, childList: true, subtree: true };
reconnect() {
if (this.active) {
this.disconnect();
this.observe();
}
}
get root() { return this._root }
set root(root) { this._root = root; this.reconnect(); }
get options() { return this._options }
set options(options) { this._options = options; this.reconnect(); }
get subscriptions() {
return this._subscriptions || (this._subscriptions = []);
}
subscribe(id, filter, callback, type) {
if (this.subscriptions.find(sub => sub.id === id)) return;
this.subscriptions.push({
id,
filter,
callback,
type
});
/**
* Subscribes to mutations.
* @param {Function} callback A function to call when on a mutation
* @param {Function} filter A function to call to filter mutations
* @param {Any} bind Something to bind the callback to
* @param {Boolean} group Whether to call the callback with an array of mutations instead of a single mutation
* @return {Object}
*/
subscribe(callback, filter, bind, group) {
const subscription = { callback, filter, bind, group };
this.subscriptions.push(subscription);
this.observe();
return subscription;
}
unsubscribe(id) {
const index = this.subscriptions.find(sub => sub.id === id);
if (index < 0) return;
this.subscriptions.splice(index, 1);
/**
* Removes a subscription and disconnect if there are none left.
* @param {Object} subscription A subscription object returned by observer.subscribe
*/
unsubscribe(subscription) {
if (!this.subscriptions.includes(subscription))
subscription = this.subscriptions.find(s => s.callback === subscription);
Utils.removeFromArray(this.subscriptions, subscription);
if (!this.subscriptions.length) this.disconnect();
}
unsubscribeAll() {
this.subscriptions.splice(0, this.subscriptions.length);
this.disconnect();
}
/**
* Subscribes to mutations that affect an element matching a selector.
* @param {Function} callback A function to call when on a mutation
* @param {Function} filter A function to call to filter mutations
* @param {Any} bind Something to bind the callback to
* @param {Boolean} group Whether to call the callback with an array of mutations instead of a single mutation
* @return {Object}
*/
subscribeToQuerySelector(callback, selector, bind, group) {
return this.subscribe(callback, mutation => {
return mutation.target.matches(selector) // If the target matches the selector
|| Array.from(mutation.addedNodes).concat(Array.from(mutation.removedNodes)) // Or if either an added or removed node
.find(n => n instanceof Element && (n.matches(selector) || n.querySelector(selector))); // match or contain an element matching the selector
}, bind, group);
}
}
class Manip {
@ -95,6 +158,7 @@ class Manip {
document.execCommand('insertText', false, text);
if (activeElement && refocus) activeElement.focus();
}
static getText() {
const txt = document.querySelector('.chat form textarea');
if (!txt) return '';
@ -102,6 +166,8 @@ class Manip {
}
}
export { Manip as DOMManip };
export default class DOM {
static get manip() {
@ -112,23 +178,14 @@ export default class DOM {
return this._observer || (this._observer = new DOMObserver());
}
static get bdHead() {
return this.getElement('bd-head') || this.createElement('bd-head').appendTo('head');
}
static get bdBody() {
return this.getElement('bd-body') || this.createElement('bd-body').appendTo('body');
}
static get bdStyles() {
return this.getElement('bd-styles') || this.createElement('bd-styles').appendTo(this.bdHead);
}
static get bdThemes() {
return this.getElement('bd-themes') || this.createElement('bd-themes').appendTo(this.bdHead);
}
static get bdTooltips() {
return this.getElement('bd-tooltips') || this.createElement('bd-tooltips').appendTo(this.bdBody);
}
static get bdModals() {
return this.getElement('bd-modals') || this.createElement('bd-modals').appendTo(this.bdBody);
static get bdHead() { return this.getElement('bd-head') || this.createElement('bd-head').appendTo('head') }
static get bdBody() { return this.getElement('bd-body') || this.createElement('bd-body').appendTo('body') }
static get bdStyles() { return this.getElement('bd-styles') || this.createElement('bd-styles').appendTo(this.bdHead) }
static get bdThemes() { return this.getElement('bd-themes') || this.createElement('bd-themes').appendTo(this.bdHead) }
static get bdTooltips() { return this.getElement('bd-tooltips') || this.createElement('bd-tooltips').appendTo(this.bdBody) }
static get bdModals() { return this.getElement('bd-modals') || this.createElement('bd-modals').appendTo(this.bdBody) }
static get bdToasts() {
return this.getElement('bd-toasts') || this.createElement('bd-toasts').appendTo(this.bdBody);
}
static getElement(e) {

View File

@ -8,7 +8,7 @@
* LICENSE file in the root directory of this source tree.
*/
import { Utils, FileUtils, ClientLogger as Logger, AsyncEventEmitter } from 'common';
import { ClientLogger as Logger, AsyncEventEmitter } from 'common';
import { Settings, Events, PluginManager, ThemeManager } from 'modules';
import BaseModal from './components/common/Modal.vue';
import BasicModal from './components/bd/modals/BasicModal.vue';
@ -17,31 +17,38 @@ import ErrorModal from './components/bd/modals/ErrorModal.vue';
import SettingsModal from './components/bd/modals/SettingsModal.vue';
import PermissionModal from './components/bd/modals/PermissionModal.vue';
let modals = 0;
class Modal extends AsyncEventEmitter {
constructor(_modal, component) {
super();
for (let key in _modal)
this[key] = _modal[key];
Object.assign(this, _modal);
const modal = this;
this.component = this.component || {
template: '<custom-modal :modal="modal" />',
components: { 'custom-modal': component },
render(createElement) {
return createElement(component, {
props: {
modal,
id: modal.id,
closing: modal.closing
}
});
},
data() { return { modal }; },
mounted() {
modal.vueInstance = this;
modal.vue = this.$children[0];
}
created() { modal.vueInstance = this; },
mounted() { modal.vue = this.$children[0]; },
beforeDestroy() { modal.vue = undefined; },
destroyed() { modal.vueInstance = undefined; }
};
this.closing = false;
this.id = Date.now();
this.id = modals++;
this.vueInstance = undefined;
this.vue = undefined;
this.close = this.close.bind(this);
this.closed = this.once('closed');
}
/**
@ -65,6 +72,14 @@ export default class Modals {
static add(_modal, component) {
const modal = _modal instanceof Modal ? _modal : new Modal(_modal, component);
if (this.stack.includes(modal)) {
Logger.log('Modals', ['Trying to show a modal that is already in the modal stack.', modal]);
throw new Error('Trying to show a modal that is already in the modal stack.');
}
modal.closing = false;
modal.closed = modal.once('closed');
this.stack.push(modal);
Events.emit('bd-refresh-modals');
return modal;
@ -134,7 +149,11 @@ export default class Modals {
* @return {Modal}
*/
static basic(title, text) {
return this.add({ title, text }, BasicModal);
return this.add(this.createBasicModal(title, text));
}
static createBasicModal(title, text) {
return new Modal({ title, text }, BasicModal);
}
/**
@ -145,13 +164,16 @@ export default class Modals {
* @return {Modal}
*/
static confirm(title, text) {
return this.add(this.createConfirmModal(title, text));
}
static createConfirmModal(title, text) {
const modal = { title, text };
modal.promise = new Promise((resolve, reject) => {
modal.confirm = () => resolve(true);
modal.beforeClose = () => reject();
this.add(modal, ConfirmModal);
});
return modal;
return new Modal(modal, ConfirmModal);
}
/**
@ -163,13 +185,16 @@ export default class Modals {
* @return {Modal}
*/
static permissions(title, name, perms) {
return this.add(this.createPermissionsModal(title, name, perms));
}
static createPermissionsModal(title, name, perms) {
const modal = { title, name, perms };
modal.promise = new Promise((resolve, reject) => {
modal.confirm = () => resolve(true);
modal.beforeClose = () => reject();
this.add(modal, PermissionModal);
});
return modal;
return new Modal(modal, PermissionModal);
}
/**
@ -178,7 +203,11 @@ export default class Modals {
* @return {Modal}
*/
static error(event) {
return this.add({ event }, ErrorModal);
return this.add(this.createErrorModal(event));
}
static createErrorModal(event) {
return new Modal({ event }, ErrorModal);
}
/**
@ -221,7 +250,11 @@ export default class Modals {
* @return {Modal}
*/
static settings(settingsset, headertext, options) {
return this.add(Object.assign({
return this.add(this.createSettingsModal(settingsset, headertext, options));
}
static createSettingsModal(settingsset, headertext, options) {
return new Modal(Object.assign({
headertext: headertext ? headertext : settingsset.headertext,
settings: settingsset,
schemes: settingsset.schemes

View File

@ -10,9 +10,8 @@
import { Module, ReactComponents, ReactHelpers, MonkeyPatch, WebpackModules } from 'modules';
import { Reflection } from 'ui';
import { Utils, ClientLogger as Logger } from 'common';
import DOM from './dom';
import { BdBadge, BdMessageBadge } from './components/bd';
import { ClientLogger as Logger } from 'common';
import { BdBadge } from './components/bd';
import VueInjector from './vueinjector';
import contributors from '../data/contributors';
@ -22,7 +21,7 @@ export default class extends Module {
this.patchMessage();
this.patchChannelMember();
this.patchNameTag();
this.patchUserProfileModals();
this.patchUserProfileModal();
}
get contributors() {
@ -30,45 +29,32 @@ export default class extends Module {
}
/**
* Patches Message to use the extended NameTag.
* This is because NameTag is also used in places we don't really want any badges.
* Patches Message to render profile badges.
*/
async patchMessage() {
const Message = await ReactComponents.getComponent('Message');
this.unpatchMessageRender = MonkeyPatch('ProfileBadges', Message.component.prototype).after('render', (component, args, retVal) => {
if (!retVal.props || !retVal.props.children) return;
if (ReactHelpers.findProp(component, 'jumpSequenceId') && ReactHelpers.findProp(component, 'canFlash')) retVal = retVal.props.children;
const message = ReactHelpers.findProp(component, 'message');
if (!message || !message.author) return;
const user = message.author;
const c = contributors.find(c => c.id === user.id);
if (!c) return;
const contributor = contributors.find(c => c.id === user.id);
if (!contributor) return;
const username = ReactHelpers.findByProp(retVal, 'type', 'h2');
if (!username) return;
username.props.children.splice(1, 0, ReactHelpers.React.createElement('span', {
className: 'bd-badge-outer',
'data-userid': user.id
username.props.children.splice(1, 0, VueInjector.createReactElement(BdBadge, {
contributor,
type: 'nametag'
}));
});
this.unpatchMessageMount = MonkeyPatch('ProfileBadges', Message.component.prototype).after('componentDidMount', component => {
const element = ReactHelpers.ReactDOM.findDOMNode(component);
if (!element) return;
this.injectMessageBadges(element);
});
this.unpatchMessageUpdate = MonkeyPatch('ProfileBadges', Message.component.prototype).after('componentDidUpdate', component => {
const element = ReactHelpers.ReactDOM.findDOMNode(component);
if (!element) return;
this.injectMessageBadges(element);
});
// Rerender all messages
for (const message of document.querySelectorAll('.message')) {
Reflection(message).forceUpdate();
}
Message.forceUpdateAll();
}
/**
@ -92,9 +78,7 @@ export default class extends Module {
// Rerender all channel members
if (this.PatchedNameTag) {
for (const channelMember of document.querySelectorAll('.member-2FrNV0')) {
Reflection(channelMember).forceUpdate();
}
ChannelMember.forceUpdateAll();
}
}
@ -104,8 +88,8 @@ export default class extends Module {
async patchNameTag() {
if (this.PatchedNameTag) return this.PatchedNameTag;
const ProfileBadges = this;
const NameTag = await ReactComponents.getComponent('NameTag', {selector: '.nameTag-26T3kW'});
const selector = '.' + WebpackModules.getClassName('nameTag', 'username', 'discriminator', 'ownerIcon');
const NameTag = await ReactComponents.getComponent('NameTag', {selector});
this.PatchedNameTag = class extends NameTag.component {
render() {
@ -115,94 +99,55 @@ export default class extends Module {
const user = ReactHelpers.findProp(this, 'user');
if (!user) return;
const c = contributors.find(c => c.id === user.id);
if (!c) return;
const contributor = contributors.find(c => c.id === user.id);
if (!contributor) return;
retVal.props.children.splice(1, 0, ReactHelpers.React.createElement('span', {
className: 'bd-badge-outer',
'data-userid': user.id
retVal.props.children.splice(1, 0, VueInjector.createReactElement(BdBadge, {
contributor,
type: 'nametag'
}));
} catch (err) {
Logger.err('ProfileBadges', ['Error thrown while rendering a NameTag', err]);
}
return retVal;
}
componentDidMount() {
const element = ReactHelpers.ReactDOM.findDOMNode(this);
if (!element) return;
ProfileBadges.injectMessageBadges(element);
}
componentDidUpdate() {
const element = ReactHelpers.ReactDOM.findDOMNode(this);
if (!element) return;
ProfileBadges.injectMessageBadges(element);
}
};
// Rerender all channel members
if (this.unpatchChannelMemberRender) {
for (const channelMember of document.querySelectorAll('.member-2FrNV0')) {
Reflection(channelMember).forceUpdate();
}
const ChannelMember = await ReactComponents.getComponent('ChannelMember');
ChannelMember.forceUpdateAll();
}
return this.PatchedNameTag;
}
injectMessageBadges(element) {
for (const beo of element.getElementsByClassName('bd-badge-outer'))
this.injectMessageBadge(beo);
}
injectMessageBadge(root) {
while (root.firstChild) {
root.removeChild(root.firstChild);
}
const { userid } = root.dataset;
if (!userid) return;
const c = contributors.find(c => c.id === userid);
if (!c) return;
VueInjector.inject(root, {
components: { BdMessageBadge },
data: { c },
template: '<BdMessageBadge :developer="c.developer" :webdev="c.webdev" :contributor="c.contributor" />'
}, DOM.createElement('span'));
root.classList.add('bd-has-badge');
}
/**
* Patches UserProfileModals to inject profile badges into the modal once opened.
* TODO: just patch the modal component
* Patches UserProfileModal to render profile badges.
*/
async patchUserProfileModals() {
const UserProfileModals = WebpackModules.getModuleByName('UserProfileModals');
async patchUserProfileModal() {
const UserProfileModal = await ReactComponents.getComponent('UserProfileModal');
MonkeyPatch('BdUI', UserProfileModals).after('open', async (context, [userid]) => {
const c = contributors.find(c => c.id === userid);
if (!c) return;
this.unpatchUserProfileModal = MonkeyPatch('ProfileBadges', UserProfileModal.component.prototype).after('renderBadges', (component, args, retVal, setRetVal) => {
const user = ReactHelpers.findProp(component, 'user');
if (!user) return;
const contributor = contributors.find(c => c.id === user.id);
if (!contributor) return;
const root = await Utils.until(() => document.querySelector('[class*="headerInfo"]'));
const el = DOM.createElement('div', null, 'bdprofilebadges');
root.insertBefore(el.element, root.firstChild.nextSibling);
const element = VueInjector.createReactElement(BdBadge, {
contributor,
type: 'profile-modal'
});
this.injectProfileBadge(userid, el.element);
if (!retVal) {
setRetVal(ReactHelpers.React.createElement('div', {
className: 'bd-profile-badges-wrap',
children: element
}));
} else retVal.props.children.splice(0, 0, element);
});
}
injectProfileBadge(userid, root) {
const c = contributors.find(c => c.id === userid);
if (!c) return;
VueInjector.inject(root, {
components: { BdBadge },
data: { c },
template: '<BdBadge :developer="c.developer" :webdev="c.webdev" :contributor="c.contributor" />',
});
UserProfileModal.forceUpdateAll();
}
}

View File

@ -8,7 +8,8 @@
* LICENSE file in the root directory of this source tree.
*/
import { ClientLogger as Logger } from 'common';
import { Filters, ClientLogger as Logger } from 'common';
import { ReactComponents } from 'modules';
class Reflection {
static reactInternalInstance(node) {
@ -84,71 +85,134 @@ class Reflection {
}
static getState(node) {
try {
return this.reactInternalInstance(node).return.stateNode.state;
} catch (err) {
return null;
}
const stateNode = this.getStateNode(node);
if (stateNode) return stateNode.state;
}
static getStateNode(node) {
try {
return this.reactInternalInstance(node).return.stateNode;
} catch (err) {
return null;
return this.getStateNodes(node)[0];
}
static getStateNodes(node) {
const instance = this.reactInternalInstance(node);
const stateNodes = [];
let lastInstance = instance;
do {
if (lastInstance.return.stateNode instanceof HTMLElement) break;
if (lastInstance.return.stateNode) stateNodes.push(lastInstance.return.stateNode);
lastInstance = lastInstance.return;
} while (lastInstance.return);
return stateNodes;
}
static getComponentStateNode(node, component) {
if (component instanceof ReactComponents.ReactComponent) component = component.component;
for (let stateNode of this.getStateNodes(node)) {
if (stateNode instanceof component) return stateNode;
}
}
static getComponent(node, first = true) {
// IMPORTANT TODO Currently only checks the first found component. For example channel-member will not return the correct component
try {
return this.reactInternalInstance(node).return.type;
} catch (err) {
return null;
}
/*
if (!node) return null;
if (first) node = this.reactInternalInstance(node);
if (node.hasOwnProperty('return')) {
if (node.return.hasOwnProperty('return') && !node.return.type) return node.type;
return this.getComponent(node.return, false);
}
if (node.hasOwnProperty('type')) return node.type;
return null;
*/
static findStateNode(node, filter, first = true) {
return this.getStateNodes(node)[first ? 'find' : 'filter'](filter);
}
static getComponent(node) {
return this.getComponents(node)[0];
}
static getComponents(node) {
const instance = this.reactInternalInstance(node);
const components = [];
let lastInstance = instance;
do {
if (typeof lastInstance.return.type === 'string') break;
if (lastInstance.return.type) components.push(lastInstance.return.type);
lastInstance = lastInstance.return;
} while (lastInstance.return);
return components;
}
static findComponent(node, filter, first = true) {
return this.getComponents(node)[first ? 'find' : 'filter'](filter);
}
}
const propsProxyHandler = {
get(node, prop) {
return Reflection.findProp(node, prop);
}
};
export default function (node) {
return new class {
return new class ReflectionInstance {
constructor(node) {
if ('string' === typeof node) node = document.querySelector(node);
this.node = this.el = this.element = node;
if (typeof node === 'string') node = document.querySelector(node);
this.node = /* this.el = this.element = */ node;
}
get el() { return this.node }
get element() { return this.node }
get reactInternalInstance() {
return Reflection.reactInternalInstance(this.node);
}
get props() {
return 'not yet implemented';
return new Proxy(this.node, propsProxyHandler);
}
get state() {
return Reflection.getState(this.node);
}
get stateNode() {
return Reflection.getStateNode(this.node);
}
get reactInternalInstance() {
return Reflection.reactInternalInstance(this.node);
get stateNodes() {
return Reflection.getStateNodes(this.node);
}
getComponentStateNode(component) {
return Reflection.getComponentStateNode(this.node, component);
}
findStateNode(filter) {
if (typeof filter === 'function') return Reflection.findStateNode(this.node, filter);
if (filter) return Reflection.getComponentStateNode(this.node, filter);
return Reflection.getStateNode(this.node);
}
get component() {
return Reflection.getComponent(this.node);
}
forceUpdate() {
get components() {
return Reflection.getComponents(this.node);
}
getComponentByProps(props, selector) {
return Reflection.findComponent(this.node, Filters.byProperties(props, selector));
}
getComponentByPrototypes(props, selector) {
return Reflection.findComponent(this.node, Filters.byPrototypeFields(props, selector));
}
getComponentByRegex(regex, selector) {
return Reflection.findComponent(this.node, Filters.byCode(regex, selector));
}
getComponentByDisplayName(name) {
return Reflection.findComponent(this.node, Filters.byDisplayName(name));
}
forceUpdate(filter) {
try {
const stateNode = Reflection.getStateNode(this.node);
const stateNode = this.findStateNode(filter);
if (!stateNode || !stateNode.forceUpdate) return;
stateNode.forceUpdate();
} catch (err) {
Logger.err('Reflection', err);
}
}
prop(propName) {
const split = propName.split('.');
const first = Reflection.findProp(this.node, split[0]);

75
client/src/ui/toasts.js Normal file
View File

@ -0,0 +1,75 @@
/**
* BetterDiscord Toasts
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
* All rights reserved.
* https://betterdiscord.net
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
let toasts = 0;
export default class Toasts {
/**
* This shows a popup message at the bottom of the screen similar to Android Toasts. This is useful for small user feedback.
*
* @param {string} message The message to be displayed in the toast
* @param {Object} options Options object. Optional parameter.
* @param {string} options.type Changes the type of the toast stylistically and semantically. Choices: "basic", "info", "success", "error", "warning". Default: "basic"
* @param {string} options.icon URL to custom icon to show in the toast. Having this overrides the default icon for the toast type.
* @param {string|Object|Array} options.additionalClasses Additional classes to add to the toast element. Can be used to style it. Optional.
* @param {number} options.timeout Adjusts the time (in ms) the toast should be shown for before disappearing automatically. Default: 3000
* @returns {Promise} This promise resolves when the toast is removed from the DOM.
*/
static async push(message, options = {}) {
const {type = 'basic', icon, additionalClasses, timeout = 3000} = options;
const toast = {id: toasts++, message, type, icon, additionalClasses, closing: false};
this.stack.push(toast);
await new Promise(resolve => setTimeout(resolve, timeout));
toast.closing = true;
await new Promise(resolve => setTimeout(resolve, 300));
this.stack.splice(this.stack.indexOf(toast), 1);
}
/**
* This is a shortcut for `type = "success"` in {@link Toasts#push}. The parameters and options are the same.
*/
static async success(message, options = {}) {
options.type = 'success';
return this.push(message, options);
}
/**
* This is a shortcut for `type = "error"` in {@link Toasts#push}. The parameters and options are the same.
*/
static async error(message, options = {}) {
options.type = 'error';
return this.push(message, options);
}
/**
* This is a shortcut for `type = "info"` in {@link Toasts#push}. The parameters and options are the same.
*/
static async info(message, options = {}) {
options.type = 'info';
return this.push(message, options);
}
/**
* This is a shortcut for `type = "warning"` in {@link Toasts#push}. The parameters and options are the same.
*/
static async warning(message, options = {}) {
options.type = 'warning';
return this.push(message, options);
}
/**
* An array of active toasts.
*/
static get stack() {
return this._stack || (this._stack = []);
}
}

View File

@ -1,7 +1,11 @@
export { default as DOM } from './dom';
export { default as DOM, DOMObserver, DOMManip } from './dom';
export { default as BdUI } from './bdui';
export { default as VueInjector } from './vueinjector';
export { default as BdMenu, BdMenuItems } from './bdmenu';
export { default as Modals } from './modals';
export { default as ProfileBadges } from './profilebadges';
export { default as Toasts } from './toasts';
export { default as VueInjector } from './vueinjector';
export { default as Reflection } from './reflection';
export { default as ProfileBadges } from './profilebadges';
export { default as ClassNormaliser } from './classnormaliser';

Some files were not shown because too many files have changed in this diff Show More