diff --git a/src/def.d.ts b/src/def.d.ts deleted file mode 100644 index 5fe8b6a..0000000 --- a/src/def.d.ts +++ /dev/null @@ -1,544 +0,0 @@ -import * as _spitroast from "spitroast"; -import _React from "react"; -import _RN from "react-native"; -import _Clipboard from "@react-native-clipboard/clipboard"; -import _moment from "moment"; -import _chroma from "chroma-js"; -import _lodash from "lodash"; - -type MetroModules = { [id: number]: any }; - -// Component types -interface SummaryProps { - label: string; - icon?: string; - noPadding?: boolean; - noAnimation?: boolean; - children: JSX.Element | JSX.Element[]; -} - -interface CodeblockProps { - selectable?: boolean; - style?: _RN.TextStyle; - children?: string; -} - -interface SearchProps { - onChangeText?: (v: string) => void; - placeholder?: string; - style?: _RN.TextStyle; -} - -interface ErrorBoundaryState { - hasErr: boolean; - errText?: string; -} - -interface TabulatedScreenTab { - id: string; - title: string; - render?: React.ComponentType; - onPress?: (tab?: string) => void; -} - -interface TabulatedScreenProps { - tabs: TabulatedScreenTab[]; -} - -// Helper types for API functions -type PropIntellisense

