diff --git a/client/src/modules/pluginapi.js b/client/src/modules/pluginapi.js
index 387b7515..ac5bfae7 100644
--- a/client/src/modules/pluginapi.js
+++ b/client/src/modules/pluginapi.js
@@ -12,6 +12,7 @@ import { EmoteModule } from 'builtin';
import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs';
import { BdMenu, Modals, DOM, DOMObserver, VueInjector, Toasts, Notifications, BdContextMenu, DiscordContextMenu } from 'ui';
import * as CommonComponents from 'commoncomponents';
+import { default as Components } from '../ui/components/generic';
import { Utils, Filters, ClientLogger as Logger, ClientIPC, AsyncEventEmitter } from 'common';
import Settings from './settings';
import ExtModuleManager from './extmodulemanager';
@@ -24,6 +25,9 @@ import DiscordApi from './discordapi';
import { ReactComponents, ReactHelpers } from './reactcomponents';
import { Patcher, MonkeyPatch } from './patcher';
import GlobalAc from '../ui/autocomplete';
+import Vue from 'vue';
+import path from 'path';
+import Globals from './globals';
export default class PluginApi {
@@ -61,6 +65,7 @@ export default class PluginApi {
get EventsWrapper() { return EventsWrapper }
get CommonComponents() { return CommonComponents }
+ get Components() { return Components }
get Filters() { return Filters }
get Discord() { return DiscordApi }
get DiscordApi() { return DiscordApi }
@@ -105,7 +110,9 @@ export default class PluginApi {
removeFromArray: (...args) => Utils.removeFromArray.apply(Utils, args),
defineSoftGetter: (...args) => Utils.defineSoftGetter.apply(Utils, args),
wait: (...args) => Utils.wait.apply(Utils, args),
- until: (...args) => Utils.until.apply(Utils, args)
+ until: (...args) => Utils.until.apply(Utils, args),
+ findInTree: (...args) => Utils.findInTree.apply(Utils, args),
+ findInReactTree: (...args) => Utils.findInReactTree.apply(Utils, args)
};
}
@@ -605,6 +612,10 @@ export default class PluginApi {
});
}
+ Vuewrap(id, component, props) {
+ return VueInjector.createReactElement(Vue.component(id, component), props);
+ }
+
}
// Stop plugins from modifying the plugin API for all plugins
diff --git a/client/src/modules/pluginmanager.js b/client/src/modules/pluginmanager.js
index 079a9ae1..e1c0d04b 100644
--- a/client/src/modules/pluginmanager.js
+++ b/client/src/modules/pluginmanager.js
@@ -130,6 +130,12 @@ export default class extends ContentManager {
static unloadContentHook(content, reload) {
delete Globals.require.cache[Globals.require.resolve(content.paths.mainPath)];
+ const uncache = [];
+ for (const required in Globals.require.cache) {
+ if (!required.includes(content.paths.contentPath)) continue;
+ uncache.push(Globals.require.resolve(required));
+ }
+ for (const u of uncache) delete Globals.require.cache[u];
}
/**
diff --git a/client/src/ui/components/generic/Button.vue b/client/src/ui/components/generic/Button.vue
new file mode 100644
index 00000000..190d5507
--- /dev/null
+++ b/client/src/ui/components/generic/Button.vue
@@ -0,0 +1,21 @@
+/**
+ * BetterDiscord Generic Button Component
+ * Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
+ * All rights reserved.
+ * https://betterdiscord.net
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+*/
+
+
+
+ {{text}}
+
+
+
+
diff --git a/client/src/ui/components/generic/ButtonGroup.vue b/client/src/ui/components/generic/ButtonGroup.vue
new file mode 100644
index 00000000..d57011cd
--- /dev/null
+++ b/client/src/ui/components/generic/ButtonGroup.vue
@@ -0,0 +1,23 @@
+/**
+ * BetterDiscord Generic Button Group Component
+ * Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
+ * All rights reserved.
+ * https://betterdiscord.net
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+*/
+
+
+
+
+
+
+
+
diff --git a/client/src/ui/components/generic/index.js b/client/src/ui/components/generic/index.js
new file mode 100644
index 00000000..e89b4651
--- /dev/null
+++ b/client/src/ui/components/generic/index.js
@@ -0,0 +1,29 @@
+import VrWrapper from '../../vrwrapper';
+
+import ButtonGroupComponent from './ButtonGroup.vue';
+class ButtonGroupWrapper extends VrWrapper {
+ get component() { return ButtonGroupComponent }
+ constructor(props) {
+ super();
+ this.props = props;
+ }
+}
+
+import ButtonComponent from './Button.vue';
+class ButtonWrapper extends VrWrapper {
+ get component() { return ButtonComponent }
+ constructor(props) {
+ super();
+ this.props = props;
+ }
+}
+
+export default class {
+ static Button(props) {
+ return new ButtonWrapper(props);
+ }
+
+ static ButtonGroup(props) {
+ return new ButtonGroupWrapper(props);
+ }
+}
diff --git a/tests/ext/plugins/Render Example/components/reactcomponent.js b/tests/ext/plugins/Render Example/components/reactcomponent.js
new file mode 100644
index 00000000..36222d14
--- /dev/null
+++ b/tests/ext/plugins/Render Example/components/reactcomponent.js
@@ -0,0 +1,7 @@
+module.exports = (React, props) => {
+ return React.createElement(
+ 'button',
+ { className: 'exampleCustomElement', onClick: props.onClick },
+ 'r'
+ );
+}
diff --git a/tests/ext/plugins/Render Example/components/vuecomponent.js b/tests/ext/plugins/Render Example/components/vuecomponent.js
new file mode 100644
index 00000000..197f72cc
--- /dev/null
+++ b/tests/ext/plugins/Render Example/components/vuecomponent.js
@@ -0,0 +1,13 @@
+module.exports = (VueWrap, props) => {
+ return VueWrap('somecomponent', {
+ render: function (createElement) {
+ return createElement('button', {
+ class: 'exampleCustomElement',
+ on: {
+ click: this.onClick
+ }
+ }, 'v');
+ },
+ props: ['onClick']
+ }, props);
+}
diff --git a/tests/ext/plugins/Render Example/config.json b/tests/ext/plugins/Render Example/config.json
new file mode 100644
index 00000000..ea3abb01
--- /dev/null
+++ b/tests/ext/plugins/Render Example/config.json
@@ -0,0 +1,17 @@
+{
+ "info": {
+ "id": "render-example",
+ "name": "Render Example",
+ "authors": [
+ {
+ "name": "Jiiks",
+ "discord_id": "81388395867156480",
+ "github_username": "Jiiks",
+ "twitter_username": "Jiiksi"
+ }
+ ],
+ "version": 1.0,
+ "description": "Example for rendering stuff"
+ },
+ "main": "index.js"
+}
diff --git a/tests/ext/plugins/Render Example/index.js b/tests/ext/plugins/Render Example/index.js
new file mode 100644
index 00000000..60297e5d
--- /dev/null
+++ b/tests/ext/plugins/Render Example/index.js
@@ -0,0 +1,128 @@
+/**
+ * This is an example of how you should add custom elements instead of manipulating the DOM directly
+ */
+
+// Import custom components
+const customVueComponent = require('./components/vuecomponent');
+const customReactComponent = require('./components/reactcomponent');
+
+module.exports = (Plugin, Api, Vendor) => {
+
+ // Destructure some apis
+ const { Logger, ReactComponents, Patcher, monkeyPatch, Reflection, Utils, CssUtils, VueInjector, Vuewrap, requireUncached } = Api;
+ const { Vue } = Vendor;
+ const { React } = Reflection.modules; // This should be in vendor
+
+ return class extends Plugin {
+
+ async onStart() {
+ this.injectStyle();
+ this.patchGuildTextChannel();
+ this.patchMessages();
+ return true;
+ }
+
+ async onStop() {
+ // The automatic unpatcher is not there yet
+ Patcher.unpatchAll();
+ CssUtils.deleteAllStyles();
+
+ // Force update elements to remove our changes
+ const GuildTextChannel = await ReactComponents.getComponent('GuildTextChannel');
+ GuildTextChannel.forceUpdateAll();
+ const MessageContent = await ReactComponents.getComponent('MessageContent', { selector: Reflection.resolve('container', 'containerCozy', 'containerCompact', 'edited').selector });
+ MessageContent.forceUpdateAll();
+ return true;
+ }
+
+ /* Inject some style for our custom element */
+ async injectStyle() {
+ const css = `
+ .exampleCustomElement {
+ background: #7a7d82;
+ color: #FFF;
+ border-radius: 5px;
+ font-size: 12px;
+ font-weight: 600;
+ opacity: .5;
+ &:hover {
+ opacity: 1;
+ }
+ }
+ .exampleBtnGroup {
+ .bd-button {
+ font-size: 14px;
+ padding: 5px;
+ }
+ }
+ `;
+ await CssUtils.injectSass(css);
+ }
+
+ async patchGuildTextChannel() {
+ // Get the GuildTextChannel component and patch it's render function
+ const GuildTextChannel = await ReactComponents.getComponent('GuildTextChannel');
+ monkeyPatch(GuildTextChannel.component.prototype).after('render', this.injectCustomElements.bind(this));
+ // Force update to see our changes immediatly
+ GuildTextChannel.forceUpdateAll();
+ }
+
+ async patchMessages() {
+ // Get Message component and patch it's render function
+ const MessageContent = await ReactComponents.getComponent('MessageContent', { selector: Reflection.resolve('container', 'containerCozy', 'containerCompact', 'edited').selector });
+ monkeyPatch(MessageContent.component.prototype).after('render', this.injectGenericComponents.bind(this));
+ // Force update to see our changes immediatly
+ MessageContent.forceUpdateAll();
+ }
+
+ /*
+ * Injecting a custom React element using React.createElement
+ * https://reactjs.org/docs/react-api.html#createelement
+ * Injecting a custom Vue element using Vue.component
+ * https://vuejs.org/v2/guide/render-function.html
+ **/
+ injectCustomElements(that, args, returnValue) {
+ // Get the child we want using a treewalker since we know the child we want has a channel property and children.
+ const child = Utils.findInReactTree(returnValue, filter => filter.hasOwnProperty('channel') && filter.children);
+ if (!child) return;
+ // If children is not an array make it into one
+ if (!child.children instanceof Array) child.children = [child.children];
+
+ // Add our custom components to children
+ child.children.push(customReactComponent(React, { onClick: e => this.handleClick(e, child.channel) }));
+ child.children.push(customVueComponent(Vuewrap, { onClick: e => this.handleClick(e, child.channel) }));
+ }
+
+ /**
+ * Inject generic components provided by BD
+ */
+ injectGenericComponents(that, args, returnValue) {
+ // If children is not an array make it into one
+ if (!returnValue.props.children instanceof Array) returnValue.props.children = [returnValue.props.children];
+ // Add a generic Button component provided by BD
+ returnValue.props.children.push(Api.Components.ButtonGroup({
+ classes: [ 'exampleBtnGroup' ], // Additional classes for button group
+ buttons: [
+ {
+ classes: ['exampleBtn'], // Additional classes for button
+ text: 'Hello World!', // Text for button
+ onClick: e => Logger.log('Hello World!') // Button click handler
+ },
+ {
+ classes: ['exampleBtn'],
+ text: 'Button',
+ onClick: e => Logger.log('Button!')
+ }
+ ]
+ }).render()); // Render will return the wrapped component that can then be displayed
+ }
+
+ /**
+ * Will log the channel object
+ */
+ handleClick(e, channel) {
+ Logger.log('Clicked!', channel);
+ }
+ }
+
+};