GooseMod v5.0.0: React settings sidebar, native / included patcher and context menu, separate version info, minor optimizations and fixes

pull/8/head
Oj18 4 years ago
parent 6fd54b3f7e
commit 71113fd576

@ -1,5 +1,18 @@
# GooseMod Changelog
## v5.0.0 [2020-11-09]
- ### Features
- Native / React settings sidebar injection instead of DOM, resulting in no lag caused by GooseMod when opening settings and them being there with no delay
- Added native patcher library / API (based on Powercord <3)
- Added native context menu injection library / API, this now allows modules to easily add their own entries to context menus with only a couple lines oF JS instead of needing to reinvent the wheel everytime
- Moved version info into separate version info section, there is now a divider after Discord's version info and then GooseMod's own
- ### Tweaks
- Slightly optimized Module Store settings UI generation
- Module Store no longer readjusts settings UI width again when importing or removing a module
## v4.10.0 [2020-11-05]
- ### Features

20
dist/index.js vendored

File diff suppressed because one or more lines are too long

2
dist/index.js.map vendored

File diff suppressed because one or more lines are too long

@ -5,6 +5,9 @@ import * as Logger from './util/logger';
import WebpackModules from './util/discord/webpackModules';
import fixLocalStorage from './util/discord/fixLocalStorage';
import * as Patcher from './util/patcher';
import * as ReactUtils from './util/react';
import showToast from './ui/toast';
import confirmDialog from './ui/modals/confirm';
@ -36,7 +39,8 @@ const scopeSetterFncs = [
Changelog.setThisScope,
GoosemodChangelog.setThisScope,
PackModal.setThisScope
PackModal.setThisScope,
Patcher.setThisScope
];
const importsToAssign = {
@ -69,7 +73,10 @@ const importsToAssign = {
changelog: Changelog,
goosemodChangelog: GoosemodChangelog,
packModal: PackModal
packModal: PackModal,
patcher: Patcher,
reactUtils: ReactUtils
};
const init = async function () {
@ -92,7 +99,7 @@ const init = async function () {
this.disabledModules = {};
this.lastVersion = localStorage.getItem('goosemodLastVersion');
this.version = '4.10.0';
this.version = '5.0.0';
this.versionHash = '<hash>'; // Hash of built final js file is inserted here via build script
fetch('https://goosemod-api.netlify.app/injectVersion.json').then((x) => x.json().then((latestInjectVersionInfo) => {
@ -129,9 +136,10 @@ const init = async function () {
this.saveInterval = setInterval(this.saveModuleSettings, 3000);
this.remove = () => {
this.patcher.uninject('gm-settings');
clearInterval(this.messageEasterEggs.interval);
clearInterval(this.saveInterval);
clearInterval(this.checkSettingsOpenInterval);
localStorage.removeItem('goosemodLastVersion');
@ -142,7 +150,9 @@ const init = async function () {
for (let p in this.modules) {
if (this.modules.hasOwnProperty(p) && this.modules[p].remove !== undefined) {
this.modules[p].remove();
try {
this.modules[p].remove();
} catch (e) { }
}
}
};

@ -51,7 +51,7 @@ export default {
item.buttonText = 'Remove';
item.showToggle = true;
if (goosemodScope.settings.isSettingsOpen() && !goosemodScope.initialImport) goosemodScope.settings.createFromItems();
// if (goosemodScope.settings.isSettingsOpen() && !goosemodScope.initialImport) goosemodScope.settings.createFromItems();
},
moduleRemoved: async (m) => {
@ -115,7 +115,6 @@ export default {
await goosemodScope.moduleStoreAPI.importModule(m.filename);
goosemodScope.settings.createFromItems();
goosemodScope.settings.openSettingItem('Module Store');
},
isToggled: () => goosemodScope.modules[m.filename] !== undefined,
@ -135,12 +134,8 @@ export default {
goosemodScope.modules[m.filename].remove();
delete goosemodScope.modules[m.filename];
goosemodScope.settings.createFromItems();
goosemodScope.settings.openSettingItem('Module Store');
}
goosemodScope.settings.createFromItems();
goosemodScope.settings.openSettingItem('Module Store');
}
});

@ -1,5 +1,4 @@
import sleep from '../util/sleep';
// import ab2str from '../util/ab2str';
let goosemodScope = {};
@ -21,7 +20,8 @@ export const removeModuleUI = (field, where) => {
goosemodScope.clearModuleSetting(field);
goosemodScope.settings.createFromItems();
// goosemodScope.settings.createFromItems();
goosemodScope.settings.openSettingItem(where);
};
@ -38,12 +38,16 @@ export const closeSettings = () => {
};
export const openSettings = () => {
settingsButtonEl.click();
document.querySelector('button[aria-label="User Settings"]').click();
};
export const openSettingItem = (name) => {
try {
[...settingsSidebarGooseModContainer.children].find((x) => x.textContent === name).click();
const children = [...settingsSidebarEl.children];
children[1].click(); // To refresh / regenerate
children.find((x) => x.textContent === name).click();
return true;
} catch (e) {
return false;
@ -64,7 +68,7 @@ export const reopenSettings = async () => {
// Settings UI stuff
let settingsButtonEl;
/*let settingsButtonEl;
(async function() {
settingsButtonEl = document.querySelector('button[aria-label="User Settings"]');
@ -77,9 +81,9 @@ let settingsButtonEl;
}
settingsButtonEl.addEventListener('click', injectInSettings);
})();
})();*/
let settingsLayerEl, settingsSidebarEl, settingsSidebarGooseModContainer, settingsMainEl, settingsClasses;
let settingsLayerEl, settingsSidebarEl;
//const settings = {
export let items = [];
@ -96,7 +100,7 @@ export const createSeparator = () => {
goosemodScope.settings.items.push(['separator']);
};
export const createFromItems = () => {
/*export const createFromItems = () => {
settingsSidebarGooseModContainer.innerHTML = '';
for (let i of goosemodScope.settings.items) {
@ -112,9 +116,9 @@ export const createFromItems = () => {
break;
}
}
};
};*/
export const _createItem = (panelName, content, clickHandler, danger = false) => {
export const _createItem = (panelName, content) => {
let parentEl = document.createElement('div');
let headerEl = document.createElement('h2');
@ -1002,7 +1006,9 @@ export const _createItem = (panelName, content, clickHandler, danger = false) =>
contentEl.appendChild(specialContainerEl);
}
let el = document.createElement('div');
return parentEl;
/*let el = document.createElement('div');
el.classList.add(settingsClasses['item']);
el.classList.add(settingsClasses['themed']);
@ -1063,10 +1069,10 @@ export const _createItem = (panelName, content, clickHandler, danger = false) =>
if (panelName === 'Local Modules' && window.DiscordNative === undefined) return;
settingsSidebarGooseModContainer.appendChild(el);
settingsSidebarGooseModContainer.appendChild(el);*/
};
export const _createHeading = (headingName) => {
/*export const _createHeading = (headingName) => {
let el = document.createElement('div');
el.className = settingsClasses['header'];
@ -1083,10 +1089,10 @@ export const _createSeparator = () => {
el.className = settingsClasses['separator'];
settingsSidebarGooseModContainer.appendChild(el);
};
};*/
//};
let tryingToInject = false;
/*let tryingToInject = false;
export const injectInSettings = async () => {
if (goosemodScope.removed) return;
@ -1158,9 +1164,128 @@ export const checkSettingsOpenInterval = setInterval(async () => {
if (el && !el.querySelector('nav > div').classList.contains('goosemod-settings-injected')) {
await goosemodScope.settings.injectInSettings();
}
}, 100);
}, 100);*/
export const makeGooseModSettings = () => {
const SettingsView = goosemodScope.webpackModules.findByDisplayName('SettingsView');
const { React } = goosemodScope.webpackModules.common;
goosemodScope.patcher.inject('gm-settings', SettingsView.prototype, 'getPredicateSections', (_, sections) => {
// console.log(sections);
const dividers = sections.filter(c => c.section === 'DIVIDER');
//if (changelog) {
sections.splice(
sections.indexOf(dividers[dividers.length - 2]) + 1, 0,
...goosemodScope.settings.items.map((i) => {
switch (i[0]) {
case 'item':
let obj = {
section: i[1],
label: i[1],
predicate: () => { alert(1); },
element: function() {
if (typeof i[3] === 'function') {
document.getElementsByClassName('selected-3s45Ha')[0].click();
i[3]();
return React.createElement('div');
}
settingsLayerEl = document.querySelector('div[aria-label="USER_SETTINGS"]');
settingsSidebarEl = settingsLayerEl.querySelector('nav > div');
if (i[1] === 'Module Store') { // Settings expansion for Module Store panel
setTimeout(() => {
document.querySelector('.sidebarRegion-VFTUkN').style.maxWidth = '218px';
document.querySelector('.contentColumnDefault-1VQkGM').style.maxWidth = '100%';
}, 10);
settingsSidebarEl.addEventListener('click', (e) => {
if (e.clientX === 0) return; // <el>.click() - not an actual user click - as it has no mouse position coords (0, 0)
document.querySelector('.sidebarRegion-VFTUkN').style.maxWidth = '50%';
document.querySelector('.contentColumnDefault-1VQkGM').style.maxWidth = '740px';
});
}
let contentEl = goosemodScope.settings._createItem(i[1], i[2]);
const ref = React.useRef(null);
React.useEffect(() => { ref.current.appendChild(contentEl); }, []);
return React.createElement('div', {
ref
});
//return React.createElement(VanillaElement, { vanillaChild: contentEl });
}
};
if (i[4]) obj.color = '#f04747';
return obj;
//goosemodScope.settings._createItem(i[1], i[2], i[3], i[4]);
case 'heading':
return {
section: 'HEADER',
label: i[1]
};
case 'separator':
return {
section: 'DIVIDER'
};
}
}),
{
section: 'DIVIDER'
}
);
//}
const versionInfo = sections[sections.length - 1];
const versionInfoEl = versionInfo.element();
let goosemodVersionInfo = React.cloneElement(versionInfoEl);
goosemodVersionInfo.props.children = [];
let goosemodVersion = React.cloneElement(versionInfoEl.props.children[0]);
goosemodVersion.props.children[0] = 'GooseMod';
goosemodVersion.props.children[2] = goosemodScope.version;
goosemodVersion.props.children[4].props.children[1] = goosemodScope.versionHash.substring(0, 7);
goosemodVersionInfo.props.children.push(goosemodVersion);
let untetheredVersion = React.cloneElement(versionInfoEl.props.children[1] || versionInfoEl.props.children[2]);
untetheredVersion.props.children[0] = 'GooseMod Untethered ';
untetheredVersion.props.children[1] = goosemodScope.untetheredVersion || 'N/A';
goosemodVersionInfo.props.children.push(untetheredVersion);
sections.push(
{
section: 'DIVIDER'
},
{
section: 'CUSTOM',
element: () => goosemodVersionInfo
}
);
console.log('gm', sections);
return sections;
});
goosemodScope.settings.createHeading('GooseMod');
goosemodScope.settings.createItem('Local Modules', ['',
@ -1178,7 +1303,6 @@ export const makeGooseModSettings = () => {
}
}
goosemodScope.settings.createFromItems();
goosemodScope.settings.openSettingItem('Local Modules');
},
},
@ -1203,8 +1327,6 @@ export const makeGooseModSettings = () => {
selectors[s.children[0].children[0].children[2].innerText.toLowerCase()] = s.classList.contains(selectedClass);
}
console.log(selectors);
for (let c of cards) {
const title = c.getElementsByClassName('title-31JmR4')[0];
@ -1236,8 +1358,6 @@ export const makeGooseModSettings = () => {
await goosemodScope.moduleStoreAPI.updateStoreSetting();
goosemodScope.settings.createFromItems();
goosemodScope.settings.openSettingItem('Module Store');
},
width: 120

@ -6,6 +6,8 @@ const obj = { // https://github.com/rauenzi/BetterDiscordApp/blob/master/src/mod
delete obj.req.m.__extra_id__;
delete obj.req.c.__extra_id__;
obj.generateCommons();
},
find: (filter) => {
@ -39,6 +41,12 @@ const obj = { // https://github.com/rauenzi/BetterDiscordApp/blob/master/src/mod
findByPrototypes: (...protoNames) => obj.find(module => module.prototype && protoNames.every(protoProp => module.prototype[protoProp] !== undefined)),
findByDisplayName: (displayName) => obj.find(module => module.displayName === displayName),
generateCommons: () => {
obj.common.React = obj.findByProps('createElement');
},
common: {}
};
obj.init();

@ -0,0 +1,95 @@
const generateIdSegment = () => Math.random().toString(36).replace(/[^a-z0-9]+/g, ''); // Random 12 char string
const generateId = (segments = 3) => new Array(segments).fill(0).map(() => generateIdSegment()).join(''); // Chain random 12 char strings together X times
// Based on Powercord's Injector - <3
// https://github.com/powercord-org/powercord/blob/v2/src/fake_node_modules/powercord/injector/index.js
let injectionIndex = [];
export const inject = (injectionId, mod, funcName, patch, pre = false) => {
if (!mod) {
return console.error(`Tried to patch undefined (Injection ID "${injectionId}")`);
}
if (injectionIndex.find(i => i.id === injectionId)) {
return console.error(`Injection ID "${injectionId}" is already used!`);
}
if (!mod.__goosemodInjectionId || !mod.__goosemodInjectionId[funcName]) { // First injection for the targetted function
const id = generateId(); // Random ID to identify function
mod.__goosemodInjectionId = Object.assign((mod.__goosemodInjectionId || {}), { [funcName]: id });
mod[funcName] = (_oldMethod => function (...args) { // Override the function to do run injections pre and after
const finalArgs = _runPreInjections(id, args, this);
if (finalArgs !== false && Array.isArray(finalArgs)) {
const returned = _oldMethod ? _oldMethod.call(this, ...finalArgs) : void 0;
return _runInjections(id, finalArgs, returned, this);
}
})(mod[funcName]);
injectionIndex[id] = [];
}
injectionIndex.push({ // Add injection to index to keep track
module: mod.__goosemodInjectionId[funcName],
id: injectionId,
method: patch,
pre
});
};
export const uninject = (injectionId) => { // Remove injection from index (if there)
injectionIndex = injectionIndex.filter(i => i.id !== injectionId);
};
export const isInjected = (injectionId) => injectionIndex.some(i => i.id === injectionId); // Check if the given id is in the index
const _runPreInjections = (modId, originArgs, _this) => { // Run pre injections (wrapper)
const injections = injectionIndex.filter(i => i.module === modId && i.pre);
if (injections.length === 0) {
return originArgs;
}
return _runPreInjectionsRecursive(injections, originArgs, _this);
};
const _runPreInjectionsRecursive = (injections, originalArgs, _this) => { // Run pre injections (actual)
const injection = injections.pop();
let args = injection.method.call(_this, originalArgs);
if (args === false) {
return false;
}
if (!Array.isArray(args)) {
console.error(`Pre-injection ${injection.id} returned something invalid. Injection will be ignored.`);
args = originalArgs;
}
if (injections.length > 0) {
return _runPreInjectionsRecursive(injections, args, _this);
}
return args;
};
const _runInjections = (modId, originArgs, originReturn, _this) => { // Run post injections
let finalReturn = originReturn;
const injections = injectionIndex.filter(i => i.module === modId && !i.pre);
injections.forEach(i => {
try {
finalReturn = i.method.call(_this, originArgs, finalReturn);
} catch (e) {
console.error(`Failed to run injection "${i.id}"`, e);
}
});
return finalReturn;
};

@ -0,0 +1,98 @@
import { inject, uninject } from './base';
import { getOwnerInstance, findInReactTree } from '../react';
let goosemodScope = {};
export const setThisScope = (scope) => {
goosemodScope = scope;
};
export const labelToId = (label) => label.toLowerCase().replace(/ /g, '-');
export const getInjectId = (id) => `gm-cm-${id}`;
export const patchTypeToNavId = (type) => {
switch (type) {
case 'user':
return 'user-context';
case 'message':
return 'message';
default:
return type;
}
};
export const getExtraInfo = (type) => {
try {
switch (type) {
case 'message':
return getOwnerInstance(document.getElementById('message'))._reactInternalFiber.child.memoizedProps.children.props.children.props;
case 'user':
return getOwnerInstance(document.querySelector('#user-context'))._reactInternalFiber.return.memoizedProps;
default:
return undefined;
}
} catch (e) { return undefined; }
};
export const add = (type, itemProps) => {
const { React } = goosemodScope.webpackModules.common;
const Menu = goosemodScope.webpackModules.findByProps('MenuItem');
const wantedNavId = patchTypeToNavId(type);
const id = itemProps.id || labelToId(itemProps.label);
if (!itemProps.id) {
itemProps.id = id;
}
const origAction = itemProps.action;
itemProps.action = function() {
return origAction(arguments, getExtraInfo(type));
};
inject(getInjectId(id), Menu, 'default', (args) => {
const [ { navId, children } ] = args;
if (navId !== wantedNavId) {
return args;
}
console.log('inj');
const alreadyHasItem = findInReactTree(children, child => child && child.props && child.props.id === itemProps.id);
if (alreadyHasItem) return args;
const item = React.createElement(Menu.MenuItem, itemProps);
console.log(item);
let goosemodGroup = findInReactTree(children, child => child && child.props && child.props.goosemod === true);
console.log(goosemodGroup);
if (!goosemodGroup) {
goosemodGroup = React.createElement(Menu.MenuGroup, { goosemod: true }, item);
console.log('a', goosemodGroup);
children.push([ React.createElement(Menu.MenuSeparator), goosemodGroup ]);
} else {
console.log('b', goosemodGroup);
if (!Array.isArray(goosemodGroup.props.children)) {
goosemodGroup.props.children = [ goosemodGroup.props.children ];
}
goosemodGroup.props.children.push(item);
}
return args;
}, true);
};
export const remove = (label) => uninject(getInjectId(labelToId(label)));

@ -0,0 +1,9 @@
export { inject, uninject, isInjected } from './base';
import * as _contextMenu from './contextMenu';
export const contextMenu = _contextMenu;
export const setThisScope = (scope) => {
_contextMenu.setThisScope(scope);
};

86
src/util/react.js vendored

@ -0,0 +1,86 @@
// https://gist.github.com/noodlebox/047a9f57a8a714d88ca4a60672a22c81
export function getReactInstance (e) { return e[Object.keys(e).find(k => k.startsWith("__reactInternalInstance"))]; }
// https://rauenzi.github.io/BDPluginLibrary/docs/modules_reacttools.js.html
export function getOwnerInstance(node, {include, exclude = ["Popout", "Tooltip", "Scroller", "BackgroundFlash"], filter = _ => _} = {}) {
if (node === undefined) return undefined;
const excluding = include === undefined;
const nameFilter = excluding ? exclude : include;
function getDisplayName(owner) {
const type = owner.type;
if (!type) return null;
return type.displayName || type.name || null;
}
function classFilter(owner) {
const name = getDisplayName(owner);
return (name !== null && !!(nameFilter.includes(name) ^ excluding));
}
let curr = getReactInstance(node);
for (curr = curr && curr.return; curr !== null; curr = curr.return) {
if (curr === null) continue;
const owner = curr.stateNode;
if (owner !== null && !(owner instanceof HTMLElement) && classFilter(curr) && filter(owner)) return owner;
}
return null;
}
export function findInReactTree(tree, searchFilter) {
return findInTree(tree, searchFilter, {walkable: ["props", "children", "child", "sibling"]});
}
export function findInTree(tree, filter, {walkable = null, ignore = []} = {}) {
if (!tree || typeof tree !== 'object') {
return null;
}
if (typeof filter === 'string') {
if (tree.hasOwnProperty(filter)) {
return tree[filter];
}
return;
} else if (filter(tree)) {
return tree;
}
let returnValue = null;
if (Array.isArray(tree)) {
for (const value of tree) {
returnValue = findInTree(value, filter, {
walkable,
ignore
});
if (returnValue) {
return returnValue;
}
}
} else {
const walkables = !walkable ? Object.keys(tree) : walkable;
for (const key of walkables) {
if (!tree.hasOwnProperty(key) || ignore.includes(key)) {
continue;
}
returnValue = findInTree(tree[key], filter, {
walkable,
ignore
});
if (returnValue) {
return returnValue;
}
}
}
return returnValue;
}
Loading…
Cancel
Save