Skip to content

Commit 30716b5

Browse files
authored
Merge pull request #251 from JsSucks/example-plugin
Render example
2 parents ad64743 + b18559a commit 30716b5

File tree

9 files changed

+256
-1
lines changed

9 files changed

+256
-1
lines changed

client/src/modules/pluginapi.js

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { EmoteModule } from 'builtin';
1212
import { SettingsSet, SettingsCategory, Setting, SettingsScheme } from 'structs';
1313
import { BdMenu, Modals, DOM, DOMObserver, VueInjector, Toasts, Notifications, BdContextMenu, DiscordContextMenu } from 'ui';
1414
import * as CommonComponents from 'commoncomponents';
15+
import { default as Components } from '../ui/components/generic';
1516
import { Utils, Filters, ClientLogger as Logger, ClientIPC, AsyncEventEmitter } from 'common';
1617
import Settings from './settings';
1718
import ExtModuleManager from './extmodulemanager';
@@ -24,6 +25,9 @@ import DiscordApi from './discordapi';
2425
import { ReactComponents, ReactHelpers } from './reactcomponents';
2526
import { Patcher, MonkeyPatch } from './patcher';
2627
import GlobalAc from '../ui/autocomplete';
28+
import Vue from 'vue';
29+
import path from 'path';
30+
import Globals from './globals';
2731

2832
export default class PluginApi {
2933

@@ -61,6 +65,7 @@ export default class PluginApi {
6165
get EventsWrapper() { return EventsWrapper }
6266

6367
get CommonComponents() { return CommonComponents }
68+
get Components() { return Components }
6469
get Filters() { return Filters }
6570
get Discord() { return DiscordApi }
6671
get DiscordApi() { return DiscordApi }
@@ -105,7 +110,9 @@ export default class PluginApi {
105110
removeFromArray: (...args) => Utils.removeFromArray.apply(Utils, args),
106111
defineSoftGetter: (...args) => Utils.defineSoftGetter.apply(Utils, args),
107112
wait: (...args) => Utils.wait.apply(Utils, args),
108-
until: (...args) => Utils.until.apply(Utils, args)
113+
until: (...args) => Utils.until.apply(Utils, args),
114+
findInTree: (...args) => Utils.findInTree.apply(Utils, args),
115+
findInReactTree: (...args) => Utils.findInReactTree.apply(Utils, args)
109116
};
110117
}
111118

@@ -605,6 +612,10 @@ export default class PluginApi {
605612
});
606613
}
607614

615+
Vuewrap(id, component, props) {
616+
return VueInjector.createReactElement(Vue.component(id, component), props);
617+
}
618+
608619
}
609620

610621
// Stop plugins from modifying the plugin API for all plugins

