/* * 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 { 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 ; }; } /** * Recursively merges defaults into an object and returns the same object * @param obj Object * @param defaults Defaults * @returns obj */ export function mergeDefaults(obj: T, defaults: T): T { for (const key in defaults) { const v = defaults[key]; if (typeof v === "object" && !Array.isArray(v)) { obj[key] ??= {} as any; mergeDefaults(obj[key], v); } else { obj[key] ??= v; } } 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" */ export function classes(...classes: string[]) { return classes.filter(c => typeof c === "string").join(" "); } /** * Returns a promise that resolves after the specified amount of time */ 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); } else { toastMessage = "Your browser does not support copying to clipboard"; } Toasts.show({ message: toastMessage, id: Toasts.genId(), type: Toasts.Type.SUCCESS }); } /** * Check if obj is a true object: of type "object" and not null or array */ export function isObject(obj: unknown): obj is object { return typeof obj === "object" && obj !== null && !Array.isArray(obj); } /** * Returns null if value is not a URL, otherwise return URL object. * Avoids having to wrap url checks in a try/catch */ export function parseUrl(urlString: string): URL | null { try { return new URL(urlString); } catch { return null; } } /** * Checks whether an element is on screen */ export const checkIntersecting = (el: Element) => { const elementBox = el.getBoundingClientRect(); const documentHeight = Math.max(document.documentElement.clientHeight, window.innerHeight); return !(elementBox.bottom < 0 || elementBox.top - documentHeight >= 0); }; export function identity(value: T): T { return value; }