diff options
author | Vendicated <vendicated@riseup.net> | 2022-10-01 00:42:50 +0200 |
---|---|---|
committer | Vendicated <vendicated@riseup.net> | 2022-10-01 00:42:50 +0200 |
commit | 8161a07dba401f04dac93f861e6b2cffe53ab7e3 (patch) | |
tree | 1499e825bcd6a162bc43507f492b262e9b1901ed | |
parent | 9aaa47ea4e9ac068bf5fcbb997e31d722f43f1e5 (diff) | |
download | Vencord-8161a07dba401f04dac93f861e6b2cffe53ab7e3.tar.gz Vencord-8161a07dba401f04dac93f861e6b2cffe53ab7e3.tar.bz2 Vencord-8161a07dba401f04dac93f861e6b2cffe53ab7e3.zip |
Add in client updater, Notices API
-rw-r--r-- | src/Vencord.ts | 35 | ||||
-rw-r--r-- | src/api/Notices.ts | 24 | ||||
-rw-r--r-- | src/api/index.ts | 1 | ||||
-rw-r--r-- | src/api/settings.ts | 6 | ||||
-rw-r--r-- | src/components/ErrorBoundary.tsx | 8 | ||||
-rw-r--r-- | src/components/Flex.tsx | 1 | ||||
-rw-r--r-- | src/components/Link.tsx | 19 | ||||
-rw-r--r-- | src/components/Settings.tsx | 43 | ||||
-rw-r--r-- | src/components/Updater.tsx | 128 | ||||
-rw-r--r-- | src/ipcMain.ts | 89 | ||||
-rw-r--r-- | src/plugins/apiNotices.ts | 24 | ||||
-rw-r--r-- | src/plugins/clickableRoleDot.ts | 2 | ||||
-rw-r--r-- | src/plugins/index.ts | 2 | ||||
-rw-r--r-- | src/utils/IpcEvents.ts | 5 | ||||
-rw-r--r-- | src/utils/misc.tsx | 30 | ||||
-rw-r--r-- | src/utils/types.ts | 2 | ||||
-rw-r--r-- | src/utils/updater.ts | 51 | ||||
-rw-r--r-- | src/webpack/common.tsx | 80 | ||||
-rw-r--r-- | src/webpack/webpack.ts | 22 | ||||
-rw-r--r-- | tsconfig.json | 1 |
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/**/*"] |