client/src/modules/pluginmanager.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,12 @@ export default class extends ContentManager {
130130

131131
static unloadContentHook(content, reload) {
132132
delete Globals.require.cache[Globals.require.resolve(content.paths.mainPath)];
133+
const uncache = [];
134+
for (const required in Globals.require.cache) {
135+
if (!required.includes(content.paths.contentPath)) continue;
136+
uncache.push(Globals.require.resolve(required));
137+
}
138+
for (const u of uncache) delete Globals.require.cache[u];
133139
}
134140

135141
/**
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* BetterDiscord Generic Button Component
3+
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
4+
* All rights reserved.
5+
* https://betterdiscord.net
6+
*
7+
* This source code is licensed under the MIT license found in the
8+
* LICENSE file in the root directory of this source tree.
9+
*/
10+
11+
<template>
12+
<div class="bd-button" :class="classes" @click="onClick">
13+
{{text}}
14+
</div>
15+
</template>
16+
17+
<script>
18+
export default {
19+
props: ['classes', 'text', 'onClick']
20+
}
21+
</script>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* BetterDiscord Generic Button Group Component
3+
* Copyright (c) 2015-present Jiiks/JsSucks - https://github.com/Jiiks / https://github.com/JsSucks
4+
* All rights reserved.
5+
* https://betterdiscord.net
6+
*
7+
* This source code is licensed under the MIT license found in the
8+
* LICENSE file in the root directory of this source tree.
9+
*/
10+
11+
<template>
12+
<div class="bd-buttonGroup" :class="classes">
13+
<Button v-for="(button, index) in buttons" :text="button.text" :classes="button.classes" :onClick="button.onClick" :key="index"/>
14+
</div>
15+
</template>
16+
17+
<script>
18+
import Button from './Button.vue';
19+
export default {
20+
props: ['buttons', 'classes'],
21+
components: { Button }
22+
}
23+
</script>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import VrWrapper from '../../vrwrapper';
2+
3+
import ButtonGroupComponent from './ButtonGroup.vue';
4+
class ButtonGroupWrapper extends VrWrapper {
5+
get component() { return ButtonGroupComponent }
6+
constructor(props) {
7+
super();
8+
this.props = props;
9+
}
10+
}
11+
12+
import ButtonComponent from './Button.vue';
13+
class ButtonWrapper extends VrWrapper {
14+
get component() { return ButtonComponent }
15+
constructor(props) {
16+
super();
17+
this.props = props;
18+
}
19+
}
20+
21+
export default class {
22+
static Button(props) {
23+
return new ButtonWrapper(props);
24+
}
25+
26+
static ButtonGroup(props) {
27+
return new ButtonGroupWrapper(props);
28+
}
29+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
module.exports = (React, props) => {
2+
return React.createElement(
3+
'button',
4+
{ className: 'exampleCustomElement', onClick: props.onClick },
5+
'r'
6+
);
7+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
module.exports = (VueWrap, props) => {
2+
return VueWrap('somecomponent', {
3+
render: function (createElement) {
4+
return createElement('button', {
5+
class: 'exampleCustomElement',
6+
on: {
7+
click: this.onClick
8+
}
9+
}, 'v');
10+
},
11+
props: ['onClick']
12+
}, props);
13+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"info": {
3+
"id": "render-example",
4+
"name": "Render Example",
5+
"authors": [
6+
{
7+
"name": "Jiiks",
8+
"discord_id": "81388395867156480",
9+
"github_username": "Jiiks",
10+
"twitter_username": "Jiiksi"
11+
}
12+
],
13+
"version": 1.0,
14+
"description": "Example for rendering stuff"
15+
},
16+
"main": "index.js"
17+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/**
2+
* This is an example of how you should add custom elements instead of manipulating the DOM directly
3+
*/
4+
5+
// Import custom components
6+
const customVueComponent = require('./components/vuecomponent');
7+
const customReactComponent = require('./components/reactcomponent');
8+
9+
module.exports = (Plugin, Api, Vendor) => {
10+
11+
// Destructure some apis
12+
const { Logger, ReactComponents, Patcher, monkeyPatch, Reflection, Utils, CssUtils, VueInjector, Vuewrap, requireUncached } = Api;
13+
const { Vue } = Vendor;
14+
const { React } = Reflection.modules; // This should be in vendor
15+
16+
return class extends Plugin {
17+
18+
async onStart() {
19+
this.injectStyle();
20+
this.patchGuildTextChannel();
21+
this.patchMessages();
22+
return true;
23+
}
24+
25+
async onStop() {
26+
// The automatic unpatcher is not there yet
27+
Patcher.unpatchAll();
28+
CssUtils.deleteAllStyles();
29+
30+
// Force update elements to remove our changes
31+
const GuildTextChannel = await ReactComponents.getComponent('GuildTextChannel');
32+
GuildTextChannel.forceUpdateAll();
33+
const MessageContent = await ReactComponents.getComponent('MessageContent', { selector: Reflection.resolve('container', 'containerCozy', 'containerCompact', 'edited').selector });
34+
MessageContent.forceUpdateAll();
35+
return true;
36+
}
37+
38+
/* Inject some style for our custom element */
39+
async injectStyle() {
40+
const css = `
41+
.exampleCustomElement {
42+
background: #7a7d82;
43+
color: #FFF;
44+
border-radius: 5px;
45+
font-size: 12px;
46+
font-weight: 600;
47+
opacity: .5;
48+
&:hover {
49+
opacity: 1;
50+
}
51+
}
52+
.exampleBtnGroup {
53+
.bd-button {
54+
font-size: 14px;
55+
padding: 5px;
56+
}
57+
}
58+
`;
59+
await CssUtils.injectSass(css);
60+
}
61+
62+
async patchGuildTextChannel() {
63+
// Get the GuildTextChannel component and patch it's render function
64+
const GuildTextChannel = await ReactComponents.getComponent('GuildTextChannel');
65+
monkeyPatch(GuildTextChannel.component.prototype).after('render', this.injectCustomElements.bind(this));
66+
// Force update to see our changes immediatly
67+
GuildTextChannel.forceUpdateAll();
68+
}
69+
70+
async patchMessages() {
71+
// Get Message component and patch it's render function
72+
const MessageContent = await ReactComponents.getComponent('MessageContent', { selector: Reflection.resolve('container', 'containerCozy', 'containerCompact', 'edited').selector });
73+
monkeyPatch(MessageContent.component.prototype).after('render', this.injectGenericComponents.bind(this));
74+
// Force update to see our changes immediatly
75+
MessageContent.forceUpdateAll();
76+
}
77+
78+
/*
79+
* Injecting a custom React element using React.createElement
80+
* https://reactjs.org/docs/react-api.html#createelement
81+
* Injecting a custom Vue element using Vue.component
82+
* https://vuejs.org/v2/guide/render-function.html
83+
**/
84+
injectCustomElements(that, args, returnValue) {
85+
// Get the child we want using a treewalker since we know the child we want has a channel property and children.
86+
const child = Utils.findInReactTree(returnValue, filter => filter.hasOwnProperty('channel') && filter.children);
87+
if (!child) return;
88+
// If children is not an array make it into one
89+
if (!child.children instanceof Array) child.children = [child.children];
90+
91+
// Add our custom components to children
92+
child.children.push(customReactComponent(React, { onClick: e => this.handleClick(e, child.channel) }));
93+
child.children.push(customVueComponent(Vuewrap, { onClick: e => this.handleClick(e, child.channel) }));
94+
}
95+
96+
/**
97+
* Inject generic components provided by BD
98+
*/
99+
injectGenericComponents(that, args, returnValue) {
100+
// If children is not an array make it into one
101+
if (!returnValue.props.children instanceof Array) returnValue.props.children = [returnValue.props.children];
102+
// Add a generic Button component provided by BD
103+
returnValue.props.children.push(Api.Components.ButtonGroup({
104+
classes: [ 'exampleBtnGroup' ], // Additional classes for button group
105+
buttons: [
106+
{
107+
classes: ['exampleBtn'], // Additional classes for button
108+
text: 'Hello World!', // Text for button
109+
onClick: e => Logger.log('Hello World!') // Button click handler
110+
},
111+
{
112+
classes: ['exampleBtn'],
113+
text: 'Button',
114+
onClick: e => Logger.log('Button!')
115+
}
116+
]
117+
}).render()); // Render will return the wrapped component that can then be displayed
118+
}
119+
120+
/**
121+
* Will log the channel object
122+
*/
123+
handleClick(e, channel) {
124+
Logger.log('Clicked!', channel);
125+
}
126+
}
127+
128+
};

0 commit comments

Comments
 (0)