Add in client updater, Notices API

This commit is contained in:
Vendicated 2022-10-01 00:42:50 +02:00
parent 9aaa47ea4e
commit 8161a07dba
No known key found for this signature in database
GPG Key ID: EC781ADFB93EFFA3
20 changed files with 525 additions and 48 deletions

@ -1,12 +1,41 @@
export * as Plugins from "./plugins"; export * as Plugins from "./plugins";
export * as Webpack from "./webpack"; export * as Webpack from "./webpack";
export * as Api from "./api"; export * as Api from "./api";
export { Settings } from "./api/settings"; import { popNotice, showNotice } from "./api/Notices";
import { Settings } from "./api/settings";
import { startAllPlugins } from "./plugins";
export { Settings };
import "./utils/patchWebpack"; import "./utils/patchWebpack";
import "./utils/quickCss"; import "./utils/quickCss";
import { waitFor } from "./webpack"; import { checkForUpdates, UpdateLogger } from './utils/updater';
import { onceReady } from "./webpack";
import { Router } from "./webpack/common";
export let Components; export let Components;
waitFor("useState", () => setTimeout(() => import("./components").then(mod => Components = mod), 0)); async function init() {
await onceReady;
startAllPlugins();
Components = await import("./components");
try {
const isOutdated = await checkForUpdates();
if (isOutdated && Settings.notifyAboutUpdates)
setTimeout(() => {
showNotice(
"A Vencord update is available!",
"View Update",
() => {
popNotice();
Router.open("Vencord");
}
);
}, 10000);
} catch (err) {
UpdateLogger.error("Failed to check for updates", err);
}
}
init();

24
src/api/Notices.ts Normal file

@ -0,0 +1,24 @@
import { waitFor } from "../webpack";
let NoticesModule: any;
waitFor(m => m.show && m.dismiss && !m.suppressAll, m => NoticesModule = m);
export const noticesQueue = [] as any[];
export let currentNotice: any = null;
export function popNotice() {
NoticesModule.dismiss();
}
export function nextNotice() {
currentNotice = noticesQueue.shift();
if (currentNotice) {
NoticesModule.show(...currentNotice, "VencordNotice");
}
}
export function showNotice(message: string, buttonText: string, onOkClick: () => void) {
noticesQueue.push(["GENERIC", message, buttonText, onOkClick]);
if (!currentNotice) nextNotice();
}

@ -1 +1,2 @@
export * as MessageEvents from "./MessageEvents"; export * as MessageEvents from "./MessageEvents";
export * as Notices from "./Notices";

