From 5344f0017a33064ea3ed45a15a2b5f57aa922091 Mon Sep 17 00:00:00 2001 From: Beef Date: Thu, 13 Apr 2023 18:42:14 +0000 Subject: [PATCH] [SafeMode] Initial implementation (#61) * [UI > Components] Add Discord's button * [SafeMode] Initial work, basic ErrorBoundary patch * [SafeMode] Custom error boundary (#57) * [SafeMode] Add mostly complete custom error boundary * [SafeMode] Use Button from @ui/components in error boundary * [SafeMode] Wrap the error boundary in our own error boundary * [SafeMode > ErrorBoundary] Code-style changes --------- Co-authored-by: Beef * [TS] Add basic type for Discord's button * [UI] Move Codeblock to components, and use it * [UI > Settings] Allow disabling the ErrorBoundary in CustomPage * [UI > Settings] Move the ErrorBoundary triggers to Developer * [TS] Add Codeblock to types * [TS] Use ButtonColors in Button type * [SafeMode > ErrorBoundary] Rework * [UI] Add HelpMessage to components * [SafeMode] Proper implementation * [Global] SafeMode is optional (#59) * [UI > Developer] Restore the balance * [SafeMode > ErrorBoundary] Optimise for tablet UI * [SafeMode] Last-minute fixes --------- Co-authored-by: Jack <30497388+FieryFlames@users.noreply.github.com> Co-authored-by: Jack Matthews --- src/def.d.ts | 15 ++- src/index.ts | 2 + src/lib/debug.ts | 21 +++- src/lib/plugins.ts | 34 +++--- src/lib/windowObject.ts | 2 +- src/ui/components/Codeblock.tsx | 30 +++++ src/ui/components/ErrorBoundary.tsx | 16 +-- src/ui/components/index.ts | 5 +- src/ui/safeMode.tsx | 133 +++++++++++++++++++++++ src/ui/settings/components/Card.tsx | 6 +- src/ui/settings/components/ThemeCard.tsx | 5 +- src/ui/settings/index.tsx | 12 +- src/ui/settings/pages/Developer.tsx | 23 ++++ src/ui/settings/pages/General.tsx | 9 +- src/ui/settings/pages/Plugins.tsx | 6 + src/ui/settings/pages/Themes.tsx | 21 +++- 16 files changed, 293 insertions(+), 47 deletions(-) create mode 100644 src/ui/components/Codeblock.tsx create mode 100644 src/ui/safeMode.tsx diff --git a/src/def.d.ts b/src/def.d.ts index bfe7838..1a9b7cd 100644 --- a/src/def.d.ts +++ b/src/def.d.ts @@ -18,7 +18,13 @@ interface SummaryProps { } interface ErrorBoundaryProps { - children: JSX.Element | JSX.Element[], + children: JSX.Element | JSX.Element[]; +} + +interface CodeblockProps { + selectable?: boolean; + style?: _RN.TextStyle; + children?: string; } // Helper types for API functions @@ -125,6 +131,10 @@ interface Theme { interface Settings { debuggerUrl: string; developerSettings: boolean; + safeMode?: { + enabled: boolean; + currentThemeId?: string; + }; } interface ApplicationCommand { @@ -404,9 +414,12 @@ interface VendettaObject { General: PropIntellisense<"Button" | "Text" | "View">; Search: _React.ComponentType; Alert: _React.ComponentType; + Button: _React.ComponentType & { Looks: any, Colors: ButtonColors, Sizes: any }; + HelpMessage: _React.ComponentType; // Vendetta Summary: _React.ComponentType; ErrorBoundary: _React.ComponentType; + Codeblock: _React.ComponentType; } toasts: { showToast: (content: string, asset: number) => void; diff --git a/src/index.ts b/src/index.ts index c1e0e5c..44b07d5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,6 +3,7 @@ import { patchCommands } from "@lib/commands"; import { initPlugins } from "@lib/plugins"; 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"; @@ -15,6 +16,7 @@ export default async () => { patchAssets(), patchCommands(), initFixes(), + initSafeMode(), initSettings(), initQuickInstall(), ]); diff --git a/src/lib/debug.ts b/src/lib/debug.ts index 07acd6a..2d7d24a 100644 --- a/src/lib/debug.ts +++ b/src/lib/debug.ts @@ -1,12 +1,27 @@ import { RNConstants } from "@types"; import { ReactNative as RN } from "@metro/common"; import { after } from "@lib/patcher"; -import { ClientInfoManager, DeviceManager } from "@lib/native"; +import { ClientInfoManager, DeviceManager, BundleUpdaterManager } from "@lib/native"; +import { getCurrentTheme, selectTheme } from "@lib/themes"; 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 async function toggleSafeMode() { + settings.safeMode = { ...settings.safeMode, enabled: !settings.safeMode?.enabled } + if (window.__vendetta_loader?.features.themes) { + if (getCurrentTheme()?.id) settings.safeMode!.currentThemeId = getCurrentTheme()!.id; + if (settings.safeMode?.enabled) { + await selectTheme("default"); + } else if (settings.safeMode?.currentThemeId) { + await selectTheme(settings.safeMode?.currentThemeId); + } + } + setTimeout(BundleUpdaterManager.reload); +} + export function connectToDebugger(url: string) { if (socket !== undefined && socket.readyState !== WebSocket.CLOSED) socket.close(); @@ -16,7 +31,7 @@ export function connectToDebugger(url: string) { } socket = new WebSocket(`ws://${url}`); - + socket.addEventListener("open", () => showToast("Connected to debugger.", getAssetIDByName("Check"))); socket.addEventListener("message", (message: any) => { try { @@ -32,7 +47,7 @@ export function connectToDebugger(url: string) { }); } -export function patchLogHook() { +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]); diff --git a/src/lib/plugins.ts b/src/lib/plugins.ts index b1b688c..04edce7 100644 --- a/src/lib/plugins.ts +++ b/src/lib/plugins.ts @@ -1,6 +1,7 @@ import { Indexable, PluginManifest, Plugin } from "@types"; import { awaitSyncWrapper, createMMKVBackend, createStorage, wrapSync } from "@lib/storage"; import { MMKVManager } from "@lib/native"; +import settings from "@lib/settings"; import logger, { logModule } from "@lib/logger"; import safeFetch from "@utils/safeFetch"; @@ -76,9 +77,11 @@ export async function startPlugin(id: string) { if (!plugin) throw new Error("Attempted to start non-existent plugin"); try { - const pluginRet: EvaledPlugin = await evalPlugin(plugin); - loadedPlugins[id] = pluginRet; - pluginRet.onLoad?.(); + 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); @@ -99,15 +102,17 @@ export function stopPlugin(id: string, disable = true) { const plugin = plugins[id]; const pluginRet = loadedPlugins[id]; if (!plugin) throw new Error("Attempted to stop non-existent plugin"); - if (!pluginRet) throw new Error("Attempted to stop a non-started plugin"); - try { - pluginRet.onUnload?.(); - } catch(e) { - logger.error(`Plugin ${plugin.id} errored whilst unloading`, e); + if (!settings.safeMode?.enabled) { + try { + pluginRet?.onUnload?.(); + } catch(e) { + logger.error(`Plugin ${plugin.id} errored whilst unloading`, e); + } + + delete loadedPlugins[id]; } - delete loadedPlugins[id]; disable && (plugin.enabled = false); } @@ -120,13 +125,16 @@ export function removePlugin(id: string) { } export async function initPlugins() { + await awaitSyncWrapper(settings); await awaitSyncWrapper(plugins); const allIds = Object.keys(plugins); - // Loop over any plugin that is enabled, update it if allowed, then start it. - await Promise.allSettled(allIds.filter(pl => plugins[pl].enabled).map(async (pl) => (plugins[pl].update && await fetchPlugin(pl), 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)); + if (!settings.safeMode?.enabled) { + // Loop over any plugin that is enabled, update it if allowed, then start it. + await Promise.allSettled(allIds.filter(pl => plugins[pl].enabled).map(async (pl) => (plugins[pl].update && await fetchPlugin(pl), 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; } diff --git a/src/lib/windowObject.ts b/src/lib/windowObject.ts index 8fb6540..9a8245e 100644 --- a/src/lib/windowObject.ts +++ b/src/lib/windowObject.ts @@ -22,7 +22,7 @@ export default async (unloads: any[]): Promise => ({ metro: { ...metro, common: { ...common } }, constants, utils, - debug: utils.without(debug, "versionHash", "patchLogHook"), + debug: utils.without(debug, "versionHash", "patchLogHook", "toggleSafeMode"), ui: { components, toasts, diff --git a/src/ui/components/Codeblock.tsx b/src/ui/components/Codeblock.tsx new file mode 100644 index 0000000..d3e9540 --- /dev/null +++ b/src/ui/components/Codeblock.tsx @@ -0,0 +1,30 @@ +import { CodeblockProps } from "@types"; +import { ReactNative as RN, stylesheet, constants } from "@metro/common"; +import { semanticColors } from "@ui/color"; + +const styles = stylesheet.createThemedStyleSheet({ + codeBlock: { + fontFamily: constants.Fonts.CODE_SEMIBOLD, + fontSize: 12, + textAlignVertical: "center", + backgroundColor: semanticColors.BACKGROUND_SECONDARY, + color: semanticColors.TEXT_NORMAL, + borderWidth: 1, + borderRadius: 4, + borderColor: semanticColors.BACKGROUND_TERTIARY, + padding: 10, + }, +}); + +// 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: , + }); +} \ No newline at end of file diff --git a/src/ui/components/ErrorBoundary.tsx b/src/ui/components/ErrorBoundary.tsx index ef9c811..63de2b8 100644 --- a/src/ui/components/ErrorBoundary.tsx +++ b/src/ui/components/ErrorBoundary.tsx @@ -1,7 +1,6 @@ import { ErrorBoundaryProps } from "@types"; import { React, ReactNative as RN, stylesheet, constants } from "@metro/common"; -import { findByProps } from "@metro/filters"; -import { Forms } from "@ui/components"; +import { Forms, Button, Codeblock } from "@ui/components"; import { semanticColors } from "@ui/color"; interface ErrorBoundaryState { @@ -9,8 +8,6 @@ interface ErrorBoundaryState { errText?: string; } -const Button = findByProps("Looks", "Colors", "Sizes") as any; - const styles = stylesheet.createThemedStyleSheet({ view: { flex: 1, @@ -22,15 +19,6 @@ const styles = stylesheet.createThemedStyleSheet({ textAlign: "center", marginBottom: 5, }, - codeblock: { - fontFamily: constants.Fonts.CODE_SEMIBOLD, - includeFontPadding: false, - fontSize: 12, - backgroundColor: semanticColors.BACKGROUND_SECONDARY, - padding: 5, - borderRadius: 5, - marginBottom: 5, - } }); export default class ErrorBoundary extends React.Component { @@ -47,7 +35,7 @@ export default class ErrorBoundary extends React.Component Uh oh. - {this.state.errText} + {this.state.errText}