From 0d5e2d0696da494aee2126b4cadbca7e07066b89 Mon Sep 17 00:00:00 2001 From: Vendicated Date: Sat, 6 May 2023 01:36:00 +0200 Subject: [skip ci] Refactor utils --- src/utils/Logger.ts | 2 +- src/utils/cloud.tsx | 4 +- src/utils/constants.ts | 5 ++ src/utils/dependencies.ts | 2 +- src/utils/index.ts | 13 +++-- src/utils/lazy.ts | 87 +++++++++++++++++++++++++++++++ src/utils/misc.tsx | 110 +-------------------------------------- src/utils/modal.tsx | 2 +- src/utils/proxyLazy.ts | 82 ----------------------------- src/utils/quickCss.ts | 2 +- src/utils/react.ts | 62 ---------------------- src/utils/react.tsx | 128 ++++++++++++++++++++++++++++++++++++++++++++++ src/utils/settingsSync.ts | 4 +- src/utils/text.ts | 39 ++++++++++++++ src/utils/updater.ts | 2 +- 15 files changed, 277 insertions(+), 267 deletions(-) create mode 100644 src/utils/lazy.ts delete mode 100644 src/utils/proxyLazy.ts delete mode 100644 src/utils/react.ts create mode 100644 src/utils/react.tsx (limited to 'src/utils') diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index 88ebb43..1ae4762 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -export default class Logger { +export class Logger { /** * Returns the console format args for a title with the specified background colour and black text * @param color Background colour diff --git a/src/utils/cloud.tsx b/src/utils/cloud.tsx index b31091f..5f853bd 100644 --- a/src/utils/cloud.tsx +++ b/src/utils/cloud.tsx @@ -18,11 +18,11 @@ import * as DataStore from "@api/DataStore"; import { showNotification } from "@api/Notifications"; -import { Settings } from "@api/settings"; +import { Settings } from "@api/Settings"; import { findByProps } from "@webpack"; import { UserStore } from "@webpack/common"; -import Logger from "./Logger"; +import { Logger } from "./Logger"; import { openModal } from "./modal"; export const cloudLogger = new Logger("Cloud", "#39b7e0"); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 69eb604..a10a0a5 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -19,6 +19,11 @@ import gitHash from "~git-hash"; import gitRemote from "~git-remote"; +export { + gitHash, + gitRemote +}; + export const WEBPACK_CHUNK = "webpackChunkdiscord_app"; export const REACT_GLOBAL = "Vencord.Webpack.Common.React"; export const VENCORD_USER_AGENT = `Vencord/${gitHash}${gitRemote ? ` (https://github.com/${gitRemote})` : ""}`; diff --git a/src/utils/dependencies.ts b/src/utils/dependencies.ts index a09a87b..67bf502 100644 --- a/src/utils/dependencies.ts +++ b/src/utils/dependencies.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { makeLazy } from "./misc"; +import { makeLazy } from "./lazy"; /* Add dynamically loaded dependencies for plugins here. diff --git a/src/utils/index.ts b/src/utils/index.ts index 6723a70..fd15f3d 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -17,15 +17,18 @@ */ export * from "./ChangeList"; -export * as Constants from "./constants"; +export * from "./constants"; export * from "./debounce"; -export * as Discord from "./discord"; -export { default as Logger } from "./Logger"; +export * from "./discord"; +export * from "./guards"; +export * from "./lazy"; +export * from "./localStorage"; +export * from "./Logger"; export * from "./margins"; export * from "./misc"; -export * as Modals from "./modal"; +export * from "./modal"; export * from "./onceDefined"; export * from "./onlyOnce"; -export * from "./proxyLazy"; +export * from "./patches"; export * from "./Queue"; export * from "./text"; diff --git a/src/utils/lazy.ts b/src/utils/lazy.ts new file mode 100644 index 0000000..1e1dd5f --- /dev/null +++ b/src/utils/lazy.ts @@ -0,0 +1,87 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +export function makeLazy(factory: () => T): () => T { + let cache: T; + return () => cache ?? (cache = factory()); +} + +// Proxies demand that these properties be unmodified, so proxyLazy +// will always return the function default for them. +const unconfigurable = ["arguments", "caller", "prototype"]; + +const handler: ProxyHandler = {}; + +const GET_KEY = Symbol.for("vencord.lazy.get"); +const CACHED_KEY = Symbol.for("vencord.lazy.cached"); + +for (const method of [ + "apply", + "construct", + "defineProperty", + "deleteProperty", + "get", + "getOwnPropertyDescriptor", + "getPrototypeOf", + "has", + "isExtensible", + "ownKeys", + "preventExtensions", + "set", + "setPrototypeOf" +]) { + handler[method] = + (target: any, ...args: any[]) => Reflect[method](target[GET_KEY](), ...args); +} + +handler.ownKeys = target => { + const v = target[GET_KEY](); + const keys = Reflect.ownKeys(v); + for (const key of unconfigurable) { + if (!keys.includes(key)) keys.push(key); + } + return keys; +}; + +handler.getOwnPropertyDescriptor = (target, p) => { + if (typeof p === "string" && unconfigurable.includes(p)) + return Reflect.getOwnPropertyDescriptor(target, p); + + const descriptor = Reflect.getOwnPropertyDescriptor(target[GET_KEY](), p); + + if (descriptor) Object.defineProperty(target, p, descriptor); + return descriptor; +}; + +/** + * Wraps the result of {@see makeLazy} in a Proxy you can consume as if it wasn't lazy. + * On first property access, the lazy is evaluated + * @param factory lazy factory + * @returns Proxy + * + * Note that the example below exists already as an api, see {@link findByPropsLazy} + * @example const mod = proxyLazy(() => findByProps("blah")); console.log(mod.blah); + */ +export function proxyLazy(factory: () => T): T { + const proxyDummy: { (): void;[CACHED_KEY]?: T;[GET_KEY](): T; } = Object.assign(function () { }, { + [CACHED_KEY]: void 0, + [GET_KEY]: () => proxyDummy[CACHED_KEY] ??= factory(), + }); + + return new Proxy(proxyDummy, handler) as any; +} diff --git a/src/utils/misc.tsx b/src/utils/misc.tsx index b6a6423..59475cb 100644 --- a/src/utils/misc.tsx +++ b/src/utils/misc.tsx @@ -16,79 +16,7 @@ * along with this program. If not, see . */ -import { Clipboard, React, Toasts, useEffect, useState } from "@webpack/common"; - -/** - * Makes a lazy function. On first call, the value is computed. - * On subsequent calls, the same computed value will be returned - * @param factory Factory function - */ -export function makeLazy(factory: () => T): () => T { - let cache: T; - return () => cache ?? (cache = factory()); -} - -type AwaiterRes = [T, any, boolean]; -interface AwaiterOpts { - fallbackValue: T, - deps?: unknown[], - onError?(e: any): void, -} -/** - * Await a promise - * @param factory Factory - * @param fallbackValue The fallback value that will be used until the promise resolved - * @returns [value, error, isPending] - */ -export function useAwaiter(factory: () => Promise): AwaiterRes; -export function useAwaiter(factory: () => Promise, providedOpts: AwaiterOpts): AwaiterRes; -export function useAwaiter(factory: () => Promise, providedOpts?: AwaiterOpts): AwaiterRes { - const opts: Required> = Object.assign({ - fallbackValue: null, - deps: [], - onError: null, - }, providedOpts); - const [state, setState] = useState({ - value: opts.fallbackValue, - error: null, - pending: true - }); - - useEffect(() => { - let isAlive = true; - if (!state.pending) setState({ ...state, pending: true }); - - factory() - .then(value => isAlive && setState({ value, error: null, pending: false })) - .catch(error => isAlive && (setState({ value: null, error, pending: false }), opts.onError?.(error))); - - return () => void (isAlive = false); - }, opts.deps); - - return [state.value, state.error, state.pending]; -} - -/** - * Returns a function that can be used to force rerender react components - */ -export function useForceUpdater() { - const [, set] = useState(0); - return () => set(s => s + 1); -} - -/** - * A lazy component. The factory method is called on first render. For example useful - * for const Component = LazyComponent(() => findByDisplayName("...").default) - * @param factory Function returning a Component - * @returns Result of factory function - */ -export function LazyComponent(factory: () => React.ComponentType) { - const get = makeLazy(factory); - return (props: T & JSX.IntrinsicAttributes) => { - const Component = get(); - return ; - }; -} +import { Clipboard, Toasts } from "@webpack/common"; /** * Recursively merges defaults into an object and returns the same object @@ -109,34 +37,6 @@ export function mergeDefaults(obj: T, defaults: T): T { return obj; } - -/** - * Join an array of strings in a human readable way (1, 2 and 3) - * @param elements Elements - */ -export function humanFriendlyJoin(elements: string[]): string; -/** - * Join an array of strings in a human readable way (1, 2 and 3) - * @param elements Elements - * @param mapper Function that converts elements to a string - */ -export function humanFriendlyJoin(elements: T[], mapper: (e: T) => string): string; -export function humanFriendlyJoin(elements: any[], mapper: (e: any) => string = s => s): string { - const { length } = elements; - if (length === 0) return ""; - if (length === 1) return mapper(elements[0]); - - let s = ""; - - for (let i = 0; i < length; i++) { - s += mapper(elements[i]); - if (length - i > 2) s += ", "; - else if (length - i > 1) s += " and "; - } - - return s; -} - /** * Calls .join(" ") on the arguments * classes("one", "two") => "one two" @@ -152,14 +52,6 @@ export function sleep(ms: number): Promise { return new Promise(r => setTimeout(r, ms)); } -/** - * Wrap the text in ``` with an optional language - */ -export function makeCodeblock(text: string, language?: string) { - const chars = "```"; - return `${chars}${language || ""}\n${text.replaceAll("```", "\\`\\`\\`")}\n${chars}`; -} - export function copyWithToast(text: string, toastMessage = "Copied to clipboard!") { if (Clipboard.SUPPORTS_COPY) { Clipboard.copy(text); diff --git a/src/utils/modal.tsx b/src/utils/modal.tsx index 35aaaf8..1738623 100644 --- a/src/utils/modal.tsx +++ b/src/utils/modal.tsx @@ -19,7 +19,7 @@ import { filters, mapMangledModuleLazy } from "@webpack"; import type { ComponentType, PropsWithChildren, ReactNode, Ref } from "react"; -import { LazyComponent } from "./misc"; +import { LazyComponent } from "./react"; export enum ModalSize { SMALL = "small", diff --git a/src/utils/proxyLazy.ts b/src/utils/proxyLazy.ts deleted file mode 100644 index b1fca6e..0000000 --- a/src/utils/proxyLazy.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -// Proxies demand that these properties be unmodified, so proxyLazy -// will always return the function default for them. -const unconfigurable = ["arguments", "caller", "prototype"]; - -const handler: ProxyHandler = {}; - -const GET_KEY = Symbol.for("vencord.lazy.get"); -const CACHED_KEY = Symbol.for("vencord.lazy.cached"); - -for (const method of [ - "apply", - "construct", - "defineProperty", - "deleteProperty", - "get", - "getOwnPropertyDescriptor", - "getPrototypeOf", - "has", - "isExtensible", - "ownKeys", - "preventExtensions", - "set", - "setPrototypeOf" -]) { - handler[method] = - (target: any, ...args: any[]) => Reflect[method](target[GET_KEY](), ...args); -} - -handler.ownKeys = target => { - const v = target[GET_KEY](); - const keys = Reflect.ownKeys(v); - for (const key of unconfigurable) { - if (!keys.includes(key)) keys.push(key); - } - return keys; -}; - -handler.getOwnPropertyDescriptor = (target, p) => { - if (typeof p === "string" && unconfigurable.includes(p)) - return Reflect.getOwnPropertyDescriptor(target, p); - - const descriptor = Reflect.getOwnPropertyDescriptor(target[GET_KEY](), p); - - if (descriptor) Object.defineProperty(target, p, descriptor); - return descriptor; -}; - -/** - * Wraps the result of {@see makeLazy} in a Proxy you can consume as if it wasn't lazy. - * On first property access, the lazy is evaluated - * @param factory lazy factory - * @returns Proxy - * - * Note that the example below exists already as an api, see {@link findByPropsLazy} - * @example const mod = proxyLazy(() => findByProps("blah")); console.log(mod.blah); - */ -export function proxyLazy(factory: () => T): T { - const proxyDummy: { (): void; [CACHED_KEY]?: T; [GET_KEY](): T; } = Object.assign(function () { }, { - [CACHED_KEY]: void 0, - [GET_KEY]: () => proxyDummy[CACHED_KEY] ??= factory(), - }); - - return new Proxy(proxyDummy, handler) as any; -} diff --git a/src/utils/quickCss.ts b/src/utils/quickCss.ts index 1b3f78d..fe35a3c 100644 --- a/src/utils/quickCss.ts +++ b/src/utils/quickCss.ts @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -import { addSettingsListener, Settings } from "@api/settings"; +import { addSettingsListener, Settings } from "@api/Settings"; let style: HTMLStyleElement; diff --git a/src/utils/react.ts b/src/utils/react.ts deleted file mode 100644 index e5e1f67..0000000 --- a/src/utils/react.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Vencord, a modification for Discord's desktop app - * Copyright (c) 2022 Vendicated and contributors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . -*/ - -import { React, useState } from "@webpack/common"; - -import { checkIntersecting } from "./misc"; - -/** - * Check if an element is on screen - * @param intersectOnly If `true`, will only update the state when the element comes into view - * @returns [refCallback, isIntersecting] - */ -export const useIntersection = (intersectOnly = false): [ - refCallback: React.RefCallback, - isIntersecting: boolean, -] => { - const observerRef = React.useRef(null); - const [isIntersecting, setIntersecting] = useState(false); - - const refCallback = (element: Element | null) => { - observerRef.current?.disconnect(); - observerRef.current = null; - - if (!element) return; - - if (checkIntersecting(element)) { - setIntersecting(true); - if (intersectOnly) return; - } - - observerRef.current = new IntersectionObserver(entries => { - for (const entry of entries) { - if (entry.target !== element) continue; - if (entry.isIntersecting && intersectOnly) { - setIntersecting(true); - observerRef.current?.disconnect(); - observerRef.current = null; - } else { - setIntersecting(entry.isIntersecting); - } - } - }); - observerRef.current.observe(element); - }; - - return [refCallback, isIntersecting]; -}; diff --git a/src/utils/react.tsx b/src/utils/react.tsx new file mode 100644 index 0000000..69c1df2 --- /dev/null +++ b/src/utils/react.tsx @@ -0,0 +1,128 @@ +/* + * Vencord, a modification for Discord's desktop app + * Copyright (c) 2022 Vendicated and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . +*/ + +import { React, useEffect, useReducer, useState } from "@webpack/common"; + +import { makeLazy } from "./lazy"; +import { checkIntersecting } from "./misc"; + +/** + * Check if an element is on screen + * @param intersectOnly If `true`, will only update the state when the element comes into view + * @returns [refCallback, isIntersecting] + */ +export const useIntersection = (intersectOnly = false): [ + refCallback: React.RefCallback, + isIntersecting: boolean, +] => { + const observerRef = React.useRef(null); + const [isIntersecting, setIntersecting] = useState(false); + + const refCallback = (element: Element | null) => { + observerRef.current?.disconnect(); + observerRef.current = null; + + if (!element) return; + + if (checkIntersecting(element)) { + setIntersecting(true); + if (intersectOnly) return; + } + + observerRef.current = new IntersectionObserver(entries => { + for (const entry of entries) { + if (entry.target !== element) continue; + if (entry.isIntersecting && intersectOnly) { + setIntersecting(true); + observerRef.current?.disconnect(); + observerRef.current = null; + } else { + setIntersecting(entry.isIntersecting); + } + } + }); + observerRef.current.observe(element); + }; + + return [refCallback, isIntersecting]; +}; + +type AwaiterRes = [T, any, boolean]; +interface AwaiterOpts { + fallbackValue: T; + deps?: unknown[]; + onError?(e: any): void; +} +/** + * Await a promise + * @param factory Factory + * @param fallbackValue The fallback value that will be used until the promise resolved + * @returns [value, error, isPending] + */ + +export function useAwaiter(factory: () => Promise): AwaiterRes; +export function useAwaiter(factory: () => Promise, providedOpts: AwaiterOpts): AwaiterRes; +export function useAwaiter(factory: () => Promise, providedOpts?: AwaiterOpts): AwaiterRes { + const opts: Required> = Object.assign({ + fallbackValue: null, + deps: [], + onError: null, + }, providedOpts); + const [state, setState] = useState({ + value: opts.fallbackValue, + error: null, + pending: true + }); + + useEffect(() => { + let isAlive = true; + if (!state.pending) setState({ ...state, pending: true }); + + factory() + .then(value => isAlive && setState({ value, error: null, pending: false })) + .catch(error => isAlive && (setState({ value: null, error, pending: false }), opts.onError?.(error))); + + return () => void (isAlive = false); + }, opts.deps); + + return [state.value, state.error, state.pending]; +} +/** + * Returns a function that can be used to force rerender react components + */ + +export function useForceUpdater(): () => void; +export function useForceUpdater(withDep: true): [unknown, () => void]; +export function useForceUpdater(withDep?: true) { + const r = useReducer(x => x + 1, 0); + return withDep ? r : r[1]; +} +/** + * A lazy component. The factory method is called on first render. For example useful + * for const Component = LazyComponent(() => findByDisplayName("...").default) + * @param factory Function returning a Component + * @returns Result of factory function + */ + +export function LazyComponent(factory: () => React.ComponentType) { + const get = makeLazy(factory); + return (props: T) => { + const Component = get(); + return ; + }; +} diff --git a/src/utils/settingsSync.ts b/src/utils/settingsSync.ts index 3ec2d43..ef04391 100644 --- a/src/utils/settingsSync.ts +++ b/src/utils/settingsSync.ts @@ -17,12 +17,12 @@ */ import { showNotification } from "@api/Notifications"; -import { PlainSettings, Settings } from "@api/settings"; +import { PlainSettings, Settings } from "@api/Settings"; import { Toasts } from "@webpack/common"; import { deflateSync, inflateSync } from "fflate"; import { getCloudAuth, getCloudUrl } from "./cloud"; -import Logger from "./Logger"; +import { Logger } from "./Logger"; import { saveFile } from "./web"; export async function importSettings(data: string) { diff --git a/src/utils/text.ts b/src/utils/text.ts index 115b3e2..63f6007 100644 --- a/src/utils/text.ts +++ b/src/utils/text.ts @@ -92,3 +92,42 @@ export function formatDuration(time: number, unit: Units, short: boolean = false return res.length ? res : `0 ${getUnitStr(unit, false, short)}`; } + +/** + * Join an array of strings in a human readable way (1, 2 and 3) + * @param elements Elements + */ +export function humanFriendlyJoin(elements: string[]): string; +/** + * Join an array of strings in a human readable way (1, 2 and 3) + * @param elements Elements + * @param mapper Function that converts elements to a string + */ +export function humanFriendlyJoin(elements: T[], mapper: (e: T) => string): string; +export function humanFriendlyJoin(elements: any[], mapper: (e: any) => string = s => s): string { + const { length } = elements; + if (length === 0) + return ""; + if (length === 1) + return mapper(elements[0]); + + let s = ""; + + for (let i = 0; i < length; i++) { + s += mapper(elements[i]); + if (length - i > 2) + s += ", "; + else if (length - i > 1) + s += " and "; + } + + return s; +} + +/** + * Wrap the text in ``` with an optional language + */ +export function makeCodeblock(text: string, language?: string) { + const chars = "```"; + return `${chars}${language || ""}\n${text.replaceAll("```", "\\`\\`\\`")}\n${chars}`; +} diff --git a/src/utils/updater.ts b/src/utils/updater.ts index ce99aa4..2e2bfe1 100644 --- a/src/utils/updater.ts +++ b/src/utils/updater.ts @@ -18,7 +18,7 @@ import gitHash from "~git-hash"; -import Logger from "./Logger"; +import { Logger } from "./Logger"; import { relaunch } from "./native"; import { IpcRes } from "./types"; -- cgit