@ -4,6 +4,7 @@ import { React } from "../webpack/common";
import { mergeDefaults } from '../utils/misc'; import { mergeDefaults } from '../utils/misc';
interface Settings { interface Settings {
notifyAboutUpdates: boolean;
unsafeRequire: boolean; unsafeRequire: boolean;
useQuickCss: boolean; useQuickCss: boolean;
plugins: { plugins: {
@ -15,10 +16,11 @@ interface Settings {
} }
const DefaultSettings: Settings = { const DefaultSettings: Settings = {
notifyAboutUpdates: true,
unsafeRequire: false, unsafeRequire: false,
useQuickCss: true, useQuickCss: true,
plugins: {} plugins: {}
} as any; };
for (const plugin in plugins) { for (const plugin in plugins) {
DefaultSettings.plugins[plugin] = { DefaultSettings.plugins[plugin] = {
@ -77,7 +79,7 @@ export const Settings = makeProxy(settings);
* @returns Settings * @returns Settings
*/ */
export function useSettings() { export function useSettings() {
const [, forceUpdate] = React.useReducer(x => ({}), {}); const [, forceUpdate] = React.useReducer(() => ({}), {});
React.useEffect(() => { React.useEffect(() => {
subscriptions.add(forceUpdate); subscriptions.add(forceUpdate);

@ -1,5 +1,5 @@
import Logger from "../utils/logger"; import Logger from "../utils/logger";
import { React } from "../webpack/common"; import { Card, React } from "../webpack/common";
interface Props { interface Props {
fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; }>>; fallback?: React.ComponentType<React.PropsWithChildren<{ error: any; }>>;
@ -16,7 +16,7 @@ export default class ErrorBoundary extends React.Component<React.PropsWithChildr
static wrap<T = any>(Component: React.ComponentType<T>): (props: T) => React.ReactElement { static wrap<T = any>(Component: React.ComponentType<T>): (props: T) => React.ReactElement {
return (props) => ( return (props) => (
<ErrorBoundary> <ErrorBoundary>
<Component {...props} /> <Component {...props as any/* I hate react typings ??? */} />
</ErrorBoundary> </ErrorBoundary>
); );
} }
@ -49,7 +49,7 @@ export default class ErrorBoundary extends React.Component<React.PropsWithChildr
/>; />;
return ( return (
<div style={{ <Card style={{
overflow: "hidden", overflow: "hidden",
padding: "2em", padding: "2em",
backgroundColor: color + "30", backgroundColor: color + "30",
@ -65,7 +65,7 @@ export default class ErrorBoundary extends React.Component<React.PropsWithChildr
<pre>{this.state.error} <pre>{this.state.error}
</pre> </pre>
</code> </code>
</div> </Card>
); );
} }
} }

@ -4,6 +4,7 @@ import type { React } from '../webpack/common';
export function Flex(props: React.PropsWithChildren<{ export function Flex(props: React.PropsWithChildren<{
flexDirection?: React.CSSProperties["flexDirection"]; flexDirection?: React.CSSProperties["flexDirection"];
style?: React.CSSProperties; style?: React.CSSProperties;
className?: string;
}>) { }>) {
props.style ??= {}; props.style ??= {};
props.style.flexDirection ||= props.flexDirection; props.style.flexDirection ||= props.flexDirection;

19
src/components/Link.tsx Normal file

@ -0,0 +1,19 @@
import { React } from "../webpack/common";
interface Props {
href: string;
disabled?: boolean;
style?: React.CSSProperties;
}
export function Link(props: React.PropsWithChildren<Props>) {
if (props.disabled) {
props.style ??= {};
props.style.pointerEvents = "none";
}
return (
<a href={props.href} target="_blank" style={props.style}>
{props.children}
</a>
);
}

@ -1,16 +1,19 @@
import { humanFriendlyJoin, useAwaiter } from "../utils/misc"; import { classes, humanFriendlyJoin, lazy, useAwaiter } from "../utils/misc";
import Plugins from 'plugins'; import Plugins from 'plugins';
import { useSettings } from "../api/settings"; import { useSettings } from "../api/settings";
import IpcEvents from "../utils/IpcEvents"; import IpcEvents from "../utils/IpcEvents";
import { Button, Switch, Forms, React } from "../webpack/common"; import { Button, Switch, Forms, React, Margins } from "../webpack/common";
import ErrorBoundary from "./ErrorBoundary"; import ErrorBoundary from "./ErrorBoundary";
import { startPlugin } from "../plugins"; import { startPlugin } from "../plugins";
import { stopPlugin } from '../plugins/index'; import { stopPlugin } from '../plugins/index';
import { Flex } from './Flex'; import { Flex } from './Flex';
import { isOutdated } from "../utils/updater";
import { Updater } from "./Updater";
export default ErrorBoundary.wrap(function Settings(props) { export default ErrorBoundary.wrap(function Settings(props) {
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), "Loading..."); const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), "Loading...");
const [outdated, setOutdated] = React.useState(isOutdated);
const settings = useSettings(); const settings = useSettings();
const depMap = React.useMemo(() => { const depMap = React.useMemo(() => {
@ -31,8 +34,24 @@ export default ErrorBoundary.wrap(function Settings(props) {
return ( return (
<Forms.FormSection tag="h1" title="Vencord"> <Forms.FormSection tag="h1" title="Vencord">
<Forms.FormText>SettingsDir: {settingsDir}</Forms.FormText> {outdated && (
<Flex style={{ marginTop: "8px", marginBottom: "8px" }}> <>
<Forms.FormTitle tag="h5">Updater</Forms.FormTitle>
<Updater setIsOutdated={setOutdated} />
</>
)}
<Forms.FormDivider />
<Forms.FormTitle tag="h5" className={outdated ? `${Margins.marginTop20} ${Margins.marginBottom8}` : ""}>
Settings
</Forms.FormTitle>
<Forms.FormText>
SettingsDir: {settingsDir}
</Forms.FormText>
<Flex className={classes(Margins.marginBottom20)}>
<Button <Button
onClick={() => VencordNative.ipc.invoke(IpcEvents.OPEN_PATH, settingsDir)} onClick={() => VencordNative.ipc.invoke(IpcEvents.OPEN_PATH, settingsDir)}
size={Button.Sizes.SMALL} size={Button.Sizes.SMALL}
@ -48,7 +67,7 @@ export default ErrorBoundary.wrap(function Settings(props) {
Open QuickCSS File Open QuickCSS File
</Button> </Button>
</Flex> </Flex>
<Forms.FormTitle tag="h5">Settings</Forms.FormTitle>
<Switch <Switch
value={settings.useQuickCss} value={settings.useQuickCss}
onChange={v => settings.useQuickCss = v} onChange={v => settings.useQuickCss = v}
@ -56,6 +75,13 @@ export default ErrorBoundary.wrap(function Settings(props) {
> >
Use QuickCss Use QuickCss
</Switch> </Switch>
<Switch
value={settings.notifyAboutUpdates}
onChange={v => settings.notifyAboutUpdates = v}
note="Shows a Toast on StartUp"
>
Get notified about new Updates
</Switch>
<Switch <Switch
value={settings.unsafeRequire} value={settings.unsafeRequire}
onChange={v => settings.unsafeRequire = v} onChange={v => settings.unsafeRequire = v}
@ -63,8 +89,13 @@ export default ErrorBoundary.wrap(function Settings(props) {
> >
Enable Unsafe Require Enable Unsafe Require
</Switch> </Switch>
<Forms.FormDivider /> <Forms.FormDivider />
<Forms.FormTitle tag="h5">Plugins</Forms.FormTitle>
<Forms.FormTitle tag="h5" className={classes(Margins.marginTop20, Margins.marginBottom8)}>
Plugins
</Forms.FormTitle>
{sortedPlugins.map(p => { {sortedPlugins.map(p => {
const enabledDependants = depMap[p.name]?.filter(d => settings.plugins[d].enabled); const enabledDependants = depMap[p.name]?.filter(d => settings.plugins[d].enabled);
const dependency = enabledDependants?.length; const dependency = enabledDependants?.length;

128
src/components/Updater.tsx Normal file

@ -0,0 +1,128 @@
import gitHash from "git-hash";
import { changes, checkForUpdates, getRepo, rebuild, update, UpdateLogger } from "../utils/updater";
import { React, Forms, Button, Margins, Alerts, Card, Parser } from '../webpack/common';
import { Flex } from "./Flex";
import { useAwaiter } from '../utils/misc';
import { Link } from "./Link";
interface Props {
setIsOutdated(b: boolean): void;
}
function withDispatcher(dispatcher: React.Dispatch<React.SetStateAction<boolean>>, action: () => any) {
return async () => {
dispatcher(true);
try {
await action();
} catch (e: any) {
UpdateLogger.error("Failed to update", e);
if (!e) {
var err = "An unknown error occurred (error is undefined).\nPlease try again.";
} else if (e.code && e.cmd) {
const { code, path, cmd, stderr } = e;
if (code === "ENOENT")
var err = `Command \`${path}\` not found.\nPlease install it and try again`;
else {
var err = `An error occured while running \`${cmd}\`:\n`;
err += stderr || `Code \`${code}\`. See the console for more info`;
}
} else {
var err = "An unknown error occurred. See the console for more info.";
}
Alerts.show({
title: "Oops!",
body: err.split("\n").map(line => <div>{Parser.parse(line)}</div>)
});
}
finally {
dispatcher(false);
}
};
};
export function Updater(p: Props) {
const [repo, err, repoPending] = useAwaiter(getRepo, "Loading...");
const [isChecking, setIsChecking] = React.useState(false);
const [isUpdating, setIsUpdating] = React.useState(false);
const [updates, setUpdates] = React.useState(changes);
React.useEffect(() => {
if (err)
UpdateLogger.error("Failed to retrieve repo", err);
}, [err]);
return (
<>
<Forms.FormText>Repo: {repoPending ? repo : err ? "Failed to retrieve - check console" : (
<Link href={repo}>
{repo.split("/").slice(-2).join("/")}
</Link>
)} ({gitHash})</Forms.FormText>
<Forms.FormText className={Margins.marginBottom8}>
There are {updates.length} Updates
</Forms.FormText>
<Card style={{ padding: ".5em" }}>
{updates.map(({ hash, author, message }) => (
<div>
<Link href={`${repo}/commit/${hash}`} disabled={repoPending}>
<code>{hash}</code>
</Link>
<span style={{
marginLeft: "0.5em",
color: "var(--text-normal)"
}}>{message} - {author}</span>
</div>
))}
</Card>
<Flex className={`${Margins.marginBottom8} ${Margins.marginTop8}`}>
<Button
size={Button.Sizes.SMALL}
disabled={isUpdating || isChecking}
onClick={withDispatcher(setIsUpdating, async () => {
if (await update()) {
p.setIsOutdated(false);
const needFullRestart = await rebuild();
await new Promise<void>(r => {
Alerts.show({
title: "Update Success!",
body: "Successfully updated. Restart now to apply the changes?",
confirmText: "Restart",
cancelText: "Not now!",
onConfirm() {
if (needFullRestart)
window.DiscordNative.app.relaunch();
else
location.reload();
r();
},
onCancel: r
});
});
}
})}
>
Update
</Button>
<Button
size={Button.Sizes.SMALL}
disabled={isUpdating || isChecking}
onClick={withDispatcher(setIsChecking, async () => {
const res = await checkForUpdates();
if (res) {
setUpdates(changes);
} else {
p.setIsOutdated(false);
}
})}
>
Refresh
</Button>
</Flex>
</>
);
}

@ -1,17 +1,50 @@
// TODO: refactor this mess
import { execFile as cpExecFile } from 'child_process';
import { createHash } from "crypto";
import { app, BrowserWindow, ipcMain, shell } from "electron"; import { app, BrowserWindow, ipcMain, shell } from "electron";
import { mkdirSync, readFileSync, watch } from "fs"; import { createReadStream, mkdirSync, readFileSync, watch } from "fs";
import { open, readFile, writeFile } from "fs/promises"; import { open, readFile, writeFile } from "fs/promises";
import { join } from 'path'; import { join } from 'path';
import { promisify } from "util";
import { debounce } from "./utils/debounce"; import { debounce } from "./utils/debounce";
import IpcEvents from './utils/IpcEvents'; import IpcEvents from './utils/IpcEvents';
const VENCORD_SRC_DIR = join(__dirname, "..");
const DATA_DIR = join(app.getPath("userData"), "..", "Vencord"); const DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
const SETTINGS_DIR = join(DATA_DIR, "settings"); const SETTINGS_DIR = join(DATA_DIR, "settings");
const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css"); const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css");
const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json"); const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json");
const execFile = promisify(cpExecFile);
mkdirSync(SETTINGS_DIR, { recursive: true }); mkdirSync(SETTINGS_DIR, { recursive: true });
async function calculateHashes() {
const hashes = {} as Record<string, string>;
await Promise.all(
["patcher.js", "preload.js", "renderer.js"].map(file => new Promise<void>(r => {
const fis = createReadStream(join(__dirname, file));
const hash = createHash("sha1", { encoding: "hex" });
fis.once("end", () => {
hash.end();
hashes[file] = hash.read();
r();
});
fis.pipe(hash);
}))
);
return hashes;
}
function git(...args: string[]) {
return execFile("git", args, {
cwd: VENCORD_SRC_DIR
});
}
function readCss() { function readCss() {
return readFile(QUICKCSS_PATH, "utf-8").catch(() => ""); return readFile(QUICKCSS_PATH, "utf-8").catch(() => "");
} }
@ -24,11 +57,65 @@ function readSettings() {
} }
} }
function serializeErrors(func: (...args: any[]) => any) {
return async function () {
try {
return {
ok: true,
value: await func(...arguments)
};
} catch (e: any) {
return {
ok: false,
error: e instanceof Error ? {
// prototypes get lost, so turn error into plain object
...e
} : e
};
}
};
}
ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR); ipcMain.handle(IpcEvents.GET_SETTINGS_DIR, () => SETTINGS_DIR);
ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss()); ipcMain.handle(IpcEvents.GET_QUICK_CSS, () => readCss());
ipcMain.handle(IpcEvents.OPEN_PATH, (_, ...pathElements) => shell.openPath(join(...pathElements))); ipcMain.handle(IpcEvents.OPEN_PATH, (_, ...pathElements) => shell.openPath(join(...pathElements)));
ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => shell.openExternal(url)); ipcMain.handle(IpcEvents.OPEN_EXTERNAL, (_, url) => shell.openExternal(url));
ipcMain.handle(IpcEvents.GET_UPDATES, serializeErrors(async () => {
await git("fetch");
const res = await git("log", `HEAD...origin/main`, "--pretty=format:%h-%s");
const commits = res.stdout.trim();
return commits ? commits.split("\n").map(line => {
const [author, hash, ...rest] = line.split("/");
return {
hash, author, message: rest.join("/")
};
}) : [];
}));
ipcMain.handle(IpcEvents.UPDATE, serializeErrors(async () => {
const res = await git("pull");
return res.stdout.includes("Fast-forward");
}));
ipcMain.handle(IpcEvents.BUILD, serializeErrors(async () => {
const res = await execFile("node", ["build.mjs"], {
cwd: VENCORD_SRC_DIR
});
return !res.stderr.includes("Build failed");
}));
ipcMain.handle(IpcEvents.GET_HASHES, serializeErrors(calculateHashes));
ipcMain.handle(IpcEvents.GET_REPO, serializeErrors(async () => {
const res = await git("remote", "get-url", "origin");
return res.stdout.trim()
.replace(/git@(.+):/, "https://$1/")
.replace(/\.git$/, "");
}));
// .on because we need Settings synchronously (ipcRenderer.sendSync) // .on because we need Settings synchronously (ipcRenderer.sendSync)
ipcMain.on(IpcEvents.GET_SETTINGS, (e) => e.returnValue = readSettings()); ipcMain.on(IpcEvents.GET_SETTINGS, (e) => e.returnValue = readSettings());

24
src/plugins/apiNotices.ts Normal file

@ -0,0 +1,24 @@
import definePlugin from "../utils/types";
export default definePlugin({
name: "ApiNotices",
description: "Fixes notices being automatically dismissed",
author: "Vendicated",
required: true,
patches: [
{
find: "updateNotice:",
replacement: [
{
match: /;(.{1,2}=null;)(?=.{0,50}updateNotice)/g,
replace:
';if(Vencord.Api.Notices.currentNotice)return !1;$1'
},
{
match: /(?<=NOTICE_DISMISS:function.+?){(?=if\(null==(.+?)\))/,
replace: '{if($1?.id=="VencordNotice")return ($1=null,Vencord.Api.Notices.nextNotice(),true);'
}
]
}
],
});

@ -17,7 +17,7 @@ export default definePlugin({
], ],
copyToClipBoard(color: string) { copyToClipBoard(color: string) {
DiscordNative.clipboard.copy(color); window.DiscordNative.clipboard.copy(color);
Toasts.show({ Toasts.show({
message: "Copied to Clipboard!", message: "Copied to Clipboard!",
type: Toasts.Type.SUCCESS, type: Toasts.Type.SUCCESS,

@ -16,7 +16,7 @@ for (const plugin of Object.values(Plugins)) if (plugin.patches && Settings.plug
} }
} }
export function startAll() { export function startAllPlugins() {
for (const plugin in Plugins) if (Settings.plugins[plugin].enabled) { for (const plugin in Plugins) if (Settings.plugins[plugin].enabled) {
startPlugin(Plugins[plugin]); startPlugin(Plugins[plugin]);
} }

@ -19,4 +19,9 @@ export default strEnum({
SET_SETTINGS: "VencordSetSettings", SET_SETTINGS: "VencordSetSettings",
OPEN_EXTERNAL: "VencordOpenExternal", OPEN_EXTERNAL: "VencordOpenExternal",
OPEN_PATH: "VencordOpenPath", OPEN_PATH: "VencordOpenPath",
GET_UPDATES: "VencordGetUpdates",
GET_REPO: "VencordGetRepo",
GET_HASHES: "VencordGetHashes",
UPDATE: "VencordUpdate",
BUILD: "VencordBuild"
} as const); } as const);

@ -1,3 +1,4 @@
import { FilterFn, find } from "../webpack";
import { React } from "../webpack/common"; import { React } from "../webpack/common";
/** /**
@ -7,9 +8,22 @@ import { React } from "../webpack/common";
*/ */
export function lazy<T>(factory: () => T): () => T { export function lazy<T>(factory: () => T): () => T {
let cache: T; let cache: T;
return () => { return () => cache ?? (cache = factory());
return cache ?? (cache = factory()); }
};
/**
* Do a lazy webpack search. Searches the module on first property access
* @param filter Filter function
* @returns Proxy. Note that only get and set are implemented, all other operations will have unexpected
* results.
*/
export function lazyWebpack<T = any>(filter: FilterFn): T {
const getMod = lazy(() => find(filter));
return new Proxy({}, {
get: (_, prop) => getMod()[prop],
set: (_, prop, v) => getMod()[prop] = v
}) as T;
} }
/** /**
@ -48,7 +62,7 @@ export function useAwaiter<T>(factory: () => Promise<T>, fallbackValue: T | null
export function LazyComponent<T = any>(factory: () => React.ComponentType<T>) { export function LazyComponent<T = any>(factory: () => React.ComponentType<T>) {
return (props: T) => { return (props: T) => {
const Component = React.useMemo(factory, []); const Component = React.useMemo(factory, []);
return <Component {...props} />; return <Component {...props as any /* I hate react typings ??? */} />;
}; };
} }
@ -98,3 +112,11 @@ export function humanFriendlyJoin(elements: any[], mapper: (e: any) => string =
return s; return s;
} }
/**
* Calls .join(" ") on the arguments
* classes("one", "two") => "one two"
*/
export function classes(...classes: string[]) {
return classes.join(" ");
}

@ -29,3 +29,5 @@ interface PluginDef {
dependencies?: string[], dependencies?: string[],
required?: boolean; required?: boolean;
} }
export type IpcRes<V = any> = { ok: true; value: V; } | { ok: false, error: any; };

51
src/utils/updater.ts Normal file

@ -0,0 +1,51 @@
import IpcEvents from "./IpcEvents";
import Logger from "./logger";
import { IpcRes } from './types';
export const UpdateLogger = new Logger("Updater", "white");
export let isOutdated = false;
export let changes: Record<"hash" | "author" | "message", string>[];
async function Unwrap<T>(p: Promise<IpcRes<T>>) {
const res = await p;
if (res.ok) return res.value;
throw res.error;
}
export async function checkForUpdates() {
changes = await Unwrap(VencordNative.ipc.invoke<IpcRes<typeof changes>>(IpcEvents.GET_UPDATES));
return (isOutdated = changes.length > 0);
}
export async function update() {
if (!isOutdated) return true;
const res = await Unwrap(VencordNative.ipc.invoke<IpcRes<boolean>>(IpcEvents.UPDATE));
if (res)
isOutdated = false;
return res;
}
export function getRepo() {
return Unwrap(VencordNative.ipc.invoke<IpcRes<string>>(IpcEvents.GET_REPO));
}
type Hashes = Record<"patcher.js" | "preload.js" | "renderer.js", string>;
/**
* @returns true if hard restart is required
*/
export async function rebuild() {
const oldHashes = await Unwrap(VencordNative.ipc.invoke<IpcRes<Hashes>>(IpcEvents.GET_HASHES));
if (!await Unwrap(VencordNative.ipc.invoke<IpcRes<boolean>>(IpcEvents.BUILD)))
throw new Error("The Build failed. Please try manually building the new update");
const newHashes = await Unwrap(VencordNative.ipc.invoke<IpcRes<Hashes>>(IpcEvents.GET_HASHES));
return oldHashes["patcher.js"] !== newHashes["patcher.js"] ||
oldHashes["preload.js"] !== newHashes["preload.js"];
}

@ -1,17 +1,43 @@
import { startAll } from "../plugins"; import { waitFor, filters, _resolveReady } from './webpack';
import { waitFor, filters, findByProps } from './webpack';
import type Components from "discord-types/components"; import type Components from "discord-types/components";
import type Stores from "discord-types/stores"; import type Stores from "discord-types/stores";
import type Other from "discord-types/other"; import type Other from "discord-types/other";
import { lazyWebpack } from '../utils/misc';
export const Margins = lazyWebpack(filters.byProps(["marginTop20"]));
export let FluxDispatcher: Other.FluxDispatcher; export let FluxDispatcher: Other.FluxDispatcher;
export let React: typeof import("react"); export let React: typeof import("react");
export let UserStore: Stores.UserStore; export let UserStore: Stores.UserStore;
export const Forms: any = {}; export const Forms = {} as {
FormTitle: Components.FormTitle;
FormSection: any;
FormDivider: any;
FormText: Components.FormText;
};
export let Card: Components.Card;
export let Button: any; export let Button: any;
export let Switch: any; export let Switch: any;
export let Tooltip: Components.Tooltip; export let Tooltip: Components.Tooltip;
export let Router: any;
export let Parser: any;
export let Alerts: {
show(alert: {
title: any;
body: React.ReactNode;
className?: string;
confirmColor?: string;
cancelText?: string;
confirmText?: string;
secondaryConfirmText?: string;
onCancel?(): void;
onConfirm?(): void;
onConfirmSecondary?(): void;
}): void;
/** This is a noop, it does nothing. */
close(): void;
};
const ToastType = { const ToastType = {
MESSAGE: 0, MESSAGE: 0,
SUCCESS: 1, SUCCESS: 1,
@ -27,11 +53,10 @@ export const Toasts = {
Type: ToastType, Type: ToastType,
Position: ToastPosition, Position: ToastPosition,
// what's less likely than getting 0 from Math.random()? Getting it twice in a row // what's less likely than getting 0 from Math.random()? Getting it twice in a row
genId: () => (Math.random() || Math.random()).toString(36).slice(2) genId: () => (Math.random() || Math.random()).toString(36).slice(2),
} as {
Type: typeof ToastType, // hack to merge with the following interface, dunno if there's a better way
Position: typeof ToastPosition; ...{} as {
genId(): string;
show(data: { show(data: {
message: string, message: string,
id: string, id: string,
@ -49,6 +74,7 @@ export const Toasts = {
}; };
}): void; }): void;
pop(): void; pop(): void;
}
}; };
waitFor("useState", m => React = m); waitFor("useState", m => React = m);
@ -56,7 +82,7 @@ waitFor(["dispatch", "subscribe"], m => {
FluxDispatcher = m; FluxDispatcher = m;
const cb = () => { const cb = () => {
m.unsubscribe("CONNECTION_OPEN", cb); m.unsubscribe("CONNECTION_OPEN", cb);
startAll(); _resolveReady();
}; };
m.subscribe("CONNECTION_OPEN", cb); m.subscribe("CONNECTION_OPEN", cb);
}); });
@ -64,6 +90,7 @@ waitFor(["getCurrentUser", "initialize"], m => UserStore = m);
waitFor(["Hovers", "Looks", "Sizes"], m => Button = m); waitFor(["Hovers", "Looks", "Sizes"], m => Button = m);
waitFor(filters.byCode("helpdeskArticleId"), m => Switch = m); waitFor(filters.byCode("helpdeskArticleId"), m => Switch = m);
waitFor(["Positions", "Colors"], m => Tooltip = m); waitFor(["Positions", "Colors"], m => Tooltip = m);
waitFor(m => m.Types?.PRIMARY === "cardPrimary", m => Card = m);
waitFor(m => m.Tags && filters.byCode("errorSeparator")(m), m => Forms.FormTitle = m); waitFor(m => m.Tags && filters.byCode("errorSeparator")(m), m => Forms.FormTitle = m);
waitFor(m => m.Tags && filters.byCode("titleClassName", "sectionTitle")(m), m => Forms.FormSection = m); waitFor(m => m.Tags && filters.byCode("titleClassName", "sectionTitle")(m), m => Forms.FormSection = m);
@ -78,3 +105,8 @@ waitFor(m => {
// This is the same module but this is easier // This is the same module but this is easier
waitFor(filters.byCode("currentToast?"), m => Toasts.show = m); waitFor(filters.byCode("currentToast?"), m => Toasts.show = m);
waitFor(filters.byCode("currentToast:null"), m => Toasts.pop = m); waitFor(filters.byCode("currentToast:null"), m => Toasts.pop = m);
waitFor(["show", "close"], m => Alerts = m);
waitFor("parseTopic", m => Parser = m);
waitFor(["open", "saveAccountChanges"], m => Router = m);

@ -1,5 +1,12 @@
import type { WebpackInstance } from "discord-types/other"; import type { WebpackInstance } from "discord-types/other";
export let _resolveReady: () => void;
/**
* Fired once a gateway connection to Discord has been established.
* This indicates that the core webpack modules have been initialised
*/
export const onceReady = new Promise<void>(r => _resolveReady = r);
export let wreq: WebpackInstance; export let wreq: WebpackInstance;
export let cache: WebpackInstance["c"]; export let cache: WebpackInstance["c"];
@ -68,8 +75,19 @@ export function findAll(filter: FilterFn, getDefault = true) {
const ret = [] as any[]; const ret = [] as any[];
for (const key in cache) { for (const key in cache) {
const mod = cache[key]; const mod = cache[key];
if (mod?.exports && filter(mod.exports)) ret.push(mod.exports); if (!mod?.exports) continue;
if (mod?.exports?.default && filter(mod.exports.default)) ret.push(getDefault ? mod.exports.default : mod.exports);
if (filter(mod.exports))
ret.push(mod.exports);
else if (typeof mod.exports !== "object")
continue;
if (mod.exports.default && filter(mod.exports.default))
ret.push(getDefault ? mod.exports.default : mod.exports);
else for (const nestedMod in mod.exports) if (nestedMod.length < 3) {
const nested = mod.exports[nestedMod];
if (nested && filter(nested)) ret.push(nested);
}
} }
return ret; return ret;

@ -10,6 +10,7 @@
"target": "ESNEXT", "target": "ESNEXT",
// https://esbuild.github.io/api/#jsx-factory // https://esbuild.github.io/api/#jsx-factory
"jsxFactory": "Vencord.Webpack.Common.React.createElement", "jsxFactory": "Vencord.Webpack.Common.React.createElement",
"jsxFragmentFactory": "Vencord.Webpack.Common.React.Fragment",
"jsx": "react" "jsx": "react"
}, },
"include": ["src/**/*"] "include": ["src/**/*"]