= Record & Record; -type PropsFinder = (...props: T[]) => PropIntellisense; -type PropsFinderAll = (...props: T[]) => PropIntellisense[]; - -type LoggerFunction = (...messages: any[]) => void; -interface Logger { - log: LoggerFunction; - info: LoggerFunction; - warn: LoggerFunction; - error: LoggerFunction; - time: LoggerFunction; - trace: LoggerFunction; - verbose: LoggerFunction; -} - -type SearchTree = Record; -type SearchFilter = (tree: SearchTree) => boolean; -interface FindInTreeOptions { - walkable?: string[]; - ignore?: string[]; - maxDepth?: number; -} - -interface Asset { - name: string; - id: number; -} - -export enum ButtonColors { - BRAND = "brand", - RED = "red", - GREEN = "green", - PRIMARY = "primary", - TRANSPARENT = "transparent", - GREY = "grey", - LIGHTGREY = "lightgrey", - WHITE = "white", - LINK = "link" -} - -interface ConfirmationAlertOptions { - title?: string; - content: string | JSX.Element | (string | JSX.Element)[]; - confirmText?: string; - confirmColor?: ButtonColors; - onConfirm: () => void; - secondaryConfirmText?: string; - onConfirmSecondary?: () => void; - cancelText?: string; - onCancel?: () => void; - isDismissable?: boolean; -} - -interface InputAlertProps { - title?: string; - confirmText?: string; - confirmColor?: ButtonColors; - onConfirm: (input: string) => (void | Promise); - cancelText?: string; - placeholder?: string; - initialValue?: string; - secureTextEntry?: boolean; -} - -interface Author { - name: string; - id?: string; -} - -// See https://github.com/vendetta-mod/polymanifest -interface PluginManifest { - name: string; - description: string; - authors: Author[]; - main: string; - hash: string; - // Vendor-specific field, contains our own data - vendetta?: { - icon?: string; - }; -} - -interface Plugin { - id: string; - manifest: PluginManifest; - enabled: boolean; - update: boolean; - js: string; -} - -interface ThemeData { - name: string; - description?: string; - authors?: Author[]; - spec: number; - semanticColors?: Record; - rawColors?: Record; - background?: { - url: string; - blur?: number; - /** - * The alpha value of the background. - * `CHAT_BACKGROUND` of semanticColors alpha value will be ignored when this is specified - */ - alpha?: number; - } -} - -interface Theme { - id: string; - selected: boolean; - data: ThemeData; -} - -interface Settings extends StorageObject { - debuggerUrl: string; - developerSettings: boolean; - debugBridgeEnabled: boolean; - rdtEnabled: boolean; - errorBoundaryEnabled: boolean; - inspectionDepth: number; - safeMode?: { - enabled: boolean; - currentThemeId?: string; - }; -} - -interface ApplicationCommand { - description: string; - name: string; - options: ApplicationCommandOption[]; - execute: (args: any[], ctx: CommandContext) => CommandResult | void | Promise | Promise; - id?: string; - applicationId: string; - displayName: string; - displayDescription: string; - inputType: ApplicationCommandInputType; - type: ApplicationCommandType; -} - -export enum ApplicationCommandInputType { - BUILT_IN, - BUILT_IN_TEXT, - BUILT_IN_INTEGRATION, - BOT, - PLACEHOLDER, -} - -interface ApplicationCommandOption { - name: string; - description: string; - required?: boolean; - type: ApplicationCommandOptionType; - displayName: string; - displayDescription: string; -} - -export enum ApplicationCommandOptionType { - SUB_COMMAND = 1, - SUB_COMMAND_GROUP, - STRING, - INTEGER, - BOOLEAN, - USER, - CHANNEL, - ROLE, - MENTIONABLE, - NUMBER, - ATTACHMENT, -} - -export enum ApplicationCommandType { - CHAT = 1, - USER, - MESSAGE, -} - -interface CommandContext { - channel: any; - guild: any; -} - -interface CommandResult { - content: string; - tts?: boolean; -} - -interface RNConstants extends _RN.PlatformConstants { - // Android - Version: number; - Release: string; - Serial: string; - Fingerprint: string; - Model: string; - Brand: string; - Manufacturer: string; - ServerHost?: string; - - // iOS - forceTouchAvailable: boolean; - interfaceIdiom: string; - osVersion: string; - systemName: string; -} - -/** - * A key-value storage based upon `SharedPreferences` on Android. - * - * These types are based on Android though everything should be the same between - * platforms. - */ -interface MMKVManager { - /** - * Get the value for the given `key`, or null - * @param key The key to fetch - */ - getItem: (key: string) => Promise; - /** - * Deletes the value for the given `key` - * @param key The key to delete - */ - removeItem: (key: string) => void; - /** - * Sets the value of `key` to `value` - */ - setItem: (key: string, value: string) => void; - /** - * Goes through every item in storage and returns it, excluding the - * keys specified in `exclude`. - * @param exclude A list of items to exclude from result - */ - refresh: (exclude: string[]) => Promise>; - /** - * You will be murdered if you use this function. - * Clears ALL of Discord's settings. - */ - clear: () => void; -} - -interface FileManager { - /** - * @param path **Full** path to file - */ - fileExists: (path: string) => Promise; - /** - * Allowed URI schemes on Android: `file://`, `content://` ([See here](https://developer.android.com/reference/android/content/ContentResolver#accepts-the-following-uri-schemes:_3)) - */ - getSize: (uri: string) => Promise; - /** - * @param path **Full** path to file - * @param encoding Set to `base64` in order to encode response - */ - readFile(path: string, encoding: "base64" | "utf8"): Promise; - saveFileToGallery?(uri: string, fileName: string, fileType: "PNG" | "JPEG"): Promise; - /** - * Beware! This function has differing functionality on iOS and Android. - * @param storageDir Either `cache` or `documents`. - * @param path Path in `storageDir`, parents are recursively created. - * @param data The data to write to the file - * @param encoding Set to `base64` if `data` is base64 encoded. - * @returns Promise that resolves to path of the file once it got written - */ - writeFile(storageDir: "cache" | "documents", path: string, data: string, encoding: "base64" | "utf8"): Promise; - removeFile(storageDir: "cache" | "documents", path: string): Promise; - getConstants: () => { - /** - * The path the `documents` storage dir (see {@link writeFile}) represents. - */ - DocumentsDirPath: string; - CacheDirPath: string; - }; - /** - * Will apparently cease to exist some time in the future so please use {@link getConstants} instead. - * @deprecated - */ - DocumentsDirPath: string; -} - -type EmitterEvent = "SET" | "GET" | "DEL"; - -interface EmitterListenerData { - path: string[]; - value?: any; -} - -type EmitterListener = ( - event: EmitterEvent, - data: EmitterListenerData | any -) => any; - -type EmitterListeners = Record> - -interface Emitter { - listeners: EmitterListeners; - on: (event: EmitterEvent, listener: EmitterListener) => void; - off: (event: EmitterEvent, listener: EmitterListener) => void; - once: (event: EmitterEvent, listener: EmitterListener) => void; - emit: (event: EmitterEvent, data: EmitterListenerData) => void; -} - -interface StorageObject> { - [key: symbol]: keyof T | Emitter; -} - -interface StorageBackend { - get: () => unknown | Promise; - set: (data: unknown) => void | Promise; -} - -interface LoaderConfig extends StorageObject { - customLoadUrl: { - enabled: boolean; - url: string; - }; - loadReactDevTools: boolean; -} - -interface LoaderIdentity { - name: string; - features: { - loaderConfig?: boolean; - devtools?: { - prop: string; - version: string; - }, - themes?: { - prop: string; - } - } -} - -interface DiscordStyleSheet { - [index: string]: any, - createStyles: >(sheet: T | (() => T)) => () => T; - createThemedStyleSheet: typeof import("react-native").StyleSheet.create; -} - -interface VendettaObject { - patcher: { - after: typeof _spitroast.after; - before: typeof _spitroast.before; - instead: typeof _spitroast.instead; - }; - metro: { - find: (filter: (m: any) => boolean) => any; - findAll: (filter: (m: any) => boolean) => any[]; - findByProps: PropsFinder; - findByPropsAll: PropsFinderAll; - findByName: (name: string, defaultExp?: boolean) => any; - findByNameAll: (name: string, defaultExp?: boolean) => any[]; - findByDisplayName: (displayName: string, defaultExp?: boolean) => any; - findByDisplayNameAll: (displayName: string, defaultExp?: boolean) => any[]; - findByTypeName: (typeName: string, defaultExp?: boolean) => any; - findByTypeNameAll: (typeName: string, defaultExp?: boolean) => any[]; - findByStoreName: (name: string) => any; - common: { - constants: PropIntellisense<"Fonts" | "Permissions">; - channels: PropIntellisense<"getVoiceChannelId">; - i18n: PropIntellisense<"Messages">; - url: PropIntellisense<"openURL">; - toasts: PropIntellisense<"open" | "close">; - stylesheet: DiscordStyleSheet; - clipboard: typeof _Clipboard; - assets: PropIntellisense<"registerAsset">; - invites: PropIntellisense<"acceptInviteAndTransitionToInviteChannel">; - commands: PropIntellisense<"getBuiltInCommands">; - navigation: PropIntellisense<"pushLazy">; - navigationStack: PropIntellisense<"createStackNavigator">; - NavigationNative: PropIntellisense<"NavigationContainer">; - // You may ask: "Why not just install Flux's types?" - // Answer: Discord have a (presumably proprietary) fork. It's wildly different. - Flux: PropIntellisense<"connectStores">; - FluxDispatcher: PropIntellisense<"_currentDispatchActionType">; - React: typeof _React; - ReactNative: typeof _RN; - moment: typeof _moment; - chroma: typeof _chroma; - lodash: typeof _lodash; - util: PropIntellisense<"inspect" | "isNullOrUndefined">; - }; - }; - constants: { - DISCORD_SERVER: string; - DISCORD_SERVER_ID: string; - PLUGINS_CHANNEL_ID: string; - THEMES_CHANNEL_ID: string; - GITHUB: string; - PROXY_PREFIX: string; - HTTP_REGEX: RegExp; - HTTP_REGEX_MULTI: RegExp; - }; - utils: { - findInReactTree: (tree: SearchTree, filter: SearchFilter) => any; - findInTree: (tree: SearchTree, filter: SearchFilter, options: FindInTreeOptions) => any; - safeFetch: (input: RequestInfo | URL, options?: RequestInit, timeout?: number) => Promise; - unfreeze: (obj: object) => object; - without: (object: O, ...keys: K) => Omit; - }; - debug: { - connectToDebugger: (url: string) => void; - // TODO: Type output? - getDebugInfo: () => void; - } - ui: { - components: { - // Discord - Forms: PropIntellisense<"Form" | "FormSection">; - General: PropIntellisense<"Button" | "Text" | "View">; - Alert: _React.ComponentType; - Button: _React.ComponentType & { Looks: any, Colors: ButtonColors, Sizes: any }; - HelpMessage: _React.ComponentType; - SafeAreaView: typeof _RN.SafeAreaView; - // Vendetta - Summary: _React.ComponentType; - ErrorBoundary: _React.ComponentType; - Codeblock: _React.ComponentType; - Search: _React.ComponentType; - TabulatedScreen: _React.ComponentType - } - toasts: { - showToast: (content: string, asset?: number) => void; - }; - alerts: { - showConfirmationAlert: (options: ConfirmationAlertOptions) => void; - showCustomAlert: (component: _React.ComponentType, props: any) => void; - showInputAlert: (options: InputAlertProps) => void; - }; - assets: { - all: Record; - find: (filter: (a: any) => void) => Asset | null | undefined; - getAssetByName: (name: string) => Asset; - getAssetByID: (id: number) => Asset; - getAssetIDByName: (name: string) => number; - }; - // TODO: Make a vain attempt to type these - semanticColors: Record; - rawColors: Record; - }; - plugins: { - plugins: Record; - fetchPlugin: (id: string) => Promise; - installPlugin: (id: string, enabled?: boolean) => Promise; - startPlugin: (id: string) => Promise; - stopPlugin: (id: string, disable?: boolean) => void; - removePlugin: (id: string) => void; - getSettings: (id: string) => JSX.Element; - }; - themes: { - themes: Record; - fetchTheme: (id: string, selected?: boolean) => Promise; - installTheme: (id: string) => Promise; - selectTheme: (id: string) => Promise; - removeTheme: (id: string) => Promise; - getCurrentTheme: () => Theme | null; - updateThemes: () => Promise; - }; - commands: { - registerCommand: (command: ApplicationCommand) => () => void; - }; - storage: { - createProxy: (target: T) => { proxy: T, emitter: Emitter }; - useProxy: (storage: StorageObject) => T; - createStorage: (backend: StorageBackend) => Promise>; - wrapSync: >(store: T) => Awaited; - awaitSyncWrapper: (store: any) => Promise; - createMMKVBackend: (store: string) => StorageBackend; - createFileBackend: (file: string) => StorageBackend; - }; - settings: Settings; - loader: { - identity?: LoaderIdentity; - config: LoaderConfig; - }; - logger: Logger; - version: string; - unload: () => void; -} - -interface VendettaPluginObject { - id: string; - manifest: PluginManifest; - storage: Record; -} - -declare global { - type React = typeof _React; - const __vendettaVersion: string; - - interface Window { - [key: PropertyKey]: any; - modules: MetroModules; - vendetta: VendettaObject; - React: typeof _React; - __vendetta_loader?: LoaderIdentity; - } -} diff --git a/src/entry.ts b/src/entry.ts deleted file mode 100644 index 7f257e5..0000000 --- a/src/entry.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ClientInfoManager } from "@lib/native"; - -// This logs in the native logging implementation, e.g. logcat -console.log("Binding your Discord app in chains..."); - -// Make 'freeze' and 'seal' do nothing -Object.freeze = Object; -Object.seal = Object; - -// Prevent Discord from assigning the broken toString polyfill, so the app loads on 221.6+ -const origToString = Function.prototype.toString; -Object.defineProperty(Function.prototype, "toString", { - value: origToString, - configurable: true, - writable: false, -}); - -import(".").then((m) => m.default()).catch((e) => { - console.log(e?.stack ?? e.toString()); - alert([ - "Failed to bind your Discord app!\n", - `Build Number: ${ClientInfoManager.Build}`, - `Bound: ${__vendettaVersion}`, - e?.stack || e.toString(), - ].join("\n")); -}); diff --git a/src/index.ts b/src/index.ts deleted file mode 100644 index 42489f7..0000000 --- a/src/index.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { ReactNative as RN } from "@metro/common"; -import { connectToDebugger, connectToRDT, patchLogHook } from "@lib/debug"; -import { awaitSyncWrapper } from "@lib/storage"; -import { patchCommands } from "@lib/commands"; -import { initPlugins } from "@lib/plugins"; -import { patchChatBackground } from "@lib/themes"; -import { patchAssets } from "@ui/assets"; -import initQuickInstall from "@ui/quickInstall"; -import initSafeMode from "@ui/safeMode"; -import initSettings from "@ui/settings"; -import initFixes from "@lib/fixes"; -import logger from "@lib/logger"; -import windowObject from "@lib/windowObject"; -import settings from "@lib/settings"; - -export default async () => { - // Load everything in parallel - const unloads = await Promise.all([ - patchLogHook(), - patchAssets(), - patchCommands(), - patchChatBackground(), - initFixes(), - initSafeMode(), - initSettings(), - initQuickInstall(), - ]); - - // Wait for our settings proxy shit to be ready - await awaitSyncWrapper(settings); - - // Assign window object - window.vendetta = await windowObject(unloads); - - // Init developer tools - if (settings.debugBridgeEnabled) connectToDebugger(settings.debuggerUrl); - if (settings.rdtEnabled) connectToRDT(); - - // Once done, load plugins - unloads.push(await initPlugins()); - - // Do the funny - await RN.Image.prefetch("https://bound-mod.github.io/assets/images/fools.png"); - - // We good :) - logger.log("Your Discord app has been successfully bound in chains!"); -} diff --git a/src/lib/commands.ts b/src/lib/commands.ts deleted file mode 100644 index 4717d0b..0000000 --- a/src/lib/commands.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { ApplicationCommand, ApplicationCommandType } from "@types"; -import { commands as commandsModule } from "@metro/common"; -import { after } from "@lib/patcher"; - -let commands: ApplicationCommand[] = []; - -export function patchCommands() { - const unpatch = after("getBuiltInCommands", commandsModule, ([type], res: ApplicationCommand[]) => { - if (type === ApplicationCommandType.CHAT) return res.concat(commands); - }); - - return () => { - commands = []; - unpatch(); - }; -} - -export function registerCommand(command: ApplicationCommand): () => void { - // Get built in commands - const builtInCommands = commandsModule.getBuiltInCommands(ApplicationCommandType.CHAT, true, false); - builtInCommands.sort((a: ApplicationCommand, b: ApplicationCommand) => parseInt(b.id!) - parseInt(a.id!)); - - const lastCommand = builtInCommands[builtInCommands.length - 1]; - - // Override the new command's id to the last command id - 1 - command.id = (parseInt(lastCommand.id, 10) - 1).toString(); - - // Add it to the commands array - commands.push(command); - - // Return command id so it can be unregistered - return () => (commands = commands.filter(({ id }) => id !== command.id)); -} diff --git a/src/lib/constants.ts b/src/lib/constants.ts deleted file mode 100644 index e003a19..0000000 --- a/src/lib/constants.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const DISCORD_SERVER = "https://discord.gg/n9QQ4XhhJP"; -export const DISCORD_SERVER_ID = "1015931589865246730"; -export const PLUGINS_CHANNEL_ID = "1091880384561684561"; -export const THEMES_CHANNEL_ID = "1091880434939482202"; -export const GITHUB = "https://github.com/bound-mod"; -export const PROXY_PREFIX = "https://vd-plugins.github.io/proxy"; -export const HTTP_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)$/; -export const HTTP_REGEX_MULTI = /https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&//=]*)/g; diff --git a/src/lib/debug.ts b/src/lib/debug.ts deleted file mode 100644 index 063e373..0000000 --- a/src/lib/debug.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { RNConstants } from "@types"; -import { ReactNative as RN } from "@metro/common"; -import { after } from "@lib/patcher"; -import { getCurrentTheme, selectTheme } from "@lib/themes"; -import { ClientInfoManager, DeviceManager } from "@lib/native"; -import { getAssetIDByName } from "@ui/assets"; -import { showToast } from "@ui/toasts"; -import settings from "@lib/settings"; -import logger from "@lib/logger"; -export let socket: WebSocket; - -export function setSafeMode(state: boolean) { - settings.safeMode = { ...settings.safeMode, enabled: state }; - - if (window.__vendetta_loader?.features.themes) { - if (getCurrentTheme()?.id) settings.safeMode!.currentThemeId = getCurrentTheme()!.id; - if (settings.safeMode?.enabled) { - selectTheme("default"); - } else if (settings.safeMode?.currentThemeId) { - selectTheme(settings.safeMode?.currentThemeId); - } - } -} - -export function connectToDebugger(url: string) { - if (socket !== undefined && socket.readyState !== WebSocket.CLOSED) socket.close(); - - if (!url) { - showToast("Invalid debugger URL!", getAssetIDByName("Small")); - return; - } - - socket = new WebSocket(`ws://${url}`); - - socket.addEventListener("open", () => showToast("Connected to debugger.", getAssetIDByName("Check"))); - socket.addEventListener("message", (message: any) => { - try { - (0, eval)(message.data); - } catch (e) { - console.error(e); - } - }); - - socket.addEventListener("error", (err: any) => { - console.log(`Debugger error: ${err.message}`); - showToast("An error occurred with the debugger connection!", getAssetIDByName("Small")); - }); -} - -export const connectToRDT = () => window.__vendetta_rdc?.connectToDevTools({ - host: settings.debuggerUrl.split(":")?.[0], - resolveRNStyle: RN.StyleSheet.flatten, -}); - -export function patchLogHook() { - const unpatch = after("nativeLoggingHook", globalThis, (args) => { - if (socket?.readyState === WebSocket.OPEN) socket.send(JSON.stringify({ message: args[0], level: args[1] })); - logger.log(args[0]); - }); - - return () => { - socket && socket.close(); - unpatch(); - } -} - -export const versionHash: string = __vendettaVersion; - -export function getDebugInfo() { - // Hermes - const hermesProps = window.HermesInternal.getRuntimeProperties(); - const hermesVer = hermesProps["OSS Release Version"]; - const padding = "for RN "; - - // RN - const PlatformConstants = RN.Platform.constants as RNConstants; - const rnVer = PlatformConstants.reactNativeVersion; - - return { - vendetta: { - version: versionHash, - loader: window.__vendetta_loader?.name.replaceAll("Vendetta", "Bound") /* <--- awful hack lmao */ ?? "Unknown", - }, - discord: { - version: ClientInfoManager.Version, - build: ClientInfoManager.Build, - }, - react: { - version: React.version, - nativeVersion: hermesVer.startsWith(padding) ? hermesVer.substring(padding.length) : `${rnVer.major}.${rnVer.minor}.${rnVer.patch}`, - }, - hermes: { - version: hermesVer, - buildType: hermesProps["Build"], - bytecodeVersion: hermesProps["Bytecode Version"], - }, - ...RN.Platform.select( - { - android: { - os: { - name: "Android", - version: PlatformConstants.Release, - sdk: PlatformConstants.Version - }, - }, - ios: { - os: { - name: PlatformConstants.systemName, - version: PlatformConstants.osVersion - }, - } - } - )!, - ...RN.Platform.select( - { - android: { - device: { - manufacturer: PlatformConstants.Manufacturer, - brand: PlatformConstants.Brand, - model: PlatformConstants.Model, - codename: DeviceManager.device - } - }, - ios: { - device: { - manufacturer: DeviceManager.deviceManufacturer, - brand: DeviceManager.deviceBrand, - model: DeviceManager.deviceModel, - codename: DeviceManager.device - } - } - } - )! - } -} diff --git a/src/lib/emitter.ts b/src/lib/emitter.ts deleted file mode 100644 index 1bfbc08..0000000 --- a/src/lib/emitter.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { Emitter, EmitterEvent, EmitterListener, EmitterListenerData, EmitterListeners } from "@types"; - -export enum Events { - GET = "GET", - SET = "SET", - DEL = "DEL", -}; - -export default function createEmitter(): Emitter { - return { - listeners: Object.values(Events).reduce((acc, val: string) => ((acc[val] = new Set()), acc), {}) as EmitterListeners, - - on(event: EmitterEvent, listener: EmitterListener) { - if (!this.listeners[event].has(listener)) this.listeners[event].add(listener); - }, - - off(event: EmitterEvent, listener: EmitterListener) { - this.listeners[event].delete(listener); - }, - - once(event: EmitterEvent, listener: EmitterListener) { - const once = (event: EmitterEvent, data: EmitterListenerData) => { - this.off(event, once); - listener(event, data); - }; - this.on(event, once); - }, - - emit(event: EmitterEvent, data: EmitterListenerData) { - for (const listener of this.listeners[event]) listener(event, data); - }, - }; -} diff --git a/src/lib/fixes.ts b/src/lib/fixes.ts deleted file mode 100644 index 135e1f0..0000000 --- a/src/lib/fixes.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { FluxDispatcher, moment } from "@metro/common"; -import { findByProps, findByStoreName } from "@metro/filters"; -import logger from "@lib/logger"; - -const ThemeManager = findByProps("updateTheme", "overrideTheme"); -const AMOLEDThemeManager = findByProps("setAMOLEDThemeEnabled"); -const ThemeStore = findByStoreName("ThemeStore"); -const UnsyncedUserSettingsStore = findByStoreName("UnsyncedUserSettingsStore"); - -function onDispatch({ locale }: { locale: string }) { - // Theming - // Based on https://github.com/Aliucord/AliucordRN/blob/main/src/ui/patchTheme.ts - try { - if (ThemeManager) { - ThemeManager.overrideTheme(ThemeStore?.theme ?? "dark"); - if (AMOLEDThemeManager && UnsyncedUserSettingsStore.useAMOLEDTheme === 2) AMOLEDThemeManager.setAMOLEDThemeEnabled(true); - } - } catch(e) { - logger.error("Failed to fix theme...", e); - } - - // Timestamps - try { - // TODO: Test if this works with all locales - moment.locale(locale.toLowerCase()); - } catch(e) { - logger.error("Failed to fix timestamps...", e); - } - - // We're done here! - FluxDispatcher.unsubscribe("I18N_LOAD_SUCCESS", onDispatch); -} - -export default () => FluxDispatcher.subscribe("I18N_LOAD_SUCCESS", onDispatch); \ No newline at end of file diff --git a/src/lib/logger.ts b/src/lib/logger.ts deleted file mode 100644 index 4aa7607..0000000 --- a/src/lib/logger.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Logger } from "@types"; -import { findByProps } from "@metro/filters"; - -export const logModule = findByProps("setLogFn").default; -const logger: Logger = new logModule("Bound"); - -export default logger; diff --git a/src/lib/metro/common.ts b/src/lib/metro/common.ts deleted file mode 100644 index ccec70c..0000000 --- a/src/lib/metro/common.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { find, findByProps, findByStoreName } from "@metro/filters"; -import { DiscordStyleSheet } from "@types"; -import { ReactNative as RN } from "@lib/preinit"; -import type { StyleSheet } from "react-native"; - -const ThemeStore = findByStoreName("ThemeStore"); -const colorModule = findByProps("colors", "unsafe_rawColors"); -const colorResolver = colorModule?.internal ?? colorModule?.meta; - -// Reimplementation of Discord's createThemedStyleSheet, which was removed since 204201 -// Not exactly a 1:1 reimplementation, but sufficient to keep compatibility with existing plugins -function createThemedStyleSheet>(sheet: T) { - if (!colorModule) return; - for (const key in sheet) { - // @ts-ignore - sheet[key] = new Proxy(RN.StyleSheet.flatten(sheet[key]), { - get(target, prop, receiver) { - const res = Reflect.get(target, prop, receiver); - return colorResolver.isSemanticColor(res) - ? colorResolver.resolveSemanticColor(ThemeStore.theme, res) - : res - } - }); - } - - return sheet; -} - -// Discord -export const constants = findByProps("Fonts", "Permissions"); -export const channels = findByProps("getVoiceChannelId"); -export const i18n = findByProps("Messages"); -export const url = findByProps("openURL", "openDeeplink"); -export const toasts = find(m => m.open && m.close && !m.startDrag && !m.init && !m.openReplay && !m.setAlwaysOnTop && !m.setAccountFlag); - -// Compatible with pre-204201 versions since createThemedStyleSheet is undefined. -export const stylesheet = { - ...find(m => m.createStyles && !m.ActionSheet), - createThemedStyleSheet, - ...findByProps("createThemedStyleSheet") as {}, -} as DiscordStyleSheet; - -export const clipboard = findByProps("setString", "getString", "hasString") as typeof import("@react-native-clipboard/clipboard").default; -export const assets = findByProps("registerAsset"); -export const invites = findByProps("acceptInviteAndTransitionToInviteChannel"); -export const commands = findByProps("getBuiltInCommands"); -export const navigation = findByProps("pushLazy"); -export const navigationStack = findByProps("createStackNavigator"); -export const NavigationNative = findByProps("NavigationContainer"); -export const { TextStyleSheet } = findByProps("TextStyleSheet"); - -// Flux -export const Flux = findByProps("connectStores"); -export const FluxDispatcher = findByProps("_currentDispatchActionType"); - -// React -export const React = window.React as typeof import("react"); -export { ReactNative } from "@lib/preinit"; - -// Moment -export const moment = findByProps("isMoment") as typeof import("moment"); - -// chroma.js -export { chroma } from "@lib/preinit"; - -// Lodash -export const lodash = findByProps("forEachRight") as typeof import("lodash"); - -// The node:util polyfill for RN -// TODO: Find types for this -export const util = findByProps("inspect", "isNullOrUndefined"); diff --git a/src/lib/metro/filters.ts b/src/lib/metro/filters.ts deleted file mode 100644 index 1a78242..0000000 --- a/src/lib/metro/filters.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { MetroModules, PropsFinder, PropsFinderAll } from "@types"; - -// Metro require -declare const __r: (moduleId: number) => any; - -// Internal Metro error reporting logic -const originalHandler = window.ErrorUtils.getGlobalHandler(); - -// Function to blacklist a module, preventing it from being searched again -const blacklist = (id: number) => Object.defineProperty(window.modules, id, { - value: window.modules[id], - enumerable: false, - configurable: true, - writable: true -}); - -// Blacklist any "bad-actor" modules, e.g. the dreaded null proxy, the window itself, or undefined modules -for (const key in window.modules) { - const id = Number(key); - const module = window.modules[id]?.publicModule?.exports; - - if (!module || module === window || module["proxygone"] === null) { - blacklist(id); - continue; - } -} - -// Function to filter through modules -const filterModules = (modules: MetroModules, single = false) => (filter: (m: any) => boolean) => { - const found = []; - - for (const key in modules) { - const id = Number(key); - const module = modules[id]?.publicModule?.exports; - - // HACK: Override the function used to report fatal JavaScript errors (that crash the app) to prevent module-requiring side effects - // Credit to @pylixonly (492949202121261067) for the initial version of this fix - if (!modules[id].isInitialized) try { - window.ErrorUtils.setGlobalHandler(() => {}); - __r(id); - window.ErrorUtils.setGlobalHandler(originalHandler); - } catch {} - - if (!module) { - blacklist(id); - continue; - } - - if (module.default && module.__esModule && filter(module.default)) { - if (single) return module.default; - found.push(module.default); - } - - if (filter(module)) { - if (single) return module; - else found.push(module); - } - } - - if (!single) return found; -} - -export const modules = window.modules; -export const find = filterModules(modules, true); -export const findAll = filterModules(modules); - -const propsFilter = (props: (string | symbol)[]) => (m: any) => props.every((p) => m[p] !== undefined); -const nameFilter = (name: string, defaultExp: boolean) => (defaultExp ? (m: any) => m?.name === name : (m: any) => m?.default?.name === name); -const dNameFilter = (displayName: string, defaultExp: boolean) => (defaultExp ? (m: any) => m?.displayName === displayName : (m: any) => m?.default?.displayName === displayName); -const tNameFilter = (typeName: string, defaultExp: boolean) => (defaultExp ? (m: any) => m?.type?.name === typeName : (m: any) => m?.default?.type?.name === typeName); -const storeFilter = (name: string) => (m: any) => m.getName && m.getName.length === 0 && m.getName() === name; - -export const findByProps: PropsFinder = (...props) => find(propsFilter(props)); -export const findByPropsAll: PropsFinderAll = (...props) => findAll(propsFilter(props)); -export const findByName = (name: string, defaultExp = true) => find(nameFilter(name, defaultExp)); -export const findByNameAll = (name: string, defaultExp = true) => findAll(nameFilter(name, defaultExp)); -export const findByDisplayName = (displayName: string, defaultExp = true) => find(dNameFilter(displayName, defaultExp)); -export const findByDisplayNameAll = (displayName: string, defaultExp = true) => findAll(dNameFilter(displayName, defaultExp)); -export const findByTypeName = (typeName: string, defaultExp = true) => find(tNameFilter(typeName, defaultExp)); -export const findByTypeNameAll = (typeName: string, defaultExp = true) => findAll(tNameFilter(typeName, defaultExp)); -export const findByStoreName = (name: string) => find(storeFilter(name)); diff --git a/src/lib/native.ts b/src/lib/native.ts deleted file mode 100644 index 2665bdf..0000000 --- a/src/lib/native.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { MMKVManager as _MMKVManager, FileManager as _FileManager } from "@types"; -const nmp = window.nativeModuleProxy; - -export const MMKVManager = nmp.MMKVManager as _MMKVManager; -//! 173.10 renamed this to RTNFileManager. -export const FileManager = (nmp.DCDFileManager ?? nmp.RTNFileManager) as _FileManager; -//! 173.13 renamed this to RTNClientInfoManager. -export const ClientInfoManager = nmp.InfoDictionaryManager ?? nmp.RTNClientInfoManager; -//! 173.14 renamed this to RTNDeviceManager. -export const DeviceManager = nmp.DCDDeviceManager ?? nmp.RTNDeviceManager; -export const BundleUpdaterManager = nmp.BundleUpdaterManager; \ No newline at end of file diff --git a/src/lib/patcher.ts b/src/lib/patcher.ts deleted file mode 100644 index fbff9d6..0000000 --- a/src/lib/patcher.ts +++ /dev/null @@ -1,4 +0,0 @@ -import * as _spitroast from "spitroast"; - -export * from "spitroast"; -export default { ..._spitroast }; \ No newline at end of file diff --git a/src/lib/plugins.ts b/src/lib/plugins.ts deleted file mode 100644 index a73c20b..0000000 --- a/src/lib/plugins.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { PluginManifest, Plugin } from "@types"; -import { safeFetch } from "@lib/utils"; -import { awaitSyncWrapper, createMMKVBackend, createStorage, purgeStorage, wrapSync } from "@lib/storage"; -import { allSettled } from "@lib/polyfills"; -import logger, { logModule } from "@lib/logger"; -import settings from "@lib/settings"; - -type EvaledPlugin = { - onLoad?(): void; - onUnload(): void; - settings: JSX.Element; -}; - -export const plugins = wrapSync(createStorage>(createMMKVBackend("VENDETTA_PLUGINS"))); -const loadedPlugins: Record = {}; - -export async function fetchPlugin(id: string) { - if (!id.endsWith("/")) id += "/"; - const existingPlugin = plugins[id]; - - let pluginManifest: PluginManifest; - - try { - pluginManifest = await (await safeFetch(id + "manifest.json", { cache: "no-store" })).json(); - } catch { - throw new Error(`Failed to fetch manifest for ${id}`); - } - - let pluginJs: string | undefined; - - if (existingPlugin?.manifest.hash !== pluginManifest.hash) { - try { - // by polymanifest spec, plugins should always specify their main file, but just in case - pluginJs = await (await safeFetch(id + (pluginManifest.main || "index.js"), { cache: "no-store" })).text(); - } catch {} // Empty catch, checked below - } - - if (!pluginJs && !existingPlugin) throw new Error(`Failed to fetch JS for ${id}`); - - plugins[id] = { - id: id, - manifest: pluginManifest, - enabled: existingPlugin?.enabled ?? false, - update: existingPlugin?.update ?? true, - js: pluginJs ?? existingPlugin.js, - }; -} - -export async function installPlugin(id: string, enabled = true) { - if (!id.endsWith("/")) id += "/"; - if (typeof id !== "string" || id in plugins) throw new Error("Plugin already installed"); - await fetchPlugin(id); - if (enabled) await startPlugin(id); -} - -export async function evalPlugin(plugin: Plugin) { - const vendettaForPlugins = { - ...window.vendetta, - plugin: { - id: plugin.id, - manifest: plugin.manifest, - // Wrapping this with wrapSync is NOT an option. - storage: await createStorage>(createMMKVBackend(plugin.id)), - }, - logger: new logModule(`Vendetta » ${plugin.manifest.name}`), - }; - const pluginString = `vendetta=>{return ${plugin.js}}\n//# sourceURL=${plugin.id}`; - - const raw = (0, eval)(pluginString)(vendettaForPlugins); - const ret = typeof raw == "function" ? raw() : raw; - return ret?.default ?? ret ?? {}; -} - -export async function startPlugin(id: string) { - if (!id.endsWith("/")) id += "/"; - const plugin = plugins[id]; - if (!plugin) throw new Error("Attempted to start non-existent plugin"); - - try { - if (!settings.safeMode?.enabled) { - const pluginRet: EvaledPlugin = await evalPlugin(plugin); - loadedPlugins[id] = pluginRet; - pluginRet.onLoad?.(); - } - plugin.enabled = true; - } catch (e) { - logger.error(`Plugin ${plugin.id} errored whilst loading, and will be unloaded`, e); - - try { - loadedPlugins[plugin.id]?.onUnload?.(); - } catch (e2) { - logger.error(`Plugin ${plugin.id} errored whilst unloading`, e2); - } - - delete loadedPlugins[id]; - plugin.enabled = false; - } -} - -export function stopPlugin(id: string, disable = true) { - if (!id.endsWith("/")) id += "/"; - const plugin = plugins[id]; - const pluginRet = loadedPlugins[id]; - if (!plugin) throw new Error("Attempted to stop non-existent plugin"); - - if (!settings.safeMode?.enabled) { - try { - pluginRet?.onUnload?.(); - } catch (e) { - logger.error(`Plugin ${plugin.id} errored whilst unloading`, e); - } - - delete loadedPlugins[id]; - } - - disable && (plugin.enabled = false); -} - -export async function removePlugin(id: string) { - if (!id.endsWith("/")) id += "/"; - const plugin = plugins[id]; - if (plugin.enabled) stopPlugin(id); - delete plugins[id]; - await purgeStorage(id); -} - -export async function initPlugins() { - await awaitSyncWrapper(settings); - await awaitSyncWrapper(plugins); - const allIds = Object.keys(plugins); - - if (!settings.safeMode?.enabled) { - // Loop over any plugin that is enabled, update it if allowed, then start it. - await allSettled(allIds.filter(pl => plugins[pl].enabled).map(async (pl) => (plugins[pl].update && await fetchPlugin(pl).catch((e: Error) => logger.error(e.message)), await startPlugin(pl)))); - // Wait for the above to finish, then update all disabled plugins that are allowed to. - allIds.filter(pl => !plugins[pl].enabled && plugins[pl].update).forEach(pl => fetchPlugin(pl)); - }; - - return stopAllPlugins; -} - -const stopAllPlugins = () => Object.keys(loadedPlugins).forEach(p => stopPlugin(p, false)); - -export const getSettings = (id: string) => loadedPlugins[id]?.settings; diff --git a/src/lib/polyfills.ts b/src/lib/polyfills.ts deleted file mode 100644 index 7c745e6..0000000 --- a/src/lib/polyfills.ts +++ /dev/null @@ -1,5 +0,0 @@ -//! Starting from 202.4, Promise.allSettled may be undefined due to conflicting then/promise versions, so we use our own. -const allSettledFulfill = (value: T) => ({ status: "fulfilled", value }); -const allSettledReject = (reason: T) => ({ status: "rejected", reason }); -const mapAllSettled = (item: T) => Promise.resolve(item).then(allSettledFulfill, allSettledReject); -export const allSettled = (iterator: T) => Promise.all(Array.from(iterator).map(mapAllSettled)); diff --git a/src/lib/preinit.ts b/src/lib/preinit.ts deleted file mode 100644 index e4ea813..0000000 --- a/src/lib/preinit.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { initThemes } from "@lib/themes"; -import { instead } from "@lib/patcher"; - -// Hoist required modules -// This used to be in filters.ts, but things became convoluted - -const basicFind = (filter: (m: any) => any | string) => { - for (const key in window.modules) { - const exp = window.modules[key]?.publicModule.exports; - if (exp && filter(exp)) return exp; - } -} - -const requireNativeComponent = basicFind(m => m?.default?.name === "requireNativeComponent"); - -if (requireNativeComponent) { - // > "Tried to register two views with the same name DCDVisualEffectView" - // This serves as a workaround for the crashing You tab on Android starting from version 192.x - // How? We simply ignore it. - instead("default", requireNativeComponent, (args, orig) => { - try { - return orig(...args); - } catch { - return args[0]; - } - }) -} - -// Hoist React on window -window.React = basicFind(m => m.createElement) as typeof import("react"); - -// Export ReactNative -export const ReactNative = basicFind(m => m.AppRegistry) as typeof import("react-native"); - -// Export chroma.js -export const chroma = basicFind(m => m.brewer) as typeof import("chroma-js"); - -// Themes -if (window.__vendetta_loader?.features.themes) { - try { - initThemes(); - } catch (e) { - console.error("[Bound] Failed to initialize themes...", e); - } -} diff --git a/src/lib/settings.ts b/src/lib/settings.ts deleted file mode 100644 index 4eea331..0000000 --- a/src/lib/settings.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { LoaderConfig, Settings } from "@types"; -import { createFileBackend, createMMKVBackend, createStorage, wrapSync } from "@lib/storage"; - -export default wrapSync(createStorage(createMMKVBackend("VENDETTA_SETTINGS"))); -export const loaderConfig = wrapSync(createStorage(createFileBackend("vendetta_loader.json"))); diff --git a/src/lib/storage/backends.ts b/src/lib/storage/backends.ts deleted file mode 100644 index 8a544c1..0000000 --- a/src/lib/storage/backends.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { StorageBackend } from "@types"; -import { MMKVManager, FileManager } from "@lib/native"; -import { ReactNative as RN } from "@metro/common"; - -const ILLEGAL_CHARS_REGEX = /[<>:"\/\\|?*]/g; - -const filePathFixer = (file: string): string => RN.Platform.select({ - default: file, - ios: FileManager.saveFileToGallery ? file : `Documents/${file}`, -}); - -const getMMKVPath = (name: string): string => { - if (ILLEGAL_CHARS_REGEX.test(name)) { - // Replace forbidden characters with hyphens - name = name.replace(ILLEGAL_CHARS_REGEX, '-').replace(/-+/g, '-'); - } - - return `vd_mmkv/${name}`; -} - -export const purgeStorage = async (store: string) => { - if (await MMKVManager.getItem(store)) { - MMKVManager.removeItem(store); - } - - const mmkvPath = getMMKVPath(store); - if (await FileManager.fileExists(`${FileManager.getConstants().DocumentsDirPath}/${mmkvPath}`)) { - await FileManager.removeFile?.("documents", mmkvPath); - } -} - -export const createMMKVBackend = (store: string) => { - const mmkvPath = getMMKVPath(store); - return createFileBackend(mmkvPath, (async () => { - try { - const path = `${FileManager.getConstants().DocumentsDirPath}/${mmkvPath}`; - if (await FileManager.fileExists(path)) return; - - let oldData = await MMKVManager.getItem(store) ?? "{}"; - - // From the testing on Android, it seems to return this if the data is too large - if (oldData === "!!LARGE_VALUE!!") { - const cachePath = `${FileManager.getConstants().CacheDirPath}/mmkv/${store}`; - if (await FileManager.fileExists(cachePath)) { - oldData = await FileManager.readFile(cachePath, "utf8") - } else { - console.log(`${store}: Experienced data loss :(`); - oldData = "{}"; - } - } - - await FileManager.writeFile("documents", filePathFixer(mmkvPath), oldData, "utf8"); - if (await MMKVManager.getItem(store) !== null) { - MMKVManager.removeItem(store); - console.log(`Successfully migrated ${store} store from MMKV storage to fs`); - } - } catch (err) { - console.error("Failed to migrate to fs from MMKVManager ", err) - } - })()); -} - -export const createFileBackend = (file: string, migratePromise?: Promise): StorageBackend => { - let created: boolean; - return { - get: async () => { - await migratePromise; - const path = `${FileManager.getConstants().DocumentsDirPath}/${file}`; - if (!created && !(await FileManager.fileExists(path))) return (created = true), FileManager.writeFile("documents", filePathFixer(file), "{}", "utf8"); - return JSON.parse(await FileManager.readFile(path, "utf8")); - }, - set: async (data) => { - await migratePromise; - await FileManager.writeFile("documents", filePathFixer(file), JSON.stringify(data), "utf8"); - } - }; -}; diff --git a/src/lib/storage/index.ts b/src/lib/storage/index.ts deleted file mode 100644 index 8df15f7..0000000 --- a/src/lib/storage/index.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { Emitter, StorageBackend, StorageObject } from "@types"; -import createEmitter from "@lib/emitter"; - -const emitterSymbol = Symbol.for("vendetta.storage.emitter"); -const syncAwaitSymbol = Symbol.for("vendetta.storage.accessor"); -const storageErrorSymbol = Symbol.for("vendetta.storage.error"); - -export function createProxy(target: any = {}): { proxy: any; emitter: Emitter } { - const emitter = createEmitter(); - - function createProxy(target: any, path: string[]): any { - return new Proxy(target, { - get(target, prop: string) { - if ((prop as unknown) === emitterSymbol) return emitter; - - const newPath = [...path, prop]; - const value: any = target[prop]; - - if (value !== undefined && value !== null) { - emitter.emit("GET", { - path: newPath, - value, - }); - if (typeof value === "object") { - return createProxy(value, newPath); - } - return value; - } - - return value; - }, - - set(target, prop: string, value) { - target[prop] = value; - emitter.emit("SET", { - path: [...path, prop], - value, - }); - // we do not care about success, if this actually does fail we have other problems - return true; - }, - - deleteProperty(target, prop: string) { - const success = delete target[prop]; - if (success) - emitter.emit("DEL", { - path: [...path, prop], - }); - return success; - }, - }); - } - - return { - proxy: createProxy(target, []), - emitter, - }; -} - -export function useProxy(storage: StorageObject): T { - if (storage[storageErrorSymbol]) throw storage[storageErrorSymbol]; - - const emitter = storage[emitterSymbol] as any as Emitter; - - if (!emitter) throw new Error("InvalidArgumentExcpetion - storage[emitterSymbol] is " + typeof emitter); - - const [, forceUpdate] = React.useReducer((n) => ~n, 0); - - React.useEffect(() => { - const listener = () => forceUpdate(); - - emitter.on("SET", listener); - emitter.on("DEL", listener); - - return () => { - emitter.off("SET", listener); - emitter.off("DEL", listener); - }; - }, []); - - return storage as T; -} - -export async function createStorage(backend: StorageBackend): Promise> { - const data = await backend.get(); - const { proxy, emitter } = createProxy(data); - - const handler = () => backend.set(proxy); - emitter.on("SET", handler); - emitter.on("DEL", handler); - - return proxy; -} - -export function wrapSync>(store: T): Awaited { - let awaited: any = undefined; - let error: any = undefined; - - const awaitQueue: (() => void)[] = []; - const awaitInit = (cb: () => void) => (awaited ? cb() : awaitQueue.push(cb)); - - store.then((v) => { - awaited = v; - awaitQueue.forEach((cb) => cb()); - }).catch((e) => { - error = e; - }); - - return new Proxy({} as Awaited, { - ...Object.fromEntries( - Object.getOwnPropertyNames(Reflect) - // @ts-expect-error - .map((k) => [k, (t: T, ...a: any[]) => Reflect[k](awaited ?? t, ...a)]) - ), - get(target, prop, recv) { - if (prop === storageErrorSymbol) return error; - if (prop === syncAwaitSymbol) return awaitInit; - return Reflect.get(awaited ?? target, prop, recv); - }, - }); -} - -export const awaitSyncWrapper = (store: any) => new Promise((res) => store[syncAwaitSymbol](res)); - -export * from "@lib/storage/backends"; diff --git a/src/lib/themes.ts b/src/lib/themes.ts deleted file mode 100644 index 15d4003..0000000 --- a/src/lib/themes.ts +++ /dev/null @@ -1,236 +0,0 @@ -import { Theme, ThemeData } from "@types"; -import { ReactNative as RN, chroma } from "@metro/common"; -import { findInReactTree, safeFetch } from "@lib/utils"; -import { findByName, findByProps } from "@metro/filters"; -import { instead, after } from "@lib/patcher"; -import { createFileBackend, createMMKVBackend, createStorage, wrapSync, awaitSyncWrapper } from "@lib/storage"; -import logger from "./logger"; - -//! As of 173.10, early-finding this does not work. -// Somehow, this is late enough, though? -export const color = findByProps("SemanticColor"); - -export const themes = wrapSync(createStorage>(createMMKVBackend("VENDETTA_THEMES"))); - -const semanticAlternativeMap: Record = { - "BG_BACKDROP": "BACKGROUND_FLOATING", - "BG_BASE_PRIMARY": "BACKGROUND_PRIMARY", - "BG_BASE_SECONDARY": "BACKGROUND_SECONDARY", - "BG_BASE_TERTIARY": "BACKGROUND_SECONDARY_ALT", - "BG_MOD_FAINT": "BACKGROUND_MODIFIER_ACCENT", - "BG_MOD_STRONG": "BACKGROUND_MODIFIER_ACCENT", - "BG_MOD_SUBTLE": "BACKGROUND_MODIFIER_ACCENT", - "BG_SURFACE_OVERLAY": "BACKGROUND_FLOATING", - "BG_SURFACE_OVERLAY_TMP": "BACKGROUND_FLOATING", - "BG_SURFACE_RAISED": "BACKGROUND_MOBILE_PRIMARY" -} - -async function writeTheme(theme: Theme | {}) { - if (typeof theme !== "object") throw new Error("Theme must be an object"); - - // Save the current theme as vendetta_theme.json. When supported by loader, - // this json will be written to window.__vendetta_theme and be used to theme the native side. - await createFileBackend("vendetta_theme.json").set(theme); -} - -export function patchChatBackground() { - const currentBackground = getCurrentTheme()?.data?.background; - if (!currentBackground) return; - - const MessagesWrapperConnected = findByName("MessagesWrapperConnected", false); - if (!MessagesWrapperConnected) return; - const { MessagesWrapper } = findByProps("MessagesWrapper"); - if (!MessagesWrapper) return; - - const patches = [ - after("default", MessagesWrapperConnected, (_, ret) => React.createElement(RN.ImageBackground, { - style: { flex: 1, height: "100%" }, - source: { uri: currentBackground.url }, - blurRadius: typeof currentBackground.blur === "number" ? currentBackground.blur : 0, - children: ret, - })), - after("render", MessagesWrapper.prototype, (_, ret) => { - const Messages = findInReactTree(ret, (x) => "HACK_fixModalInteraction" in x?.props && x?.props?.style); - if (Messages) - Messages.props.style = Object.assign( - RN.StyleSheet.flatten(Messages.props.style ?? {}), - { - backgroundColor: "#0000" - } - ); - else - logger.error("Didn't find Messages when patching MessagesWrapper!"); - }) - ]; - - return () => patches.forEach(x => x()); -} - -function normalizeToHex(colorString: string): string { - if (chroma.valid(colorString)) return chroma(colorString).hex(); - - const color = Number(RN.processColor(colorString)); - - return chroma.rgb( - color >> 16 & 0xff, // red - color >> 8 & 0xff, // green - color & 0xff, // blue - color >> 24 & 0xff // alpha - ).hex(); -} - -// Process data for some compatiblity with native side -function processData(data: ThemeData) { - if (data.semanticColors) { - const semanticColors = data.semanticColors; - - for (const key in semanticColors) { - for (const index in semanticColors[key]) { - semanticColors[key][index] &&= normalizeToHex(semanticColors[key][index] as string); - } - } - } - - if (data.rawColors) { - const rawColors = data.rawColors; - - for (const key in rawColors) { - data.rawColors[key] = normalizeToHex(rawColors[key]); - } - - if (RN.Platform.OS === "android") applyAndroidAlphaKeys(rawColors); - } - - return data; -} - -function applyAndroidAlphaKeys(rawColors: Record) { - // these are native Discord Android keys - const alphaMap: Record = { - "BLACK_ALPHA_60": ["BLACK", 0.6], - "BRAND_NEW_360_ALPHA_20": ["BRAND_360", 0.2], - "BRAND_NEW_360_ALPHA_25": ["BRAND_360", 0.25], - "BRAND_NEW_500_ALPHA_20": ["BRAND_500", 0.2], - "PRIMARY_DARK_500_ALPHA_20": ["PRIMARY_500", 0.2], - "PRIMARY_DARK_700_ALPHA_60": ["PRIMARY_700", 0.6], - "STATUS_GREEN_500_ALPHA_20": ["GREEN_500", 0.2], - "STATUS_RED_500_ALPHA_20": ["RED_500", 0.2], - }; - - for (const key in alphaMap) { - const [colorKey, alpha] = alphaMap[key]; - if (!rawColors[colorKey]) continue; - rawColors[key] = chroma(rawColors[colorKey]).alpha(alpha).hex(); - } -} - -export async function fetchTheme(id: string, selected = false) { - let themeJSON: any; - - try { - themeJSON = await (await safeFetch(id, { cache: "no-store" })).json(); - } catch { - throw new Error(`Failed to fetch theme at ${id}`); - } - - themes[id] = { - id: id, - selected: selected, - data: processData(themeJSON), - }; - - // TODO: Should we prompt when the selected theme is updated? - if (selected) writeTheme(themes[id]); -} - -export async function installTheme(id: string) { - if (typeof id !== "string" || id in themes) throw new Error("Theme already installed"); - await fetchTheme(id); -} - -export async function selectTheme(id: string) { - if (id === "default") return await writeTheme({}); - const selectedThemeId = Object.values(themes).find(i => i.selected)?.id; - - if (selectedThemeId) themes[selectedThemeId].selected = false; - themes[id].selected = true; - await writeTheme(themes[id]); -} - -export async function removeTheme(id: string) { - const theme = themes[id]; - if (theme.selected) await selectTheme("default"); - delete themes[id]; - - return theme.selected; -} - -export function getCurrentTheme(): Theme | null { - const themeProp = window.__vendetta_loader?.features?.themes?.prop; - if (!themeProp) return null; - return window[themeProp] || null; -} - -export async function updateThemes() { - await awaitSyncWrapper(themes); - const currentTheme = getCurrentTheme(); - await Promise.allSettled(Object.keys(themes).map(id => fetchTheme(id, currentTheme?.id === id))); -} - -export async function initThemes() { - //! Native code is required here! - // Awaiting the sync wrapper is too slow, to the point where semanticColors are not correctly overwritten. - // We need a workaround, and it will unfortunately have to be done on the native side. - // await awaitSyncWrapper(themes); - - const selectedTheme = getCurrentTheme(); - if (!selectedTheme) return; - - const oldRaw = color.default.unsafe_rawColors; - - color.default.unsafe_rawColors = new Proxy(oldRaw, { - get: (_, colorProp: string) => { - if (!selectedTheme) return Reflect.get(oldRaw, colorProp); - - return selectedTheme.data?.rawColors?.[colorProp] ?? Reflect.get(oldRaw, colorProp); - } - }); - - instead("resolveSemanticColor", color.default.meta ?? color.default.internal, (args, orig) => { - if (!selectedTheme) return orig(...args); - - const [theme, propIndex] = args; - const [name, colorDef] = extractInfo(theme, propIndex); - - const themeIndex = theme === "amoled" ? 2 : theme === "light" ? 1 : 0; - - //! As of 192.7, Tabs v2 uses BG_ semantic colors instead of BACKGROUND_ ones - const alternativeName = semanticAlternativeMap[name] ?? name; - - const semanticColorVal = (selectedTheme.data?.semanticColors?.[name] ?? selectedTheme.data?.semanticColors?.[alternativeName])?.[themeIndex]; - if (name === "CHAT_BACKGROUND" && typeof selectedTheme.data?.background?.alpha === "number") { - return chroma(semanticColorVal || "black").alpha(1 - selectedTheme.data.background.alpha).hex(); - } - - if (semanticColorVal) return semanticColorVal; - - const rawValue = selectedTheme.data?.rawColors?.[colorDef.raw]; - if (rawValue) { - // Set opacity if needed - return colorDef.opacity === 1 ? rawValue : chroma(rawValue).alpha(colorDef.opacity).hex(); - } - - // Fallback to default - return orig(...args); - }); - - await updateThemes(); -} - -function extractInfo(themeMode: string, colorObj: any): [name: string, colorDef: any] { - // @ts-ignore - assigning to extractInfo._sym - const propName = colorObj[extractInfo._sym ??= Object.getOwnPropertySymbols(colorObj)[0]]; - const colorDef = color.SemanticColor[propName]; - - return [propName, colorDef[themeMode.toLowerCase()]]; -} \ No newline at end of file diff --git a/src/lib/utils/findInReactTree.ts b/src/lib/utils/findInReactTree.ts deleted file mode 100644 index 002f3e3..0000000 --- a/src/lib/utils/findInReactTree.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { SearchFilter } from "@types"; -import { findInTree } from "@lib/utils"; - -export default (tree: { [key: string]: any }, filter: SearchFilter): any => findInTree(tree, filter, { - walkable: ["props", "children", "child", "sibling"], -}); \ No newline at end of file diff --git a/src/lib/utils/findInTree.ts b/src/lib/utils/findInTree.ts deleted file mode 100644 index f8a19cd..0000000 --- a/src/lib/utils/findInTree.ts +++ /dev/null @@ -1,45 +0,0 @@ -// This has been completely reimplemented at this point, but the disclaimer at the end of disclaimers still counts. -// https://github.com/Cordwood/Cordwood/blob/91c0b971bbf05e112927df75415df99fa105e1e7/src/lib/utils/findInTree.ts - -import { FindInTreeOptions, SearchTree, SearchFilter } from "@types"; - -function treeSearch(tree: SearchTree, filter: SearchFilter, opts: Required, depth: number): any { - if (depth > opts.maxDepth) return; - if (!tree) return; - - try { - if (filter(tree)) return tree; - } catch {} - - if (Array.isArray(tree)) { - for (const item of tree) { - if (typeof item !== "object" || item === null) continue; - - try { - const found = treeSearch(item, filter, opts, depth + 1); - if (found) return found; - } catch {} - } - } else if (typeof tree === "object") { - for (const key of Object.keys(tree)) { - if (typeof tree[key] !== "object" || tree[key] === null) continue; - if (opts.walkable.length && !opts.walkable.includes(key)) continue; - if (opts.ignore.includes(key)) continue; - - try { - const found = treeSearch(tree[key], filter, opts, depth + 1); - if (found) return found; - } catch {} - } - } -} - -export default ( - tree: SearchTree, - filter: SearchFilter, - { - walkable = [], - ignore = [], - maxDepth = 100 - }: FindInTreeOptions = {}, -): any | undefined => treeSearch(tree, filter, { walkable, ignore, maxDepth }, 0); diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts deleted file mode 100644 index 8a3c295..0000000 --- a/src/lib/utils/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Makes mass-importing utils cleaner, chosen over moving utils to one file - -export { default as findInReactTree } from "@lib/utils/findInReactTree"; -export { default as findInTree } from "@lib/utils/findInTree"; -export { default as safeFetch } from "@lib/utils/safeFetch"; -export { default as unfreeze } from "@lib/utils/unfreeze"; -export { default as without } from "@lib/utils/without"; \ No newline at end of file diff --git a/src/lib/utils/safeFetch.ts b/src/lib/utils/safeFetch.ts deleted file mode 100644 index a4f7685..0000000 --- a/src/lib/utils/safeFetch.ts +++ /dev/null @@ -1,17 +0,0 @@ -// A really basic fetch wrapper which throws on non-ok response codes - -export default async function safeFetch(input: RequestInfo | URL, options?: RequestInit, timeout = 10000) { - const req = await fetch(input, { - signal: timeoutSignal(timeout), - ...options - }); - - if (!req.ok) throw new Error("Request returned non-ok"); - return req; -} - -function timeoutSignal(ms: number): AbortSignal { - const controller = new AbortController(); - setTimeout(() => controller.abort(`Timed out after ${ms}ms`), ms); - return controller.signal; -} diff --git a/src/lib/utils/unfreeze.ts b/src/lib/utils/unfreeze.ts deleted file mode 100644 index c1a2e32..0000000 --- a/src/lib/utils/unfreeze.ts +++ /dev/null @@ -1,6 +0,0 @@ -// https://stackoverflow.com/a/68339174 - -export default function unfreeze(obj: object) { - if (Object.isFrozen(obj)) return Object.assign({}, obj); - return obj; -} \ No newline at end of file diff --git a/src/lib/utils/without.ts b/src/lib/utils/without.ts deleted file mode 100644 index dd7567e..0000000 --- a/src/lib/utils/without.ts +++ /dev/null @@ -1,5 +0,0 @@ -export default function without(object: O, ...keys: K): Omit { - const cloned = { ...object }; - keys.forEach((k) => delete cloned[k]); - return cloned; -} \ No newline at end of file diff --git a/src/lib/windowObject.ts b/src/lib/windowObject.ts deleted file mode 100644 index 9d3112f..0000000 --- a/src/lib/windowObject.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { VendettaObject } from "@types"; -import patcher from "@lib/patcher"; -import logger from "@lib/logger"; -import settings, { loaderConfig } from "@lib/settings"; -import * as constants from "@lib/constants"; -import * as debug from "@lib/debug"; -import * as plugins from "@lib/plugins"; -import * as themes from "@lib/themes"; -import * as commands from "@lib/commands"; -import * as storage from "@lib/storage"; -import * as metro from "@metro/filters"; -import * as common from "@metro/common"; -import * as components from "@ui/components"; -import * as toasts from "@ui/toasts"; -import * as alerts from "@ui/alerts"; -import * as assets from "@ui/assets"; -import * as color from "@ui/color"; -import * as utils from "@lib/utils"; - -export default async (unloads: any[]): Promise => ({ - patcher: utils.without(patcher, "unpatchAll"), - metro: { ...metro, common: { ...common } }, - constants, - utils, - debug: utils.without(debug, "versionHash", "patchLogHook", "setSafeMode"), - ui: { - components, - toasts, - alerts, - assets, - ...color, - }, - plugins: utils.without(plugins, "initPlugins", "evalPlugin"), - themes: utils.without(themes, "initThemes"), - commands: utils.without(commands, "patchCommands"), - storage, - settings, - loader: { - identity: window.__vendetta_loader, - config: loaderConfig, - }, - logger, - version: debug.versionHash, - unload: () => { - unloads.filter(i => typeof i === "function").forEach(p => p()); - // @ts-expect-error explode - delete window.vendetta; - }, -}); diff --git a/src/ui/alerts.ts b/src/ui/alerts.ts deleted file mode 100644 index b35a3fe..0000000 --- a/src/ui/alerts.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ConfirmationAlertOptions, InputAlertProps } from "@types"; -import { findByProps } from "@metro/filters"; -import InputAlert from "@ui/components/InputAlert"; - -const Alerts = findByProps("openLazy", "close"); - -interface InternalConfirmationAlertOptions extends Omit { - content?: ConfirmationAlertOptions["content"]; - body?: ConfirmationAlertOptions["content"]; -}; - -export function showConfirmationAlert(options: ConfirmationAlertOptions) { - const internalOptions = options as InternalConfirmationAlertOptions; - - internalOptions.body = options.content; - delete internalOptions.content; - - internalOptions.isDismissable ??= true; - - return Alerts.show(internalOptions); -}; - -export const showCustomAlert = (component: React.ComponentType, props: any) => Alerts.openLazy({ - importer: async () => () => React.createElement(component, props), -}); - -export const showInputAlert = (options: InputAlertProps) => showCustomAlert(InputAlert, options); diff --git a/src/ui/assets.ts b/src/ui/assets.ts deleted file mode 100644 index 842abe9..0000000 --- a/src/ui/assets.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Asset } from "@types"; -import { assets } from "@metro/common"; -import { after } from "@lib/patcher"; - -export const all: Record = {}; - -export function patchAssets() { - const unpatch = after("registerAsset", assets, (args: Asset[], id: number) => { - const asset = args[0]; - all[asset.name] = { ...asset, id: id }; - }); - - for (let id = 1; ; id++) { - const asset = assets.getAssetByID(id); - if (!asset) break; - if (all[asset.name]) continue; - all[asset.name] = { ...asset, id: id }; - }; - - return unpatch; -} - -export const find = (filter: (a: any) => void): Asset | null | undefined => Object.values(all).find(filter); -export const getAssetByName = (name: string): Asset => all[name]; -export const getAssetByID = (id: number): Asset => assets.getAssetByID(id); -export const getAssetIDByName = (name: string) => all[name]?.id; \ No newline at end of file diff --git a/src/ui/color.ts b/src/ui/color.ts deleted file mode 100644 index 84dd916..0000000 --- a/src/ui/color.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { constants } from "@metro/common"; -import { color } from "@lib/themes"; - -//! This module is only found on 165.0+, under the assumption that iOS 165.0 is the same as Android 165.0. -//* In 167.1, most if not all traces of the old color modules were removed. -//* In 168.6, Discord restructured EVERYTHING again. SemanticColor on this module no longer works when passed to a stylesheet. We must now use what you see below. -//* In 173.10, Discord restructured a lot of the app. These changes included making the color module impossible to early-find. -//? To stop duplication, it is now exported in our theming code. -//? These comments are preserved for historical purposes. -// const colorModule = findByProps("colors", "meta"); - -//? SemanticColor and default.colors are effectively ThemeColorMap -export const semanticColors = (color?.default?.colors ?? constants?.ThemeColorMap); - -//? RawColor and default.unsafe_rawColors are effectively Colors -//* Note that constants.Colors does still appear to exist on newer versions despite Discord not internally using it - what the fuck? -export const rawColors = (color?.default?.unsafe_rawColors ?? constants?.Colors); \ No newline at end of file diff --git a/src/ui/components/Codeblock.tsx b/src/ui/components/Codeblock.tsx deleted file mode 100644 index 5c29921..0000000 --- a/src/ui/components/Codeblock.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { CodeblockProps } from "@types"; -import { ReactNative as RN, stylesheet, constants } from "@metro/common"; -import { semanticColors } from "@ui/color"; -import { cardStyle } from "@ui/shared"; - -const styles = stylesheet.createThemedStyleSheet({ - codeBlock: { - ...cardStyle, - color: semanticColors.TEXT_NORMAL, - fontFamily: constants.Fonts.CODE_SEMIBOLD, - fontSize: 12, - textAlignVertical: "center", - paddingHorizontal: 12, - }, -}); - -// iOS doesn't support the selectable property on RN.Text... -const InputBasedCodeblock = ({ style, children }: CodeblockProps) => -const TextBasedCodeblock = ({ selectable, style, children }: CodeblockProps) => {children} - -export default function Codeblock({ selectable, style, children }: CodeblockProps) { - if (!selectable) return ; - - return RN.Platform.select({ - ios: , - default: , - }); -} diff --git a/src/ui/components/ErrorBoundary.tsx b/src/ui/components/ErrorBoundary.tsx deleted file mode 100644 index d1285fb..0000000 --- a/src/ui/components/ErrorBoundary.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { ErrorBoundaryState } from "@types"; -import { React, constants, TextStyleSheet } from "@metro/common"; -import { Tabs, Forms } from "@ui/components"; - -export default class ErrorBoundary extends React.PureComponent { - state: ErrorBoundaryState = { hasErr: false }; - - static getDerivedStateFromError = (error: Error) => ({ hasErr: true, errText: error.message }); - - render() { - if (!this.state.hasErr) return this.props.children; - - return ( - - - Uh oh. - {this.state.errText} - this.setState({ hasErr: false, errText: undefined })} /> - - - ) - } -} diff --git a/src/ui/components/InputAlert.tsx b/src/ui/components/InputAlert.tsx deleted file mode 100644 index 527c24d..0000000 --- a/src/ui/components/InputAlert.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { InputAlertProps } from "@types"; -import { findByProps } from "@metro/filters"; -import { Forms, Alert } from "@ui/components"; - -const { FormInput } = Forms; -const Alerts = findByProps("openLazy", "close"); - -export default function InputAlert({ title, confirmText, confirmColor, onConfirm, cancelText, placeholder, initialValue = "", secureTextEntry }: InputAlertProps) { - const [value, setValue] = React.useState(initialValue); - const [error, setError] = React.useState(""); - - function onConfirmWrapper() { - const asyncOnConfirm = Promise.resolve(onConfirm(value)) - - asyncOnConfirm.then(() => { - Alerts.close(); - }).catch((e: Error) => { - setError(e.message); - }); - }; - - return ( - Alerts.close()} - > - { - setValue(typeof v === "string" ? v : v.text); - if (error) setError(""); - }} - returnKeyType="done" - onSubmitEditing={onConfirmWrapper} - error={error || undefined} - secureTextEntry={secureTextEntry} - autoFocus={true} - showBorder={true} - style={{ paddingVertical: 5, alignSelf: "stretch", paddingHorizontal: 0 }} - /> - - ); -}; \ No newline at end of file diff --git a/src/ui/components/Search.tsx b/src/ui/components/Search.tsx deleted file mode 100644 index a8a589c..0000000 --- a/src/ui/components/Search.tsx +++ /dev/null @@ -1,32 +0,0 @@ -// https://github.com/pyoncord/Pyoncord/blob/08c6b5ee1580991704640385b715d772859f34b7/src/lib/ui/components/Search.tsx - -import { SearchProps } from "@types"; -import { ReactNative as RN } from "@metro/common"; -import { getAssetIDByName } from "@ui/assets"; -import { Tabs } from "@ui/components"; - -const SearchIcon = () => ; - -export default ({ onChangeText, placeholder, style }: SearchProps) => { - const [query, setQuery] = React.useState(""); - - const onChange = (value: string) => { - setQuery(value); - onChangeText?.(value); - }; - - return - - -}; diff --git a/src/ui/components/Summary.tsx b/src/ui/components/Summary.tsx deleted file mode 100644 index 60d3507..0000000 --- a/src/ui/components/Summary.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { SummaryProps } from "@types"; -import { ReactNative as RN } from "@metro/common"; -import { getAssetIDByName } from "@ui/assets"; -import { Forms } from "@ui/components"; - -export default function Summary({ label, icon, noPadding = false, noAnimation = false, children }: SummaryProps) { - const { FormRow, FormDivider } = Forms; - const [hidden, setHidden] = React.useState(true); - - return ( - <> - } - trailing={} - onPress={() => { - setHidden(!hidden); - if (!noAnimation) RN.LayoutAnimation.configureNext(RN.LayoutAnimation.Presets.easeInEaseOut); - }} - /> - {!hidden && <> - - {children} - } - - ) -} \ No newline at end of file diff --git a/src/ui/components/TabulatedScreen.tsx b/src/ui/components/TabulatedScreen.tsx deleted file mode 100644 index cb2506c..0000000 --- a/src/ui/components/TabulatedScreen.tsx +++ /dev/null @@ -1,30 +0,0 @@ -// https://github.com/maisymoe/strife/blob/54f4768ef41d66e682a0917b078129df5c34f0f8/plugins/Mockups/src/shared/TabulatedScreen.tsx - -import { TabulatedScreenProps, TabulatedScreenTab } from "@types"; -import { React, ReactNative as RN } from "@metro/common"; -import { findByProps } from "@metro/filters"; - -const { BadgableTabBar } = findByProps("BadgableTabBar"); - -export default ({ tabs }: TabulatedScreenProps) => { - const [activeTab, setActiveTab] = React.useState(tabs[0]); - - return ( - - {activeTab.render && } - - { - const tab = tabs.find(t => t.id === id); - if (!tab) return; - - tab.onPress?.(tab.id); - tab.render && setActiveTab(tab); - }} - /> - - - ) -} diff --git a/src/ui/components/index.ts b/src/ui/components/index.ts deleted file mode 100644 index 786f9d1..0000000 --- a/src/ui/components/index.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { ReactNative as RN } from "@metro/common"; -import { findByDisplayName, findByName, findByProps, find } from "@metro/filters"; - -// https://github.com/pyoncord/Pyoncord/blob/08c6b5ee1580991704640385b715d772859f34b7/src/lib/ui/components/discord/Redesign.ts#L4C1-L4C98 -const findSingular = (prop: string) => find(m => m[prop] && Object.keys(m).length === 1)?.[prop]; - -// Discord -export const Forms = findByProps("Form", "FormSection"); -export const Tabs = { - ...findByProps("TableRow", "TableRowGroup"), - RedesignSwitch: findSingular("FormSwitch"), - RedesignCheckbox: findSingular("FormCheckbox"), -} as Record; -export const General = findByProps("Button", "Text", "View"); -export const Alert = findByDisplayName("FluxContainer(Alert)"); -export const Button = findByProps("Looks", "Colors", "Sizes") as React.ComponentType & { Looks: any, Colors: any, Sizes: any }; -export const HelpMessage = findByName("HelpMessage"); -// React Native's included SafeAreaView only adds padding on iOS. -export const SafeAreaView = findByProps("useSafeAreaInsets").SafeAreaView as typeof RN.SafeAreaView; - -// Vendetta -export { default as Summary } from "@ui/components/Summary"; -export { default as ErrorBoundary } from "@ui/components/ErrorBoundary"; -export { default as Codeblock } from "@ui/components/Codeblock"; -export { default as Search } from "@ui/components/Search"; -export { default as TabulatedScreen } from "@ui/components/TabulatedScreen"; diff --git a/src/ui/quickInstall/forumPost.tsx b/src/ui/quickInstall/forumPost.tsx deleted file mode 100644 index 367789b..0000000 --- a/src/ui/quickInstall/forumPost.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { findByName, findByProps } from "@metro/filters"; -import { DISCORD_SERVER_ID, PLUGINS_CHANNEL_ID, THEMES_CHANNEL_ID, HTTP_REGEX_MULTI, PROXY_PREFIX } from "@lib/constants"; -import { after } from "@lib/patcher"; -import { installPlugin } from "@lib/plugins"; -import { installTheme } from "@lib/themes"; -import { findInReactTree } from "@lib/utils"; -import { getAssetIDByName } from "@ui/assets"; -import { showToast } from "@ui/toasts"; -import { Forms } from "@ui/components"; - -const ForumPostLongPressActionSheet = findByName("ForumPostLongPressActionSheet", false); -const { FormRow, FormIcon } = Forms; - -const { useFirstForumPostMessage } = findByProps("useFirstForumPostMessage"); -const { hideActionSheet } = findByProps("openLazy", "hideActionSheet"); - -export default () => after("default", ForumPostLongPressActionSheet, ([{ thread }], res) => { - if (thread.guild_id !== DISCORD_SERVER_ID) return; - - // Determine what type of addon this is. - let postType: "Plugin" | "Theme"; - if (thread.parent_id === PLUGINS_CHANNEL_ID) { - postType = "Plugin"; - } else if (thread.parent_id === THEMES_CHANNEL_ID && window.__vendetta_loader?.features.themes) { - postType = "Theme"; - } else return; - - const { firstMessage } = useFirstForumPostMessage(thread); - - let urls = firstMessage?.content?.match(HTTP_REGEX_MULTI); - if (!urls) return; - - if (postType === "Plugin") { - urls = urls.filter((url: string) => url.startsWith(PROXY_PREFIX)); - } else { - urls = urls.filter((url: string) => url.endsWith(".json")); - }; - - const url = urls[0]; - if (!url) return; - - const actions = findInReactTree(res, (t) => t?.[0]?.key); - const ActionsSection = actions[0].type; - - actions.unshift( - } - label={`Install ${postType}`} - onPress={() => - (postType === "Plugin" ? installPlugin : installTheme)(url).then(() => { - showToast(`Successfully installed ${thread.name}`, getAssetIDByName("Check")); - }).catch((e: Error) => { - showToast(e.message, getAssetIDByName("Small")); - }).finally(() => hideActionSheet()) - } - /> - ); -}); diff --git a/src/ui/quickInstall/index.ts b/src/ui/quickInstall/index.ts deleted file mode 100644 index 38e5f77..0000000 --- a/src/ui/quickInstall/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import patchForumPost from "@ui/quickInstall/forumPost"; -import patchUrl from "@ui/quickInstall/url"; - -export default function initQuickInstall() { - const patches = new Array; - - patches.push(patchForumPost()); - patches.push(patchUrl()); - - return () => patches.forEach(p => p()); -}; diff --git a/src/ui/quickInstall/url.tsx b/src/ui/quickInstall/url.tsx deleted file mode 100644 index 0eb4edc..0000000 --- a/src/ui/quickInstall/url.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { findByProps, find } from "@metro/filters"; -import { ReactNative as RN, channels, url } from "@metro/common"; -import { PROXY_PREFIX, THEMES_CHANNEL_ID } from "@lib/constants"; -import { after, instead } from "@lib/patcher"; -import { installPlugin } from "@lib/plugins"; -import { installTheme } from "@lib/themes"; -import { showConfirmationAlert } from "@ui/alerts"; -import { getAssetIDByName } from "@ui/assets"; -import { showToast } from "@ui/toasts"; - -const showSimpleActionSheet = find((m) => m?.showSimpleActionSheet && !Object.getOwnPropertyDescriptor(m, "showSimpleActionSheet")?.get); -const handleClick = findByProps("handleClick"); -const { openURL } = url; -const { getChannelId } = channels; -const { getChannel } = findByProps("getChannel"); - -const { TextStyleSheet } = findByProps("TextStyleSheet"); - -function typeFromUrl(url: string) { - if (url.startsWith(PROXY_PREFIX)) { - return "Plugin"; - } else if (url.endsWith(".json") && window.__vendetta_loader?.features.themes) { - return "Theme"; - } else return; -} - -function installWithToast(type: "Plugin" | "Theme", url: string) { - (type === "Plugin" ? installPlugin : installTheme)(url) - .then(() => { - showToast("Successfully installed", getAssetIDByName("Check")); - }) - .catch((e: Error) => { - showToast(e.message, getAssetIDByName("Small")); - }); -} - -export default () => { - const patches = new Array(); - - patches.push( - after("showSimpleActionSheet", showSimpleActionSheet, (args) => { - if (args[0].key !== "LongPressUrl") return; - const { - header: { title: url }, - options, - } = args[0]; - - const urlType = typeFromUrl(url); - if (!urlType) return; - - options.push({ - label: `Install ${urlType}`, - onPress: () => installWithToast(urlType, url), - }); - }) - ); - - patches.push( - instead("handleClick", handleClick, async function (this: any, args, orig) { - const { href: url } = args[0]; - - const urlType = typeFromUrl(url); - if (!urlType) return orig.apply(this, args); - - // Make clicking on theme links only work in #themes, should there be a theme proxy in the future, this can be removed. - if (urlType === "Theme" && getChannel(getChannelId())?.parent_id !== THEMES_CHANNEL_ID) return orig.apply(this, args); - - showConfirmationAlert({ - title: "Hold Up", - content: ["This link is a ", {urlType}, ", would you like to install it?"], - onConfirm: () => installWithToast(urlType, url), - confirmText: "Install", - cancelText: "Cancel", - secondaryConfirmText: "Open in Browser", - onConfirmSecondary: () => openURL(url), - }); - }) - ); - - return () => patches.forEach((p) => p()); -}; diff --git a/src/ui/safeMode.tsx b/src/ui/safeMode.tsx deleted file mode 100644 index bdb01db..0000000 --- a/src/ui/safeMode.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { ReactNative as RN, TextStyleSheet, stylesheet } from "@metro/common"; -import { findByName, findByProps } from "@metro/filters"; -import { after } from "@lib/patcher"; -import { setSafeMode } from "@lib/debug"; -import { DeviceManager } from "@lib/native"; -import { semanticColors } from "@ui/color"; -import { cardStyle } from "@ui/shared"; -import { Tabs, Codeblock, ErrorBoundary as _ErrorBoundary, SafeAreaView } from "@ui/components"; -import settings from "@lib/settings"; - -const ErrorBoundary = findByName("ErrorBoundary"); - -// Let's just pray they have this. -const { BadgableTabBar } = findByProps("BadgableTabBar"); - -const styles = stylesheet.createThemedStyleSheet({ - container: { - flex: 1, - backgroundColor: semanticColors.BACKGROUND_PRIMARY, - paddingHorizontal: 16, - }, - header: { - flex: 1, - flexDirection: "row", - justifyContent: "center", - alignItems: "center", - marginTop: 8, - marginBottom: 16, - ...cardStyle, - }, - headerTitle: { - ...TextStyleSheet["heading-lg/semibold"], - color: semanticColors.HEADER_PRIMARY, - marginBottom: 4, - }, - headerDescription: { - ...TextStyleSheet["text-sm/medium"], - color: semanticColors.TEXT_MUTED, - }, - body: { - flex: 6, - }, - footer: { - flexDirection: DeviceManager.isTablet ? "row" : "column", - justifyContent: "center", - marginBottom: 16, - }, -}); - -interface Tab { - id: string; - title: string; - trimWhitespace?: boolean; -} - -interface Button { - text: string; - // TODO: Proper types for the below - variant?: string; - size?: string; - onPress: () => void; -} - -const tabs: Tab[] = [ - { id: "stack", title: "Stack Trace" }, - { id: "component", title: "Component", trimWhitespace: true }, -]; - -export default () => after("render", ErrorBoundary.prototype, function (this: any, _, ret) { - if (!(settings.errorBoundaryEnabled ?? true)) return; - if (!this.state.error) return; - - // Not using setState here as we don't want to cause a re-render, we want this to be set in the initial render - this.state.activeTab ??= "stack"; - const tabData = tabs.find(t => t.id === this.state.activeTab); - const errorText: string = this.state.error[this.state.activeTab]; - - // This is in the patch and not outside of it so that we can use `this`, e.g. for setting state - const buttons: Button[] = [ - { text: "Restart Discord", onPress: this.handleReload }, - ...!settings.safeMode?.enabled ? [{ text: "Restart in Recovery Mode", onPress: setSafeMode }] : [], - { variant: "destructive", text: "Retry Render", onPress: () => this.setState({ info: null, error: null }) }, - ] - - return ( - <_ErrorBoundary> - - - - {ret.props.title} - {ret.props.body} - - {ret.props.Illustration && } - - - - {/* - TODO: I tried to get this working as intended using regex and failed. - When trimWhitespace is true, each line should have it's whitespace removed but with it's spaces kept. - */} - {tabData?.trimWhitespace ? errorText?.split("\n").filter(i => i.length !== 0).map(i => i.trim()).join("\n") : errorText} - - - {/* Are errors caught by ErrorBoundary guaranteed to have the component stack? */} - { this.setState({ activeTab: tab }) }} - /> - - - - {buttons.map(button => { - const buttonIndex = buttons.indexOf(button) !== 0 ? 8 : 0; - - return - })} - - - - ) -}); diff --git a/src/ui/settings/components/AddonPage.tsx b/src/ui/settings/components/AddonPage.tsx deleted file mode 100644 index 601d6ee..0000000 --- a/src/ui/settings/components/AddonPage.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { ReactNative as RN } from "@metro/common"; -import { useProxy } from "@lib/storage"; -import { HelpMessage, ErrorBoundary, Search } from "@ui/components"; -import { CardWrapper } from "@ui/settings/components/Card"; -import settings from "@lib/settings"; - -interface AddonPageProps { - items: Record; - safeModeMessage: string; - safeModeExtras?: JSX.Element | JSX.Element[]; - card: React.ComponentType>; -} - -export default function AddonPage({ items, safeModeMessage, safeModeExtras, card: CardComponent }: AddonPageProps) { - useProxy(settings) - useProxy(items); - const [search, setSearch] = React.useState(""); - - return ( - - {/* TODO: Implement better searching than just by ID */} - - {settings.safeMode?.enabled && - {safeModeMessage} - {safeModeExtras} - } - setSearch(v.toLowerCase())} - placeholder="Search" - /> - } - style={{ paddingHorizontal: 12, paddingTop: 12 }} - contentContainerStyle={{ paddingBottom: 20 }} - data={Object.values(items).filter(i => i.id?.toLowerCase().includes(search))} - renderItem={({ item, index }) => } - /> - - ) -} diff --git a/src/ui/settings/components/AssetDisplay.tsx b/src/ui/settings/components/AssetDisplay.tsx deleted file mode 100644 index 581dd2f..0000000 --- a/src/ui/settings/components/AssetDisplay.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Asset } from "@types"; -import { ReactNative as RN, clipboard } from "@metro/common"; -import { showToast } from "@ui/toasts"; -import { getAssetIDByName } from "@ui/assets"; -import { Forms } from "@ui/components"; - -interface AssetDisplayProps { asset: Asset } - -const { FormRow } = Forms; - -export default function AssetDisplay({ asset }: AssetDisplayProps) { - return ( - } - onPress={() => { - clipboard.setString(asset.name); - showToast("Copied asset name to clipboard.", getAssetIDByName("toast_copy_link")); - }} - /> - ) -} \ No newline at end of file diff --git a/src/ui/settings/components/Card.tsx b/src/ui/settings/components/Card.tsx deleted file mode 100644 index f4f158d..0000000 --- a/src/ui/settings/components/Card.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { ReactNative as RN, stylesheet } from "@metro/common"; -import { findByProps } from "@metro/filters"; -import { getAssetIDByName } from "@ui/assets"; -import { semanticColors } from "@ui/color"; -import { Forms, Tabs } from "@ui/components"; - -const { FormRow } = Forms; -const { RedesignSwitch, RedesignCheckbox } = Tabs; -const { hideActionSheet } = findByProps("openLazy", "hideActionSheet"); -const { showSimpleActionSheet } = findByProps("showSimpleActionSheet"); - -// TODO: These styles work weirdly. iOS has cramped text, Android with low DPI probably does too. Fix? -const styles = stylesheet.createThemedStyleSheet({ - card: { - backgroundColor: semanticColors?.BACKGROUND_SECONDARY, - borderRadius: 16, - }, - header: { - padding: 0, - backgroundColor: semanticColors?.BACKGROUND_TERTIARY, - borderTopLeftRadius: 16, - borderTopRightRadius: 16, - }, - actions: { - flexDirection: "row-reverse", - alignItems: "center", - }, - icon: { - width: 22, - height: 22, - marginLeft: 5, - tintColor: semanticColors?.INTERACTIVE_NORMAL, - }, -}) - -interface Action { - icon: string; - onPress: () => void; -} - -interface OverflowAction extends Action { - label: string; - isDestructive?: boolean; -} - -export interface CardWrapper { - item: T; - index: number; -} - -interface CardProps { - index?: number; - headerLabel: string | React.ComponentType; - headerIcon?: string; - toggleType?: "switch" | "radio"; - toggleValue?: boolean; - onToggleChange?: (v: boolean) => void; - descriptionLabel?: string | React.ComponentType; - actions?: Action[]; - overflowTitle?: string; - overflowActions?: OverflowAction[]; -} - -export default function Card(props: CardProps) { - let pressableState = props.toggleValue ?? false; - - return ( - - } - trailing={props.toggleType && (props.toggleType === "switch" ? - () - : - ( { - pressableState = !pressableState; - props.onToggleChange?.(pressableState) - }}> - - ) - )} - /> - - {props.overflowActions && showSimpleActionSheet({ - key: "CardOverflow", - header: { - title: props.overflowTitle, - icon: props.headerIcon && , - onClose: () => hideActionSheet(), - }, - options: props.overflowActions?.map(i => ({ ...i, icon: getAssetIDByName(i.icon) })), - })} - > - - } - {props.actions?.map(({ icon, onPress }) => ( - - - - ))} - - } - /> - - ) -} diff --git a/src/ui/settings/components/InstallButton.tsx b/src/ui/settings/components/InstallButton.tsx deleted file mode 100644 index 9bf55bb..0000000 --- a/src/ui/settings/components/InstallButton.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { ReactNative as RN, stylesheet, clipboard } from "@metro/common"; -import { HTTP_REGEX_MULTI } from "@lib/constants"; -import { showInputAlert } from "@ui/alerts"; -import { getAssetIDByName } from "@ui/assets"; -import { semanticColors } from "@ui/color"; - -const styles = stylesheet.createThemedStyleSheet({ - icon: { - marginRight: 10, - tintColor: semanticColors.HEADER_PRIMARY, - }, -}); - -interface InstallButtonProps { - alertTitle: string; - installFunction: (id: string) => Promise; -} - -export default function InstallButton({ alertTitle, installFunction: fetchFunction }: InstallButtonProps) { - return ( - - clipboard.getString().then((content) => - showInputAlert({ - title: alertTitle, - initialValue: content.match(HTTP_REGEX_MULTI)?.[0] ?? "", - placeholder: "https://example.com/", - onConfirm: (input: string) => fetchFunction(input), - confirmText: "Install", - cancelText: "Cancel", - }) - ) - }> - - - ); -} diff --git a/src/ui/settings/components/PluginCard.tsx b/src/ui/settings/components/PluginCard.tsx deleted file mode 100644 index e4ffdec..0000000 --- a/src/ui/settings/components/PluginCard.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { ButtonColors, Plugin } from "@types"; -import { NavigationNative, clipboard } from "@metro/common"; -import { removePlugin, startPlugin, stopPlugin, getSettings, fetchPlugin } from "@lib/plugins"; -import { MMKVManager } from "@lib/native"; -import { getAssetIDByName } from "@ui/assets"; -import { showToast } from "@ui/toasts"; -import { showConfirmationAlert } from "@ui/alerts"; -import Card, { CardWrapper } from "@ui/settings/components/Card"; - -async function stopThenStart(plugin: Plugin, callback: Function) { - if (plugin.enabled) stopPlugin(plugin.id, false); - callback(); - if (plugin.enabled) await startPlugin(plugin.id); -} - -export default function PluginCard({ item: plugin, index }: CardWrapper) { - const settings = getSettings(plugin.id); - const navigation = NavigationNative.useNavigation(); - const [removed, setRemoved] = React.useState(false); - - // This is needed because of React™ - if (removed) return null; - - return ( - i.name).join(", ")}`} - headerIcon={plugin.manifest.vendetta?.icon || "ic_application_command_24px"} - toggleType="switch" - toggleValue={plugin.enabled} - onToggleChange={(v: boolean) => { - try { - if (v) startPlugin(plugin.id); else stopPlugin(plugin.id); - } catch (e) { - showToast((e as Error).message, getAssetIDByName("Small")); - } - }} - descriptionLabel={plugin.manifest.description} - overflowTitle={plugin.manifest.name} - overflowActions={[ - { - icon: "ic_sync_24px", - label: "Refetch", - onPress: async () => { - stopThenStart(plugin, () => { - fetchPlugin(plugin.id).then(async () => { - showToast("Successfully refetched plugin.", getAssetIDByName("toast_image_saved")); - }).catch(() => { - showToast("Failed to refetch plugin!", getAssetIDByName("Small")); - }) - }); - }, - }, - { - icon: "copy", - label: "Copy URL", - onPress: () => { - clipboard.setString(plugin.id); - showToast("Copied plugin URL to clipboard.", getAssetIDByName("toast_copy_link")); - } - }, - { - icon: "ic_download_24px", - label: plugin.update ? "Disable updates" : "Enable updates", - onPress: () => { - plugin.update = !plugin.update; - showToast(`${plugin.update ? "Enabled" : "Disabled"} updates for ${plugin.manifest.name}.`, getAssetIDByName("toast_image_saved")); - } - }, - { - icon: "ic_duplicate", - label: "Clear data", - isDestructive: true, - onPress: () => showConfirmationAlert({ - title: "Wait!", - content: `Are you sure you wish to clear the data of ${plugin.manifest.name}?`, - confirmText: "Clear", - cancelText: "Cancel", - confirmColor: ButtonColors.RED, - onConfirm: () => { - stopThenStart(plugin, () => { - try { - MMKVManager.removeItem(plugin.id); - showToast(`Cleared data for ${plugin.manifest.name}.`, getAssetIDByName("trash")); - } catch { - showToast(`Failed to clear data for ${plugin.manifest.name}!`, getAssetIDByName("Small")); - } - }); - } - }), - }, - { - icon: "ic_message_delete", - label: "Delete", - isDestructive: true, - onPress: () => showConfirmationAlert({ - title: "Wait!", - content: `Are you sure you wish to delete ${plugin.manifest.name}? This will clear all of the plugin's data.`, - confirmText: "Delete", - cancelText: "Cancel", - confirmColor: ButtonColors.RED, - onConfirm: () => { - try { - removePlugin(plugin.id); - setRemoved(true); - } catch (e) { - showToast((e as Error).message, getAssetIDByName("Small")); - } - } - }), - }, - ]} - actions={[ - ...(settings ? [{ - icon: "settings", - onPress: () => navigation.push("VendettaCustomPage", { - title: plugin.manifest.name, - render: settings, - }) - }] : []), - ]} - /> - ) -} diff --git a/src/ui/settings/components/SettingsSection.tsx b/src/ui/settings/components/SettingsSection.tsx deleted file mode 100644 index 8e4ee04..0000000 --- a/src/ui/settings/components/SettingsSection.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { NavigationNative } from "@metro/common"; -import { useProxy } from "@lib/storage"; -import { getAssetIDByName } from "@ui/assets"; -import { getRenderableScreens } from "@ui/settings/data"; -import { ErrorBoundary, Forms } from "@ui/components"; -import settings from "@lib/settings"; - -const { FormRow, FormSection, FormDivider } = Forms; - -export default function SettingsSection() { - const navigation = NavigationNative.useNavigation(); - useProxy(settings); - - const screens = getRenderableScreens() - - return ( - - - {screens.map((s, i) => ( - <> - } - trailing={FormRow.Arrow} - onPress={() => navigation.push(s.key)} - /> - {i !== screens.length - 1 && } - - ))} - - - ) -} diff --git a/src/ui/settings/components/ThemeCard.tsx b/src/ui/settings/components/ThemeCard.tsx deleted file mode 100644 index f90763a..0000000 --- a/src/ui/settings/components/ThemeCard.tsx +++ /dev/null @@ -1,91 +0,0 @@ -import { ButtonColors, Theme } from "@types"; -import { clipboard } from "@metro/common"; -import { fetchTheme, removeTheme, selectTheme } from "@lib/themes"; -import { useProxy } from "@lib/storage"; -import { BundleUpdaterManager } from "@lib/native"; -import { getAssetIDByName } from "@ui/assets"; -import { showConfirmationAlert } from "@ui/alerts"; -import { showToast } from "@ui/toasts"; -import settings from "@lib/settings"; -import Card, { CardWrapper } from "@ui/settings/components/Card"; - -async function selectAndReload(value: boolean, id: string) { - await selectTheme(value ? id : "default"); - BundleUpdaterManager.reload(); -} - -export default function ThemeCard({ item: theme, index }: CardWrapper) { - useProxy(settings); - const [removed, setRemoved] = React.useState(false); - - // This is needed because of React™ - if (removed) return null; - - const authors = theme.data.authors; - - return ( - i.name).join(", ")}` : ""}`} - descriptionLabel={theme.data.description ?? "No description."} - toggleType={!settings.safeMode?.enabled ? "radio" : undefined} - toggleValue={theme.selected} - onToggleChange={(v: boolean) => { - selectAndReload(v, theme.id); - }} - overflowTitle={theme.data.name} - overflowActions={[ - { - icon: "ic_sync_24px", - label: "Refetch", - onPress: () => { - fetchTheme(theme.id, theme.selected).then(() => { - if (theme.selected) { - showConfirmationAlert({ - title: "Theme refetched", - content: "A reload is required to see the changes. Do you want to reload now?", - confirmText: "Reload", - cancelText: "Cancel", - confirmColor: ButtonColors.RED, - onConfirm: () => BundleUpdaterManager.reload(), - }) - } else { - showToast("Successfully refetched theme.", getAssetIDByName("toast_image_saved")); - } - }).catch(() => { - showToast("Failed to refetch theme!", getAssetIDByName("Small")); - }); - }, - }, - { - icon: "copy", - label: "Copy URL", - onPress: () => { - clipboard.setString(theme.id); - showToast("Copied theme URL to clipboard.", getAssetIDByName("toast_copy_link")); - } - }, - { - icon: "ic_message_delete", - label: "Delete", - isDestructive: true, - onPress: () => showConfirmationAlert({ - title: "Wait!", - content: `Are you sure you wish to delete ${theme.data.name}?`, - confirmText: "Delete", - cancelText: "Cancel", - confirmColor: ButtonColors.RED, - onConfirm: () => { - removeTheme(theme.id).then((wasSelected) => { - setRemoved(true); - if (wasSelected) selectAndReload(false, theme.id); - }).catch((e: Error) => { - showToast(e.message, getAssetIDByName("Small")); - }); - } - }) - }, - ]} - /> - ) -} diff --git a/src/ui/settings/components/Version.tsx b/src/ui/settings/components/Version.tsx deleted file mode 100644 index 033accf..0000000 --- a/src/ui/settings/components/Version.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { clipboard } from "@metro/common"; -import { getAssetIDByName } from "@ui/assets"; -import { showToast } from "@ui/toasts"; -import { Forms } from "@ui/components"; - -interface VersionProps { - label: string; - version: string; - icon: string; -} - -const { FormRow, FormText } = Forms; - -export default function Version({ label, version, icon }: VersionProps) { - return ( - } - trailing={{version}} - onPress={() => { - clipboard.setString(`${label} - ${version}`); - showToast("Copied version to clipboard.", getAssetIDByName("toast_copy_link")); - }} - /> - ) -} \ No newline at end of file diff --git a/src/ui/settings/data.tsx b/src/ui/settings/data.tsx deleted file mode 100644 index cecc30d..0000000 --- a/src/ui/settings/data.tsx +++ /dev/null @@ -1,138 +0,0 @@ -import { ReactNative as RN, NavigationNative, stylesheet, lodash } from "@metro/common"; -import { installPlugin } from "@lib/plugins"; -import { installTheme } from "@lib/themes"; -import { showConfirmationAlert } from "@ui/alerts"; -import { semanticColors } from "@ui/color"; -import { showToast } from "@ui/toasts"; -import { without } from "@lib/utils"; -import { getAssetIDByName } from "@ui/assets"; -import settings from "@lib/settings"; -import ErrorBoundary from "@ui/components/ErrorBoundary"; -import InstallButton from "@ui/settings/components/InstallButton"; -import General from "@ui/settings/pages/General"; -import Plugins from "@ui/settings/pages/Plugins"; -import Themes from "@ui/settings/pages/Themes"; -import Secret from "@ui/settings/pages/Secret"; -import { PROXY_PREFIX } from "@/lib/constants"; - -interface Screen { - [index: string]: any; - key: string; - title: string; - icon?: string; - shouldRender?: () => boolean; - options?: Record; - render: React.ComponentType; -} - -const styles = stylesheet.createThemedStyleSheet({ container: { flex: 1, backgroundColor: semanticColors.BACKGROUND_MOBILE_PRIMARY } }); -const formatKey = (key: string, youKeys: boolean) => youKeys ? lodash.snakeCase(key).toUpperCase() : key; -// If a function is passed, it is called with the screen object, and the return value is mapped. If a string is passed, we map to the value of the property with that name on the screen. Else, just map to the given data. -// Question: Isn't this overengineered? -// Answer: Maybe. -const keyMap = (screens: Screen[], data: string | ((s: Screen) => any) | null) => Object.fromEntries(screens.map(s => [s.key, typeof data === "function" ? data(s) : typeof data === "string" ? s[data] : data])); - -export const getScreens = (youKeys = false): Screen[] => [ - { - key: formatKey("VendettaSettings", youKeys), - title: "Settings", - icon: "settings", - render: General, - }, - { - key: formatKey("VendettaPlugins", youKeys), - title: "Plugins", - icon: "debug", - options: { - headerRight: () => ( - { - if (!input.startsWith(PROXY_PREFIX) && !settings.developerSettings) - setImmediate(() => showConfirmationAlert({ - title: "Unproxied Plugin", - content: "The plugin you are trying to install has not been proxied/verified by Bound's staff. Are you sure you want to continue?", - confirmText: "Install", - onConfirm: () => - installPlugin(input) - .then(() => showToast("Installed plugin", getAssetIDByName("Check"))) - .catch((x) => showToast(x?.message ?? `${x}`, getAssetIDByName("Small"))), - cancelText: "Cancel", - })); - else return await installPlugin(input); - }} - /> - ), - }, - render: Plugins, - }, - { - key: formatKey("VendettaThemes", youKeys), - title: "Design", - icon: "PencilSparkleIcon", - // TODO: bad - shouldRender: () => window.__vendetta_loader?.features.hasOwnProperty("themes") ?? false, - options: { - headerRight: () => !settings.safeMode?.enabled && , - }, - render: Themes, - }, - { - key: formatKey("BoundUpdater", youKeys), - title: "Updater", - icon: "ic_download_24px", - render: Secret, - }, - { - key: formatKey("VendettaCustomPage", youKeys), - title: "Bound Page", - shouldRender: () => false, - render: ({ render: PageView, noErrorBoundary, ...options }: { render: React.ComponentType; noErrorBoundary: boolean } & Record) => { - const navigation = NavigationNative.useNavigation(); - - navigation.addListener("focus", () => navigation.setOptions(without(options, "render", "noErrorBoundary"))); - return noErrorBoundary ? : - }, - }, -]; - -export const getRenderableScreens = (youKeys = false) => getScreens(youKeys).filter(s => s.shouldRender?.() ?? true); - -export const getPanelsScreens = () => keyMap(getScreens(), (s) => ({ - title: s.title, - render: s.render, - ...s.options, -})); - -export const getYouData = () => { - const screens = getScreens(true); - - return { - getLayout: () => ({ - title: "Bound", - label: "Bound", - // We can't use our keyMap function here since `settings` is an array not an object - settings: getRenderableScreens(true).map(s => s.key) - }), - titleConfig: keyMap(screens, "title"), - relationships: keyMap(screens, null), - rendererConfigs: keyMap(screens, (s) => { - const WrappedComponent = React.memo(({ navigation, route }: any) => { - navigation.addListener("focus", () => navigation.setOptions(s.options)); - return - }); - - return { - type: "route", - title: () => s.title, - icon: s.icon ? getAssetIDByName(s.icon) : null, - screen: { - // TODO: This is bad, we should not re-convert the key casing - // For some context, just using the key here would make the route key be VENDETTA_CUSTOM_PAGE in you tab, which breaks compat with panels UI navigation - route: lodash.chain(s.key).camelCase().upperFirst().value(), - getComponent: () => WrappedComponent, - } - } - }), - }; -}; diff --git a/src/ui/settings/index.ts b/src/ui/settings/index.ts deleted file mode 100644 index 8fd59b0..0000000 --- a/src/ui/settings/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import patchPanels from "@ui/settings/patches/panels"; -import patchYou from "@ui/settings/patches/you"; - -export default function initSettings() { - const patches = [ - patchPanels(), - patchYou(), - ] - - return () => patches.forEach(p => p?.()); -} diff --git a/src/ui/settings/pages/AssetBrowser.tsx b/src/ui/settings/pages/AssetBrowser.tsx deleted file mode 100644 index 139c769..0000000 --- a/src/ui/settings/pages/AssetBrowser.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { ReactNative as RN } from "@metro/common"; -import { all } from "@ui/assets"; -import { Forms, Search, ErrorBoundary } from "@ui/components"; -import AssetDisplay from "@ui/settings/components/AssetDisplay"; - -const { FormDivider } = Forms; - -export default function AssetBrowser() { - const [search, setSearch] = React.useState(""); - - return ( - - - setSearch(v)} - placeholder="Search" - /> - a.name.includes(search) || a.id.toString() === search)} - renderItem={({ item }) => } - ItemSeparatorComponent={FormDivider} - keyExtractor={item => item.name} - /> - - - ) -} \ No newline at end of file diff --git a/src/ui/settings/pages/Developer.tsx b/src/ui/settings/pages/Developer.tsx deleted file mode 100644 index 880f6b0..0000000 --- a/src/ui/settings/pages/Developer.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import { ReactNative as RN, NavigationNative } from "@metro/common"; -import { findByProps } from "@metro/filters"; -import { connectToDebugger, connectToRDT, socket } from "@lib/debug"; -import { BundleUpdaterManager } from "@lib/native"; -import { useProxy } from "@lib/storage"; -import { showToast } from "@ui/toasts"; -import { getAssetIDByName } from "@ui/assets"; -import { Forms, Tabs, ErrorBoundary } from "@ui/components"; -import settings, { loaderConfig } from "@lib/settings"; -import AssetBrowser from "@ui/settings/pages/AssetBrowser"; -import Secret from "@ui/settings/pages/Secret"; - -const { Stack, TableRow, TableRowIcon, TableSwitchRow, TableRowGroup, TextInput, Slider } = Tabs; -const { hideActionSheet } = findByProps("openLazy", "hideActionSheet"); -const { showSimpleActionSheet } = findByProps("showSimpleActionSheet"); - -export default function Developer() { - const navigation = NavigationNative.useNavigation(); - - useProxy(settings); - useProxy(loaderConfig); - - return ( - - - - - { - settings.debugBridgeEnabled = v; - try { - v ? connectToDebugger(settings.debuggerUrl) : socket.close(); - } catch {} - }} - /> - - { - settings.debuggerUrl = v; - }} - /> - - - {window.__vendetta_loader?.features.loaderConfig && - showToast("I was too lazy to edit the native side for this - maisy")} - /> - { - settings.rdtEnabled = v; - if (v) connectToRDT(); - }} - /> - showToast("Why is this even needed - maisy")} - /> - - { - loaderConfig.customLoadUrl.url = v; - }} - /> - - } - - { - settings.errorBoundaryEnabled = v; - }} - /> - showSimpleActionSheet({ - key: "ErrorBoundaryTools", - header: { - title: "Which ErrorBoundary do you want to trip?", - icon: , - onClose: () => hideActionSheet(), - }, - options: [ - // @ts-expect-error - // Of course, to trigger an error, we need to do something incorrectly. The below will do! - { label: "Bound", onPress: () => navigation.push("VendettaCustomPage", { render: () => }) }, - { label: "Discord", isDestructive: true, onPress: () => navigation.push("VendettaCustomPage", { noErrorBoundary: true }) }, - ], - })} - arrow - /> - - - } - /> - - { - settings.inspectionDepth = v; - }} - minimumValue={1} - maximumValue={6} - step={1} - /> - - } - onPress={() => navigation.push("VendettaCustomPage", { - render: Secret, - })} - arrow - /> - - - } - onPress={() => BundleUpdaterManager.reload()} - arrow - /> - } - onPress={() => window.gc?.()} - arrow - /> - } - onPress={() => navigation.push("VendettaCustomPage", { - title: "Asset Browser", - render: AssetBrowser, - })} - arrow - /> - - - - - ) -} diff --git a/src/ui/settings/pages/General.tsx b/src/ui/settings/pages/General.tsx deleted file mode 100644 index c625186..0000000 --- a/src/ui/settings/pages/General.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import { ReactNative as RN, NavigationNative, url } from "@metro/common"; -import { DISCORD_SERVER, GITHUB } from "@lib/constants"; -import { setSafeMode } from "@lib/debug"; -import { useProxy } from "@lib/storage"; -import { plugins } from "@lib/plugins"; -import { themes } from "@lib/themes"; -import { showToast } from "@ui/toasts"; -import { getAssetIDByName } from "@ui/assets"; -import { Tabs, ErrorBoundary } from "@ui/components"; -import settings from "@lib/settings"; -import Developer from "@ui/settings/pages/Developer"; -import Secret from "@ui/settings/pages/Secret"; - -const { Stack, TableRow, TableRowIcon, TableSwitchRow, TableRowGroup } = Tabs; - -export default function General() { - const navigation = NavigationNative.useNavigation(); - - useProxy(settings); - useProxy(plugins); - useProxy(themes); - - return ( - - - - - } - value={settings.safeMode?.enabled} - onValueChange={(v: boolean) => { - setSafeMode(v); - // hack - settings.safeMode!.enabled = v; - }} - /> - - - } - onPress={() => navigation.push("VendettaCustomPage", { - render: Secret, - })} - arrow - /> - } - onPress={() => navigation.push("VendettaCustomPage", { title: "Development Settings", render: Developer })} - arrow - /> - - - } - trailing={} - /> - } - trailing={} - /> - - - } - onPress={() => url.openDeeplink(DISCORD_SERVER)} - arrow - /> - } - onPress={() => url.openURL(GITHUB)} - arrow - /> - } - onPress={() => showToast("nuh uh")} - arrow - /> - - - - - ) -} diff --git a/src/ui/settings/pages/Plugins.tsx b/src/ui/settings/pages/Plugins.tsx deleted file mode 100644 index 222cbb4..0000000 --- a/src/ui/settings/pages/Plugins.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Plugin } from "@types"; -import { useProxy } from "@lib/storage"; -import { plugins } from "@lib/plugins"; -import settings from "@lib/settings"; -import AddonPage from "@ui/settings/components/AddonPage"; -import PluginCard from "@ui/settings/components/PluginCard"; - -export default function Plugins() { - useProxy(settings) - - return ( - - items={plugins} - safeModeMessage="You are in Recovery Mode, so plugins cannot be loaded. Disable any misbehaving plugins, then return to Normal Mode from the General settings page." - card={PluginCard} - /> - ) -} diff --git a/src/ui/settings/pages/Secret.tsx b/src/ui/settings/pages/Secret.tsx deleted file mode 100644 index 754d7e6..0000000 --- a/src/ui/settings/pages/Secret.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { ReactNative as RN } from "@metro/common"; -import { ErrorBoundary } from "@ui/components"; - -export default function General() { - return ( - - - - ) -} diff --git a/src/ui/settings/pages/Themes.tsx b/src/ui/settings/pages/Themes.tsx deleted file mode 100644 index 4aeedab..0000000 --- a/src/ui/settings/pages/Themes.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import { Theme, ButtonColors } from "@types"; -import { useProxy } from "@lib/storage"; -import { themes } from "@lib/themes"; -import { Button, TabulatedScreen } from "@ui/components"; -import settings from "@lib/settings"; -import AddonPage from "@ui/settings/components/AddonPage"; -import ThemeCard from "@ui/settings/components/ThemeCard"; -import Secret from "@ui/settings/pages/Secret"; - -export default function Themes() { - useProxy(settings); - - return ( - - items={themes} - safeModeMessage={`You are in Recovery Mode, meaning themes have been temporarily disabled.${settings.safeMode?.currentThemeId ? " If a theme appears to be causing the issue, you can press below to disable it persistently." : ""}`} - safeModeExtras={settings.safeMode?.currentThemeId ?