Merge pull request #312 from samuelthomas2774/component-patches

React component patches
This commit is contained in:
Alexei Stukov 2019-03-12 22:44:26 +02:00 committed by GitHub
commit 5cb4bc15bd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 298 additions and 184 deletions

1
.gitignore vendored
View File

@ -4,6 +4,7 @@ dist
etc etc
release release
tests/tmp
tests/log.txt tests/log.txt
# User data # User data

View File

@ -21,7 +21,7 @@ export default new class BlockedMessages extends BuiltinModule {
async enabled(e) { async enabled(e) {
const MessageListComponents = Reflection.module.byProps('BlockedMessageGroup'); const MessageListComponents = Reflection.module.byProps('BlockedMessageGroup');
MessageListComponents.OriginalBlockedMessageGroup = MessageListComponents.BlockedMessageGroup; MessageListComponents.OriginalBlockedMessageGroup = MessageListComponents.BlockedMessageGroup;
MessageListComponents.BlockedMessageGroup = () => { return null; }; MessageListComponents.BlockedMessageGroup = () => null;
this.cancelBlockedMessages = () => { this.cancelBlockedMessages = () => {
MessageListComponents.BlockedMessageGroup = MessageListComponents.OriginalBlockedMessageGroup; MessageListComponents.BlockedMessageGroup = MessageListComponents.OriginalBlockedMessageGroup;
delete MessageListComponents.OriginalBlockedMessageGroup; delete MessageListComponents.OriginalBlockedMessageGroup;

View File

@ -22,10 +22,10 @@ export default class BuiltinModule {
this.patch = this.patch.bind(this); this.patch = this.patch.bind(this);
} }
init() { async init() {
this.setting.on('setting-updated', this._settingUpdated); this.setting.on('setting-updated', this._settingUpdated);
if (this.setting.value) { if (this.setting.value) {
if (this.enabled) this.enabled(); if (this.enabled) await this.enabled();
if (this.applyPatches) this.applyPatches(); if (this.applyPatches) this.applyPatches();
} }
} }
@ -38,16 +38,15 @@ export default class BuiltinModule {
return Patcher.getPatchesByCaller(`BD:${this.moduleName}`); return Patcher.getPatchesByCaller(`BD:${this.moduleName}`);
} }
_settingUpdated(e) { async _settingUpdated(e) {
const { value } = e; if (e.value) {
if (value === true) { if (this.enabled) await this.enabled(e);
if (this.enabled) this.enabled(e); if (this.applyPatches) await this.applyPatches();
if (this.applyPatches) this.applyPatches(); if (this.rerenderPatchedComponents) this.rerenderPatchedComponents();
return; } else {
} if (this.disabled) await this.disabled(e);
if (value === false) {
if (this.disabled) this.disabled(e);
this.unpatch(); this.unpatch();
if (this.rerenderPatchedComponents) this.rerenderPatchedComponents();
} }
} }
@ -75,12 +74,14 @@ export default class BuiltinModule {
*/ */
patch(module, fnName, cb, when = 'after') { patch(module, fnName, cb, when = 'after') {
if (!['before', 'after', 'instead'].includes(when)) when = 'after'; if (!['before', 'after', 'instead'].includes(when)) when = 'after';
Patch(`BD:${this.moduleName}`, module)[when](fnName, cb.bind(this)); return Patch(`BD:${this.moduleName}`, module)[when](fnName, cb.bind(this));
} }
childPatch(module, fnName, child, cb, when = 'after') { childPatch(module, fnName, child, cb, when = 'after') {
const last = child.pop();
this.patch(module, fnName, (component, args, retVal) => { this.patch(module, fnName, (component, args, retVal) => {
this.patch(retVal[child[0]], child[1], cb, when); const unpatch = this.patch(child.reduce((obj, key) => obj[key], retVal), last, function(...args) {unpatch(); return cb.call(this, component, ...args);}, when);
}); });
} }

View File

@ -42,6 +42,10 @@ export default new class ColoredText extends BuiltinModule {
this.intensitySetting.off('setting-updated', this._intensityUpdated); this.intensitySetting.off('setting-updated', this._intensityUpdated);
} }
rerenderPatchedComponents() {
if (this.MessageContent) this.MessageContent.forceUpdateAll();
}
/* Methods */ /* Methods */
_intensityUpdated() { _intensityUpdated() {
this.MessageContent.forceUpdateAll(); this.MessageContent.forceUpdateAll();
@ -50,16 +54,16 @@ export default new class ColoredText extends BuiltinModule {
/* Patches */ /* Patches */
async applyPatches() { async applyPatches() {
if (this.patches.length) return; if (this.patches.length) return;
this.MessageContent = await ReactComponents.getComponent('MessageContent', { selector: Reflection.resolve('container', 'containerCozy', 'containerCompact', 'edited').selector }, m => m.defaultProps && m.defaultProps.hasOwnProperty('disableButtons')); this.MessageContent = await ReactComponents.getComponent('MessageContent');
this.patch(this.MessageContent.component.prototype, 'render', this.injectColoredText); this.patch(this.MessageContent.component.prototype, 'render', this.injectColoredText);
this.MessageContent.forceUpdateAll();
} }
/** /**
* Set markup text colour to match role colour * Set markup text colour to match role colour
*/ */
injectColoredText(thisObject, args, originalReturn) { injectColoredText(thisObject, args, originalReturn) {
this.patch(originalReturn.props, 'children', function(obj, args, returnValue) { const unpatch = this.patch(originalReturn.props, 'children', (obj, args, returnValue) => {
unpatch();
const { TinyColor } = Reflection.modules; const { TinyColor } = Reflection.modules;
const markup = Utils.findInReactTree(returnValue, m => m && m.props && m.props.className && m.props.className.includes('da-markup')); const markup = Utils.findInReactTree(returnValue, m => m && m.props && m.props.className && m.props.className.includes('da-markup'));
const roleColor = thisObject.props.message.colorString; const roleColor = thisObject.props.message.colorString;

View File

@ -9,8 +9,8 @@
*/ */
import { Settings, Cache, Events } from 'modules'; import { Settings, Cache, Events } from 'modules';
import BuiltinModule from './BuiltinModule'; import BuiltinModule from '../BuiltinModule';
import { Reflection, ReactComponents, MonkeyPatch, Patcher, DiscordApi, Security } from 'modules'; import { Reflection, ReactComponents, DiscordApi, Security } from 'modules';
import { VueInjector, Modals, Toasts } from 'ui'; import { VueInjector, Modals, Toasts } from 'ui';
import { ClientLogger as Logger, ClientIPC } from 'common'; import { ClientLogger as Logger, ClientIPC } from 'common';
import { request } from 'vendor'; import { request } from 'vendor';
@ -172,7 +172,7 @@ export default new class E2EE extends BuiltinModule {
this.patch(Dispatcher, 'dispatch', this.dispatcherPatch, 'before'); this.patch(Dispatcher, 'dispatch', this.dispatcherPatch, 'before');
this.patchMessageContent(); this.patchMessageContent();
const ChannelTextArea = await ReactComponents.getComponent('ChannelTextArea', { selector: Reflection.resolve('channelTextArea', 'emojiButton').selector }); const ChannelTextArea = await ReactComponents.getComponent('ChannelTextArea');
this.patchChannelTextArea(ChannelTextArea); this.patchChannelTextArea(ChannelTextArea);
this.patchChannelTextAreaSubmit(ChannelTextArea); this.patchChannelTextAreaSubmit(ChannelTextArea);
ChannelTextArea.forceUpdateAll(); ChannelTextArea.forceUpdateAll();
@ -236,12 +236,14 @@ export default new class E2EE extends BuiltinModule {
} }
async patchMessageContent() { async patchMessageContent() {
const MessageContent = await ReactComponents.getComponent('MessageContent', { selector: Reflection.resolve('container', 'containerCozy', 'containerCompact', 'edited').selector }, m => m.defaultProps && m.defaultProps.hasOwnProperty('disableButtons')); const MessageContent = await ReactComponents.getComponent('MessageContent');
this.patch(MessageContent.component.prototype, 'render', this.beforeRenderMessageContent, 'before'); this.patch(MessageContent.component.prototype, 'render', this.beforeRenderMessageContent, 'before');
this.patch(MessageContent.component.prototype, 'render', this.afterRenderMessageContent); this.childPatch(MessageContent.component.prototype, 'render', ['props', 'children'], this.afterRenderMessageContent);
MessageContent.forceUpdateAll();
const ImageWrapper = await ReactComponents.getComponent('ImageWrapper', { selector: Reflection.resolve('imageWrapper').selector }); const ImageWrapper = await ReactComponents.getComponent('ImageWrapper');
this.patch(ImageWrapper.component.prototype, 'render', this.beforeRenderImageWrapper, 'before'); this.patch(ImageWrapper.component.prototype, 'render', this.beforeRenderImageWrapper, 'before');
ImageWrapper.forceUpdateAll();
} }
beforeRenderMessageContent(component) { beforeRenderMessageContent(component) {
@ -285,10 +287,16 @@ export default new class E2EE extends BuiltinModule {
component.props.message.contentParsed = create.contentParsed; component.props.message.contentParsed = create.contentParsed;
} }
afterRenderMessageContent(component, args, retVal) { afterRenderMessageContent(component, _childrenObject, args, retVal) {
if (!component.props.message.bd_encrypted) return; if (!component.props.message.bd_encrypted) return;
const buttons = Utils.findInReactTree(retVal, m => Array.isArray(m) && m[1].props && m[1].props.currentUserId);
const { className } = Reflection.resolve('buttonContainer', 'avatar', 'username');
const buttonContainer = Utils.findInReactTree(retVal, m => m && m.className && m.className.indexOf(className) !== -1);
if (!buttonContainer) return;
const buttons = buttonContainer.children.props.children;
if (!buttons) return; if (!buttons) return;
try { try {
buttons.unshift(VueInjector.createReactElement(E2EEMessageButton)); buttons.unshift(VueInjector.createReactElement(E2EEMessageButton));
} catch (err) { } catch (err) {

View File

@ -45,7 +45,7 @@
import { E2EE } from 'builtin'; import { E2EE } from 'builtin';
import { Settings, DiscordApi, Reflection } from 'modules'; import { Settings, DiscordApi, Reflection } from 'modules';
import { Toasts } from 'ui'; import { Toasts } from 'ui';
import { MiLock, MiImagePlus, MiIcVpnKey } from '../ui/components/common/MaterialIcon'; import { MiLock, MiImagePlus, MiIcVpnKey } from 'commoncomponents';
export default { export default {
components: { components: {

View File

@ -17,7 +17,7 @@
</template> </template>
<script> <script>
import { MiLock } from '../ui/components/common/MaterialIcon'; import { MiLock } from 'commoncomponents';
export default { export default {
components: { components: {

View File

@ -0,0 +1,3 @@
export { default as default } from './E2EE';
export { default as E2EEComponent } from './E2EEComponent.vue';
export { default as E2EEMessageButton } from './E2EEMessageButton.vue';

View File

@ -9,18 +9,11 @@
*/ */
import { Settings } from 'modules'; import { Settings } from 'modules';
import BuiltinModule from '../BuiltinModule';
import BuiltinModule from './BuiltinModule'; import EmoteModule, { EMOTE_SOURCES } from './EmoteModule';
import EmoteModule from './EmoteModule'; import GlobalAc from 'autocomplete';
import GlobalAc from '../ui/autocomplete';
import { BdContextMenu } from 'ui'; import { BdContextMenu } from 'ui';
const EMOTE_SOURCES = [
'https://static-cdn.jtvnw.net/emoticons/v1/:id/1.0',
'https://cdn.frankerfacez.com/emoticon/:id/1',
'https://cdn.betterttv.net/emote/:id/1x'
]
export default new class EmoteAc extends BuiltinModule { export default new class EmoteAc extends BuiltinModule {
/* Getters */ /* Getters */

View File

@ -8,15 +8,10 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import VrWrapper from '../ui/vrwrapper'; import VrWrapper from '../../ui/vrwrapper';
import { EMOTE_SOURCES } from '.';
import EmoteComponent from './EmoteComponent.vue'; import EmoteComponent from './EmoteComponent.vue';
const EMOTE_SOURCES = [
'https://static-cdn.jtvnw.net/emoticons/v1/:id/1.0',
'https://cdn.frankerfacez.com/emoticon/:id/1',
'https://cdn.betterttv.net/emote/:id/1x'
]
export default class Emote extends VrWrapper { export default class Emote extends VrWrapper {
constructor(type, id, name) { constructor(type, id, name) {

View File

@ -5,7 +5,7 @@
<script> <script>
import { ClientLogger as Logger } from 'common'; import { ClientLogger as Logger } from 'common';
import EmoteModule from './EmoteModule'; import EmoteModule from './EmoteModule';
import { MiStar } from '../ui/components/common'; import { MiStar } from 'commoncomponents';
export default { export default {
components: { components: {

View File

@ -8,20 +8,17 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import BuiltinModule from './BuiltinModule'; import BuiltinModule from '../BuiltinModule';
import path from 'path'; import path from 'path';
import { request } from 'vendor'; import { request } from 'vendor';
import { Utils, FileUtils } from 'common'; import { Utils, FileUtils, ClientLogger as Logger } from 'common';
import { DiscordApi, Settings, Globals, Reflection, ReactComponents, Database } from 'modules'; import { DiscordApi, Settings, Globals, Reflection, ReactComponents, Database } from 'modules';
import { DiscordContextMenu } from 'ui'; import { DiscordContextMenu } from 'ui';
import Emote from './EmoteComponent.js'; import Emote from './EmoteComponent.js';
import Autocomplete from '../ui/components/common/Autocomplete.vue';
import GlobalAc from '../ui/autocomplete'; export const EMOTE_SOURCES = [
const EMOTE_SOURCES = [
'https://static-cdn.jtvnw.net/emoticons/v1/:id/1.0', 'https://static-cdn.jtvnw.net/emoticons/v1/:id/1.0',
'https://cdn.frankerfacez.com/emoticon/:id/1', 'https://cdn.frankerfacez.com/emoticon/:id/1',
'https://cdn.betterttv.net/emote/:id/1x' 'https://cdn.betterttv.net/emote/:id/1x'
@ -131,6 +128,8 @@ export default new class EmoteModule extends BuiltinModule {
this.database.set(id, { id: emote.value.id || value, type }); this.database.set(id, { id: emote.value.id || value, type });
} }
Logger.log('EmoteModule', ['Loaded emote database']);
} }
async loadUserData() { async loadUserData() {
@ -218,15 +217,18 @@ export default new class EmoteModule extends BuiltinModule {
async applyPatches() { async applyPatches() {
this.patchMessageContent(); this.patchMessageContent();
this.patchSendAndEdit(); this.patchSendAndEdit();
const ImageWrapper = await ReactComponents.getComponent('ImageWrapper', { selector: Reflection.resolve('imageWrapper').selector }); this.patchSpoiler();
this.patch(ImageWrapper.component.prototype, 'render', this.beforeRenderImageWrapper, 'before');
const MessageAccessories = await ReactComponents.getComponent('MessageAccessories');
this.patch(MessageAccessories.component.prototype, 'render', this.afterRenderMessageAccessories, 'after');
MessageAccessories.forceUpdateAll();
} }
/** /**
* Patches MessageContent render method * Patches MessageContent render method
*/ */
async patchMessageContent() { async patchMessageContent() {
const MessageContent = await ReactComponents.getComponent('MessageContent', { selector: Reflection.resolve('container', 'containerCozy', 'containerCompact', 'edited').selector }, m => m.defaultProps && m.defaultProps.hasOwnProperty('disableButtons')); const MessageContent = await ReactComponents.getComponent('MessageContent');
this.childPatch(MessageContent.component.prototype, 'render', ['props', 'children'], this.afterRenderMessageContent); this.childPatch(MessageContent.component.prototype, 'render', ['props', 'children'], this.afterRenderMessageContent);
MessageContent.forceUpdateAll(); MessageContent.forceUpdateAll();
} }
@ -240,10 +242,26 @@ export default new class EmoteModule extends BuiltinModule {
this.patch(MessageActions, 'editMessage', this.handleEditMessage, 'instead'); this.patch(MessageActions, 'editMessage', this.handleEditMessage, 'instead');
} }
async patchSpoiler() {
const Spoiler = await ReactComponents.getComponent('Spoiler');
this.childPatch(Spoiler.component.prototype, 'render', ['props', 'children', 'props', 'children'], this.afterRenderSpoiler);
Spoiler.forceUpdateAll();
}
afterRenderSpoiler(component, _childrenObject, args, retVal) {
const markup = Utils.findInReactTree(retVal, filter =>
filter &&
filter.className &&
filter.className.includes('inlineContent'));
if (!markup) return;
markup.children = this.processMarkup(markup.children);
}
/** /**
* Handle message render * Handle message render
*/ */
afterRenderMessageContent(component, args, retVal) { afterRenderMessageContent(component, _childrenObject, args, retVal) {
const markup = Utils.findInReactTree(retVal, filter => const markup = Utils.findInReactTree(retVal, filter =>
filter && filter &&
filter.className && filter.className &&
@ -256,11 +274,13 @@ export default new class EmoteModule extends BuiltinModule {
/** /**
* Handle send message * Handle send message
*/ */
async handleSendMessage(component, args, orig) { async handleSendMessage(MessageActions, args, orig) {
if (!args.length) return orig(...args); if (!args.length) return orig(...args);
const { content } = args[1]; const { content } = args[1];
if (!content) return orig(...args); if (!content) return orig(...args);
Logger.log('EmoteModule', ['Sending message', MessageActions, args, orig]);
const emoteAsImage = Settings.getSetting('emotes', 'default', 'emoteasimage').value && const emoteAsImage = Settings.getSetting('emotes', 'default', 'emoteasimage').value &&
(DiscordApi.currentChannel.type === 'DM' || DiscordApi.currentChannel.checkPermissions(DiscordApi.modules.DiscordPermissions.ATTACH_FILES)); (DiscordApi.currentChannel.type === 'DM' || DiscordApi.currentChannel.checkPermissions(DiscordApi.modules.DiscordPermissions.ATTACH_FILES));
@ -271,7 +291,7 @@ export default new class EmoteModule extends BuiltinModule {
const emote = this.findByName(isEmote[1], true); const emote = this.findByName(isEmote[1], true);
if (!emote) return word; if (!emote) return word;
this.addToMostUsed(emote); this.addToMostUsed(emote);
return emote ? `:${isEmote[1]}:` : word; return emote ? `;${isEmote[1]};` : word;
} }
return word; return word;
}).join(' '); }).join(' ');
@ -305,23 +325,27 @@ export default new class EmoteModule extends BuiltinModule {
if (!content) return orig(...args); if (!content) return orig(...args);
args[2].content = args[2].content.split(' ').map(word => { args[2].content = args[2].content.split(' ').map(word => {
const isEmote = /;(.*?);/g.exec(word); const isEmote = /;(.*?);/g.exec(word);
return isEmote ? `:${isEmote[1]}:` : word; return isEmote ? `;${isEmote[1]};` : word;
}).join(' '); }).join(' ');
return orig(...args); return orig(...args);
} }
/** /**
* Handle imagewrapper render * Handle MessageAccessories render
*/ */
beforeRenderImageWrapper(component, args, retVal) { afterRenderMessageAccessories(component, args, retVal) {
if (!component.props || !component.props.src) return; if (!component.props || !component.props.message) return;
if (!component.props.message.attachments || component.props.message.attachments.length !== 1) return;
const src = component.props.original || component.props.src.split('?')[0]; const filename = component.props.message.attachments[0].filename;
if (!src || !src.includes('.bdemote.')) return; const match = filename.match(/([^/]*)\.bdemote\.(gif|png)$/i);
const emoteName = src.split('/').pop().split('.')[0]; if (!match) return;
const emote = this.findByName(emoteName);
const emote = this.findByName(match[1]);
if (!emote) return; if (!emote) return;
retVal.props.children = emote.render();
emote.jumboable = true;
retVal.props.children[2] = emote.render();
} }
/** /**
@ -339,14 +363,14 @@ export default new class EmoteModule extends BuiltinModule {
for (const child of markup) { for (const child of markup) {
if (typeof child !== 'string') { if (typeof child !== 'string') {
if (typeof child === 'object') { if (typeof child === 'object') {
const isEmoji = Utils.findInReactTree(child, 'emojiName'); const isEmoji = Utils.findInReactTree(child, filter => filter && filter.emojiName);
if (isEmoji) child.props.children.props.jumboable = jumboable; if (isEmoji) isEmoji.jumboable = jumboable;
} }
newMarkup.push(child); newMarkup.push(child);
continue; continue;
} }
if (!/:(\w+):/g.test(child)) { if (!/;(\w+);/g.test(child)) {
newMarkup.push(child); newMarkup.push(child);
continue; continue;
} }
@ -355,7 +379,7 @@ export default new class EmoteModule extends BuiltinModule {
let s = ''; let s = '';
for (const word of words) { for (const word of words) {
const isemote = /:(.*?):/g.exec(word); const isemote = /;(.*?);/g.exec(word);
if (!isemote) { if (!isemote) {
s += word; s += word;
continue; continue;

View File

@ -0,0 +1,4 @@
export { default as EmoteModule, EMOTE_SOURCES } from './EmoteModule';
export { default as Emote } from './EmoteComponent';
export { default as EmoteComponent } from './EmoteComponent.vue';
export { default as EmoteAc } from './EmoteAc';

View File

@ -1,16 +1,19 @@
import { default as EmoteModule } from './EmoteModule'; import { EmoteModule, EmoteAc } from './Emotes';
import { default as ReactDevtoolsModule } from './ReactDevtoolsModule'; import ReactDevtoolsModule from './ReactDevtoolsModule';
import { default as VueDevtoolsModule } from './VueDevToolsModule'; import VueDevtoolsModule from './VueDevToolsModule';
import { default as TrackingProtection } from './TrackingProtection'; import TrackingProtection from './TrackingProtection';
import { default as E2EE } from './E2EE'; import E2EE from './E2EE';
import { default as ColoredText } from './ColoredText'; import ColoredText from './ColoredText';
import { default as TwentyFourHour } from './24Hour'; import TwentyFourHour from './24Hour';
import { default as KillClyde } from './KillClyde'; import KillClyde from './KillClyde';
import { default as BlockedMessages } from './BlockedMessages'; import BlockedMessages from './BlockedMessages';
import { default as VoiceDisconnect } from './VoiceDisconnect'; import VoiceDisconnect from './VoiceDisconnect';
import { default as EmoteAc } from './EmoteAc';
export default class { export default class {
static get modules() {
return require('./builtin');
}
static initAll() { static initAll() {
EmoteModule.init(); EmoteModule.init();
ReactDevtoolsModule.init(); ReactDevtoolsModule.init();

View File

@ -12,7 +12,7 @@ import BuiltinModule from './BuiltinModule';
import { Reflection } from 'modules'; import { Reflection } from 'modules';
export default new class E2EE extends BuiltinModule { export default new class TrackingProtection extends BuiltinModule {
/* Getters */ /* Getters */
get moduleName() { return 'TrackingProtection' } get moduleName() { return 'TrackingProtection' }

View File

@ -1,4 +1,4 @@
export { default as EmoteModule } from './EmoteModule'; export { EmoteModule, EmoteAc } from './Emotes';
export { default as ReactDevtoolsModule } from './ReactDevtoolsModule'; export { default as ReactDevtoolsModule } from './ReactDevtoolsModule';
export { default as VueDevtoolsModule } from './VueDevToolsModule'; export { default as VueDevtoolsModule } from './VueDevToolsModule';
export { default as TrackingProtection } from './TrackingProtection'; export { default as TrackingProtection } from './TrackingProtection';

View File

@ -8,7 +8,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import { DOM, BdUI, BdMenu, Modals, Toasts, Notifications, BdContextMenu, DiscordContextMenu } from 'ui'; import { DOM, BdUI, BdMenu, Modals, Toasts, Notifications, BdContextMenu, DiscordContextMenu, Autocomplete } from 'ui';
import BdCss from './styles/index.scss'; import BdCss from './styles/index.scss';
import { Events, Globals, Settings, Database, Updater, ModuleManager, PluginManager, ThemeManager, ExtModuleManager, Vendor, Patcher, MonkeyPatch, ReactComponents, ReactHelpers, ReactAutoPatcher, DiscordApi, BdWebApi, Connectivity, Cache, Reflection, PackageInstaller } from 'modules'; import { Events, Globals, Settings, Database, Updater, ModuleManager, PluginManager, ThemeManager, ExtModuleManager, Vendor, Patcher, MonkeyPatch, ReactComponents, ReactHelpers, ReactAutoPatcher, DiscordApi, BdWebApi, Connectivity, Cache, Reflection, PackageInstaller } from 'modules';
import { ClientLogger as Logger, ClientIPC, Utils, Axi } from 'common'; import { ClientLogger as Logger, ClientIPC, Utils, Axi } from 'common';
@ -27,14 +27,14 @@ class BetterDiscord {
Logger.log('main', 'BetterDiscord starting'); Logger.log('main', 'BetterDiscord starting');
this._bd = { this._bd = {
DOM, BdUI, BdMenu, Modals, Reflection, Toasts, Notifications, BdContextMenu, DiscordContextMenu, DOM, BdUI, BdMenu, Modals, Reflection, Toasts, Notifications, BdContextMenu, DiscordContextMenu, Autocomplete,
Events, Globals, Settings, Database, Updater, Events, Globals, Settings, Database, Updater,
ModuleManager, PluginManager, ThemeManager, ExtModuleManager, PackageInstaller, ModuleManager, PluginManager, ThemeManager, ExtModuleManager, PackageInstaller,
Vendor, Vendor,
Patcher, MonkeyPatch, ReactComponents, ReactHelpers, ReactAutoPatcher, DiscordApi, Patcher, MonkeyPatch, ReactComponents, ReactHelpers, ReactAutoPatcher, DiscordApi,
EmoteModule, BuiltinManager, EmoteModule,
BdWebApi, BdWebApi,
Connectivity, Connectivity,
Cache, Cache,

View File

@ -65,6 +65,10 @@ export default class Content extends AsyncEventEmitter {
get config() { return this.settings.categories } get config() { return this.settings.categories }
get data() { return this.userConfig.data || (this.userConfig.data = {}) } get data() { return this.userConfig.data || (this.userConfig.data = {}) }
get packed() { return this.dirName.packed }
get packagePath() { return this.dirName.packagePath }
get packageName() { return this.dirName.pkg }
/** /**
* Opens a settings modal for this content. * Opens a settings modal for this content.
* @return {Modal} * @return {Modal}

View File

@ -220,6 +220,7 @@ export default class {
const unpackedPath = path.join(Globals.getPath('tmp'), packageName); const unpackedPath = path.join(Globals.getPath('tmp'), packageName);
asar.extractAll(packagePath, unpackedPath); asar.extractAll(packagePath, unpackedPath);
return this.preloadContent({ return this.preloadContent({
config, config,
contentPath: unpackedPath, contentPath: unpackedPath,
@ -228,8 +229,8 @@ export default class {
packageName, packageName,
packed: true packed: true
}, reload, index); }, reload, index);
} catch (err) { } catch (err) {
Logger.log('ContentManager', ['Error extracting packed content', err]);
throw err; throw err;
} }
} }
@ -322,12 +323,6 @@ export default class {
return content; return content;
} catch (err) { } catch (err) {
throw err; throw err;
} finally {
if (typeof dirName === 'object' && dirName.packed) {
rimraf(dirName.contentPath, err => {
if (err) Logger.err(err);
});
}
} }
} }
@ -353,6 +348,7 @@ export default class {
await unload; await unload;
await FileUtils.recursiveDeleteDirectory(content.paths.contentPath); await FileUtils.recursiveDeleteDirectory(content.paths.contentPath);
if (content.packed) await FileUtils.recursiveDeleteDirectory(content.packagePath);
return true; return true;
} catch (err) { } catch (err) {
Logger.err(this.moduleName, err); Logger.err(this.moduleName, err);
@ -384,7 +380,7 @@ export default class {
if (this.unloadContentHook) this.unloadContentHook(content); if (this.unloadContentHook) this.unloadContentHook(content);
if (reload) return content.packed ? this.preloadPackedContent(content.packed.pkg, true, index) : this.preloadContent(content.dirName, true, index); if (reload) return content.packed ? this.preloadPackedContent(content.packagePath, true, index) : this.preloadContent(content.dirName, true, index);
this.localContent.splice(index, 1); this.localContent.splice(index, 1);
} catch (err) { } catch (err) {

View File

@ -36,10 +36,6 @@ export default new class extends Module {
async first() { async first() {
const config = await ClientIPC.send('getConfig'); const config = await ClientIPC.send('getConfig');
config.paths.push({
id: 'tmp',
path: path.join(config.paths.find(p => p.id === 'base').path, 'tmp')
});
this.setState({ config }); this.setState({ config });
// This is for Discord to stop error reporting :3 // This is for Discord to stop error reporting :3

View File

@ -6,14 +6,14 @@ import rimraf from 'rimraf';
import { request } from 'vendor'; import { request } from 'vendor';
import { Modals } from 'ui'; import { Modals } from 'ui';
import { Utils } from 'common'; import { Utils, FileUtils } from 'common';
import PluginManager from './pluginmanager'; import PluginManager from './pluginmanager';
import Globals from './globals'; import Globals from './globals';
import Security from './security'; import Security from './security';
import { ReactComponents } from './reactcomponents';
import Reflection from './reflection'; import Reflection from './reflection';
import DiscordApi from './discordapi'; import DiscordApi from './discordapi';
import ThemeManager from './thememanager'; import ThemeManager from './thememanager';
import { MonkeyPatch } from './patcher';
import { DOM } from 'ui'; import { DOM } from 'ui';
export default class PackageInstaller { export default class PackageInstaller {
@ -84,15 +84,10 @@ export default class PackageInstaller {
await oldContent.unload(true); await oldContent.unload(true);
if (oldContent.packed && oldContent.packed.packageName !== nameOrId) { if (oldContent.packed && oldContent.packageName !== nameOrId) {
rimraf(oldContent.packed.packagePath, err => { await FileUtils.deleteFile(oldContent.packagePath).catch(err => null);
if (err) throw err;
});
} else {
rimraf(oldContent.contentPath, err => {
if (err) throw err;
});
} }
await FileUtils.recursiveDeleteDirectory(oldContent.contentPath).catch(err => null);
return manager.preloadPackedContent(outputName); return manager.preloadPackedContent(outputName);
} catch (err) { } catch (err) {
@ -133,41 +128,51 @@ export default class PackageInstaller {
} }
} }
static async handleDrop(stateNode, e, original) {
if (!e.dataTransfer.files.length || !e.dataTransfer.files[0].name.endsWith('.bd')) return original && original.call(stateNode, e);
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
if (stateNode) stateNode.clearDragging();
const currentChannel = DiscordApi.currentChannel;
const canUpload = currentChannel ?
currentChannel.checkPermissions(Reflection.modules.DiscordConstants.Permissions.SEND_MESSAGES) &&
currentChannel.checkPermissions(Reflection.modules.DiscordConstants.Permissions.ATTACH_FILES) : false;
const files = Array.from(e.dataTransfer.files).slice(0);
const actionCode = await this.dragAndDropHandler(e.dataTransfer.files[0].path, canUpload);
if (actionCode === 0 && stateNode) stateNode.promptToUpload(files, currentChannel.id, true, !e.shiftKey);
}
/** /**
* Patches Discord upload area for .bd files * Patches Discord upload area for .bd files
*/ */
static async uploadAreaPatch() { static async uploadAreaPatch(UploadArea) {
const { selector } = Reflection.resolve('uploadArea');
this.UploadArea = await ReactComponents.getComponent('UploadArea', { selector });
const reflect = Reflection.DOM(selector);
const stateNode = reflect.getComponentStateNode(this.UploadArea);
const callback = async function (e) {
if (!e.dataTransfer.files.length || !e.dataTransfer.files[0].name.endsWith('.bd')) return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
stateNode.clearDragging();
const currentChannel = DiscordApi.currentChannel;
const canUpload = currentChannel ? currentChannel.checkPermissions(Reflection.modules.DiscordConstants.Permissions.ATTACH_FILES) : false;
const files = Array.from(e.dataTransfer.files).slice(0);
const actionCode = await PackageInstaller.dragAndDropHandler(e.dataTransfer.files[0].path, canUpload);
if (actionCode === 0) stateNode.promptToUpload(files, currentChannel.id, true, !e.shiftKey);
};
// Add a listener to root for when not in a channel // Add a listener to root for when not in a channel
const root = DOM.getElement('#app-mount'); const root = DOM.getElement('#app-mount');
root.addEventListener('drop', callback); const rootHandleDrop = this.handleDrop.bind(this, undefined);
root.addEventListener('drop', rootHandleDrop);
// Remove their handler, add ours, then read theirs to give ours priority to stop theirs when we get a .bd file. const unpatchUploadAreaHandleDrop = MonkeyPatch('BD:ReactComponents', UploadArea.component.prototype).instead('handleDrop', (component, [e], original) => this.handleDrop(component, e, original));
reflect.element.removeEventListener('drop', stateNode.handleDrop);
reflect.element.addEventListener('drop', callback);
reflect.element.addEventListener('drop', stateNode.handleDrop);
this.unpatchUploadArea = function () { this.unpatchUploadArea = () => {
reflect.element.removeEventListener('drop', callback); unpatchUploadAreaHandleDrop();
root.removeEventListener('drop', callback); root.removeEventListener('drop', rootHandleDrop);
this.unpatchUploadArea = undefined;
}; };
for (const element of document.querySelectorAll(UploadArea.important.selector)) {
const stateNode = Reflection.DOM(element).getComponentStateNode(UploadArea);
element.removeEventListener('drop', stateNode.handleDrop);
stateNode.handleDrop = UploadArea.component.prototype.handleDrop.bind(stateNode);
element.addEventListener('drop', stateNode.handleDrop);
stateNode.forceUpdate();
}
} }
} }

View File

@ -109,21 +109,9 @@ export default class extends ContentManager {
throw {message: `Plugin ${info.name} did not return a class that extends Plugin.`}; throw {message: `Plugin ${info.name} did not return a class that extends Plugin.`};
const instance = new plugin({ const instance = new plugin({
configs, info, main, configs, info, main, paths
paths: {
contentPath: paths.contentPath,
dirName: packed ? packed.packageName : paths.dirName,
mainPath: paths.mainPath
}
}); });
if (packed) instance.packed = {
pkg: packed.pkg,
packageName: packed.packageName,
packagePath: packed.packagePath,
packed: true
}; else instance.packed = false;
if (instance.enabled && this.loaded) { if (instance.enabled && this.loaded) {
instance.userConfig.enabled = false; instance.userConfig.enabled = false;
instance.start(false); instance.start(false);

View File

@ -173,9 +173,18 @@ class ReactComponent {
this.important = important; this.important = important;
} }
get elements() {
if (!this.important || !this.important.selector) return [];
return document.querySelectorAll(this.important.selector);
}
get stateNodes() {
return [...this.elements].map(e => Reflection.DOM(e).getComponentStateNode(this));
}
forceUpdateAll() { forceUpdateAll() {
if (!this.important || !this.important.selector) return; for (const e of this.elements) {
for (const e of document.querySelectorAll(this.important.selector)) {
Reflection.DOM(e).forceUpdate(this); Reflection.DOM(e).forceUpdate(this);
} }
} }
@ -186,6 +195,7 @@ export class ReactComponents {
static get unknownComponents() { return this._unknownComponents || (this._unknownComponents = []) } static get unknownComponents() { return this._unknownComponents || (this._unknownComponents = []) }
static get listeners() { return this._listeners || (this._listeners = []) } static get listeners() { return this._listeners || (this._listeners = []) }
static get nameSetters() { return this._nameSetters || (this._nameSetters = []) } static get nameSetters() { return this._nameSetters || (this._nameSetters = []) }
static get componentAliases() { return this._componentAliases || (this._componentAliases = []) }
static get ReactComponent() { return ReactComponent } static get ReactComponent() { return ReactComponent }
@ -222,6 +232,8 @@ export class ReactComponents {
* @return {Promise => ReactComponent} * @return {Promise => ReactComponent}
*/ */
static async getComponent(name, important, filter) { static async getComponent(name, important, filter) {
name = this.getComponentName(name);
const have = this.components.find(c => c.id === name); const have = this.components.find(c => c.id === name);
if (have) return have; if (have) return have;
@ -239,7 +251,13 @@ export class ReactComponents {
let component, reflect; let component, reflect;
for (const element of elements) { for (const element of elements) {
reflect = Reflection.DOM(element); reflect = Reflection.DOM(element);
component = filter ? reflect.components.find(filter) : reflect.component; component = filter ? reflect.components.find(component => {
try {
return filter.call(undefined, component);
} catch (err) {
return false;
}
}) : reflect.component;
if (component) break; if (component) break;
} }
@ -276,6 +294,19 @@ export class ReactComponents {
}); });
} }
static getComponentName(name) {
const resolvedAliases = [];
while (this.componentAliases[name]) {
resolvedAliases.push(name);
name = this.componentAliases[name];
if (resolvedAliases.includes(name)) break;
}
return name;
}
static setName(name, filter) { static setName(name, filter) {
const have = this.components.find(c => c.id === name); const have = this.components.find(c => c.id === name);
if (have) return have; if (have) return have;
@ -351,6 +382,21 @@ export class ReactAutoPatcher {
this.Message.forceUpdateAll(); this.Message.forceUpdateAll();
} }
static async patchMessageContent() {
const { selector } = Reflection.resolve('container', 'containerCozy', 'containerCompact', 'edited');
this.MessageContent = await ReactComponents.getComponent('MessageContent', {selector}, c => c.defaultProps && c.defaultProps.hasOwnProperty('disableButtons'));
}
static async patchSpoiler() {
const { selector } = Reflection.resolve('spoilerText', 'spoilerContainer');
this.Spoiler = await ReactComponents.getComponent('Spoiler', {selector}, c => c.prototype.renderSpoilerText);
}
static async patchMessageAccessories() {
const { selector } = Reflection.resolve('container', 'containerCozy', 'embedWrapper');
this.MessageAccessories = await ReactComponents.getComponent('MessageAccessories', {selector});
}
static async patchMessageGroup() { static async patchMessageGroup() {
const { selector } = Reflection.resolve('container', 'message', 'messageCozy'); const { selector } = Reflection.resolve('container', 'message', 'messageCozy');
this.MessageGroup = await ReactComponents.getComponent('MessageGroup', {selector}); this.MessageGroup = await ReactComponents.getComponent('MessageGroup', {selector});
@ -369,7 +415,16 @@ export class ReactAutoPatcher {
this.MessageGroup.forceUpdateAll(); this.MessageGroup.forceUpdateAll();
} }
static async patchImageWrapper() {
ReactComponents.componentAliases.ImageWrapper = 'Image';
const { selector } = Reflection.resolve('imageWrapper');
this.ImageWrapper = await ReactComponents.getComponent('ImageWrapper', {selector}, c => typeof c.defaultProps.children === 'function');
}
static async patchChannelMember() { static async patchChannelMember() {
ReactComponents.componentAliases.ChannelMember = 'MemberListItem';
const { selector } = Reflection.resolve('member', 'memberInner', 'activity'); const { selector } = Reflection.resolve('member', 'memberInner', 'activity');
this.ChannelMember = await ReactComponents.getComponent('ChannelMember', {selector}, m => m.prototype.renderActivity); this.ChannelMember = await ReactComponents.getComponent('ChannelMember', {selector}, m => m.prototype.renderActivity);
@ -385,8 +440,13 @@ export class ReactAutoPatcher {
this.ChannelMember.forceUpdateAll(); this.ChannelMember.forceUpdateAll();
} }
static async patchNameTag() {
const { selector } = Reflection.resolve('nameTag', 'username', 'discriminator', 'ownerIcon');
this.NameTag = await ReactComponents.getComponent('NameTag', {selector});
}
static async patchGuild() { static async patchGuild() {
const selector = `div.${Reflection.resolve('guild', 'guildsWrapper').className}:not(:first-child)`; const selector = `div.${Reflection.resolve('container', 'guildIcon', 'selected', 'unread').className}:not(:first-child)`;
this.Guild = await ReactComponents.getComponent('Guild', {selector}, m => m.prototype.renderBadge); 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) => { this.unpatchGuild = MonkeyPatch('BD:ReactComponents', this.Guild.component.prototype).after('render', (component, args, retVal) => {
@ -403,7 +463,7 @@ export class ReactAutoPatcher {
* The Channel component contains the header, message scroller, message form and member list. * The Channel component contains the header, message scroller, message form and member list.
*/ */
static async patchChannel() { static async patchChannel() {
const selector = '.chat'; const { selector } = Reflection.resolve('chat', 'title', 'channelName');
this.Channel = await ReactComponents.getComponent('Channel', {selector}); this.Channel = await ReactComponents.getComponent('Channel', {selector});
this.unpatchChannel = MonkeyPatch('BD:ReactComponents', this.Channel.component.prototype).after('render', (component, args, retVal) => { this.unpatchChannel = MonkeyPatch('BD:ReactComponents', this.Channel.component.prototype).after('render', (component, args, retVal) => {
@ -419,10 +479,17 @@ export class ReactAutoPatcher {
this.Channel.forceUpdateAll(); this.Channel.forceUpdateAll();
} }
static async patchChannelTextArea() {
const { selector } = Reflection.resolve('channelTextArea', 'autocomplete');
this.ChannelTextArea = await ReactComponents.getComponent('ChannelTextArea', {selector});
}
/** /**
* The GuildTextChannel component represents a text channel in the guild channel list. * The GuildTextChannel component represents a text channel in the guild channel list.
*/ */
static async patchGuildTextChannel() { static async patchGuildTextChannel() {
ReactComponents.componentAliases.GuildTextChannel = 'TextChannel';
const { selector } = Reflection.resolve('containerDefault', 'actionIcon'); const { selector } = Reflection.resolve('containerDefault', 'actionIcon');
this.GuildTextChannel = await ReactComponents.getComponent('GuildTextChannel', {selector}, c => c.prototype.renderMentionBadge); this.GuildTextChannel = await ReactComponents.getComponent('GuildTextChannel', {selector}, c => c.prototype.renderMentionBadge);
@ -435,6 +502,8 @@ export class ReactAutoPatcher {
* The GuildVoiceChannel component represents a voice channel in the guild channel list. * The GuildVoiceChannel component represents a voice channel in the guild channel list.
*/ */
static async patchGuildVoiceChannel() { static async patchGuildVoiceChannel() {
ReactComponents.componentAliases.GuildVoiceChannel = 'VoiceChannel';
const { selector } = Reflection.resolve('containerDefault', 'actionIcon'); const { selector } = Reflection.resolve('containerDefault', 'actionIcon');
this.GuildVoiceChannel = await ReactComponents.getComponent('GuildVoiceChannel', {selector}, c => c.prototype.handleVoiceConnect); this.GuildVoiceChannel = await ReactComponents.getComponent('GuildVoiceChannel', {selector}, c => c.prototype.handleVoiceConnect);
@ -447,7 +516,9 @@ export class ReactAutoPatcher {
* The DirectMessage component represents a channel in the direct messages list. * The DirectMessage component represents a channel in the direct messages list.
*/ */
static async patchDirectMessage() { static async patchDirectMessage() {
const selector = '.channel.private'; ReactComponents.componentAliases.DirectMessage = 'PrivateChannel';
const { selector } = Reflection.resolve('channel', 'avatar', 'name');
this.DirectMessage = await ReactComponents.getComponent('DirectMessage', {selector}, c => c.prototype.renderAvatar); this.DirectMessage = await ReactComponents.getComponent('DirectMessage', {selector}, c => c.prototype.renderAvatar);
this.unpatchDirectMessage = MonkeyPatch('BD:ReactComponents', this.DirectMessage.component.prototype).after('render', this._afterChannelRender); this.unpatchDirectMessage = MonkeyPatch('BD:ReactComponents', this.DirectMessage.component.prototype).after('render', this._afterChannelRender);
@ -469,15 +540,18 @@ export class ReactAutoPatcher {
} }
static async patchUserProfileModal() { static async patchUserProfileModal() {
ReactComponents.componentAliases.UserProfileModal = 'UserProfileBody';
const { selector } = Reflection.resolve('root', 'topSectionNormal'); const { selector } = Reflection.resolve('root', 'topSectionNormal');
this.UserProfileModal = await ReactComponents.getComponent('UserProfileModal', {selector}, Filters.byPrototypeFields(['renderHeader', 'renderBadges'])); this.UserProfileModal = await ReactComponents.getComponent('UserProfileModal', {selector}, c => c.prototype.renderHeader && c.prototype.renderBadges);
this.unpatchUserProfileModal = MonkeyPatch('BD:ReactComponents', this.UserProfileModal.component.prototype).after('render', (component, args, retVal) => { this.unpatchUserProfileModal = MonkeyPatch('BD:ReactComponents', this.UserProfileModal.component.prototype).after('render', (component, args, retVal) => {
const root = retVal.props.children[0] || retVal.props.children;
const { user } = component.props; const { user } = component.props;
if (!user) return; if (!user) return;
retVal.props['data-user-id'] = user.id; root.props['data-user-id'] = user.id;
if (user.bot) retVal.props.className += ' bd-isBot'; if (user.bot) root.props.className += ' bd-isBot';
if (user.id === DiscordApi.currentUser.id) retVal.props.className += ' bd-isCurrentUser'; if (user.id === DiscordApi.currentUser.id) root.props.className += ' bd-isCurrentUser';
}); });
this.UserProfileModal.forceUpdateAll(); this.UserProfileModal.forceUpdateAll();
@ -485,24 +559,28 @@ export class ReactAutoPatcher {
static async patchUserPopout() { static async patchUserPopout() {
const { selector } = Reflection.resolve('userPopout', 'headerNormal'); const { selector } = Reflection.resolve('userPopout', 'headerNormal');
this.UserPopout = await ReactComponents.getComponent('UserPopout', {selector}); this.UserPopout = await ReactComponents.getComponent('UserPopout', {selector}, c => c.prototype.renderHeader);
this.unpatchUserPopout = MonkeyPatch('BD:ReactComponents', this.UserPopout.component.prototype).after('render', (component, args, retVal) => { this.unpatchUserPopout = MonkeyPatch('BD:ReactComponents', this.UserPopout.component.prototype).after('render', (component, args, retVal) => {
const root = retVal.props.children[0] || retVal.props.children;
const { user, guild, guildMember } = component.props; const { user, guild, guildMember } = component.props;
if (!user) return; if (!user) return;
retVal.props['data-user-id'] = user.id; root.props['data-user-id'] = user.id;
if (user.bot) retVal.props.className += ' bd-isBot'; if (user.bot) root.props.className += ' bd-isBot';
if (user.id === DiscordApi.currentUser.id) retVal.props.className += ' bd-isCurrentUser'; if (user.id === DiscordApi.currentUser.id) root.props.className += ' bd-isCurrentUser';
if (guild) retVal.props['data-guild-id'] = guild.id; if (guild) root.props['data-guild-id'] = guild.id;
if (guild && user.id === guild.ownerId) retVal.props.className += ' bd-isGuildOwner'; if (guild && user.id === guild.ownerId) root.props.className += ' bd-isGuildOwner';
if (guild && guildMember) retVal.props.className += ' bd-isGuildMember'; if (guild && guildMember) root.props.className += ' bd-isGuildMember';
if (guildMember && guildMember.roles.length) retVal.props.className += ' bd-hasRoles'; if (guildMember && guildMember.roles.length) root.props.className += ' bd-hasRoles';
}); });
this.UserPopout.forceUpdateAll(); this.UserPopout.forceUpdateAll();
} }
static async patchUploadArea() { static async patchUploadArea() {
PackageInstaller.uploadAreaPatch(); const { selector } = Reflection.resolve('uploadArea');
this.UploadArea = await ReactComponents.getComponent('UploadArea', {selector});
PackageInstaller.uploadAreaPatch(this.UploadArea);
} }
} }

View File

@ -147,6 +147,11 @@ export default class Theme extends Content {
* @param {Array} files Files to watch * @param {Array} files Files to watch
*/ */
set watchfiles(files) { set watchfiles(files) {
if (this.packed) {
// Don't watch files for packed themes
return;
}
if (!files) files = []; if (!files) files = [];
for (const file of files) { for (const file of files) {

View File

@ -36,12 +36,7 @@ export default class ThemeManager extends ContentManager {
static async loadTheme(paths, configs, info, main) { static async loadTheme(paths, configs, info, main) {
try { try {
const instance = new Theme({ const instance = new Theme({
configs, info, main, configs, info, main, paths
paths: {
contentPath: paths.contentPath,
dirName: paths.dirName,
mainPath: paths.mainPath
}
}); });
if (instance.enabled) { if (instance.enabled) {
instance.userConfig.enabled = false; instance.userConfig.enabled = false;

View File

@ -27,3 +27,9 @@ body:not(.bd-hideButton) {
.bd-settingsWrapper.platform-linux { .bd-settingsWrapper.platform-linux {
transform: none; transform: none;
} }
// Remove the margin on message attachments with an emote
.da-containerCozy + .da-containerCozy > * > .bd-emote {
margin-top: -8px;
margin-bottom: -8px;
}

View File

@ -11,7 +11,7 @@ export default new class Autocomplete {
} }
async init() { async init() {
this.cta = await ReactComponents.getComponent('ChannelTextArea', { selector: Reflection.resolve('channelTextArea', 'emojiButton').selector }); this.cta = await ReactComponents.getComponent('ChannelTextArea');
MonkeyPatch('BD:Autocomplete', this.cta.component.prototype).after('render', this.channelTextAreaAfterRender.bind(this)); MonkeyPatch('BD:Autocomplete', this.cta.component.prototype).after('render', this.channelTextAreaAfterRender.bind(this));
this.initialized = true; this.initialized = true;
} }

View File

@ -9,7 +9,7 @@
*/ */
import { Utils, ClientLogger as Logger } from 'common'; import { Utils, ClientLogger as Logger } from 'common';
import { ReactComponents, Reflection, MonkeyPatch } from 'modules'; import { Reflection, MonkeyPatch } from 'modules';
import { VueInjector, Toasts } from 'ui'; import { VueInjector, Toasts } from 'ui';
import CMGroup from './components/contextmenu/Group.vue'; import CMGroup from './components/contextmenu/Group.vue';

View File

@ -87,8 +87,7 @@ export default class extends Module {
async patchNameTag() { async patchNameTag() {
if (this.PatchedNameTag) return this.PatchedNameTag; if (this.PatchedNameTag) return this.PatchedNameTag;
const selector = Reflection.resolve('nameTag', 'username', 'discriminator', 'ownerIcon').selector; const NameTag = await ReactComponents.getComponent('NameTag');
const NameTag = await ReactComponents.getComponent('NameTag', {selector});
this.PatchedNameTag = class extends NameTag.component { this.PatchedNameTag = class extends NameTag.component {
render() { render() {

View File

@ -9,6 +9,7 @@ export * from './contextmenus';
export { default as VueInjector } from './vueinjector'; export { default as VueInjector } from './vueinjector';
export { default as Reflection } from './reflection'; export { default as Reflection } from './reflection';
export { default as Autocomplete } from './autocomplete';
export { default as ProfileBadges } from './profilebadges'; export { default as ProfileBadges } from './profilebadges';
export { default as ClassNormaliser } from './classnormaliser'; export { default as ClassNormaliser } from './classnormaliser';

View File

@ -35,12 +35,16 @@ const TEST_ARGS = () => {
'client': path.resolve(_basePath, 'client', 'dist'), 'client': path.resolve(_basePath, 'client', 'dist'),
'core': path.resolve(_basePath, 'core', 'dist'), 'core': path.resolve(_basePath, 'core', 'dist'),
'data': path.resolve(_baseDataPath, 'data'), 'data': path.resolve(_baseDataPath, 'data'),
'editor': path.resolve(_basePath, 'editor', 'dist') 'editor': path.resolve(_basePath, 'editor', 'dist'),
// tmp: path.join(_basePath, 'tmp')
tmp: path.join(os.tmpdir(), 'betterdiscord', `${process.getuid()}`)
} }
} }
} }
const TEST_EDITOR = TESTS && true; const TEST_EDITOR = TESTS && true;
import process from 'process';
import os from 'os';
import path from 'path'; import path from 'path';
import sass from 'node-sass'; import sass from 'node-sass';
import { BrowserWindow as OriginalBrowserWindow, dialog, session, shell } from 'electron'; import { BrowserWindow as OriginalBrowserWindow, dialog, session, shell } from 'electron';

View File

@ -8,7 +8,6 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import Module from './modulebase'; import Module from './modulebase';
import { FileUtils } from './utils'; import { FileUtils } from './utils';
import semver from 'semver'; import semver from 'semver';

View File

@ -10,16 +10,18 @@ span {
opacity: $spanOpacity2 !important; opacity: $spanOpacity2 !important;
} }
.chat .messages-wrapper, .da-chat .da-messagesWrapper,
#friends .friends-table { .da-friendsTable {
background-image: url(map-get($relative-file-test, url)); background-image: url(map-get($relative-file-test, url));
background-size: contain; background-size: contain;
background-repeat: no-repeat; background-repeat: no-repeat;
} }
.avatar-large { @if map-has-key($avatar, url) {
background-image: url(map-get($avatar, url)) !important; .da-avatar > .da-image {
border-radius: $avatarRadius !important; background-image: url(map-get($avatar, url)) !important;
border-radius: $avatarRadius !important;
}
} }
// Can't use a for loop as then we don't get the index // Can't use a for loop as then we don't get the index