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
release
tests/tmp
tests/log.txt
# User data

View File

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

View File

@ -22,10 +22,10 @@ export default class BuiltinModule {
this.patch = this.patch.bind(this);
}
init() {
async init() {
this.setting.on('setting-updated', this._settingUpdated);
if (this.setting.value) {
if (this.enabled) this.enabled();
if (this.enabled) await this.enabled();
if (this.applyPatches) this.applyPatches();
}
}
@ -38,16 +38,15 @@ export default class BuiltinModule {
return Patcher.getPatchesByCaller(`BD:${this.moduleName}`);
}
_settingUpdated(e) {
const { value } = e;
if (value === true) {
if (this.enabled) this.enabled(e);
if (this.applyPatches) this.applyPatches();
return;
}
if (value === false) {
if (this.disabled) this.disabled(e);
async _settingUpdated(e) {
if (e.value) {
if (this.enabled) await this.enabled(e);
if (this.applyPatches) await this.applyPatches();
if (this.rerenderPatchedComponents) this.rerenderPatchedComponents();
} else {
if (this.disabled) await this.disabled(e);
this.unpatch();
if (this.rerenderPatchedComponents) this.rerenderPatchedComponents();
}
}
@ -75,12 +74,14 @@ export default class BuiltinModule {
*/
patch(module, fnName, cb, 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') {
const last = child.pop();
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);
}
rerenderPatchedComponents() {
if (this.MessageContent) this.MessageContent.forceUpdateAll();
}
/* Methods */
_intensityUpdated() {
this.MessageContent.forceUpdateAll();
@ -50,16 +54,16 @@ export default new class ColoredText extends BuiltinModule {
/* Patches */
async applyPatches() {
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.MessageContent.forceUpdateAll();
}
/**
* Set markup text colour to match role colour
*/
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 markup = Utils.findInReactTree(returnValue, m => m && m.props && m.props.className && m.props.className.includes('da-markup'));
const roleColor = thisObject.props.message.colorString;

View File

@ -9,8 +9,8 @@
*/
import { Settings, Cache, Events } from 'modules';
import BuiltinModule from './BuiltinModule';
import { Reflection, ReactComponents, MonkeyPatch, Patcher, DiscordApi, Security } from 'modules';
import BuiltinModule from '../BuiltinModule';
import { Reflection, ReactComponents, DiscordApi, Security } from 'modules';
import { VueInjector, Modals, Toasts } from 'ui';
import { ClientLogger as Logger, ClientIPC } from 'common';
import { request } from 'vendor';
@ -172,7 +172,7 @@ export default new class E2EE extends BuiltinModule {
this.patch(Dispatcher, 'dispatch', this.dispatcherPatch, 'before');
this.patchMessageContent();
const ChannelTextArea = await ReactComponents.getComponent('ChannelTextArea', { selector: Reflection.resolve('channelTextArea', 'emojiButton').selector });
const ChannelTextArea = await ReactComponents.getComponent('ChannelTextArea');
this.patchChannelTextArea(ChannelTextArea);
this.patchChannelTextAreaSubmit(ChannelTextArea);
ChannelTextArea.forceUpdateAll();
@ -236,12 +236,14 @@ export default new class E2EE extends BuiltinModule {
}
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.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');
ImageWrapper.forceUpdateAll();
}
beforeRenderMessageContent(component) {
@ -285,10 +287,16 @@ export default new class E2EE extends BuiltinModule {
component.props.message.contentParsed = create.contentParsed;
}
afterRenderMessageContent(component, args, retVal) {
afterRenderMessageContent(component, _childrenObject, args, retVal) {
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;
try {
buttons.unshift(VueInjector.createReactElement(E2EEMessageButton));
} catch (err) {

View File

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

View File

@ -17,7 +17,7 @@
</template>
<script>
import { MiLock } from '../ui/components/common/MaterialIcon';
import { MiLock } from 'commoncomponents';
export default {
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 BuiltinModule from './BuiltinModule';
import EmoteModule from './EmoteModule';
import GlobalAc from '../ui/autocomplete';
import BuiltinModule from '../BuiltinModule';
import EmoteModule, { EMOTE_SOURCES } from './EmoteModule';
import GlobalAc from 'autocomplete';
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 {
/* Getters */

View File

@ -8,15 +8,10 @@
* 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';
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 {
constructor(type, id, name) {

View File

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

View File

@ -8,20 +8,17 @@
* LICENSE file in the root directory of this source tree.
*/
import BuiltinModule from './BuiltinModule';
import BuiltinModule from '../BuiltinModule';
import path from 'path';
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 { DiscordContextMenu } from 'ui';
import Emote from './EmoteComponent.js';
import Autocomplete from '../ui/components/common/Autocomplete.vue';
import GlobalAc from '../ui/autocomplete';
const EMOTE_SOURCES = [
export 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'
@ -131,6 +128,8 @@ export default new class EmoteModule extends BuiltinModule {
this.database.set(id, { id: emote.value.id || value, type });
}
Logger.log('EmoteModule', ['Loaded emote database']);
}
async loadUserData() {
@ -218,15 +217,18 @@ export default new class EmoteModule extends BuiltinModule {
async applyPatches() {
this.patchMessageContent();
this.patchSendAndEdit();
const ImageWrapper = await ReactComponents.getComponent('ImageWrapper', { selector: Reflection.resolve('imageWrapper').selector });
this.patch(ImageWrapper.component.prototype, 'render', this.beforeRenderImageWrapper, 'before');
this.patchSpoiler();
const MessageAccessories = await ReactComponents.getComponent('MessageAccessories');
this.patch(MessageAccessories.component.prototype, 'render', this.afterRenderMessageAccessories, 'after');
MessageAccessories.forceUpdateAll();
}
/**
* Patches MessageContent render method
*/
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);
MessageContent.forceUpdateAll();
}
@ -240,10 +242,26 @@ export default new class EmoteModule extends BuiltinModule {
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
*/
afterRenderMessageContent(component, args, retVal) {
afterRenderMessageContent(component, _childrenObject, args, retVal) {
const markup = Utils.findInReactTree(retVal, filter =>
filter &&
filter.className &&
@ -256,11 +274,13 @@ export default new class EmoteModule extends BuiltinModule {
/**
* Handle send message
*/
async handleSendMessage(component, args, orig) {
async handleSendMessage(MessageActions, args, orig) {
if (!args.length) return orig(...args);
const { content } = args[1];
if (!content) return orig(...args);
Logger.log('EmoteModule', ['Sending message', MessageActions, args, orig]);
const emoteAsImage = Settings.getSetting('emotes', 'default', 'emoteasimage').value &&
(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);
if (!emote) return word;
this.addToMostUsed(emote);
return emote ? `:${isEmote[1]}:` : word;
return emote ? `;${isEmote[1]};` : word;
}
return word;
}).join(' ');
@ -305,23 +325,27 @@ export default new class EmoteModule extends BuiltinModule {
if (!content) return orig(...args);
args[2].content = args[2].content.split(' ').map(word => {
const isEmote = /;(.*?);/g.exec(word);
return isEmote ? `:${isEmote[1]}:` : word;
return isEmote ? `;${isEmote[1]};` : word;
}).join(' ');
return orig(...args);
}
/**
* Handle imagewrapper render
* Handle MessageAccessories render
*/
beforeRenderImageWrapper(component, args, retVal) {
if (!component.props || !component.props.src) return;
afterRenderMessageAccessories(component, args, retVal) {
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];
if (!src || !src.includes('.bdemote.')) return;
const emoteName = src.split('/').pop().split('.')[0];
const emote = this.findByName(emoteName);
const filename = component.props.message.attachments[0].filename;
const match = filename.match(/([^/]*)\.bdemote\.(gif|png)$/i);
if (!match) return;
const emote = this.findByName(match[1]);
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) {
if (typeof child !== 'string') {
if (typeof child === 'object') {
const isEmoji = Utils.findInReactTree(child, 'emojiName');
if (isEmoji) child.props.children.props.jumboable = jumboable;
const isEmoji = Utils.findInReactTree(child, filter => filter && filter.emojiName);
if (isEmoji) isEmoji.jumboable = jumboable;
}
newMarkup.push(child);
continue;
}
if (!/:(\w+):/g.test(child)) {
if (!/;(\w+);/g.test(child)) {
newMarkup.push(child);
continue;
}
@ -355,7 +379,7 @@ export default new class EmoteModule extends BuiltinModule {
let s = '';
for (const word of words) {
const isemote = /:(.*?):/g.exec(word);
const isemote = /;(.*?);/g.exec(word);
if (!isemote) {
s += word;
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 { default as ReactDevtoolsModule } from './ReactDevtoolsModule';
import { default as VueDevtoolsModule } from './VueDevToolsModule';
import { default as TrackingProtection } from './TrackingProtection';
import { default as E2EE } from './E2EE';
import { default as ColoredText } from './ColoredText';
import { default as TwentyFourHour } from './24Hour';
import { default as KillClyde } from './KillClyde';
import { default as BlockedMessages } from './BlockedMessages';
import { default as VoiceDisconnect } from './VoiceDisconnect';
import { default as EmoteAc } from './EmoteAc';
import { EmoteModule, EmoteAc } from './Emotes';
import ReactDevtoolsModule from './ReactDevtoolsModule';
import VueDevtoolsModule from './VueDevToolsModule';
import TrackingProtection from './TrackingProtection';
import E2EE from './E2EE';
import ColoredText from './ColoredText';
import TwentyFourHour from './24Hour';
import KillClyde from './KillClyde';
import BlockedMessages from './BlockedMessages';
import VoiceDisconnect from './VoiceDisconnect';
export default class {
static get modules() {
return require('./builtin');
}
static initAll() {
EmoteModule.init();
ReactDevtoolsModule.init();

View File

@ -12,7 +12,7 @@ import BuiltinModule from './BuiltinModule';
import { Reflection } from 'modules';
export default new class E2EE extends BuiltinModule {
export default new class TrackingProtection extends BuiltinModule {
/* Getters */
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 VueDevtoolsModule } from './VueDevToolsModule';
export { default as TrackingProtection } from './TrackingProtection';

View File

@ -8,7 +8,7 @@
* 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 { 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';
@ -27,14 +27,14 @@ class BetterDiscord {
Logger.log('main', 'BetterDiscord starting');
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,
ModuleManager, PluginManager, ThemeManager, ExtModuleManager, PackageInstaller,
Vendor,
Patcher, MonkeyPatch, ReactComponents, ReactHelpers, ReactAutoPatcher, DiscordApi,
EmoteModule,
BuiltinManager, EmoteModule,
BdWebApi,
Connectivity,
Cache,

View File

@ -65,6 +65,10 @@ export default class Content extends AsyncEventEmitter {
get config() { return this.settings.categories }
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.
* @return {Modal}

View File

@ -220,6 +220,7 @@ export default class {
const unpackedPath = path.join(Globals.getPath('tmp'), packageName);
asar.extractAll(packagePath, unpackedPath);
return this.preloadContent({
config,
contentPath: unpackedPath,
@ -228,8 +229,8 @@ export default class {
packageName,
packed: true
}, reload, index);
} catch (err) {
Logger.log('ContentManager', ['Error extracting packed content', err]);
throw err;
}
}
@ -322,12 +323,6 @@ export default class {
return content;
} catch (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 FileUtils.recursiveDeleteDirectory(content.paths.contentPath);
if (content.packed) await FileUtils.recursiveDeleteDirectory(content.packagePath);
return true;
} catch (err) {
Logger.err(this.moduleName, err);
@ -384,7 +380,7 @@ export default class {
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);
} catch (err) {

View File

@ -36,10 +36,6 @@ export default new class extends Module {
async first() {
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 is for Discord to stop error reporting :3

View File

@ -6,14 +6,14 @@ import rimraf from 'rimraf';
import { request } from 'vendor';
import { Modals } from 'ui';
import { Utils } from 'common';
import { Utils, FileUtils } from 'common';
import PluginManager from './pluginmanager';
import Globals from './globals';
import Security from './security';
import { ReactComponents } from './reactcomponents';
import Reflection from './reflection';
import DiscordApi from './discordapi';
import ThemeManager from './thememanager';
import { MonkeyPatch } from './patcher';
import { DOM } from 'ui';
export default class PackageInstaller {
@ -84,15 +84,10 @@ export default class PackageInstaller {
await oldContent.unload(true);
if (oldContent.packed && oldContent.packed.packageName !== nameOrId) {
rimraf(oldContent.packed.packagePath, err => {
if (err) throw err;
});
} else {
rimraf(oldContent.contentPath, err => {
if (err) throw err;
});
if (oldContent.packed && oldContent.packageName !== nameOrId) {
await FileUtils.deleteFile(oldContent.packagePath).catch(err => null);
}
await FileUtils.recursiveDeleteDirectory(oldContent.contentPath).catch(err => null);
return manager.preloadPackedContent(outputName);
} 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
*/
static async uploadAreaPatch() {
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);
};
static async uploadAreaPatch(UploadArea) {
// Add a listener to root for when not in a channel
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.
reflect.element.removeEventListener('drop', stateNode.handleDrop);
reflect.element.addEventListener('drop', callback);
reflect.element.addEventListener('drop', stateNode.handleDrop);
const unpatchUploadAreaHandleDrop = MonkeyPatch('BD:ReactComponents', UploadArea.component.prototype).instead('handleDrop', (component, [e], original) => this.handleDrop(component, e, original));
this.unpatchUploadArea = function () {
reflect.element.removeEventListener('drop', callback);
root.removeEventListener('drop', callback);
this.unpatchUploadArea = () => {
unpatchUploadAreaHandleDrop();
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.`};
const instance = new plugin({
configs, info, main,
paths: {
contentPath: paths.contentPath,
dirName: packed ? packed.packageName : paths.dirName,
mainPath: paths.mainPath
}
configs, info, main, paths
});
if (packed) instance.packed = {
pkg: packed.pkg,
packageName: packed.packageName,
packagePath: packed.packagePath,
packed: true
}; else instance.packed = false;
if (instance.enabled && this.loaded) {
instance.userConfig.enabled = false;
instance.start(false);

View File

@ -173,9 +173,18 @@ class ReactComponent {
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() {
if (!this.important || !this.important.selector) return;
for (const e of document.querySelectorAll(this.important.selector)) {
for (const e of this.elements) {
Reflection.DOM(e).forceUpdate(this);
}
}
@ -186,6 +195,7 @@ export class ReactComponents {
static get unknownComponents() { return this._unknownComponents || (this._unknownComponents = []) }
static get listeners() { return this._listeners || (this._listeners = []) }
static get nameSetters() { return this._nameSetters || (this._nameSetters = []) }
static get componentAliases() { return this._componentAliases || (this._componentAliases = []) }
static get ReactComponent() { return ReactComponent }
@ -222,6 +232,8 @@ export class ReactComponents {
* @return {Promise => ReactComponent}
*/
static async getComponent(name, important, filter) {
name = this.getComponentName(name);
const have = this.components.find(c => c.id === name);
if (have) return have;
@ -239,7 +251,13 @@ export class ReactComponents {
let component, reflect;
for (const element of elements) {
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;
}
@ -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) {
const have = this.components.find(c => c.id === name);
if (have) return have;
@ -351,6 +382,21 @@ export class ReactAutoPatcher {
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() {
const { selector } = Reflection.resolve('container', 'message', 'messageCozy');
this.MessageGroup = await ReactComponents.getComponent('MessageGroup', {selector});
@ -369,7 +415,16 @@ export class ReactAutoPatcher {
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() {
ReactComponents.componentAliases.ChannelMember = 'MemberListItem';
const { selector } = Reflection.resolve('member', 'memberInner', 'activity');
this.ChannelMember = await ReactComponents.getComponent('ChannelMember', {selector}, m => m.prototype.renderActivity);
@ -385,8 +440,13 @@ export class ReactAutoPatcher {
this.ChannelMember.forceUpdateAll();
}
static async patchNameTag() {
const { selector } = Reflection.resolve('nameTag', 'username', 'discriminator', 'ownerIcon');
this.NameTag = await ReactComponents.getComponent('NameTag', {selector});
}
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.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.
*/
static async patchChannel() {
const selector = '.chat';
const { selector } = Reflection.resolve('chat', 'title', 'channelName');
this.Channel = await ReactComponents.getComponent('Channel', {selector});
this.unpatchChannel = MonkeyPatch('BD:ReactComponents', this.Channel.component.prototype).after('render', (component, args, retVal) => {
@ -419,10 +479,17 @@ export class ReactAutoPatcher {
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.
*/
static async patchGuildTextChannel() {
ReactComponents.componentAliases.GuildTextChannel = 'TextChannel';
const { selector } = Reflection.resolve('containerDefault', 'actionIcon');
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.
*/
static async patchGuildVoiceChannel() {
ReactComponents.componentAliases.GuildVoiceChannel = 'VoiceChannel';
const { selector } = Reflection.resolve('containerDefault', 'actionIcon');
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.
*/
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.unpatchDirectMessage = MonkeyPatch('BD:ReactComponents', this.DirectMessage.component.prototype).after('render', this._afterChannelRender);
@ -469,15 +540,18 @@ export class ReactAutoPatcher {
}
static async patchUserProfileModal() {
ReactComponents.componentAliases.UserProfileModal = 'UserProfileBody';
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) => {
const root = retVal.props.children[0] || retVal.props.children;
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';
root.props['data-user-id'] = user.id;
if (user.bot) root.props.className += ' bd-isBot';
if (user.id === DiscordApi.currentUser.id) root.props.className += ' bd-isCurrentUser';
});
this.UserProfileModal.forceUpdateAll();
@ -485,24 +559,28 @@ export class ReactAutoPatcher {
static async patchUserPopout() {
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) => {
const root = retVal.props.children[0] || retVal.props.children;
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';
root.props['data-user-id'] = user.id;
if (user.bot) root.props.className += ' bd-isBot';
if (user.id === DiscordApi.currentUser.id) root.props.className += ' bd-isCurrentUser';
if (guild) root.props['data-guild-id'] = guild.id;
if (guild && user.id === guild.ownerId) root.props.className += ' bd-isGuildOwner';
if (guild && guildMember) root.props.className += ' bd-isGuildMember';
if (guildMember && guildMember.roles.length) root.props.className += ' bd-hasRoles';
});
this.UserPopout.forceUpdateAll();
}
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
*/
set watchfiles(files) {
if (this.packed) {
// Don't watch files for packed themes
return;
}
if (!files) 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) {
try {
const instance = new Theme({
configs, info, main,
paths: {
contentPath: paths.contentPath,
dirName: paths.dirName,
mainPath: paths.mainPath
}
configs, info, main, paths
});
if (instance.enabled) {
instance.userConfig.enabled = false;

View File

@ -27,3 +27,9 @@ body:not(.bd-hideButton) {
.bd-settingsWrapper.platform-linux {
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() {
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));
this.initialized = true;
}

View File

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

View File

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

View File

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

View File

@ -35,12 +35,16 @@ const TEST_ARGS = () => {
'client': path.resolve(_basePath, 'client', 'dist'),
'core': path.resolve(_basePath, 'core', 'dist'),
'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;
import process from 'process';
import os from 'os';
import path from 'path';
import sass from 'node-sass';
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.
*/
import Module from './modulebase';
import { FileUtils } from './utils';
import semver from 'semver';

View File

@ -10,16 +10,18 @@ span {
opacity: $spanOpacity2 !important;
}
.chat .messages-wrapper,
#friends .friends-table {
.da-chat .da-messagesWrapper,
.da-friendsTable {
background-image: url(map-get($relative-file-test, url));
background-size: contain;
background-repeat: no-repeat;
}
.avatar-large {
background-image: url(map-get($avatar, url)) !important;
border-radius: $avatarRadius !important;
@if map-has-key($avatar, url) {
.da-avatar > .da-image {
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