From b489aea48ad720fe5800e4907708b1bb8236aa95 Mon Sep 17 00:00:00 2001 From: amsyarasyiq <82711525+amsyarasyiq@users.noreply.github.com> Date: Wed, 22 May 2024 03:02:13 +0800 Subject: [PATCH] feat(metro): lazy patching! --- src/core/commands/eval.ts | 4 +- src/core/i18n/index.ts | 4 +- src/core/plugins/quickInstall/forumPost.tsx | 10 +-- src/core/plugins/quickInstall/url.tsx | 6 +- src/core/ui/components/AddonPage.tsx | 6 +- src/core/ui/components/Card.tsx | 6 +- src/core/ui/settings/pages/Developer.tsx | 6 +- src/core/ui/settings/pages/FontImporter.tsx | 4 +- src/lib/api/patcher.ts | 28 +++++++-- src/lib/managers/themes.ts | 14 ++--- src/lib/ui/alerts.ts | 4 +- src/lib/ui/color.ts | 2 +- src/lib/ui/components/ContextMenu.tsx | 4 +- src/lib/ui/components/InputAlert.tsx | 4 +- src/lib/ui/components/discord/Forms.ts | 4 +- src/lib/ui/components/discord/Redesign.tsx | 6 +- src/lib/ui/components/discord/index.ts | 8 +-- src/lib/ui/safeMode.tsx | 6 +- src/lib/ui/settings/patches/panel.tsx | 8 +-- src/lib/ui/settings/patches/shared.tsx | 4 +- src/lib/ui/settings/patches/tabs.tsx | 8 +-- src/lib/ui/styles.ts | 4 +- src/lib/ui/toasts.ts | 4 +- src/lib/utils/lazy.ts | 6 +- src/lib/utils/logger.ts | 4 +- src/lib/utils/throttle.ts | 11 +++- src/metro/caches.ts | 41 ++++++------ src/metro/common/index.ts | 42 ++++++------- src/metro/finders.ts | 12 ++-- src/metro/modules.ts | 32 +++++++++- src/metro/proxy.ts | 69 +++++++++++++++++++++ src/metro/utils.ts | 15 +++-- 32 files changed, 258 insertions(+), 128 deletions(-) create mode 100644 src/metro/proxy.ts diff --git a/src/core/commands/eval.ts b/src/core/commands/eval.ts index 56c99ba..5cd206d 100644 --- a/src/core/commands/eval.ts +++ b/src/core/commands/eval.ts @@ -2,9 +2,9 @@ import { Strings } from "@core/i18n"; import { ApplicationCommand, ApplicationCommandOptionType } from "@lib/api/commands/types"; import { settings } from "@lib/settings"; import { messageUtil } from "@metro/common"; -import { findByProps } from "@metro/utils"; +import { findByPropsProxy } from "@metro/utils"; -const util = findByProps("inspect"); +const util = findByPropsProxy("inspect"); const AsyncFunction = (async () => void 0).constructor; const ZERO_WIDTH_SPACE_CHARACTER = "\u200B"; diff --git a/src/core/i18n/index.ts b/src/core/i18n/index.ts index 9e90276..b61538f 100644 --- a/src/core/i18n/index.ts +++ b/src/core/i18n/index.ts @@ -1,10 +1,10 @@ import { FluxDispatcher } from "@metro/common"; -import { findByName } from "@metro/utils"; +import { findByNameProxy } from "@metro/utils"; import { PrimitiveType } from "intl-messageformat"; import langDefault from "./default.json"; -const IntlMessageFormat = findByName("MessageFormat") as typeof import("intl-messageformat").default; +const IntlMessageFormat = findByNameProxy("MessageFormat") as typeof import("intl-messageformat").default; type I18nKey = keyof typeof langDefault; diff --git a/src/core/plugins/quickInstall/forumPost.tsx b/src/core/plugins/quickInstall/forumPost.tsx index a5aee8b..b0d165f 100644 --- a/src/core/plugins/quickInstall/forumPost.tsx +++ b/src/core/plugins/quickInstall/forumPost.tsx @@ -6,17 +6,17 @@ import { useProxy } from "@lib/api/storage"; import { installPlugin, plugins, removePlugin } from "@lib/managers/plugins"; import { installTheme, removeTheme, themes } from "@lib/managers/themes"; import { DISCORD_SERVER_ID, HTTP_REGEX_MULTI, PLUGINS_CHANNEL_ID, PROXY_PREFIX, THEMES_CHANNEL_ID } from "@lib/utils/constants"; -import { findByProps } from "@metro/utils"; +import { findByPropsProxy } from "@metro/utils"; import { ErrorBoundary } from "@ui/components"; import { Button } from "@ui/components/discord/Redesign"; import { showToast } from "@ui/toasts"; type PostType = "Plugin" | "Theme"; -// const ForumPostLongPressActionSheet = findByName("ForumPostLongPressActionSheet", false); -// const { ActionSheetRow } = findByProps("ActionSheetRow"); -const { useFirstForumPostMessage } = findByProps("useFirstForumPostMessage"); -const forumReactions = findByProps("MostCommonForumPostReaction"); +// const ForumPostLongPressActionSheet = findByNameProxy("ForumPostLongPressActionSheet", false); +// const { ActionSheetRow } = findByPropsProxy("ActionSheetRow"); +const { useFirstForumPostMessage } = findByPropsProxy("useFirstForumPostMessage"); +const forumReactions = findByPropsProxy("MostCommonForumPostReaction"); const postMap = { Plugin: { diff --git a/src/core/plugins/quickInstall/url.tsx b/src/core/plugins/quickInstall/url.tsx index 24a63c4..b1a48c4 100644 --- a/src/core/plugins/quickInstall/url.tsx +++ b/src/core/plugins/quickInstall/url.tsx @@ -8,15 +8,15 @@ import { PROXY_PREFIX, THEMES_CHANNEL_ID } from "@lib/utils/constants"; import { channels, url } from "@metro/common"; import { byMutableProp } from "@metro/filters"; import { findExports } from "@metro/finders"; -import { findByProps } from "@metro/utils"; +import { findByPropsProxy } from "@metro/utils"; import { showConfirmationAlert } from "@ui/alerts"; import { showToast } from "@ui/toasts"; const showSimpleActionSheet = findExports(byMutableProp("showSimpleActionSheet")); -const handleClick = findByProps("handleClick"); +const handleClick = findByPropsProxy("handleClick"); const { openURL } = url; const { getChannelId } = channels; -const { getChannel } = findByProps("getChannel"); +const { getChannel } = findByPropsProxy("getChannel"); function typeFromUrl(url: string) { if (url.startsWith(PROXY_PREFIX)) { diff --git a/src/core/ui/components/AddonPage.tsx b/src/core/ui/components/AddonPage.tsx index 8cd55e5..8bd2546 100644 --- a/src/core/ui/components/AddonPage.tsx +++ b/src/core/ui/components/AddonPage.tsx @@ -5,7 +5,7 @@ import { useProxy } from "@lib/api/storage"; import { settings } from "@lib/settings"; import { HTTP_REGEX_MULTI } from "@lib/utils/constants"; import { clipboard } from "@metro/common"; -import { findByProps } from "@metro/utils"; +import { findByPropsProxy } from "@metro/utils"; import { showInputAlert } from "@ui/alerts"; import { ErrorBoundary, Search } from "@ui/components"; import { HelpMessage } from "@ui/components/discord"; @@ -43,8 +43,8 @@ function getItemsByQuery["items"][string]>(ite }).map(r => r.obj); } -const reanimated = findByProps("useSharedValue"); -const { FloatingActionButton } = findByProps("FloatingActionButton"); +const reanimated = findByPropsProxy("useSharedValue"); +const { FloatingActionButton } = findByPropsProxy("FloatingActionButton"); export default function AddonPage({ floatingButtonText, fetchFunction, items, safeModeMessage, safeModeExtras, card: CardComponent, isRemoveMode, headerComponent, onFABPress }: AddonPageProps) { useProxy(items); diff --git a/src/core/ui/components/Card.tsx b/src/core/ui/components/Card.tsx index 0255a17..5394ac5 100644 --- a/src/core/ui/components/Card.tsx +++ b/src/core/ui/components/Card.tsx @@ -1,14 +1,14 @@ import { getAssetIDByName } from "@lib/api/assets"; import { ReactNative as RN } from "@metro/common"; -import { findByProps } from "@metro/utils"; +import { findByPropsProxy } from "@metro/utils"; import { semanticColors } from "@ui/color"; import { FormRow } from "@ui/components/discord/Forms"; import { FormCheckbox, FormSwitch, IconButton } from "@ui/components/discord/Redesign"; import { createStyles, TextStyleSheet } from "@ui/styles"; import { ImageBackground, View } from "react-native"; -const { hideActionSheet } = findByProps("openLazy", "hideActionSheet"); -const { showSimpleActionSheet } = findByProps("showSimpleActionSheet"); +const { hideActionSheet } = findByPropsProxy("openLazy", "hideActionSheet"); +const { showSimpleActionSheet } = findByPropsProxy("showSimpleActionSheet"); // TODO: These styles work weirdly. iOS has cramped text, Android with low DPI probably does too. Fix? const useStyles = createStyles({ diff --git a/src/core/ui/settings/pages/Developer.tsx b/src/core/ui/settings/pages/Developer.tsx index 8af5b84..8f9e8d1 100644 --- a/src/core/ui/settings/pages/Developer.tsx +++ b/src/core/ui/settings/pages/Developer.tsx @@ -7,7 +7,7 @@ import { useProxy } from "@lib/api/storage"; import { connectToDebugger } from "@lib/debug"; import { loaderConfig, settings } from "@lib/settings"; import { NavigationNative } from "@metro/common"; -import { findByProps } from "@metro/utils"; +import { findByPropsProxy } from "@metro/utils"; import { semanticColors } from "@ui/color"; import { ErrorBoundary } from "@ui/components"; import { FormText } from "@ui/components/discord/Forms"; @@ -15,8 +15,8 @@ import { Button, Stack, TableRow, TableRowGroup, TableSwitchRow, TextInput } fro import { createStyles, TextStyleSheet } from "@ui/styles"; import { ScrollView, StyleSheet } from "react-native"; -const { hideActionSheet } = findByProps("openLazy", "hideActionSheet"); -const { showSimpleActionSheet } = findByProps("showSimpleActionSheet"); +const { hideActionSheet } = findByPropsProxy("openLazy", "hideActionSheet"); +const { showSimpleActionSheet } = findByPropsProxy("showSimpleActionSheet"); const RDT_EMBED_LINK = "https://raw.githubusercontent.com/amsyarasyiq/rdt-embedder/main/dist.js"; diff --git a/src/core/ui/settings/pages/FontImporter.tsx b/src/core/ui/settings/pages/FontImporter.tsx index 8b97675..4816bc4 100644 --- a/src/core/ui/settings/pages/FontImporter.tsx +++ b/src/core/ui/settings/pages/FontImporter.tsx @@ -4,13 +4,13 @@ import { createProxy, useProxy } from "@lib/api/storage"; import { FontDefinition, saveFont, validateFont } from "@lib/managers/fonts"; import { getCurrentTheme } from "@lib/managers/themes"; import { safeFetch } from "@lib/utils"; -import { findByProps } from "@metro/utils"; +import { findByPropsProxy } from "@metro/utils"; import { ErrorBoundary } from "@ui/components"; import { ActionSheet, BottomSheetTitleHeader, Button, IconButton, Stack, TableRow, TableRowGroup, Text, TextInput, useNavigation } from "@ui/components/discord/Redesign"; import { useMemo, useRef, useState } from "react"; import { ScrollView, View } from "react-native"; -const actionSheet = findByProps("hideActionSheet"); +const actionSheet = findByPropsProxy("hideActionSheet"); function guessFontName(urls: string[]) { const fileNames = urls.map(url => { diff --git a/src/lib/api/patcher.ts b/src/lib/api/patcher.ts index 2fabfda..d370529 100644 --- a/src/lib/api/patcher.ts +++ b/src/lib/api/patcher.ts @@ -1,8 +1,26 @@ -export { - after, - before, - instead +import { getFindProxyInfo } from "@metro/proxy"; +import { + after as _after, + before as _before, + instead as _instead } from "spitroast"; +function shim any>(fn: T) { + return function shimmed(this: any, ...args: Parameters) { + const proxyInfo = getFindProxyInfo(args[1]); + if (proxyInfo && !proxyInfo.cache) { + return void proxyInfo.subscribe(exp => { + args[1] = exp; + fn.apply(this, args); + }); + } + return fn.apply(this, args); + }; +} + +export const after = shim(_after); +export const before = shim(_before); +export const instead = shim(_instead); + /** @internal */ -export * as default from "spitroast"; +export default { after, before, instead }; diff --git a/src/lib/managers/themes.ts b/src/lib/managers/themes.ts index d476c31..5001132 100644 --- a/src/lib/managers/themes.ts +++ b/src/lib/managers/themes.ts @@ -12,7 +12,7 @@ import { Author } from "@lib/utils/types"; import { chroma } from "@metro/common"; import { byMutableProp } from "@metro/filters"; import { findExports } from "@metro/finders"; -import { findByName, findByProps, findByStoreName } from "@metro/utils"; +import { findByNameProxy, findByPropsProxy, findByStoreName } from "@metro/utils"; import { ImageBackground, Platform, processColor } from "react-native"; export interface ThemeData { @@ -41,12 +41,12 @@ export interface Theme { //! As of 173.10, early-finding this does not work. // Somehow, this is late enough, though? -export const color = findByProps("SemanticColor"); +export const color = findByPropsProxy("SemanticColor"); -const appearanceManager = findByProps("updateTheme"); -const mmkvStorage = findByProps("storage")?.parseResolve ? findByProps("storage") : findByProps("impl").impl; +const appearanceManager = findByPropsProxy("updateTheme"); +const mmkvStorage = findByPropsProxy("storage")?.parseResolve ? findByPropsProxy("storage") : findByPropsProxy("impl").impl; const ThemeStore = findByStoreName("ThemeStore"); -const formDividerModule = findByProps("DIVIDER_COLORS"); +const formDividerModule = findByPropsProxy("DIVIDER_COLORS"); export const themes = wrapSync(createStorage>(createMMKVBackend("VENDETTA_THEMES"))); @@ -75,9 +75,9 @@ async function writeTheme(theme: Theme | {}) { * @internal */ export function patchChatBackground() { - const MessagesWrapperConnected = findByName("MessagesWrapperConnected", false); + const MessagesWrapperConnected = findByNameProxy("MessagesWrapperConnected", false); if (!MessagesWrapperConnected) return; - const { MessagesWrapper } = findByProps("MessagesWrapper"); + const { MessagesWrapper } = findByPropsProxy("MessagesWrapper"); if (!MessagesWrapper) return; const patches = [ diff --git a/src/lib/ui/alerts.ts b/src/lib/ui/alerts.ts index c43e7fd..52b4dca 100644 --- a/src/lib/ui/alerts.ts +++ b/src/lib/ui/alerts.ts @@ -1,8 +1,8 @@ import { ButtonColors } from "@lib/utils/types"; -import { findByProps } from "@metro/utils"; +import { findByPropsProxy } from "@metro/utils"; import InputAlert, { InputAlertProps } from "@ui/components/InputAlert"; -const Alerts = findByProps("openLazy", "close"); +const Alerts = findByPropsProxy("openLazy", "close"); interface InternalConfirmationAlertOptions extends Omit { content?: ConfirmationAlertOptions["content"]; diff --git a/src/lib/ui/color.ts b/src/lib/ui/color.ts index e503af3..e9cfaf2 100644 --- a/src/lib/ui/color.ts +++ b/src/lib/ui/color.ts @@ -8,7 +8,7 @@ import { findByStoreName } from "@metro/utils"; //* 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"); +// const colorModule = findByPropsProxy("colors", "meta"); // ? SemanticColor and default.colors are effectively ThemeColorMap export const semanticColors = (color?.default?.colors ?? constants?.ThemeColorMap) as Record; diff --git a/src/lib/ui/components/ContextMenu.tsx b/src/lib/ui/components/ContextMenu.tsx index bde7ece..c27f985 100644 --- a/src/lib/ui/components/ContextMenu.tsx +++ b/src/lib/ui/components/ContextMenu.tsx @@ -1,9 +1,9 @@ import { instead } from "@lib/api/patcher"; import { OptionalKeys } from "@lib/utils/types"; -import { findByProps } from "@metro/utils"; +import { findByPropsProxy } from "@metro/utils"; import { AccessibilityInfo, Platform, View } from "react-native"; -const { ContextMenu: _ContextMenu } = findByProps("ContextMenu"); +const { ContextMenu: _ContextMenu } = findByPropsProxy("ContextMenu"); type ContextMenuItem = { label: string, diff --git a/src/lib/ui/components/InputAlert.tsx b/src/lib/ui/components/InputAlert.tsx index 8644d01..14fa7cb 100644 --- a/src/lib/ui/components/InputAlert.tsx +++ b/src/lib/ui/components/InputAlert.tsx @@ -1,9 +1,9 @@ import { ButtonColors } from "@lib/utils/types"; -import { findByProps } from "@metro/utils"; +import { findByPropsProxy } from "@metro/utils"; import { Alert } from "@ui/components/discord"; import { FormInput } from "@ui/components/discord/Forms"; -const Alerts = findByProps("openLazy", "close"); +const Alerts = findByPropsProxy("openLazy", "close"); export interface InputAlertProps { title?: string; diff --git a/src/lib/ui/components/discord/Forms.ts b/src/lib/ui/components/discord/Forms.ts index e21a598..ee2bb76 100644 --- a/src/lib/ui/components/discord/Forms.ts +++ b/src/lib/ui/components/discord/Forms.ts @@ -1,6 +1,6 @@ -import { findByProps } from "@metro/utils"; +import { findByPropsProxy } from "@metro/utils"; -export const Forms = findByProps("Form", "FormSection"); +export const Forms = findByPropsProxy("Form", "FormSection"); export const { Form, diff --git a/src/lib/ui/components/discord/Redesign.tsx b/src/lib/ui/components/discord/Redesign.tsx index 4cef9d4..9baec6f 100644 --- a/src/lib/ui/components/discord/Redesign.tsx +++ b/src/lib/ui/components/discord/Redesign.tsx @@ -1,5 +1,5 @@ import { findExports } from "@metro/finders"; -import { createFilterDefinition, findByProps } from "@metro/utils"; +import { createFilterDefinition, findByPropsProxy } from "@metro/utils"; import { RedesignModuleKeys } from "@ui/types"; import { useEffect } from "react"; import { View } from "react-native"; @@ -11,9 +11,9 @@ const bySingularProp = createFilterDefinition<[string]>( const findSingular = (prop: string) => findExports(bySingularProp(prop))?.[prop]; -export const Redesign = findByProps("TableRow") as Record; +export const Redesign = findByPropsProxy("TableRow") as Record; -export const CompatfulRedesign = findByProps("ActionSheetRow") as unknown as { +export const CompatfulRedesign = findByPropsProxy("ActionSheetRow") as unknown as { TextStyleSheet: typeof import("@ui/styles").TextStyleSheet; [key: string]: any; }; diff --git a/src/lib/ui/components/discord/index.ts b/src/lib/ui/components/discord/index.ts index b7c3a52..4a043cc 100644 --- a/src/lib/ui/components/discord/index.ts +++ b/src/lib/ui/components/discord/index.ts @@ -1,11 +1,11 @@ -import { findByDisplayName, findByName, findByProps } from "@metro/utils"; +import { findByDisplayName, findByNameProxy, findByPropsProxy } from "@metro/utils"; export * as Forms from "./Forms"; export * as Redesign from "./Redesign"; // Discord 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"); +export const Button = findByPropsProxy("Looks", "Colors", "Sizes") as React.ComponentType & { Looks: any, Colors: any, Sizes: any; }; +export const HelpMessage = findByNameProxy("HelpMessage"); // React Native's included SafeAreaView only adds padding on iOS. -export const SafeAreaView = findByProps("useSafeAreaInsets").SafeAreaView as any; +export const SafeAreaView = findByPropsProxy("useSafeAreaInsets").SafeAreaView as any; diff --git a/src/lib/ui/safeMode.tsx b/src/lib/ui/safeMode.tsx index 279725d..901db51 100644 --- a/src/lib/ui/safeMode.tsx +++ b/src/lib/ui/safeMode.tsx @@ -4,17 +4,17 @@ import { after } from "@lib/api/patcher"; import { toggleSafeMode } from "@lib/debug"; import { settings } from "@lib/settings"; import { ButtonColors } from "@lib/utils/types"; -import { findByName, findByProps } from "@metro/utils"; +import { findByNameProxy, findByPropsProxy } from "@metro/utils"; import { semanticColors } from "@ui/color"; import { Codeblock, ErrorBoundary as _ErrorBoundary } from "@ui/components"; import { Button, SafeAreaView } from "@ui/components/discord"; import { createThemedStyleSheet, TextStyleSheet } from "@ui/styles"; import { Text, View } from "react-native"; -const ErrorBoundary = findByName("ErrorBoundary"); +const ErrorBoundary = findByNameProxy("ErrorBoundary"); // Let's just pray they have this. -const { BadgableTabBar } = findByProps("BadgableTabBar"); +const { BadgableTabBar } = findByPropsProxy("BadgableTabBar"); const styles = createThemedStyleSheet({ container: { diff --git a/src/lib/ui/settings/patches/panel.tsx b/src/lib/ui/settings/patches/panel.tsx index 35bdaa6..5d221d3 100644 --- a/src/lib/ui/settings/patches/panel.tsx +++ b/src/lib/ui/settings/patches/panel.tsx @@ -1,9 +1,9 @@ +import { after } from "@lib/api/patcher"; import { findInReactTree } from "@lib/utils"; import { i18n, NavigationNative } from "@metro/common"; -import { findByName } from "@metro/utils"; +import { findByNameProxy } from "@metro/utils"; import { FormIcon, FormRow, FormSection } from "@ui/components/discord/Forms"; import { registeredSections } from "@ui/settings"; -import { after } from "spitroast"; import { CustomPageRenderer, wrapOnPress } from "./shared"; @@ -28,7 +28,7 @@ function SettingsSection() { } export function patchPanelUI(unpatches: (() => void | boolean)[]) { - unpatches.push(after("default", findByName("getScreens", false), (_a, screens) => ({ + unpatches.push(after("default", findByNameProxy("getScreens", false), (_a, screens) => ({ ...screens, VendettaCustomPage: { title: "Bnuuy", @@ -36,7 +36,7 @@ export function patchPanelUI(unpatches: (() => void | boolean)[]) { } }))); - const unpatch = after("default", findByName("UserSettingsOverviewWrapper", false), (_a, ret) => { + const unpatch = after("default", findByNameProxy("UserSettingsOverviewWrapper", false), (_a, ret) => { const UserSettingsOverview = findInReactTree(ret.props.children, n => n.type?.name === "UserSettingsOverview"); unpatches.push(after("renderSupportAndAcknowledgements", UserSettingsOverview.type.prototype, (_args, { props: { children } }) => { diff --git a/src/lib/ui/settings/patches/shared.tsx b/src/lib/ui/settings/patches/shared.tsx index b3de128..575e0b9 100644 --- a/src/lib/ui/settings/patches/shared.tsx +++ b/src/lib/ui/settings/patches/shared.tsx @@ -1,9 +1,9 @@ import { NavigationNative } from "@metro/common"; -import { findByProps } from "@metro/utils"; +import { findByPropsProxy } from "@metro/utils"; import { ErrorBoundary } from "@ui/components"; import { RowConfig } from "@ui/settings"; -const tabsNavigationRef = findByProps("getRootNavigationRef"); +const tabsNavigationRef = findByPropsProxy("getRootNavigationRef"); export const CustomPageRenderer = React.memo(() => { const navigation = NavigationNative.useNavigation(); diff --git a/src/lib/ui/settings/patches/tabs.tsx b/src/lib/ui/settings/patches/tabs.tsx index b967e52..6fcc5b3 100644 --- a/src/lib/ui/settings/patches/tabs.tsx +++ b/src/lib/ui/settings/patches/tabs.tsx @@ -1,13 +1,13 @@ +import { after } from "@lib/api/patcher"; import { findInReactTree } from "@lib/utils"; import { i18n } from "@metro/common"; -import { findByName, findByProps } from "@metro/utils"; +import { findByNameProxy, findByPropsProxy } from "@metro/utils"; import { registeredSections } from "@ui/settings"; -import { after } from "spitroast"; import { CustomPageRenderer, wrapOnPress } from "./shared"; -const settingConstants = findByProps("SETTING_RENDERER_CONFIG"); -const SettingsOverviewScreen = findByName("SettingsOverviewScreen", false); +const settingConstants = findByPropsProxy("SETTING_RENDERER_CONFIG"); +const SettingsOverviewScreen = findByNameProxy("SettingsOverviewScreen", false); function useIsFirstRender() { let firstRender = false; diff --git a/src/lib/ui/styles.ts b/src/lib/ui/styles.ts index 4af4cfc..b66b53f 100644 --- a/src/lib/ui/styles.ts +++ b/src/lib/ui/styles.ts @@ -1,4 +1,4 @@ -import { findByProps } from "@metro/utils"; +import { findByPropsProxy } from "@metro/utils"; import { isSemanticColor, resolveSemanticColor } from "@ui/color"; import { CompatfulRedesign } from "@ui/components/discord/Redesign"; import { DiscordTextStyles } from "@ui/types"; @@ -6,7 +6,7 @@ import { ImageStyle, StyleSheet, TextStyle, ViewStyle } from "react-native"; type NamedStyles = { [P in keyof T]: ViewStyle | TextStyle | ImageStyle }; -export const { TextStyleSheet } = findByProps("TextStyleSheet") as unknown as { +export const { TextStyleSheet } = findByPropsProxy("TextStyleSheet") as unknown as { TextStyleSheet: { [key in DiscordTextStyles]: TextStyle; }; }; diff --git a/src/lib/ui/toasts.ts b/src/lib/ui/toasts.ts index 0bfc51d..232b7cf 100644 --- a/src/lib/ui/toasts.ts +++ b/src/lib/ui/toasts.ts @@ -1,10 +1,10 @@ import { Strings } from "@core/i18n"; import { getAssetIDByName } from "@lib/api/assets"; import { toasts } from "@metro/common"; -import { findByProps } from "@metro/utils"; +import { findByPropsProxy } from "@metro/utils"; import { Platform } from "react-native"; -const { uuid4 } = findByProps("uuid4"); +const { uuid4 } = findByPropsProxy("uuid4"); export const showToast = (content: string, asset?: number) => toasts.open({ // ? In build 182205/44707, Discord changed their toasts, source is no longer used, rather icon, and a key is needed. diff --git a/src/lib/utils/lazy.ts b/src/lib/utils/lazy.ts index 4f76b76..fe6852b 100644 --- a/src/lib/utils/lazy.ts +++ b/src/lib/utils/lazy.ts @@ -35,10 +35,10 @@ Object.getOwnPropertyNames(Reflect).forEach(fnName => { * @returns A proxy that will call the factory function only when needed * @example const ChannelStore = proxyLazy(() => findByProps("getChannelId")); */ -export function proxyLazy(factory: () => T, fallback?: any): T { - const dummy = function () { } as any; +export function proxyLazy(factory: () => T, asFunction = true): T { + const dummy = asFunction ? function () { } as any : {}; dummy[factorySymbol] = () => { - return (dummy[cacheSymbol] ??= factory()) ?? fallback; + return dummy[cacheSymbol] ??= factory(); }; return new Proxy(dummy, lazyHandler) as any; diff --git a/src/lib/utils/logger.ts b/src/lib/utils/logger.ts index 06c61a4..387058c 100644 --- a/src/lib/utils/logger.ts +++ b/src/lib/utils/logger.ts @@ -1,4 +1,4 @@ -import { findByName } from "@metro/utils"; +import { findByNameProxy } from "@metro/utils"; export type LoggerFunction = (...messages: any[]) => void; export interface Logger { @@ -11,5 +11,5 @@ export interface Logger { verbose: LoggerFunction; } -export const logModule = findByName("Logger"); +export const logModule = findByNameProxy("Logger"); export const logger: Logger = new logModule("Bunny"); diff --git a/src/lib/utils/throttle.ts b/src/lib/utils/throttle.ts index d40ad8f..f0fc242 100644 --- a/src/lib/utils/throttle.ts +++ b/src/lib/utils/throttle.ts @@ -10,11 +10,20 @@ export function throttle any>( time: number = 200 ): T { let active = false; + let calledWhileActive = false; return function (this: ReturnType, ...args: Parameters) { if (!active) { active = true; funct.apply(this, args); - setTimeout(() => { active = false; }, time); + setTimeout(() => { + active = false; + if (calledWhileActive) { + calledWhileActive = false; + funct.apply(this, args); + } + }, time); + } else { + calledWhileActive = true; } } as T; } diff --git a/src/metro/caches.ts b/src/metro/caches.ts index 11cfac5..1dc59ae 100644 --- a/src/metro/caches.ts +++ b/src/metro/caches.ts @@ -1,7 +1,7 @@ import { ClientInfoManager, MMKVManager } from "@lib/api/native/modules"; import { throttle } from "@lib/utils/throttle"; -const CACHE_VERSION = 12; +const CACHE_VERSION = 18; const BUNNY_METRO_CACHE_KEY = `__bunny_metro_cache_key_v${CACHE_VERSION}__`; export enum ExportsFlags { @@ -17,27 +17,18 @@ type ModulesMap = { [id: number]: number; }; -interface MetroCacheStore { - _v: typeof CACHE_VERSION; - _buildNumber: number; - exportsIndex: Record; - findIndex: Record; - polyfillIndex: Record; - assetsIndex: Record; -} - -let _metroCache = null as unknown as MetroCacheStore; +let _metroCache = null as unknown as ReturnType; export const getMetroCache = window.__getMetroCache = () => _metroCache; function buildInitCache() { - _metroCache = { + const cache = { _v: CACHE_VERSION, - _buildNumber: ClientInfoManager.Build, - exportsIndex: {}, - findIndex: {}, - polyfillIndex: {}, - assetsIndex: {} + _buildNumber: ClientInfoManager.Build as number, + exportsIndex: {} as Record, + findIndex: {} as Record, + polyfillIndex: {} as Record, + assetsIndex: {} as Record } as const; // Make sure all assets are cached. Delay by a second @@ -47,6 +38,9 @@ function buildInitCache() { require("@metro/modules").requireModule(id); } }, 1000); + + _metroCache = cache; + return cache; } export async function initMetroCache() { @@ -64,7 +58,9 @@ export async function initMetroCache() { } } -const saveCache = throttle(() => MMKVManager.setItem(BUNNY_METRO_CACHE_KEY, JSON.stringify(_metroCache))); +const saveCache = throttle(() => { + MMKVManager.setItem(BUNNY_METRO_CACHE_KEY, JSON.stringify(_metroCache)); +}); function extractExportsFlags(moduleExports: any) { if (!moduleExports) return 0; @@ -80,13 +76,17 @@ function extractExportsFlags(moduleExports: any) { } export function indexExportsFlags(moduleId: number, moduleExports: any) { - _metroCache.exportsIndex[moduleId] ??= moduleExports.default && moduleExports.__esModule + const flags: [number, number] = moduleExports.default && moduleExports.__esModule ? [extractExportsFlags(moduleExports), extractExportsFlags(moduleExports.default)] : [extractExportsFlags(moduleExports), 0]; + + if (flags[0] !== ExportsFlags.EXISTS && flags[1] !== ExportsFlags.EXISTS) { + _metroCache.exportsIndex[moduleId] = flags; + } } export function indexBlacklistFlag(id: number) { - _metroCache.exportsIndex[id] ??= [ExportsFlags.BLACKLISTED, ExportsFlags.BLACKLISTED]; + _metroCache.exportsIndex[id] ??= [ExportsFlags.BLACKLISTED, 0]; } export function getCacherForUniq(uniq: string, allFind: boolean) { @@ -95,6 +95,7 @@ export function getCacherForUniq(uniq: string, allFind: boolean) { return { cacheId(moduleId: number, exports: any) { indexObject ??= _metroCache.findIndex[uniq] ??= {}; + // TODO: A log here perhaps indexObject[moduleId] ??= extractExportsFlags(exports); saveCache(); diff --git a/src/metro/common/index.ts b/src/metro/common/index.ts index 521e3eb..7a32e0f 100644 --- a/src/metro/common/index.ts +++ b/src/metro/common/index.ts @@ -1,38 +1,38 @@ import { Dispatcher } from "@metro/types"; -import { findByFilePath, findByProps } from "@metro/utils"; +import { findByFilePath, findByPropsProxy } from "@metro/utils"; // 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 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 constants = findByPropsProxy("Fonts", "Permissions"); +export const channels = findByPropsProxy("getVoiceChannelId"); +export const i18n = findByPropsProxy("Messages"); +export const url = findByPropsProxy("openURL", "openDeeplink"); +export const clipboard = findByPropsProxy("setString", "getString", "hasString") as typeof import("@react-native-clipboard/clipboard").default; +export const assets = findByPropsProxy("registerAsset"); +export const invites = findByPropsProxy("acceptInviteAndTransitionToInviteChannel"); +export const commands = findByPropsProxy("getBuiltInCommands"); +export const navigation = findByPropsProxy("pushLazy"); export const toasts = findByFilePath("modules/toast/native/ToastActionCreators.tsx").default; -export const messageUtil = findByProps("sendBotMessage"); -export const navigationStack = findByProps("createStackNavigator"); -export const NavigationNative = findByProps("NavigationContainer"); +export const messageUtil = findByPropsProxy("sendBotMessage"); +export const navigationStack = findByPropsProxy("createStackNavigator"); +export const NavigationNative = findByPropsProxy("NavigationContainer"); // Flux -export const Flux = findByProps("connectStores"); -export const FluxDispatcher = findByProps("_interceptors") as Dispatcher; +export const Flux = findByPropsProxy("connectStores"); +export const FluxDispatcher = findByPropsProxy("_interceptors") as Dispatcher; // React -export const React = window.React = findByProps("createElement") as typeof import("react"); -export const ReactNative = window.ReactNative = findByProps("AppRegistry") as typeof import("react-native"); +export const React = window.React = findByPropsProxy("createElement") as typeof import("react"); +export const ReactNative = window.ReactNative = findByPropsProxy("AppRegistry") as typeof import("react-native"); // Moment -export const moment = findByProps("isMoment") as typeof import("moment"); +export const moment = findByPropsProxy("isMoment") as typeof import("moment"); // chroma.js -export const chroma = findByProps("brewer") as typeof import("chroma-js"); +export const chroma = findByPropsProxy("brewer") as typeof import("chroma-js"); // Lodash -export const lodash = findByProps("forEachRight") as typeof import("lodash"); +export const lodash = findByPropsProxy("forEachRight") as typeof import("lodash"); // The node:util polyfill for RN // TODO: Find types for this -export const util = findByProps("inspect", "isNullOrUndefined"); +export const util = findByPropsProxy("inspect", "isNullOrUndefined"); diff --git a/src/metro/finders.ts b/src/metro/finders.ts index 76d8d54..9b0a299 100644 --- a/src/metro/finders.ts +++ b/src/metro/finders.ts @@ -2,7 +2,11 @@ import { getCacherForUniq } from "./caches"; import { getModules, requireModule } from "./modules"; import { FilterFn } from "./types"; -function testExports(moduleExports: any, moduleId: number, filter: FilterFn) { +export function filterExports( + moduleExports: any, + moduleId: number, filter: + FilterFn +): [any, boolean] | [void, false] { if (moduleExports.default && moduleExports.__esModule && filter(moduleExports.default, moduleId, true)) { return [filter.raw ? moduleExports : moduleExports.default, !filter.raw]; } @@ -11,7 +15,7 @@ function testExports(moduleExports: any, moduleId: number, return [moduleExports, false]; } - return []; + return [undefined, false]; } /** @@ -22,7 +26,7 @@ export function findModule(filter: FilterFn) { const { cacheId, finish } = getCacherForUniq(filter.uniq, false); for (const [id, moduleExports] of getModules(filter.uniq, false)) { - const [testedExports, defaultExp] = testExports(moduleExports, id, filter); + const [testedExports, defaultExp] = filterExports(moduleExports, id, filter); if (testedExports !== undefined) { cacheId(id, testedExports); return [id, defaultExp]; @@ -60,7 +64,7 @@ export function findAllModule(filter: FilterFn) { const foundExports: [id: number, defaultExp: boolean][] = []; for (const [id, moduleExports] of getModules(filter.uniq, true)) { - const [testedExports, defaultExp] = testExports(moduleExports, id, filter); + const [testedExports, defaultExp] = filterExports(moduleExports, id, filter); if (testedExports !== undefined) { foundExports.push([id, defaultExp]); cacheId(id, testedExports); diff --git a/src/metro/modules.ts b/src/metro/modules.ts index 8eb7721..5e0e9ef 100644 --- a/src/metro/modules.ts +++ b/src/metro/modules.ts @@ -5,6 +5,8 @@ import { before, instead } from "spitroast"; export const metroModules: Metro.ModuleList = window.modules; const metroRequire: Metro.Require = window.__r; +// eslint-disable-next-line func-call-spacing +const moduleSubscriptions = new Map void>>(); const blacklistedIds = new Set(); const noopHandler = () => undefined; const functionToString = Function.prototype.toString; @@ -61,7 +63,10 @@ function blacklistModule(id: string) { } function isBadExports(exports: any) { - return !exports || exports === window || exports[""] === null; + return !exports + || exports === window + || exports[""] === null + || (exports.__proto__ === Object.prototype && Reflect.ownKeys(exports).length === 0); } function onModuleRequire(moduleExports: any, id: Metro.ModuleID) { @@ -135,12 +140,37 @@ function onModuleRequire(moduleExports: any, id: Metro.ModuleID) { moduleExports.locale(origLocale); }); } + + moduleSubscriptions.get(Number(id))?.forEach(s => s()); } export function getImportingModuleId() { return _importingModuleId; } +export function subscribeModule(id: number, cb: () => void): () => void { + const subs = moduleSubscriptions.get(id) ?? new Set(); + + const deleteIfEmpty = () => { + subs.delete(cb); + if (moduleSubscriptions.get(id)?.size === 0) { + moduleSubscriptions.delete(id); + } + }; + const wrappedCb = () => { + cb(); + deleteIfEmpty(); + }; + + subs.add(wrappedCb); + moduleSubscriptions.set(id, subs); + + return () => { + subs.delete(wrappedCb); + deleteIfEmpty(); + }; +} + export function requireModule(id: Metro.ModuleID) { if (!metroModules[0]?.isInitialized) metroRequire(0); if (blacklistedIds.has(String(id))) return undefined; diff --git a/src/metro/proxy.ts b/src/metro/proxy.ts new file mode 100644 index 0000000..ad698e9 --- /dev/null +++ b/src/metro/proxy.ts @@ -0,0 +1,69 @@ +import { proxyLazy } from "@lib/utils/lazy"; + +import { getMetroCache } from "./caches"; +import { filterExports, findExports } from "./finders"; +import { requireModule, subscribeModule } from "./modules"; +import type { FilterFn } from "./types"; + +// TODO: "Find info" sounds too meh +const findInfoSym = Symbol.for("bunny.metro.findInfo"); + +const proxyInfoMap = new WeakMap<{}, FindProxyInfo>(); + +interface FindProxyInfo { + filter: FilterFn; + indexed: boolean; + moduleId: number | undefined; + subscribe(cb: (exports: any) => void): () => void; + unproxy(): any; + get cache(): any; +} + +function getIndexedSingleFind(filter: FilterFn) { + const id = Object.keys(getMetroCache().findIndex[filter.uniq]!).filter(k => k !== "_")[0]; + return id ? Number(id) : void 0; +} + +function subscribeModuleForFind(proxy: any, callback: (exports: any) => void) { + const info = getFindProxyInfo(proxy); + if (!info) throw new Error("Subscribing a module for non-proxy-find"); + if (!info.indexed) throw new Error("Attempting to subscribe to a non-indexed find"); + + return subscribeModule(info.moduleId!, () => { + const [exp] = filterExports( + requireModule(info.moduleId!), + info.moduleId!, + info.filter + ); + callback(exp); + }); +} + +export function getFindProxyInfo(proxy: any): FindProxyInfo | void { + return proxyInfoMap.get(proxy) as unknown as FindProxyInfo; +} + +export function createFindProxy(filter: FilterFn) { + let cache: any = undefined; + + const moduleId = getIndexedSingleFind(filter); + const info: FindProxyInfo = { + filter, + indexed: !!moduleId, + moduleId, + subscribe(cb: (exports: any) => void) { + return subscribeModuleForFind(proxy, cb); + }, + get cache() { + return cache; + }, + unproxy() { + return cache ??= findExports(filter); + } + }; + + const proxy = proxyLazy(() => cache ??= findExports(filter)); + proxyInfoMap.set(proxy, info as FindProxyInfo); + + return proxy; +} diff --git a/src/metro/utils.ts b/src/metro/utils.ts index 52d0e17..d760546 100644 --- a/src/metro/utils.ts +++ b/src/metro/utils.ts @@ -1,7 +1,7 @@ -import { proxyLazy } from "@lib/utils/lazy"; import { byDisplayName, byFilePath, byName, byProps, byStoreName, byTypeName } from "./filters"; import { findAllExports, findExports } from "./finders"; +import { createFindProxy } from "./proxy"; import { FilterCheckDef, FilterDefinition, ModuleExports } from "./types"; export function createFilterDefinition( @@ -43,25 +43,24 @@ export function createSimpleFilter( } export const findByProps = (...props: string[]) => findExports(byProps(...props)); -export const findByPropsLazy = (...props: string[]) => proxyLazy(() => findByProps(...props)); +export const findByPropsProxy = (...props: string[]) => createFindProxy(byProps(...props)); export const findByPropsAll = (...props: string[]) => findAllExports(byProps(...props)); export const findByName = (name: string, expDefault = true) => findExports(expDefault ? byName(name) : byName.byRaw(name)); -export const findByNameLazy = (name: string, expDefault = true) => proxyLazy(() => findByName(name, expDefault)); +export const findByNameProxy = (name: string, expDefault = true) => createFindProxy(expDefault ? byName(name) : byName.byRaw(name)); export const findByNameAll = (name: string, expDefault = true) => findAllExports(expDefault ? byName(name) : byName.byRaw(name)); export const findByDisplayName = (name: string, expDefault = true) => findExports(expDefault ? byDisplayName(name) : byDisplayName.byRaw(name)); -export const findByDisplayNameLazy = (name: string, expDefault = true) => proxyLazy(() => findByDisplayName(name, expDefault)); +export const findByDisplayNameProxy = (name: string, expDefault = true) => createFindProxy(expDefault ? byDisplayName(name) : byDisplayName.byRaw(name)); export const findByDisplayNameAll = (name: string, expDefault = true) => findAllExports(expDefault ? byDisplayName(name) : byDisplayName.byRaw(name)); export const findByTypeName = (name: string, expDefault = true) => findExports(expDefault ? byTypeName(name) : byTypeName.byRaw(name)); -export const findByTypeNameLazy = (name: string, expDefault = true) => proxyLazy(() => findByTypeName(name, expDefault)); +export const findByTypeNameProxy = (name: string, expDefault = true) => createFindProxy(expDefault ? byTypeName(name) : byTypeName.byRaw(name)); export const findByTypeNameAll = (name: string, expDefault = true) => findAllExports(expDefault ? byTypeName(name) : byTypeName.byRaw(name)); export const findByStoreName = (name: string) => findExports(byStoreName(name)); -export const findByStoreNameLazy = (name: string) => proxyLazy(() => findByStoreName(name)); +export const findByStoreNameProxy = (name: string) => createFindProxy(byStoreName(name)); export const findByFilePath = (path: string) => findExports(byFilePath(path)); -export const findByFilePathLazy = (path: string) => proxyLazy(() => findByFilePath(path)); - +export const findByFilePathProxy = (path: string) => createFindProxy(byFilePath(path));