aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVendicated <vendicated@riseup.net>2022-10-01 00:42:50 +0200
committerVendicated <vendicated@riseup.net>2022-10-01 00:42:50 +0200
commit8161a07dba401f04dac93f861e6b2cffe53ab7e3 (patch)
tree1499e825bcd6a162bc43507f492b262e9b1901ed
parent9aaa47ea4e9ac068bf5fcbb997e31d722f43f1e5 (diff)
downloadVencord-8161a07dba401f04dac93f861e6b2cffe53ab7e3.tar.gz
Vencord-8161a07dba401f04dac93f861e6b2cffe53ab7e3.tar.bz2
Vencord-8161a07dba401f04dac93f861e6b2cffe53ab7e3.zip
Add in client updater, Notices API
-rw-r--r--src/Vencord.ts35
-rw-r--r--src/api/Notices.ts24
-rw-r--r--src/api/index.ts1
-rw-r--r--src/api/settings.ts6
-rw-r--r--src/components/ErrorBoundary.tsx8
-rw-r--r--src/components/Flex.tsx1
-rw-r--r--src/components/Link.tsx19
-rw-r--r--src/components/Settings.tsx43
-rw-r--r--src/components/Updater.tsx128
-rw-r--r--src/ipcMain.ts89
-rw-r--r--src/plugins/apiNotices.ts24
-rw-r--r--src/plugins/clickableRoleDot.ts2
-rw-r--r--src/plugins/index.ts2
-rw-r--r--src/utils/IpcEvents.ts5
-rw-r--r--src/utils/misc.tsx30
-rw-r--r--src/utils/types.ts2
-rw-r--r--src/utils/updater.ts51
-rw-r--r--src/webpack/common.tsx80
-rw-r--r--src/webpack/webpack.ts22
-rw-r--r--tsconfig.json1
20 files changed, 525 insertions, 48 deletions
diff --git a/src/Vencord.ts b/src/Vencord.ts
index 3a405b2..cb06182 100644
--- a/src/Vencord.ts
+++ b/src/Vencord.ts
@@ -1,12 +1,41 @@
export * as Plugins from "./plugins";
export * as Webpack from "./webpack";
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/quickCss";
-import { waitFor } from "./webpack";
+import { checkForUpdates, UpdateLogger } from './utils/updater';
+import { onceReady } from "./webpack";
+import { Router } from "./webpack/common";
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();
diff --git a/src/api/Notices.ts b/src/api/Notices.ts
new file mode 100644
index 0000000..66cae0e
--- /dev/null
+++ b/src/api/Notices.ts
@@ -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();
+}
diff --git a/src/api/index.ts b/src/api/index.ts
index 0633ee8..7d39b95 100644
--- a/src/api/index.ts
+++ b/src/api/index.ts
@@ -1 +1,2 @@
export * as MessageEvents from "./MessageEvents";
+export * as Notices from "./Notices";
diff --git a/src/api/settings.ts b/src/api/settings.ts
index 4ee94d3..17f3f12 100644
--- a/src/api/settings.ts
+++ b/src/api/settings.ts
@@ -4,6 +4,7 @@ import { React } from "../webpack/common";
import { mergeDefaults } from '../utils/misc';
interface Settings {
+ notifyAboutUpdates: boolean;
unsafeRequire: boolean;
useQuickCss: boolean;
plugins: {
@@ -15,10 +16,11 @@ interface Settings {
}
const DefaultSettings: Settings = {
+ notifyAboutUpdates: true,
unsafeRequire: false,
useQuickCss: true,
plugins: {}
-} as any;
+};
for (const plugin in plugins) {
DefaultSettings.plugins[plugin] = {
@@ -77,7 +79,7 @@ export const Settings = makeProxy(settings);
* @returns Settings
*/
export function useSettings() {
- const [, forceUpdate] = React.useReducer(x => ({}), {});
+ const [, forceUpdate] = React.useReducer(() => ({}), {});
React.useEffect(() => {
subscriptions.add(forceUpdate);
diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx
index 2b754b2..5946cb1 100644
--- a/src/components/ErrorBoundary.tsx
+++ b/src/components/ErrorBoundary.tsx
@@ -1,5 +1,5 @@
import Logger from "../utils/logger";
-import { React } from "../webpack/common";
+import { Card, React } from "../webpack/common";
interface Props {
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 {
return (props) => (
<ErrorBoundary>
- <Component {...props} />
+ <Component {...props as any/* I hate react typings ??? */} />
</ErrorBoundary>
);
}
@@ -49,7 +49,7 @@ export default class ErrorBoundary extends React.Component<React.PropsWithChildr
/>;
return (
- <div style={{
+ <Card style={{
overflow: "hidden",
padding: "2em",
backgroundColor: color + "30",
@@ -65,7 +65,7 @@ export default class ErrorBoundary extends React.Component<React.PropsWithChildr
<pre>{this.state.error}
</pre>
</code>
- </div>
+ </Card>
);
}
}
diff --git a/src/components/Flex.tsx b/src/components/Flex.tsx
index c369767..881c7c2 100644
--- a/src/components/Flex.tsx
+++ b/src/components/Flex.tsx
@@ -4,6 +4,7 @@ import type { React } from '../webpack/common';
export function Flex(props: React.PropsWithChildren<{
flexDirection?: React.CSSProperties["flexDirection"];
style?: React.CSSProperties;
+ className?: string;
}>) {
props.style ??= {};
props.style.flexDirection ||= props.flexDirection;
diff --git a/src/components/Link.tsx b/src/components/Link.tsx
new file mode 100644
index 0000000..ef342d1
--- /dev/null
+++ b/src/components/Link.tsx
@@ -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>
+ );
+}
diff --git a/src/components/Settings.tsx b/src/components/Settings.tsx
index 1950d7a..dd23b73 100644
--- a/src/components/Settings.tsx
+++ b/src/components/Settings.tsx
@@ -1,16 +1,19 @@
-import { humanFriendlyJoin, useAwaiter } from "../utils/misc";
+import { classes, humanFriendlyJoin, lazy, useAwaiter } from "../utils/misc";
import Plugins from 'plugins';
import { useSettings } from "../api/settings";
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 { startPlugin } from "../plugins";
import { stopPlugin } from '../plugins/index';
import { Flex } from './Flex';
+import { isOutdated } from "../utils/updater";
+import { Updater } from "./Updater";
export default ErrorBoundary.wrap(function Settings(props) {
const [settingsDir, , settingsDirPending] = useAwaiter(() => VencordNative.ipc.invoke<string>(IpcEvents.GET_SETTINGS_DIR), "Loading...");
+ const [outdated, setOutdated] = React.useState(isOutdated);
const settings = useSettings();
const depMap = React.useMemo(() => {
@@ -31,8 +34,24 @@ export default ErrorBoundary.wrap(function Settings(props) {
return (
<Forms.FormSection tag="h1" title="Vencord">
- <Forms.FormText>SettingsDir: {settingsDir}</Forms.FormText>
- <Flex style={{ marginTop: "8px", marginBottom: "8px" }}>
+ {outdated && (
+ <>
+ <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
onClick={() => VencordNative.ipc.invoke(IpcEvents.OPEN_PATH, settingsDir)}
size={Button.Sizes.SMALL}
@@ -48,7 +67,7 @@ export default ErrorBoundary.wrap(function Settings(props) {
Open QuickCSS File
</Button>
</Flex>
- <Forms.FormTitle tag="h5">Settings</Forms.FormTitle>
+
<Switch
value={settings.useQuickCss}
onChange={v => settings.useQuickCss = v}
@@ -57,14 +76,26 @@ export default ErrorBoundary.wrap(function Settings(props) {
Use QuickCss
</Switch>
<Switch
+ value={settings.notifyAboutUpdates}
+ onChange={v => settings.notifyAboutUpdates = v}
+ note="Shows a Toast on StartUp"
+ >
+ Get notified about new Updates
+ </Switch>
+ <Switch
value={settings.unsafeRequire}
onChange={v => settings.unsafeRequire = v}
note="Enables VencordNative.require. Useful for testing, very bad for security. Leave this off unless you need it."
>
Enable Unsafe Require
</Switch>
+
<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 => {
const enabledDependants = depMap[p.name]?.filter(d => settings.plugins[d].enabled);
const dependency = enabledDependants?.length;
diff --git a/src/components/Updater.tsx b/src/components/Updater.tsx
new file mode 100644
index 0000000..e7b6d54
--- /dev/null
+++ b/src/components/Updater.tsx
@@ -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>
+ </>
+ );
+}
diff --git a/src/ipcMain.ts b/src/ipcMain.ts
index d8bf475..8ec3746 100644
--- a/src/ipcMain.ts
+++ b/src/ipcMain.ts
@@ -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 { mkdirSync, readFileSync, watch } from "fs";
+import { createReadStream, mkdirSync, readFileSync, watch } from "fs";
import { open, readFile, writeFile } from "fs/promises";
import { join } from 'path';
+import { promisify } from "util";
import { debounce } from "./utils/debounce";
import IpcEvents from './utils/IpcEvents';
+const VENCORD_SRC_DIR = join(__dirname, "..");
const DATA_DIR = join(app.getPath("userData"), "..", "Vencord");
const SETTINGS_DIR = join(DATA_DIR, "settings");
const QUICKCSS_PATH = join(SETTINGS_DIR, "quickCss.css");
const SETTINGS_FILE = join(SETTINGS_DIR, "settings.json");
+const execFile = promisify(cpExecFile);
+
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() {
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_QUICK_CSS, () => readCss());
ipcMain.handle(IpcEvents.OPEN_PATH, (_, ...pathElements) => shell.openPath(join(...pathElements)));
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)
ipcMain.on(IpcEvents.GET_SETTINGS, (e) => e.returnValue = readSettings());
diff --git a/src/plugins/apiNotices.ts b/src/plugins/apiNotices.ts
new file mode 100644
index 0000000..d58ac4b
--- /dev/null
+++ b/src/plugins/apiNotices.ts
@@ -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);'
+ }
+ ]
+ }
+ ],
+});
diff --git a/src/plugins/clickableRoleDot.ts b/src/plugins/clickableRoleDot.ts
index 63ad84e..800a742 100644
--- a/src/plugins/clickableRoleDot.ts
+++ b/src/plugins/clickableRoleDot.ts
@@ -17,7 +17,7 @@ export default definePlugin({
],
copyToClipBoard(color: string) {
- DiscordNative.clipboard.copy(color);
+ window.DiscordNative.clipboard.copy(color);
Toasts.show({
message: "Copied to Clipboard!",
type: Toasts.Type.SUCCESS,
diff --git a/src/plugins/index.ts b/src/plugins/index.ts
index 1490656..e4d0775 100644
--- a/src/plugins/index.ts
+++ b/src/plugins/index.ts
@@ -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) {
startPlugin(Plugins[plugin]);
}
diff --git a/src/utils/IpcEvents.ts b/src/utils/IpcEvents.ts
index 6061fcb..b0a53f2 100644
--- a/src/utils/IpcEvents.ts
+++ b/src/utils/IpcEvents.ts
@@ -19,4 +19,9 @@ export default strEnum({
SET_SETTINGS: "VencordSetSettings",
OPEN_EXTERNAL: "VencordOpenExternal",
OPEN_PATH: "VencordOpenPath",
+ GET_UPDATES: "VencordGetUpdates",
+ GET_REPO: "VencordGetRepo",
+ GET_HASHES: "VencordGetHashes",
+ UPDATE: "VencordUpdate",
+ BUILD: "VencordBuild"
} as const);
diff --git a/src/utils/misc.tsx b/src/utils/misc.tsx
index 8a9afe1..4159906 100644
--- a/src/utils/misc.tsx
+++ b/src/utils/misc.tsx
@@ -1,3 +1,4 @@
+import { FilterFn, find } from "../webpack";
import { React } from "../webpack/common";
/**
@@ -7,9 +8,22 @@ import { React } from "../webpack/common";
*/
export function lazy<T>(factory: () => T): () => 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>) {
return (props: T) => {
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;
}
+
+/**
+ * Calls .join(" ") on the arguments
+ * classes("one", "two") => "one two"
+ */
+export function classes(...classes: string[]) {
+ return classes.join(" ");
+}
diff --git a/src/utils/types.ts b/src/utils/types.ts
index f7936a4..05441e8 100644
--- a/src/utils/types.ts
+++ b/src/utils/types.ts
@@ -29,3 +29,5 @@ interface PluginDef {
dependencies?: string[],
required?: boolean;
}
+
+export type IpcRes<V = any> = { ok: true; value: V; } | { ok: false, error: any; };
diff --git a/src/utils/updater.ts b/src/utils/updater.ts
new file mode 100644
index 0000000..b3fa812
--- /dev/null
+++ b/src/utils/updater.ts
@@ -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"];
+}
diff --git a/src/webpack/common.tsx b/src/webpack/common.tsx
index 6e93e27..82d812b 100644
--- a/src/webpack/common.tsx
+++ b/src/webpack/common.tsx
@@ -1,17 +1,43 @@
-import { startAll } from "../plugins";
-import { waitFor, filters, findByProps } from './webpack';
+import { waitFor, filters, _resolveReady } from './webpack';
import type Components from "discord-types/components";
import type Stores from "discord-types/stores";
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 React: typeof import("react");
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 Switch: any;
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 = {
MESSAGE: 0,
SUCCESS: 1,
@@ -27,28 +53,28 @@ export const Toasts = {
Type: ToastType,
Position: ToastPosition,
// 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)
-} as {
- Type: typeof ToastType,
- Position: typeof ToastPosition;
- genId(): string;
- show(data: {
- message: string,
- id: string,
- /**
- * Toasts.Type
- */
- type: number,
- options?: {
+ genId: () => (Math.random() || Math.random()).toString(36).slice(2),
+
+ // hack to merge with the following interface, dunno if there's a better way
+ ...{} as {
+ show(data: {
+ message: string,
+ id: string,
/**
- * Toasts.Position
+ * Toasts.Type
*/
- position?: number;
- component?: React.ReactNode,
- duration?: number;
- };
- }): void;
- pop(): void;
+ type: number,
+ options?: {
+ /**
+ * Toasts.Position
+ */
+ position?: number;
+ component?: React.ReactNode,
+ duration?: number;
+ };
+ }): void;
+ pop(): void;
+ }
};
waitFor("useState", m => React = m);
@@ -56,7 +82,7 @@ waitFor(["dispatch", "subscribe"], m => {
FluxDispatcher = m;
const cb = () => {
m.unsubscribe("CONNECTION_OPEN", cb);
- startAll();
+ _resolveReady();
};
m.subscribe("CONNECTION_OPEN", cb);
});
@@ -64,6 +90,7 @@ waitFor(["getCurrentUser", "initialize"], m => UserStore = m);
waitFor(["Hovers", "Looks", "Sizes"], m => Button = m);
waitFor(filters.byCode("helpdeskArticleId"), m => Switch = 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("titleClassName", "sectionTitle")(m), m => Forms.FormSection = m);
@@ -78,3 +105,8 @@ waitFor(m => {
// This is the same module but this is easier
waitFor(filters.byCode("currentToast?"), m => Toasts.show = 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);
diff --git a/src/webpack/webpack.ts b/src/webpack/webpack.ts
index 9e550a4..ea5a7e3 100644
--- a/src/webpack/webpack.ts
+++ b/src/webpack/webpack.ts
@@ -1,5 +1,12 @@
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 cache: WebpackInstance["c"];
@@ -68,8 +75,19 @@ export function findAll(filter: FilterFn, getDefault = true) {
const ret = [] as any[];
for (const key in cache) {
const mod = cache[key];
- if (mod?.exports && filter(mod.exports)) ret.push(mod.exports);
- if (mod?.exports?.default && filter(mod.exports.default)) ret.push(getDefault ? mod.exports.default : mod.exports);
+ if (!mod?.exports) continue;
+
+ 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;
diff --git a/tsconfig.json b/tsconfig.json
index 6489d93..620512a 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -10,6 +10,7 @@
"target": "ESNEXT",
// https://esbuild.github.io/api/#jsx-factory
"jsxFactory": "Vencord.Webpack.Common.React.createElement",
+ "jsxFragmentFactory": "Vencord.Webpack.Common.React.Fragment",
"jsx": "react"
},
"include": ["src/**/*"]