GoonCord/src/ui/safeMode.tsx
2023-04-15 19:54:23 +01:00

131 lines
5.2 KiB
TypeScript

import { ButtonColors } from "@types";
import { ReactNative as RN, stylesheet } from "@metro/common";
import { findByName, findByProps, findByStoreName } from "@metro/filters";
import { after } from "@lib/patcher";
import { toggleSafeMode } from "@lib/debug";
import { DeviceManager } from "@lib/native";
import { semanticColors } from "@ui/color";
import { Button, 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 ThemeStore = findByStoreName("ThemeStore");
const { TextStyleSheet } = findByProps("TextStyleSheet");
const styles = stylesheet.createThemedStyleSheet({
container: {
flex: 1,
backgroundColor: semanticColors.BACKGROUND_PRIMARY,
paddingHorizontal: 16,
},
header: {
flex: 1,
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
marginVertical: 8,
},
headerTitle: {
...TextStyleSheet["heading-md/semibold"],
textAlign: "center",
textTransform: "uppercase",
color: semanticColors.HEADER_PRIMARY,
},
headerDescription: {
...TextStyleSheet["text-sm/medium"],
textAlign: "center",
color: semanticColors.TEXT_MUTED,
},
footer: {
flexDirection: DeviceManager.isTablet ? "row" : "column",
justifyContent: "flex-end",
marginVertical: 8,
},
});
interface Tab {
id: string;
title: string;
trimWhitespace?: boolean;
}
interface Button {
text: string;
// TODO: Proper types for the below
color?: string;
size?: string;
onPress: () => void;
}
const tabs: Tab[] = [
{ id: "message", title: "Message" },
{ id: "stack", title: "Stack Trace" },
{ id: "componentStack", title: "Component", trimWhitespace: true },
];
export default () => after("render", ErrorBoundary.prototype, function (this: any, _, ret) {
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 ??= "message";
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 Safe Mode", onPress: toggleSafeMode }] : [],
{ text: "Retry Render", color: ButtonColors.RED, onPress: () => this.setState({ info: null, error: null }) },
]
return (
<_ErrorBoundary>
<SafeAreaView style={styles.container}>
<RN.View style={styles.header}>
<RN.Image style={{ flex: 1, resizeMode: "contain", maxHeight: 96, paddingRight: 4 }} source={ThemeStore.theme === "light" ? ret.props.lightSource : ret.props.darkSource} />
<RN.View style={{ flex: 2, paddingLeft: 4 }}>
<RN.Text style={styles.headerTitle}>{ret.props.title}</RN.Text>
<RN.Text style={styles.headerDescription}>{ret.props.body}</RN.Text>
</RN.View>
</RN.View>
<RN.View style={{ flex: 6 }}>
<RN.View style={{ paddingBottom: 8 }}>
{/* Are errors caught by ErrorBoundary guaranteed to have the component stack? */}
<BadgableTabBar
tabs={tabs}
activeTab={this.state.activeTab}
onTabSelected={(tab: string) => { this.setState({ activeTab: tab }) }}
/>
</RN.View>
<Codeblock
selectable
style={{ flex: 1, textAlignVertical: "top" }}
>
{/*
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}
</Codeblock>
</RN.View>
<RN.View style={styles.footer}>
{buttons.map(button => {
const buttonIndex = buttons.indexOf(button) !== 0 ? 8 : 0;
return <Button
text={button.text}
color={button.color ?? ButtonColors.BRAND}
size={button.size ?? "small"}
onPress={button.onPress}
style={DeviceManager.isTablet ? { flex: `0.${buttons.length}`, marginLeft: buttonIndex } : { marginTop: buttonIndex }}
/>
})}
</RN.View>
</SafeAreaView>
</_ErrorBoundary>
)
});