You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
vizality/renderer/src/builtins/router/index.js

205 lines
7.4 KiB

/**
*
*/
import { findInReactTree, findInTree, getOwnerInstance } from '@vizality/util/react';
import { getModule, getModuleByDisplayName } from '@vizality/webpack';
import { waitForElement, joinClassNames } from '@vizality/util/dom';
import { patch, unpatch } from '@vizality/patcher';
import { useForceUpdate } from '@vizality/hooks';
import { isArray } from '@vizality/util/array';
import { Events } from '@vizality/constants';
import { Builtin } from '@vizality/entities';
import React, { useEffect } from 'react';
export default class Router extends Builtin {
/**
* Starts up the builtin.
* @returns {Promise<void>}
*/
async start () {
await this.patchRouter();
await this.patchViews();
await this.patchSidebar();
await this.forceRouterUpdate();
$vz.api.routes.on(Events.VIZALITY_ROUTE_ADD, this.forceRouterUpdate);
$vz.api.routes.on('routeRemove', this.forceRouterUpdate);
}
/**
* Shuts down the builtin.
* @returns {void}
*/
stop () {
$vz.api.routes.off(Events.VIZALITY_ROUTE_ADD, this.forceRouterUpdate);
$vz.api.routes.off('routeRemove', this.forceRouterUpdate);
unpatch('vz-router-routes');
unpatch('vz-router-views');
unpatch('vz-router-sidebar');
this.forceRouterUpdate();
}
/**
* Patches the app's route renderer.
* @returns {Promise<void>}
*/
async patchRouter () {
const { container } = await getModule('container', 'downloadProgressCircle', true);
const RouteRenderer = getOwnerInstance(await waitForElement(`.${container}`));
patch('vz-router-routes', RouteRenderer?.props?.children[0], 'type', (_, res) => {
try {
const { children } = findInReactTree(res, m => isArray(m.children) && m.children.length > 5);
children.push(
...$vz.api.routes.getAllRoutes()?.map(route => ({
...children[0],
props: {
/**
* Not exactly sure what this property is, but Discord added it around
* March of 2021 and sounds like something we would want to have set to true.
*/
disableTrack: true,
render: () => {
const Render = route.render;
return <Render />;
},
path: `/vizality${route.path}`
}
}))
);
} catch (err) {
return this.error('There was a problem patching the routes!', err);
}
});
}
/**
* Patches the app's views.
* @returns {Promise<void>}
*/
async patchViews () {
const Views = await getModuleByDisplayName('ViewsWithMainInterface', true);
patch('vz-router-views', Views?.prototype, 'render', (_, res) => {
try {
const routes = findInTree(res, n => isArray(n) && n[0]?.key && n[0].props?.path && n[0].props.render);
if (!isArray(routes)) {
return;
}
routes[routes.length - 2].props.path = [
...new Set(
routes[routes.length - 2]?.props?.path?.concat(
$vz.api.routes.getAllRoutes()?.map(route => `/vizality${route.path}`)
)
)
];
} catch (err) {
return this.error('There was a problem patching the views!', err);
}
});
}
/**
* Patches Discord's channel sidebar so that we can decide to render a custom one or
* none at all when registering new routes.
* @returns {Promise<void>}
*/
async patchSidebar () {
try {
const { panels } = await getModule('panels', true);
const instance = getOwnerInstance(await waitForElement(`.${panels}`));
const Routes = await getModule('handleRouteChange', true);
patch('vz-router-sidebar', instance?.props?.children[0], 'type', (_, res) => {
try {
const forceUpdate = useForceUpdate();
/**
* Handles collapsing/expanding the dashboard sidebar.
* @param {string} setting Setting key
*/
const handleSidebarCollapse = setting => {
/**
* If it's the setting we're looking for, rerender the sidebar.
*/
if (setting === 'dashboardSidebarCollapse') {
forceUpdate();
}
};
/**
* Add listeners for route changes and addon settings toggle actions.
*/
useEffect(() => {
$vz.api.settings
.on(Events.VIZALITY_SETTING_UPDATE, handleSidebarCollapse);
Routes.listeners.add(forceUpdate);
/**
* Remove the listeners on unmount.
*/
return () => {
$vz.api.settings
.removeListener(Events.VIZALITY_SETTING_UPDATE, handleSidebarCollapse);
Routes.listeners.delete(forceUpdate);
};
}, []);
const content = findInReactTree(res, n => n?.className?.startsWith('content-'));
const child = content?.children[0];
if (child?.type?.displayName === 'ChannelSidebar') {
const oType = child.type;
child.type = props => {
try {
let ret = Reflect.apply(oType, null, [ props ]);
if ($vz.api.routes.getLocation()?.pathname?.startsWith('/vizality')) {
/**
* Add and remove collapsed and expanded attributes based on the setting.
*/
ret.props.className = joinClassNames('vz-dashboard-sidebar', ret.props.className);
ret.props['vz-collapsed'] = $vz.settings.get('dashboardSidebarCollapse', false) && '';
ret.props['vz-expanded'] = !$vz.settings.get('dashboardSidebarCollapse', false) && '';
const rawPath = $vz.api.routes.getLocation().pathname.substring('vizality/'.length);
const route = $vz.api.routes.getAllRoutes()?.find(rte => rawPath.startsWith(rte.path));
if (route && route.sidebar) {
const Sidebar = route.sidebar;
ret.props.children[0] = <Sidebar />;
/**
* If the sidebar property is false, don't render one.
*/
} else {
ret = null;
}
}
return ret;
} catch (err) {
return this.error('There was a problem patching ChannelSidebar!', err);
}
};
}
} catch (err) {
return this.error('There was a problem patching the channels sidebar!', err);
}
});
} catch (err) {
return this.error('There was a problem patching the channels sidebar!', err);
}
}
/**
* Forces updates on the app's views and routes.
* @returns {Promise<void>}
*/
async forceRouterUpdate () {
try {
/**
* Views
*/
const { app } = await getModule('app', true);
const viewsInstance = getOwnerInstance(await waitForElement(`.${app}`));
findInTree(viewsInstance?._reactInternals, n => n?.historyUnlisten, { walkable: [ 'child', 'stateNode' ] })?.forceUpdate();
/**
* Routes
*/
const { container } = await getModule('container', 'downloadProgressCircle', true);
const routesInstance = getOwnerInstance(await waitForElement(`.${container}`));
routesInstance?.forceUpdate();
} catch (err) {
return this.error('There was a problem force updating the views or routes!', err);
}
}
}