From 285ae34b50bd790e2f35cb57d14a3fa57643548b Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Sun, 10 Mar 2019 16:30:13 +0000 Subject: [PATCH 01/15] Fix component patches --- client/src/modules/reactcomponents.js | 67 ++++++++++++++++++++------- 1 file changed, 51 insertions(+), 16 deletions(-) diff --git a/client/src/modules/reactcomponents.js b/client/src/modules/reactcomponents.js index 4dbdf46c..36bd416d 100644 --- a/client/src/modules/reactcomponents.js +++ b/client/src/modules/reactcomponents.js @@ -186,6 +186,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 +223,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 +242,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 +285,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; @@ -370,6 +392,8 @@ export class ReactAutoPatcher { } 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); @@ -386,7 +410,7 @@ export class ReactAutoPatcher { } 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 +427,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) => { @@ -423,6 +447,8 @@ export class ReactAutoPatcher { * 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 +461,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 +475,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 +499,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,18 +518,20 @@ 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) => { + Logger.log('ReactComponents', ['Rendering UserPopout', 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(); From fcfee539287ef52812d05c3296c4227ed8d9a858 Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Sun, 10 Mar 2019 16:58:24 +0000 Subject: [PATCH 02/15] Move all component selectors + filters to ReactAutoPatcher --- client/src/builtin/ColoredText.js | 2 +- client/src/builtin/E2EE.js | 6 +++--- client/src/builtin/EmoteModule.js | 4 ++-- client/src/modules/packageinstaller.js | 9 +++------ client/src/modules/reactcomponents.js | 25 ++++++++++++++++++++++++- client/src/ui/autocomplete.js | 2 +- client/src/ui/contextmenus.js | 2 +- client/src/ui/profilebadges.js | 3 +-- 8 files changed, 36 insertions(+), 17 deletions(-) diff --git a/client/src/builtin/ColoredText.js b/client/src/builtin/ColoredText.js index 290eaf6a..f7aef90c 100644 --- a/client/src/builtin/ColoredText.js +++ b/client/src/builtin/ColoredText.js @@ -50,7 +50,7 @@ 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(); } diff --git a/client/src/builtin/E2EE.js b/client/src/builtin/E2EE.js index e85b5bad..a620013c 100644 --- a/client/src/builtin/E2EE.js +++ b/client/src/builtin/E2EE.js @@ -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,11 +236,11 @@ 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); - 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'); } diff --git a/client/src/builtin/EmoteModule.js b/client/src/builtin/EmoteModule.js index aa2f927d..9839d5c2 100644 --- a/client/src/builtin/EmoteModule.js +++ b/client/src/builtin/EmoteModule.js @@ -218,7 +218,7 @@ export default new class EmoteModule extends BuiltinModule { async applyPatches() { this.patchMessageContent(); this.patchSendAndEdit(); - 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'); } @@ -226,7 +226,7 @@ export default new class EmoteModule extends BuiltinModule { * 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(); } diff --git a/client/src/modules/packageinstaller.js b/client/src/modules/packageinstaller.js index 9d20e802..e3ab8454 100644 --- a/client/src/modules/packageinstaller.js +++ b/client/src/modules/packageinstaller.js @@ -10,7 +10,6 @@ import { Utils } 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'; @@ -136,12 +135,10 @@ export default class PackageInstaller { /** * Patches Discord upload area for .bd files */ - static async uploadAreaPatch() { - const { selector } = Reflection.resolve('uploadArea'); - this.UploadArea = await ReactComponents.getComponent('UploadArea', { selector }); + static async uploadAreaPatch(UploadArea) { + const reflect = Reflection.DOM(UploadArea.important.selector); + const stateNode = reflect.getComponentStateNode(UploadArea); - 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(); diff --git a/client/src/modules/reactcomponents.js b/client/src/modules/reactcomponents.js index 36bd416d..e591a885 100644 --- a/client/src/modules/reactcomponents.js +++ b/client/src/modules/reactcomponents.js @@ -373,6 +373,11 @@ 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 patchMessageGroup() { const { selector } = Reflection.resolve('container', 'message', 'messageCozy'); this.MessageGroup = await ReactComponents.getComponent('MessageGroup', {selector}); @@ -391,6 +396,11 @@ export class ReactAutoPatcher { this.MessageGroup.forceUpdateAll(); } + static async patchImageWrapper() { + const { selector } = Reflection.resolve('imageWrapper'); + this.ImageWrapper = await ReactComponents.getComponent('ImageWrapper', {selector}); + } + static async patchChannelMember() { ReactComponents.componentAliases.ChannelMember = 'MemberListItem'; @@ -409,6 +419,11 @@ 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('container', 'guildIcon', 'selected', 'unread').className}:not(:first-child)`; this.Guild = await ReactComponents.getComponent('Guild', {selector}, m => m.prototype.renderBadge); @@ -443,6 +458,11 @@ export class ReactAutoPatcher { this.Channel.forceUpdateAll(); } + static async patchChannelTextArea() { + const { selector } = Reflection.resolve('channelTextArea', 'emojiButton'); + this.ChannelTextArea = await ReactComponents.getComponent('ChannelTextArea', {selector}); + } + /** * The GuildTextChannel component represents a text channel in the guild channel list. */ @@ -538,6 +558,9 @@ export class ReactAutoPatcher { } static async patchUploadArea() { - PackageInstaller.uploadAreaPatch(); + const { selector } = Reflection.resolve('uploadArea'); + this.UploadArea = await ReactComponents.getComponent('UploadArea', {selector}); + + PackageInstaller.uploadAreaPatch(this.UploadArea); } } diff --git a/client/src/ui/autocomplete.js b/client/src/ui/autocomplete.js index 1150e820..2e76656e 100644 --- a/client/src/ui/autocomplete.js +++ b/client/src/ui/autocomplete.js @@ -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; } diff --git a/client/src/ui/contextmenus.js b/client/src/ui/contextmenus.js index b199626c..0c53af8f 100644 --- a/client/src/ui/contextmenus.js +++ b/client/src/ui/contextmenus.js @@ -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'; diff --git a/client/src/ui/profilebadges.js b/client/src/ui/profilebadges.js index 400d2299..900b96db 100644 --- a/client/src/ui/profilebadges.js +++ b/client/src/ui/profilebadges.js @@ -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() { From 226719b36e040ec15bbeceaf139dc73ff5e4c05f Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Sun, 10 Mar 2019 17:45:20 +0000 Subject: [PATCH 03/15] Rerender messages after loading emotes --- client/src/builtin/BuiltinModule.js | 8 ++++---- client/src/builtin/E2EE.js | 2 ++ client/src/builtin/EmoteModule.js | 4 +++- client/src/modules/reactcomponents.js | 7 ++++--- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/client/src/builtin/BuiltinModule.js b/client/src/builtin/BuiltinModule.js index 28d26794..330f685a 100644 --- a/client/src/builtin/BuiltinModule.js +++ b/client/src/builtin/BuiltinModule.js @@ -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,10 +38,10 @@ export default class BuiltinModule { return Patcher.getPatchesByCaller(`BD:${this.moduleName}`); } - _settingUpdated(e) { + async _settingUpdated(e) { const { value } = e; if (value === true) { - if (this.enabled) this.enabled(e); + if (this.enabled) await this.enabled(e); if (this.applyPatches) this.applyPatches(); return; } diff --git a/client/src/builtin/E2EE.js b/client/src/builtin/E2EE.js index a620013c..82e1a363 100644 --- a/client/src/builtin/E2EE.js +++ b/client/src/builtin/E2EE.js @@ -239,9 +239,11 @@ export default new class E2EE extends BuiltinModule { const MessageContent = await ReactComponents.getComponent('MessageContent'); this.patch(MessageContent.component.prototype, 'render', this.beforeRenderMessageContent, 'before'); this.patch(MessageContent.component.prototype, 'render', this.afterRenderMessageContent); + MessageContent.forceUpdateAll(); const ImageWrapper = await ReactComponents.getComponent('ImageWrapper'); this.patch(ImageWrapper.component.prototype, 'render', this.beforeRenderImageWrapper, 'before'); + ImageWrapper.forceUpdateAll(); } beforeRenderMessageContent(component) { diff --git a/client/src/builtin/EmoteModule.js b/client/src/builtin/EmoteModule.js index 9839d5c2..581fcc97 100644 --- a/client/src/builtin/EmoteModule.js +++ b/client/src/builtin/EmoteModule.js @@ -12,7 +12,7 @@ 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'; @@ -131,6 +131,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() { diff --git a/client/src/modules/reactcomponents.js b/client/src/modules/reactcomponents.js index e591a885..d856c28c 100644 --- a/client/src/modules/reactcomponents.js +++ b/client/src/modules/reactcomponents.js @@ -397,8 +397,10 @@ export class ReactAutoPatcher { } static async patchImageWrapper() { + ReactComponents.componentAliases.ImageWrapper = 'Image'; + const { selector } = Reflection.resolve('imageWrapper'); - this.ImageWrapper = await ReactComponents.getComponent('ImageWrapper', {selector}); + this.ImageWrapper = await ReactComponents.getComponent('ImageWrapper', {selector}, c => typeof c.defaultProps.children === 'function'); } static async patchChannelMember() { @@ -541,8 +543,7 @@ export class ReactAutoPatcher { 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) => { - Logger.log('ReactComponents', ['Rendering UserPopout', component, args, retVal]); - const root = retVal.props.children[0] || retVal.props.children; + const root = retVal.props.children[0] || retVal.props.children; const { user, guild, guildMember } = component.props; if (!user) return; root.props['data-user-id'] = user.id; From fd0032b24c8deeef12f50e392200be47ccc1ead5 Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Sun, 10 Mar 2019 18:12:43 +0000 Subject: [PATCH 04/15] Autocomplete --- client/src/index.js | 4 ++-- client/src/modules/reactcomponents.js | 2 +- client/src/ui/ui.js | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/client/src/index.js b/client/src/index.js index eea4c0da..bdeec32c 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -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,7 +27,7 @@ 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, diff --git a/client/src/modules/reactcomponents.js b/client/src/modules/reactcomponents.js index d856c28c..6c0d7cfa 100644 --- a/client/src/modules/reactcomponents.js +++ b/client/src/modules/reactcomponents.js @@ -461,7 +461,7 @@ export class ReactAutoPatcher { } static async patchChannelTextArea() { - const { selector } = Reflection.resolve('channelTextArea', 'emojiButton'); + const { selector } = Reflection.resolve('channelTextArea', 'autocomplete'); this.ChannelTextArea = await ReactComponents.getComponent('ChannelTextArea', {selector}); } diff --git a/client/src/ui/ui.js b/client/src/ui/ui.js index 11c54468..f8ecde5c 100644 --- a/client/src/ui/ui.js +++ b/client/src/ui/ui.js @@ -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'; From 5a3821ad3e86947fe76f13f9564ecfaca3a3e339 Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Sun, 10 Mar 2019 20:29:55 +0000 Subject: [PATCH 05/15] Package installer UploadArea patch --- client/src/modules/packageinstaller.js | 61 ++++++++++++++++---------- tests/ext/themes/Example/index.scss | 12 ++--- 2 files changed, 44 insertions(+), 29 deletions(-) diff --git a/client/src/modules/packageinstaller.js b/client/src/modules/packageinstaller.js index e3ab8454..8c505f1b 100644 --- a/client/src/modules/packageinstaller.js +++ b/client/src/modules/packageinstaller.js @@ -13,6 +13,7 @@ import Security from './security'; import Reflection from './reflection'; import DiscordApi from './discordapi'; import ThemeManager from './thememanager'; +import { MonkeyPatch } from './patcher'; import { DOM } from 'ui'; export default class PackageInstaller { @@ -132,39 +133,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(UploadArea) { - const reflect = Reflection.DOM(UploadArea.important.selector); - const stateNode = reflect.getComponentStateNode(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 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(); + } } } diff --git a/tests/ext/themes/Example/index.scss b/tests/ext/themes/Example/index.scss index c134c1d5..225229aa 100644 --- a/tests/ext/themes/Example/index.scss +++ b/tests/ext/themes/Example/index.scss @@ -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 From ce5bcb9b85a7ca7fa870393f58288a7fb1379349 Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Sun, 10 Mar 2019 21:29:17 +0000 Subject: [PATCH 06/15] Patch MessageAccessories instead of ImageWrapper for emotes sent as images --- client/src/builtin/EmoteModule.js | 40 ++++++++++++------- client/src/modules/reactcomponents.js | 5 +++ .../src/styles/partials/discordoverrides.scss | 6 +++ 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/client/src/builtin/EmoteModule.js b/client/src/builtin/EmoteModule.js index 581fcc97..83b83a91 100644 --- a/client/src/builtin/EmoteModule.js +++ b/client/src/builtin/EmoteModule.js @@ -220,8 +220,10 @@ export default new class EmoteModule extends BuiltinModule { async applyPatches() { this.patchMessageContent(); this.patchSendAndEdit(); - const ImageWrapper = await ReactComponents.getComponent('ImageWrapper'); - 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(); } /** @@ -258,11 +260,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)); @@ -273,7 +277,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(' '); @@ -307,23 +311,29 @@ 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) { + Logger.log('Rendering emote MessageAccessories', [component, args, retVal]); - 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); + if (!component.props || !component.props.message) return; + if (!component.props.message.attachments || component.props.message.attachments.length !== 1) return; + + 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(); } /** @@ -348,7 +358,7 @@ export default new class EmoteModule extends BuiltinModule { continue; } - if (!/:(\w+):/g.test(child)) { + if (!/;(\w+);/g.test(child)) { newMarkup.push(child); continue; } @@ -357,7 +367,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; diff --git a/client/src/modules/reactcomponents.js b/client/src/modules/reactcomponents.js index 6c0d7cfa..5d4d393a 100644 --- a/client/src/modules/reactcomponents.js +++ b/client/src/modules/reactcomponents.js @@ -378,6 +378,11 @@ export class ReactAutoPatcher { this.MessageContent = await ReactComponents.getComponent('MessageContent', {selector}, c => c.defaultProps && c.defaultProps.hasOwnProperty('disableButtons')); } + 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}); diff --git a/client/src/styles/partials/discordoverrides.scss b/client/src/styles/partials/discordoverrides.scss index 1d1dc08a..591dc9c3 100644 --- a/client/src/styles/partials/discordoverrides.scss +++ b/client/src/styles/partials/discordoverrides.scss @@ -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; +} From 2a6cbd39b71cb0e78c12fb053a7810ff908d954c Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Mon, 11 Mar 2019 16:43:18 +0000 Subject: [PATCH 07/15] Remove logging --- client/src/builtin/EmoteModule.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/src/builtin/EmoteModule.js b/client/src/builtin/EmoteModule.js index 83b83a91..d36174c4 100644 --- a/client/src/builtin/EmoteModule.js +++ b/client/src/builtin/EmoteModule.js @@ -320,8 +320,6 @@ export default new class EmoteModule extends BuiltinModule { * Handle MessageAccessories render */ afterRenderMessageAccessories(component, args, retVal) { - Logger.log('Rendering emote MessageAccessories', [component, args, retVal]); - if (!component.props || !component.props.message) return; if (!component.props.message.attachments || component.props.message.attachments.length !== 1) return; From b3ba1aef1320a96d09f8aba7b47e9015dc3633e8 Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Mon, 11 Mar 2019 17:56:29 +0000 Subject: [PATCH 08/15] Render emotes in spoilers --- client/src/builtin/BuiltinModule.js | 6 ++++-- client/src/builtin/EmoteModule.js | 17 +++++++++++++++++ client/src/modules/reactcomponents.js | 5 +++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/client/src/builtin/BuiltinModule.js b/client/src/builtin/BuiltinModule.js index 330f685a..59437f70 100644 --- a/client/src/builtin/BuiltinModule.js +++ b/client/src/builtin/BuiltinModule.js @@ -75,12 +75,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() {unpatch(); return cb.apply(this, arguments);}, when); }); } diff --git a/client/src/builtin/EmoteModule.js b/client/src/builtin/EmoteModule.js index d36174c4..6afb718e 100644 --- a/client/src/builtin/EmoteModule.js +++ b/client/src/builtin/EmoteModule.js @@ -220,6 +220,7 @@ export default new class EmoteModule extends BuiltinModule { async applyPatches() { this.patchMessageContent(); this.patchSendAndEdit(); + this.patchSpoiler(); const MessageAccessories = await ReactComponents.getComponent('MessageAccessories'); this.patch(MessageAccessories.component.prototype, 'render', this.afterRenderMessageAccessories, 'after'); @@ -244,6 +245,22 @@ 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, 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 */ diff --git a/client/src/modules/reactcomponents.js b/client/src/modules/reactcomponents.js index 5d4d393a..d642ae0d 100644 --- a/client/src/modules/reactcomponents.js +++ b/client/src/modules/reactcomponents.js @@ -378,6 +378,11 @@ export class ReactAutoPatcher { 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}); From 4aa38f458266c228e9714333a07b3543dd808061 Mon Sep 17 00:00:00 2001 From: Samuel Elliott Date: Tue, 12 Mar 2019 15:22:34 +0000 Subject: [PATCH 09/15] Move E2EE and emote module to their own directories --- client/src/builtin/{ => E2EE}/E2EE.js | 2 +- client/src/builtin/{ => E2EE}/E2EEComponent.vue | 2 +- client/src/builtin/{ => E2EE}/E2EEMessageButton.vue | 2 +- client/src/builtin/E2EE/index.js | 3 +++ client/src/builtin/{ => Emotes}/EmoteAc.js | 13 +++---------- client/src/builtin/{ => Emotes}/EmoteComponent.js | 9 ++------- client/src/builtin/{ => Emotes}/EmoteComponent.vue | 2 +- client/src/builtin/{ => Emotes}/EmoteModule.js | 7 ++----- client/src/builtin/Emotes/index.js | 4 ++++ client/src/builtin/Manager.js | 3 +-- client/src/builtin/builtin.js | 2 +- 11 files changed, 20 insertions(+), 29 deletions(-) rename client/src/builtin/{ => E2EE}/E2EE.js (99%) rename client/src/builtin/{ => E2EE}/E2EEComponent.vue (98%) rename client/src/builtin/{ => E2EE}/E2EEMessageButton.vue (90%) create mode 100644 client/src/builtin/E2EE/index.js rename client/src/builtin/{ => Emotes}/EmoteAc.js (91%) rename client/src/builtin/{ => Emotes}/EmoteComponent.js (81%) rename client/src/builtin/{ => Emotes}/EmoteComponent.vue (94%) rename client/src/builtin/{ => Emotes}/EmoteModule.js (98%) create mode 100644 client/src/builtin/Emotes/index.js diff --git a/client/src/builtin/E2EE.js b/client/src/builtin/E2EE/E2EE.js similarity index 99% rename from client/src/builtin/E2EE.js rename to client/src/builtin/E2EE/E2EE.js index 82e1a363..682f1bf7 100644 --- a/client/src/builtin/E2EE.js +++ b/client/src/builtin/E2EE/E2EE.js @@ -9,7 +9,7 @@ */ import { Settings, Cache, Events } from 'modules'; -import BuiltinModule from './BuiltinModule'; +import BuiltinModule from '../BuiltinModule'; import { Reflection, ReactComponents, MonkeyPatch, Patcher, DiscordApi, Security } from 'modules'; import { VueInjector, Modals, Toasts } from 'ui'; import { ClientLogger as Logger, ClientIPC } from 'common'; diff --git a/client/src/builtin/E2EEComponent.vue b/client/src/builtin/E2EE/E2EEComponent.vue similarity index 98% rename from client/src/builtin/E2EEComponent.vue rename to client/src/builtin/E2EE/E2EEComponent.vue index 81bfda70..1d943ff1 100644 --- a/client/src/builtin/E2EEComponent.vue +++ b/client/src/builtin/E2EE/E2EEComponent.vue @@ -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: { diff --git a/client/src/builtin/E2EEMessageButton.vue b/client/src/builtin/E2EE/E2EEMessageButton.vue similarity index 90% rename from client/src/builtin/E2EEMessageButton.vue rename to client/src/builtin/E2EE/E2EEMessageButton.vue index 9edf4968..ded7b5bc 100644 --- a/client/src/builtin/E2EEMessageButton.vue +++ b/client/src/builtin/E2EE/E2EEMessageButton.vue @@ -17,7 +17,7 @@