+
+
+
diff --git a/client/src/ui/components/contextmenu/Button.vue b/client/src/ui/components/contextmenu/Button.vue
new file mode 100644
index 00000000..995e950a
--- /dev/null
+++ b/client/src/ui/components/contextmenu/Button.vue
@@ -0,0 +1,23 @@
+/**
+ * BetterDiscord Context Menu 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.
+*/
+
+
+
+ {{item.text}}
+
{{item.hint}}
+
+
+
+
+
diff --git a/client/src/ui/components/contextmenu/Group.vue b/client/src/ui/components/contextmenu/Group.vue
new file mode 100644
index 00000000..0cfc3486
--- /dev/null
+++ b/client/src/ui/components/contextmenu/Group.vue
@@ -0,0 +1,59 @@
+/**
+ * BetterDiscord Context Menu 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/contextmenu/Toggle.vue b/client/src/ui/components/contextmenu/Toggle.vue
new file mode 100644
index 00000000..25e4f7ae
--- /dev/null
+++ b/client/src/ui/components/contextmenu/Toggle.vue
@@ -0,0 +1,27 @@
+/**
+ * BetterDiscord Context Menu Toggle 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.
+*/
+
+
+
+
{{item.text}}
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/ui/components/index.js b/client/src/ui/components/index.js
index 60616b7a..f4b8d7ab 100644
--- a/client/src/ui/components/index.js
+++ b/client/src/ui/components/index.js
@@ -3,3 +3,4 @@ export { default as BdSettings } from './BdSettings.vue';
export { default as BdModals } from './BdModals.vue';
export { default as BdToasts } from './BdToasts.vue';
export { default as BdNotifications } from './BdNotifications.vue';
+export { default as BdContextMenu } from './BdContextMenu.vue';
diff --git a/client/src/ui/contextmenus.js b/client/src/ui/contextmenus.js
new file mode 100644
index 00000000..018c8178
--- /dev/null
+++ b/client/src/ui/contextmenus.js
@@ -0,0 +1,84 @@
+/*
+ * BetterDiscord Context Menus
+ * 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.
+*/
+
+import { ReactComponents, WebpackModules, MonkeyPatch } from 'modules';
+import { VueInjector, Toasts } from 'ui';
+import CMGroup from './components/contextmenu/Group.vue';
+
+export class BdContextMenu {
+
+ /**
+ * Show a context menu
+ * @param {MouseEvent|Object} e MouseEvent or Object { x: 0, y: 0 }
+ * @param {Object[]} grops Groups of items to show in context menu
+ */
+ static show(e, groups) {
+ const x = e.x || e.clientX;
+ const y = e.y || e.clientY;
+ this.activeMenu.menu = { x, y, groups };
+ }
+
+ static get activeMenu() {
+ return this._activeMenu || (this._activeMenu = { menu: null });
+ }
+
+}
+
+export class DiscordContextMenu {
+
+ /**
+ * add items to Discord context menu
+ * @param {any} items items to add
+ * @param {Function} [filter] filter function for target filtering
+ */
+ static add(items, filter) {
+ if (!this.patched) this.patch();
+ this.menus.push({ items, filter });
+ }
+
+ static get menus() {
+ return this._menus || (this._menus = []);
+ }
+
+ static async patch() {
+ if (this.patched) return;
+ this.patched = true;
+ const self = this;
+ MonkeyPatch('BD:DiscordCMOCM', WebpackModules.getModuleByProps(['openContextMenu'])).instead('openContextMenu', (_, [e, fn], originalFn) => {
+ const overrideFn = function (...args) {
+ const res = fn(...args);
+ if (!res.hasOwnProperty('type')) return res;
+ if (!res.type.prototype || !res.type.prototype.render || res.type.prototype.render.__patched) return res;
+ MonkeyPatch('BD:DiscordCMRender', res.type.prototype).after('render', (c, a, r) => self.renderCm(c, a, r, res));
+ res.type.prototype.render.__patched = true;
+ return res;
+ }
+ return originalFn(e, overrideFn);
+ });
+ }
+
+ static renderCm(component, args, retVal, res) {
+ if (!retVal.props || !res.props) return;
+ const { target } = res.props;
+ const { top, left } = retVal.props.style;
+ if (!target || !top || !left) return;
+ if (!retVal.props.children) return;
+ if (!(retVal.props.children instanceof Array)) retVal.props.children = [retVal.props.children];
+ for (const menu of this.menus.filter(menu => { if (!menu.filter) return true; return menu.filter(target)})) {
+ retVal.props.children.push(VueInjector.createReactElement(CMGroup, {
+ top,
+ left,
+ closeMenu: () => WebpackModules.getModuleByProps(['closeContextMenu']).closeContextMenu(),
+ items: menu.items
+ }));
+ }
+ }
+
+}
diff --git a/client/src/ui/dom.js b/client/src/ui/dom.js
index 91d311f1..e7b0ae43 100644
--- a/client/src/ui/dom.js
+++ b/client/src/ui/dom.js
@@ -186,6 +186,7 @@ export default class DOM {
static get bdModals() { return this.getElement('bd-modals') || this.createElement('bd-modals').appendTo(this.bdBody) }
static get bdToasts() { return this.getElement('bd-toasts') || this.createElement('bd-toasts').appendTo(this.bdBody) }
static get bdNotifications() { return this.getElement('bd-notifications') || this.createElement('bd-notifications').appendTo(this.bdBody) }
+ static get bdContextMenu() { return this.getElement('bd-contextmenu') || this.createElement('bd-contextmenu').appendTo(this.bdBody) }
static getElement(e) {
if (e instanceof BdNode) return e.element;
diff --git a/client/src/ui/ui.js b/client/src/ui/ui.js
index 8b29f2f6..11c54468 100644
--- a/client/src/ui/ui.js
+++ b/client/src/ui/ui.js
@@ -4,6 +4,7 @@ export { default as BdMenu, BdMenuItems } from './bdmenu';
export { default as Modals } from './modals';
export { default as Toasts } from './toasts';
export { default as Notifications } from './notifications';
+export * from './contextmenus';
export { default as VueInjector } from './vueinjector';
export { default as Reflection } from './reflection';