feat: add nexx lib and better Header text
nexx shouldn't be mad, right?
This commit is contained in:
parent
27b037fe9a
commit
b98ce51645
26 changed files with 2370 additions and 21 deletions
82
nexxstuff/components/ActionSheet.tsx
Normal file
82
nexxstuff/components/ActionSheet.tsx
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
import { find, findByProps } from "@vendetta/metro";
|
||||||
|
import { ReactNative as RN } from "@vendetta/metro/common";
|
||||||
|
import { without } from "@vendetta/utils";
|
||||||
|
import type { ImageSourcePropType, ViewProps } from "react-native";
|
||||||
|
|
||||||
|
const _ActionSheet = findByProps("ActionSheet")?.ActionSheet
|
||||||
|
?? find(x => x.render?.name === "ActionSheet"); // thank you to @pylixonly for fixing this
|
||||||
|
|
||||||
|
const { ActionSheetTitleHeader, ActionSheetCloseButton } = findByProps(
|
||||||
|
"ActionSheetTitleHeader",
|
||||||
|
"ActionSheetCloseButton",
|
||||||
|
);
|
||||||
|
|
||||||
|
export const LazyActionSheet = findByProps("openLazy", "hideActionSheet") as {
|
||||||
|
openLazy: (component: Promise<any>, key: string, props?: object) => void;
|
||||||
|
hideActionSheet: () => void;
|
||||||
|
};
|
||||||
|
export const { openLazy, hideActionSheet } = LazyActionSheet;
|
||||||
|
|
||||||
|
export const { showSimpleActionSheet } = findByProps(
|
||||||
|
"showSimpleActionSheet",
|
||||||
|
) as {
|
||||||
|
showSimpleActionSheet: (props: {
|
||||||
|
key: "CardOverflow";
|
||||||
|
header: {
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
onClose?: () => void;
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
label: string;
|
||||||
|
icon?: ImageSourcePropType;
|
||||||
|
isDestructive?: boolean;
|
||||||
|
onPress?: () => void;
|
||||||
|
}[];
|
||||||
|
}) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ActionSheetProps = React.PropsWithChildren<
|
||||||
|
ViewProps & {
|
||||||
|
title: string;
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const ActionSheet = ((props: ActionSheetProps) => {
|
||||||
|
return (
|
||||||
|
<_ActionSheet>
|
||||||
|
<ActionSheetTitleHeader
|
||||||
|
title={props.title}
|
||||||
|
trailing={
|
||||||
|
<ActionSheetCloseButton
|
||||||
|
onPress={props.onClose
|
||||||
|
?? (() => {
|
||||||
|
hideActionSheet();
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<RN.View {...without(props, "title", "onClose")} />
|
||||||
|
</_ActionSheet>
|
||||||
|
);
|
||||||
|
}) as {
|
||||||
|
(props: ActionSheetProps): JSX.Element;
|
||||||
|
open: <Sheet extends React.FunctionComponent<any>>(
|
||||||
|
sheet: Sheet,
|
||||||
|
props: Parameters<Sheet>[0],
|
||||||
|
) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
ActionSheet.open = (sheet, props) => {
|
||||||
|
openLazy(
|
||||||
|
new Promise(res => {
|
||||||
|
res({
|
||||||
|
default: sheet,
|
||||||
|
});
|
||||||
|
}) as any,
|
||||||
|
"ActionSheet",
|
||||||
|
props,
|
||||||
|
);
|
||||||
|
};
|
||||||
50
nexxstuff/components/AutoRow.tsx
Normal file
50
nexxstuff/components/AutoRow.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { findByProps } from "@vendetta/metro";
|
||||||
|
import { ReactNative as RN, stylesheet } from "@vendetta/metro/common";
|
||||||
|
import { semanticColors } from "@vendetta/ui";
|
||||||
|
import { Forms } from "@vendetta/ui/components";
|
||||||
|
|
||||||
|
const { FormRow } = Forms;
|
||||||
|
const { TableRow } = findByProps("TableRow");
|
||||||
|
|
||||||
|
export const { useInMainTabsExperiment } = findByProps(
|
||||||
|
"useInMainTabsExperiment",
|
||||||
|
"isInMainTabsExperiment",
|
||||||
|
);
|
||||||
|
|
||||||
|
export default function AutoRow({
|
||||||
|
label,
|
||||||
|
icon,
|
||||||
|
onPress,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
icon: number;
|
||||||
|
onPress?: () => void;
|
||||||
|
}) {
|
||||||
|
const styles = stylesheet.createThemedStyleSheet({
|
||||||
|
icon: {
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
tintColor: semanticColors.INTERACTIVE_ICON_DEFAULT,
|
||||||
|
opacity: 0.6,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const tabbed = useInMainTabsExperiment();
|
||||||
|
|
||||||
|
if (tabbed) {
|
||||||
|
return (
|
||||||
|
<TableRow
|
||||||
|
label={label}
|
||||||
|
icon={<RN.Image style={styles.icon} source={icon} />}
|
||||||
|
onPress={onPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormRow
|
||||||
|
label={label}
|
||||||
|
leading={<FormRow.Icon source={icon} />}
|
||||||
|
onPress={onPress}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
111
nexxstuff/components/BetterTableRow.tsx
Normal file
111
nexxstuff/components/BetterTableRow.tsx
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
import { ReactNative as RN, stylesheet } from "@vendetta/metro/common";
|
||||||
|
import { semanticColors } from "@vendetta/ui";
|
||||||
|
|
||||||
|
import Text from "./Text";
|
||||||
|
|
||||||
|
export function BetterTableRowTitle({
|
||||||
|
title,
|
||||||
|
onPress,
|
||||||
|
icon,
|
||||||
|
padding,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
onPress?: () => void;
|
||||||
|
icon?: number;
|
||||||
|
padding?: boolean;
|
||||||
|
}) {
|
||||||
|
const styles = stylesheet.createThemedStyleSheet({
|
||||||
|
androidRipple: {
|
||||||
|
color: semanticColors.ANDROID_RIPPLE,
|
||||||
|
cornerRadius: 4,
|
||||||
|
} as any,
|
||||||
|
icon: {
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
marginTop: 1.5,
|
||||||
|
tintColor: semanticColors.TEXT_MUTED,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const UseCompontent = onPress ? RN.Pressable : RN.View;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<UseCompontent
|
||||||
|
android_ripple={styles.androidRipple}
|
||||||
|
onPress={onPress}
|
||||||
|
style={{
|
||||||
|
marginBottom: 8,
|
||||||
|
marginHorizontal: padding ? 16 : 0,
|
||||||
|
marginTop: padding ? 8 : 0,
|
||||||
|
gap: 4,
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{icon && (
|
||||||
|
<RN.Image
|
||||||
|
style={styles.icon}
|
||||||
|
source={icon}
|
||||||
|
resizeMode="cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Text variant="text-sm/semibold" color="TEXT_MUTED">
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
</UseCompontent>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function BetterTableRowGroup({
|
||||||
|
title,
|
||||||
|
onTitlePress,
|
||||||
|
icon,
|
||||||
|
children,
|
||||||
|
padding,
|
||||||
|
nearby,
|
||||||
|
}: React.PropsWithChildren<{
|
||||||
|
title?: string | React.ReactNode;
|
||||||
|
onTitlePress?: () => void;
|
||||||
|
icon?: number;
|
||||||
|
padding?: boolean;
|
||||||
|
nearby?: boolean;
|
||||||
|
}>) {
|
||||||
|
const styles = stylesheet.createThemedStyleSheet({
|
||||||
|
main: {
|
||||||
|
backgroundColor: semanticColors.CARD_BACKGROUND_DEFAULT,
|
||||||
|
borderColor: semanticColors.BORDER_MUTED,
|
||||||
|
borderWidth: 1,
|
||||||
|
borderRadius: 16,
|
||||||
|
overflow: "hidden",
|
||||||
|
flex: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RN.View style={{ marginHorizontal: 16, marginTop: nearby ? 8 : 16 }}>
|
||||||
|
{title
|
||||||
|
? (
|
||||||
|
typeof title === "string"
|
||||||
|
? (
|
||||||
|
<BetterTableRowTitle
|
||||||
|
title={title}
|
||||||
|
onPress={onTitlePress}
|
||||||
|
icon={icon}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
: title
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
<RN.View style={styles.main}>
|
||||||
|
{padding
|
||||||
|
? (
|
||||||
|
<RN.View
|
||||||
|
style={{ paddingHorizontal: 16, paddingVertical: 16 }}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</RN.View>
|
||||||
|
)
|
||||||
|
: children}
|
||||||
|
</RN.View>
|
||||||
|
</RN.View>
|
||||||
|
);
|
||||||
|
}
|
||||||
43
nexxstuff/components/InlineCheckbox.tsx
Normal file
43
nexxstuff/components/InlineCheckbox.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { find } from "@vendetta/metro";
|
||||||
|
import { React, ReactNative as RN } from "@vendetta/metro/common";
|
||||||
|
|
||||||
|
import Text from "./Text";
|
||||||
|
|
||||||
|
const { FormCheckbox } = find(x => x?.FormCheckbox && !x?.FormCardSection);
|
||||||
|
|
||||||
|
export default function InlineCheckbox({
|
||||||
|
label,
|
||||||
|
initialValue,
|
||||||
|
update,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
initialValue: boolean;
|
||||||
|
update: (val: boolean) => void;
|
||||||
|
}) {
|
||||||
|
const [value, setValue] = React.useState(initialValue);
|
||||||
|
update(value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RN.Pressable
|
||||||
|
accessibilityLabel={label}
|
||||||
|
accessibilityRole="checkbox"
|
||||||
|
accessibilityState={{
|
||||||
|
selected: value,
|
||||||
|
}}
|
||||||
|
onPress={() => {
|
||||||
|
setValue(!value);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 8,
|
||||||
|
marginTop: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FormCheckbox checked={value} />
|
||||||
|
<Text variant="text-md/normal" color="TEXT_DEFAULT">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
</RN.Pressable>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
nexxstuff/components/Modal.tsx
Normal file
27
nexxstuff/components/Modal.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import { without } from "@vendetta/utils";
|
||||||
|
|
||||||
|
import { modalCloseButton, Navigator, popModal } from "../types";
|
||||||
|
|
||||||
|
export default function Modal(
|
||||||
|
props: React.PropsWithChildren<{
|
||||||
|
mkey: string;
|
||||||
|
headerRight?: React.FunctionComponent;
|
||||||
|
title?: string;
|
||||||
|
}>,
|
||||||
|
) {
|
||||||
|
if (!Navigator || !modalCloseButton) return null;
|
||||||
|
return (
|
||||||
|
<Navigator
|
||||||
|
initialRouteName={props.mkey}
|
||||||
|
screens={{
|
||||||
|
[props.mkey]: Object.assign(
|
||||||
|
without(props, "mkey", "children"),
|
||||||
|
{
|
||||||
|
headerLeft: modalCloseButton?.(() => popModal(props.mkey)),
|
||||||
|
render: () => props.children,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
34
nexxstuff/components/RichText.tsx
Normal file
34
nexxstuff/components/RichText.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { ReactNative as RN } from "@vendetta/metro/common";
|
||||||
|
|
||||||
|
import Text from "./Text";
|
||||||
|
|
||||||
|
export namespace RichText {
|
||||||
|
export function Bold({
|
||||||
|
children,
|
||||||
|
onPress,
|
||||||
|
}: React.PropsWithChildren<{
|
||||||
|
onPress?: () => void;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<Text variant={"text-md/bold"} onPress={onPress}>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Underline({
|
||||||
|
children,
|
||||||
|
onPress,
|
||||||
|
}: React.PropsWithChildren<{
|
||||||
|
onPress?: () => void;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<RN.Text
|
||||||
|
style={{ textDecorationLine: "underline" }}
|
||||||
|
onPress={onPress}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</RN.Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
36
nexxstuff/components/Skeleton.tsx
Normal file
36
nexxstuff/components/Skeleton.tsx
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { React, stylesheet } from "@vendetta/metro/common";
|
||||||
|
import { semanticColors } from "@vendetta/ui";
|
||||||
|
import type { DimensionValue, ViewStyle } from "react-native";
|
||||||
|
|
||||||
|
import { Reanimated } from "$/deps";
|
||||||
|
|
||||||
|
export default function Skeleton({
|
||||||
|
height,
|
||||||
|
style,
|
||||||
|
}: {
|
||||||
|
height: DimensionValue;
|
||||||
|
style?: ViewStyle;
|
||||||
|
}) {
|
||||||
|
const baseStyle = stylesheet.createThemedStyleSheet({
|
||||||
|
skeleton: {
|
||||||
|
height,
|
||||||
|
borderRadius: 16,
|
||||||
|
backgroundColor: semanticColors.BACKGROUND_MOD_STRONG,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const opacity = Reanimated.useSharedValue(1);
|
||||||
|
React.useEffect(() => {
|
||||||
|
opacity.value = Reanimated.withRepeat(
|
||||||
|
Reanimated.withTiming(0.5, { duration: 1000 }),
|
||||||
|
-1,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Reanimated.default.View
|
||||||
|
style={[baseStyle.skeleton, style, { opacity }]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
50
nexxstuff/components/SliderIcon.tsx
Normal file
50
nexxstuff/components/SliderIcon.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
import { ReactNative as RN, stylesheet } from "@vendetta/metro/common";
|
||||||
|
import { semanticColors } from "@vendetta/ui";
|
||||||
|
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||||
|
|
||||||
|
export default function SliderIcon({
|
||||||
|
side,
|
||||||
|
onPress,
|
||||||
|
}: {
|
||||||
|
side: "start" | "end";
|
||||||
|
onPress: () => void;
|
||||||
|
}) {
|
||||||
|
const styles = stylesheet.createThemedStyleSheet({
|
||||||
|
androidRipple: {
|
||||||
|
color: semanticColors.ANDROID_RIPPLE,
|
||||||
|
},
|
||||||
|
iconSecondary: {
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
tintColor: "transparent",
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
tintColor: semanticColors.INTERACTIVE_ICON_DEFAULT,
|
||||||
|
position: "absolute",
|
||||||
|
top: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const icon = side === "start" ? "CircleMinusIcon" : "CirclePlusIcon";
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RN.Pressable
|
||||||
|
android_ripple={styles.androidRipple}
|
||||||
|
accessible={false}
|
||||||
|
onPress={onPress}
|
||||||
|
>
|
||||||
|
<RN.View>
|
||||||
|
<RN.Image
|
||||||
|
style={styles.iconSecondary}
|
||||||
|
source={getAssetIDByName(`${icon}-secondary`)}
|
||||||
|
/>
|
||||||
|
<RN.Image
|
||||||
|
style={styles.icon}
|
||||||
|
source={getAssetIDByName(`${icon}-primary`)}
|
||||||
|
/>
|
||||||
|
</RN.View>
|
||||||
|
</RN.Pressable>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
nexxstuff/components/SmartMention.tsx
Normal file
49
nexxstuff/components/SmartMention.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import { findByProps, findByStoreName } from "@vendetta/metro";
|
||||||
|
import { React } from "@vendetta/metro/common";
|
||||||
|
|
||||||
|
import Text from "./Text";
|
||||||
|
|
||||||
|
const { showUserProfile } = findByProps("showUserProfile");
|
||||||
|
const { fetchProfile } = findByProps("fetchProfile");
|
||||||
|
|
||||||
|
const UserStore = findByStoreName("UserStore");
|
||||||
|
|
||||||
|
export default function SmartMention({
|
||||||
|
userId,
|
||||||
|
color,
|
||||||
|
loadUsername,
|
||||||
|
children,
|
||||||
|
}: React.PropsWithChildren<{
|
||||||
|
userId: string;
|
||||||
|
color?: string;
|
||||||
|
loadUsername?: boolean;
|
||||||
|
}>) {
|
||||||
|
const [loadedUsername, setLoadedUsername] = React.useState<null | string>(
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
React.useEffect(
|
||||||
|
() =>
|
||||||
|
!loadedUsername
|
||||||
|
&& loadUsername
|
||||||
|
&& (UserStore.getUser(userId)
|
||||||
|
? setLoadedUsername(UserStore.getUser(userId).username)
|
||||||
|
: fetchProfile(userId).then(x => {
|
||||||
|
setLoadedUsername(x.user.username);
|
||||||
|
})),
|
||||||
|
[loadUsername],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Text
|
||||||
|
variant="text-md/bold"
|
||||||
|
color={color ?? "TEXT_DEFAULT"}
|
||||||
|
onPress={() =>
|
||||||
|
UserStore.getUser(userId)
|
||||||
|
? showUserProfile({ userId })
|
||||||
|
: fetchProfile(userId).then(() => showUserProfile({ userId }))}
|
||||||
|
>
|
||||||
|
{loadUsername ? `@${loadedUsername ?? "..."}` : children}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
53
nexxstuff/components/SuperAwesomeIcon.tsx
Normal file
53
nexxstuff/components/SuperAwesomeIcon.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { ReactNative as RN, stylesheet } from "@vendetta/metro/common";
|
||||||
|
import { semanticColors } from "@vendetta/ui";
|
||||||
|
|
||||||
|
export default function SuperAwesomeIcon({
|
||||||
|
onPress,
|
||||||
|
onLongPress,
|
||||||
|
icon,
|
||||||
|
style,
|
||||||
|
destructive,
|
||||||
|
color,
|
||||||
|
}: {
|
||||||
|
onPress?: () => void;
|
||||||
|
onLongPress?: () => void;
|
||||||
|
destructive?: boolean;
|
||||||
|
color?: any;
|
||||||
|
icon: number;
|
||||||
|
style: "header" | "card" | import("react-native").ImageStyle;
|
||||||
|
}) {
|
||||||
|
const styles = stylesheet.createThemedStyleSheet({
|
||||||
|
headerStyleIcon: {
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
marginRight: 10,
|
||||||
|
tintColor: semanticColors.TEXT_STRONG,
|
||||||
|
},
|
||||||
|
cardStyleIcon: {
|
||||||
|
width: 22,
|
||||||
|
height: 22,
|
||||||
|
marginLeft: 5,
|
||||||
|
tintColor: semanticColors.INTERACTIVE_ICON_DEFAULT,
|
||||||
|
},
|
||||||
|
destructiveIcon: {
|
||||||
|
tintColor: semanticColors.TEXT_FEEDBACK_CRITICAL,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RN.TouchableOpacity onPress={onPress} onLongPress={onLongPress}>
|
||||||
|
<RN.Image
|
||||||
|
style={[
|
||||||
|
typeof style === "string"
|
||||||
|
? style === "header"
|
||||||
|
? styles.headerStyleIcon
|
||||||
|
: styles.cardStyleIcon
|
||||||
|
: style,
|
||||||
|
destructive && styles.destructiveIcon,
|
||||||
|
color && { tintColor: color },
|
||||||
|
].filter(x => !!x)}
|
||||||
|
source={icon}
|
||||||
|
/>
|
||||||
|
</RN.TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
78
nexxstuff/components/Text.tsx
Normal file
78
nexxstuff/components/Text.tsx
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { findByProps } from "@vendetta/metro";
|
||||||
|
import { React, ReactNative as RN } from "@vendetta/metro/common";
|
||||||
|
import { semanticColors } from "@vendetta/ui";
|
||||||
|
import type { TextProps } from "react-native";
|
||||||
|
|
||||||
|
import { resolveSemanticColor, TextStyleSheet, type TextStyleSheetVariant } from "../types";
|
||||||
|
|
||||||
|
export function TrailingText({ children }: React.PropsWithChildren<object>) {
|
||||||
|
return (
|
||||||
|
<Text variant="text-md/medium" color="TEXT_MUTED">
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { useThemeContext } = findByProps("useThemeContext");
|
||||||
|
|
||||||
|
export default function Text({
|
||||||
|
variant,
|
||||||
|
lineClamp,
|
||||||
|
color,
|
||||||
|
align,
|
||||||
|
style,
|
||||||
|
onPress,
|
||||||
|
getChildren,
|
||||||
|
children,
|
||||||
|
liveUpdate,
|
||||||
|
ellipsis,
|
||||||
|
}: React.PropsWithChildren<{
|
||||||
|
variant?: TextStyleSheetVariant;
|
||||||
|
color?: string;
|
||||||
|
getChildren?: () => React.ReactNode | undefined;
|
||||||
|
liveUpdate?: boolean;
|
||||||
|
|
||||||
|
lineClamp?: TextProps["numberOfLines"];
|
||||||
|
align?: "left" | "right" | "center";
|
||||||
|
style?: TextProps["style"];
|
||||||
|
onPress?: TextProps["onPress"];
|
||||||
|
ellipsis?: TextProps["ellipsizeMode"];
|
||||||
|
}>) {
|
||||||
|
const themeContext = useThemeContext();
|
||||||
|
const [_, forceUpdate] = React.useReducer(x => ~x, 0);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!liveUpdate) return;
|
||||||
|
const nextSecond = () => Date.now() - new Date().setMilliseconds(1000);
|
||||||
|
|
||||||
|
let timeout = setTimeout(function update() {
|
||||||
|
forceUpdate();
|
||||||
|
timeout = setTimeout(update, nextSecond());
|
||||||
|
}, nextSecond());
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RN.Text
|
||||||
|
style={[
|
||||||
|
variant && TextStyleSheet[variant],
|
||||||
|
color
|
||||||
|
? {
|
||||||
|
color: resolveSemanticColor(
|
||||||
|
semanticColors[color],
|
||||||
|
themeContext?.theme,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
: {},
|
||||||
|
align && { textAlign: align },
|
||||||
|
style ?? {},
|
||||||
|
]}
|
||||||
|
numberOfLines={lineClamp}
|
||||||
|
onPress={onPress}
|
||||||
|
ellipsizeMode={ellipsis}
|
||||||
|
>
|
||||||
|
{getChildren?.() ?? children}
|
||||||
|
</RN.Text>
|
||||||
|
);
|
||||||
|
}
|
||||||
104
nexxstuff/components/TextBadge.tsx
Normal file
104
nexxstuff/components/TextBadge.tsx
Normal file
|
|
@ -0,0 +1,104 @@
|
||||||
|
import { React, ReactNative as RN, stylesheet } from "@vendetta/metro/common";
|
||||||
|
import { semanticColors } from "@vendetta/ui";
|
||||||
|
import type { ViewStyle } from "react-native";
|
||||||
|
|
||||||
|
import Text from "$/components/Text";
|
||||||
|
import { Reanimated } from "$/deps";
|
||||||
|
|
||||||
|
export default function TextBadge({
|
||||||
|
style,
|
||||||
|
variant,
|
||||||
|
children,
|
||||||
|
shiny,
|
||||||
|
}: React.PropsWithChildren<{
|
||||||
|
style?: ViewStyle;
|
||||||
|
variant: "primary" | "destructive";
|
||||||
|
shiny?: boolean;
|
||||||
|
}>) {
|
||||||
|
const [width, setWidth] = React.useState(0);
|
||||||
|
|
||||||
|
const styles = stylesheet.createThemedStyleSheet({
|
||||||
|
main: {
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
borderRadius: 2147483647,
|
||||||
|
paddingHorizontal: 6,
|
||||||
|
paddingVertical: 3,
|
||||||
|
backgroundColor: semanticColors[
|
||||||
|
`REDESIGN_BUTTON_${variant.toUpperCase()}_BACKGROUND`
|
||||||
|
],
|
||||||
|
marginTop: 3,
|
||||||
|
overflow: "hidden",
|
||||||
|
},
|
||||||
|
shiner: {
|
||||||
|
position: "absolute",
|
||||||
|
width: 7,
|
||||||
|
height: 80,
|
||||||
|
backgroundColor: "#fff4",
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
color: semanticColors[
|
||||||
|
`REDESIGN_BUTTON_${variant.toUpperCase()}_TEXT`
|
||||||
|
],
|
||||||
|
textTransform: "uppercase",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const shinyTranslate = Reanimated.useSharedValue(0);
|
||||||
|
const randomness = React.useRef(
|
||||||
|
1500 + Math.floor(Math.random() * 696),
|
||||||
|
).current;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (width !== 0 && shiny) {
|
||||||
|
shinyTranslate.value = Reanimated.withRepeat(
|
||||||
|
Reanimated.withSequence(
|
||||||
|
Reanimated.withTiming(-width, { duration: 0 }),
|
||||||
|
Reanimated.withDelay(
|
||||||
|
randomness,
|
||||||
|
Reanimated.withTiming(width, {
|
||||||
|
duration: 800,
|
||||||
|
easing: Reanimated.Easing.inOut(
|
||||||
|
Reanimated.Easing.cubic,
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
} else shinyTranslate.value = 500;
|
||||||
|
}, [shiny, width]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RN.View
|
||||||
|
style={[styles.main, style]}
|
||||||
|
onLayout={layout => {
|
||||||
|
setWidth(layout.nativeEvent.layout.width * 0.5 + 4);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{shiny && width !== 0 && (
|
||||||
|
<Reanimated.default.View
|
||||||
|
style={[
|
||||||
|
styles.shiner,
|
||||||
|
{
|
||||||
|
transform: [
|
||||||
|
{
|
||||||
|
rotate: "-22deg",
|
||||||
|
},
|
||||||
|
{ translateX: shinyTranslate },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Text
|
||||||
|
variant="text-xxs/bold"
|
||||||
|
align="center"
|
||||||
|
style={styles.text}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Text>
|
||||||
|
</RN.View>
|
||||||
|
);
|
||||||
|
}
|
||||||
58
nexxstuff/components/sheets/ChooseSheet.tsx
Normal file
58
nexxstuff/components/sheets/ChooseSheet.tsx
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
import { React, ReactNative as RN } from "@vendetta/metro/common";
|
||||||
|
import { Forms } from "@vendetta/ui/components";
|
||||||
|
import type { ImageSourcePropType } from "react-native";
|
||||||
|
|
||||||
|
import { ActionSheet } from "../ActionSheet";
|
||||||
|
|
||||||
|
const { FormRow } = Forms;
|
||||||
|
|
||||||
|
export default function ChooseSheet({
|
||||||
|
title,
|
||||||
|
value: _value,
|
||||||
|
options,
|
||||||
|
callback,
|
||||||
|
}: {
|
||||||
|
title: string;
|
||||||
|
value: string | number | boolean | null;
|
||||||
|
options: {
|
||||||
|
name: string;
|
||||||
|
description?: string;
|
||||||
|
value: typeof _value;
|
||||||
|
icon?: ImageSourcePropType;
|
||||||
|
iconColor?: any;
|
||||||
|
iconComponent?: React.ReactNode;
|
||||||
|
}[];
|
||||||
|
callback: (v: typeof _value) => void;
|
||||||
|
}) {
|
||||||
|
const [value, setValue] = React.useState(_value);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionSheet title={title}>
|
||||||
|
{options.map((x) => (
|
||||||
|
<FormRow
|
||||||
|
key={x.value}
|
||||||
|
label={x.name}
|
||||||
|
subLabel={x.description}
|
||||||
|
trailing={x.iconComponent
|
||||||
|
? x.iconComponent
|
||||||
|
: x.icon && (
|
||||||
|
<RN.Image
|
||||||
|
source={x.icon}
|
||||||
|
resizeMode="cover"
|
||||||
|
style={{
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
tintColor: x.iconColor,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
leading={<FormRow.Radio selected={x.value === value} />}
|
||||||
|
onPress={() => {
|
||||||
|
setValue(x.value);
|
||||||
|
callback(x.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ActionSheet>
|
||||||
|
);
|
||||||
|
}
|
||||||
227
nexxstuff/deps.ts
Normal file
227
nexxstuff/deps.ts
Normal file
|
|
@ -0,0 +1,227 @@
|
||||||
|
import type * as _FlashList from "@shopify/flash-list";
|
||||||
|
import { find, findByName, findByProps } from "@vendetta/metro";
|
||||||
|
import { ReactNative as RN } from "@vendetta/metro/common";
|
||||||
|
import type * as _Reanimated from "react-native-reanimated";
|
||||||
|
import type { StateStorage } from "zustand/middleware";
|
||||||
|
|
||||||
|
//
|
||||||
|
// JS deps
|
||||||
|
//
|
||||||
|
export const WebView = find((x: any) => x?.WebView && !x.default)
|
||||||
|
.WebView as typeof import("react-native-webview").default;
|
||||||
|
|
||||||
|
export const Svg = findByProps("SvgXml") as typeof import("react-native-svg");
|
||||||
|
|
||||||
|
export const Reanimated = findByProps("useSharedValue") as typeof _Reanimated;
|
||||||
|
|
||||||
|
export const FlashList = findByProps("FlashList")
|
||||||
|
.FlashList as typeof _FlashList.FlashList;
|
||||||
|
|
||||||
|
export const Joi = findByProps("isJoi") as unknown as typeof import("joi");
|
||||||
|
|
||||||
|
export const zustand = (findByProps("create", "useStore") || {
|
||||||
|
create: findByName("create"),
|
||||||
|
}) as typeof import("zustand");
|
||||||
|
|
||||||
|
export const zustandMW = findByProps(
|
||||||
|
"createJSONStorage",
|
||||||
|
"persist",
|
||||||
|
) as typeof import("zustand/middleware");
|
||||||
|
|
||||||
|
export const DocumentPicker = findByProps(
|
||||||
|
"pickSingle",
|
||||||
|
"isCancel",
|
||||||
|
) as typeof import("react-native-document-picker");
|
||||||
|
export const DocumentsNew = findByProps(
|
||||||
|
"pick",
|
||||||
|
"saveDocuments",
|
||||||
|
) as typeof import("@react-native-documents/picker");
|
||||||
|
|
||||||
|
const _MAS = findByProps("MobileAudioSound").MobileAudioSound;
|
||||||
|
|
||||||
|
// Messy code
|
||||||
|
export class MobileAudioSound {
|
||||||
|
// Events
|
||||||
|
public onPlay?: () => void;
|
||||||
|
public onStop?: () => void;
|
||||||
|
public onEnd?: () => void;
|
||||||
|
public onLoad?: (loaded: boolean) => void;
|
||||||
|
|
||||||
|
private mas: any;
|
||||||
|
|
||||||
|
public duration?: number;
|
||||||
|
public isLoaded?: boolean;
|
||||||
|
public isPlaying?: boolean;
|
||||||
|
|
||||||
|
/** Discord when it comes to not changing everything every fucking version!!! */
|
||||||
|
private get ensureSoundGetter() {
|
||||||
|
return this.mas?._ensureSound || this.mas?.ensureSound;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Preloads the audio, which automatically makes us better than Discord because they DON'T do that for some reason */
|
||||||
|
private async _preloadSound(skip?: boolean) {
|
||||||
|
const { _duration } = await this.ensureSoundGetter.bind(this.mas)();
|
||||||
|
this.duration = RN.Platform.select({
|
||||||
|
ios: _duration ? _duration * 1000 : _duration,
|
||||||
|
default: _duration,
|
||||||
|
});
|
||||||
|
this.isLoaded = !!_duration;
|
||||||
|
|
||||||
|
if (!skip) this.onLoad?.(!!_duration);
|
||||||
|
return !!_duration;
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public url: string,
|
||||||
|
public usage: "notification" | "voice" | "ring_tone" | "media",
|
||||||
|
public volume: number,
|
||||||
|
events?: {
|
||||||
|
onPlay?: () => void;
|
||||||
|
onStop?: () => void;
|
||||||
|
onEnd?: () => void;
|
||||||
|
onLoad?: (loaded: boolean) => void;
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
this.mas = new _MAS(
|
||||||
|
url,
|
||||||
|
{
|
||||||
|
media: "vibing_wumpus",
|
||||||
|
notification: "activity_launch",
|
||||||
|
ring_tone: "call_ringing",
|
||||||
|
voice: "mute",
|
||||||
|
}[usage],
|
||||||
|
volume,
|
||||||
|
"default",
|
||||||
|
);
|
||||||
|
this.mas.volume = volume;
|
||||||
|
|
||||||
|
this._preloadSound();
|
||||||
|
for (const [key, val] of Object.entries(events ?? {})) this[key] = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _playTimeout?: number;
|
||||||
|
|
||||||
|
/** Plays the audio */
|
||||||
|
async play() {
|
||||||
|
if (!this.isLoaded && this.isLoaded !== false) {
|
||||||
|
await this._preloadSound();
|
||||||
|
}
|
||||||
|
if (!this.isLoaded) return;
|
||||||
|
|
||||||
|
this.mas.volume = this.volume;
|
||||||
|
await this.mas.play();
|
||||||
|
this.isPlaying = true;
|
||||||
|
this.onPlay?.();
|
||||||
|
|
||||||
|
clearTimeout(this._playTimeout);
|
||||||
|
this._playTimeout = setTimeout(
|
||||||
|
() => (this.onEnd?.(), this.stop()),
|
||||||
|
this.duration,
|
||||||
|
) as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stops the audio */
|
||||||
|
async stop() {
|
||||||
|
if (!this.isLoaded) return;
|
||||||
|
|
||||||
|
this.mas.stop();
|
||||||
|
this.isPlaying = false;
|
||||||
|
this.onStop?.();
|
||||||
|
|
||||||
|
clearTimeout(this._playTimeout);
|
||||||
|
await this._preloadSound(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// raw native modules
|
||||||
|
//
|
||||||
|
|
||||||
|
// ripped straight from https://github.com/revenge-mod/revenge-bundle/blob/0a90d762e5c9996bb6613edd136fafe2b8f25908/src/lib/api/native/modules/index.ts
|
||||||
|
// why the fuck isnt that exported
|
||||||
|
const nmp = window.nativeModuleProxy;
|
||||||
|
|
||||||
|
function getNativeModule<T = any>(...names: string[]): T | undefined {
|
||||||
|
for (const name of names) {
|
||||||
|
if (globalThis.__turboModuleProxy) {
|
||||||
|
const module = globalThis.__turboModuleProxy(name);
|
||||||
|
if (module) return module as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nmp[name]) return nmp[name] as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RNCacheModule = getNativeModule<StateStorage>(
|
||||||
|
"NativeCacheModule",
|
||||||
|
"MMKVManager",
|
||||||
|
)!;
|
||||||
|
|
||||||
|
export const RNChatModule = getNativeModule<{
|
||||||
|
updateRows: (id: string, json: string) => any;
|
||||||
|
}>(
|
||||||
|
"NativeChatModule",
|
||||||
|
"DCDChatManager",
|
||||||
|
)!;
|
||||||
|
|
||||||
|
export const RNFileModule = getNativeModule<{
|
||||||
|
/**
|
||||||
|
* @param path **Full** path to file
|
||||||
|
*/
|
||||||
|
fileExists: (path: string) => Promise<boolean>;
|
||||||
|
/**
|
||||||
|
* 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<boolean>;
|
||||||
|
/**
|
||||||
|
* @param path **Full** path to file
|
||||||
|
* @param encoding Set to `base64` in order to encode response
|
||||||
|
*/
|
||||||
|
readFile(path: string, encoding: "base64" | "utf8"): Promise<string>;
|
||||||
|
saveFileToGallery?(uri: string, fileName: string, fileType: "PNG" | "JPEG"): Promise<string>;
|
||||||
|
/**
|
||||||
|
* @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<string>;
|
||||||
|
/**
|
||||||
|
* Removes a file from the path given.
|
||||||
|
* (!) On Android, this always returns false, regardless if it fails or not!
|
||||||
|
* @param storageDir Either `cache` or `documents`
|
||||||
|
* @param path Path to the file to be removed
|
||||||
|
*/
|
||||||
|
removeFile(storageDir: "cache" | "documents", path: string): Promise<unknown>;
|
||||||
|
/**
|
||||||
|
* Clear the folder from the path given
|
||||||
|
* (!) On Android, this only clears all *files* and not subdirectories!
|
||||||
|
* @param storageDir Either `cache` or `documents`
|
||||||
|
* @param path Path to the folder to be cleared
|
||||||
|
* @returns Whether the clearance succeeded
|
||||||
|
*/
|
||||||
|
clearFolder(storageDir: "cache" | "documents", path: string): Promise<boolean>;
|
||||||
|
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;
|
||||||
|
}>(
|
||||||
|
"NativeFileModule",
|
||||||
|
"DCDFileManager",
|
||||||
|
)!;
|
||||||
62
nexxstuff/hooks/usePromise.ts
Normal file
62
nexxstuff/hooks/usePromise.ts
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { React } from "@vendetta/metro/common";
|
||||||
|
|
||||||
|
type PromiseRet<Res> =
|
||||||
|
| {
|
||||||
|
fulfilled: false;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
fulfilled: true;
|
||||||
|
success: false;
|
||||||
|
error: any;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
fulfilled: true;
|
||||||
|
success: true;
|
||||||
|
response: Res;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function usePromise<Res>(
|
||||||
|
promise: (signal: AbortSignal) => Promise<Res>,
|
||||||
|
deps: any[] = [],
|
||||||
|
): PromiseRet<Res> {
|
||||||
|
const [res, setRes] = React.useState<PromiseRet<Res>>({
|
||||||
|
fulfilled: false,
|
||||||
|
});
|
||||||
|
const tracker = React.useRef(0);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const id = Date.now();
|
||||||
|
tracker.current = id;
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
setRes({
|
||||||
|
fulfilled: false,
|
||||||
|
});
|
||||||
|
promise(controller.signal)
|
||||||
|
.then(
|
||||||
|
response =>
|
||||||
|
tracker.current === id
|
||||||
|
&& setRes({
|
||||||
|
fulfilled: true,
|
||||||
|
success: true,
|
||||||
|
response,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.catch(
|
||||||
|
error =>
|
||||||
|
tracker.current === id
|
||||||
|
&& setRes({
|
||||||
|
fulfilled: true,
|
||||||
|
success: false,
|
||||||
|
error,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
controller.abort();
|
||||||
|
};
|
||||||
|
}, deps);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
68
nexxstuff/lib/intlProxy.ts
Normal file
68
nexxstuff/lib/intlProxy.ts
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
// a drop-in replacement for i18n
|
||||||
|
// https://github.com/discord/discord-intl
|
||||||
|
|
||||||
|
import { findByProps } from "@vendetta/metro";
|
||||||
|
import { i18n } from "@vendetta/metro/common";
|
||||||
|
|
||||||
|
const { intl, t: intlMap } = findByProps("intl") ?? {};
|
||||||
|
const { runtimeHashMessageKey } = findByProps("runtimeHashMessageKey") ?? {};
|
||||||
|
|
||||||
|
export function getLocale() {
|
||||||
|
const lang = intl?.currentLocale ?? i18n.getLocale?.() ?? "en";
|
||||||
|
return lang;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isUsingi18n = !!i18n.Messages.DISCORD || !intl || !intlMap
|
||||||
|
|| !runtimeHashMessageKey;
|
||||||
|
|
||||||
|
const _messages = {} as Record<string, string>;
|
||||||
|
const intlProxy = Object.freeze(
|
||||||
|
new Proxy(_messages, {
|
||||||
|
get(trg, prop, receiv) {
|
||||||
|
const key = prop.toString();
|
||||||
|
if (key in _messages) return Reflect.get(trg, key, receiv);
|
||||||
|
|
||||||
|
if (isUsingi18n) return (_messages[key] = i18n.Messages[key]);
|
||||||
|
|
||||||
|
const hash = runtimeHashMessageKey(key);
|
||||||
|
try {
|
||||||
|
return (_messages[key] = intl.string(intlMap[hash]));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(
|
||||||
|
`intlProxy: Failed to get intl message for key: ${key} (${hash})`,
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
return (_messages[key] = "");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const _format = {} as Record<string, (values: Record<string, string>) => string>;
|
||||||
|
export const intlFormat = Object.freeze(
|
||||||
|
new Proxy(_format, {
|
||||||
|
get(trg, prop, receiv) {
|
||||||
|
const key = prop.toString();
|
||||||
|
if (key in _format) return Reflect.get(trg, key, receiv);
|
||||||
|
|
||||||
|
if (isUsingi18n) {
|
||||||
|
return (_format[key] = typeof i18n.Messages[key] === "string"
|
||||||
|
? () => i18n.Messages[key]
|
||||||
|
: i18n.Messages[key].format);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hash = runtimeHashMessageKey(key);
|
||||||
|
try {
|
||||||
|
return (_format[key] = values => intl.format(intlMap[hash], values));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(
|
||||||
|
`intlFormat: Failed to get intl format message for key: ${key} (${hash})`,
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
return (_format[key] = () => "");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
export default intlProxy;
|
||||||
30
nexxstuff/lib/pinToSettings/index.ts
Normal file
30
nexxstuff/lib/pinToSettings/index.ts
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import { patchPanelUI } from "./panel";
|
||||||
|
import { patchTabsUI } from "./tabs";
|
||||||
|
|
||||||
|
export interface PinToSettingsTabs {
|
||||||
|
key: string;
|
||||||
|
title: () => string;
|
||||||
|
icon?: number;
|
||||||
|
predicate?: () => boolean;
|
||||||
|
trailing?: () => React.ReactNode;
|
||||||
|
page: React.ComponentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function patchSettingsPin(tabs: PinToSettingsTabs): () => void {
|
||||||
|
const patches: (() => void)[] = [];
|
||||||
|
|
||||||
|
let disabled = false;
|
||||||
|
|
||||||
|
const realPredicate = tabs.predicate ?? (() => true);
|
||||||
|
tabs.predicate = () => (disabled ? false : realPredicate());
|
||||||
|
|
||||||
|
patchPanelUI(tabs, patches);
|
||||||
|
patchTabsUI(tabs, patches);
|
||||||
|
patches.push(() => (disabled = true));
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
for (const x of patches) {
|
||||||
|
x();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
91
nexxstuff/lib/pinToSettings/panel.tsx
Normal file
91
nexxstuff/lib/pinToSettings/panel.tsx
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
// Based on: https://github.com/pyoncord/Bunny/blob/dev/src/lib/ui/settings/patches/panel.tsx
|
||||||
|
|
||||||
|
import { logger } from "@vendetta";
|
||||||
|
import { NavigationNative } from "@vendetta/metro/common";
|
||||||
|
import { after } from "@vendetta/patcher";
|
||||||
|
import { Forms } from "@vendetta/ui/components";
|
||||||
|
import { findInReactTree } from "@vendetta/utils";
|
||||||
|
import intlProxy from "../intlProxy";
|
||||||
|
import type { PinToSettingsTabs } from ".";
|
||||||
|
|
||||||
|
const { FormSection, FormRow } = Forms;
|
||||||
|
|
||||||
|
function Section({ tabs }: { tabs: PinToSettingsTabs }) {
|
||||||
|
const navigation = NavigationNative.useNavigation();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FormRow
|
||||||
|
label={tabs.title()}
|
||||||
|
leading={<FormRow.Icon source={tabs.icon} />}
|
||||||
|
trailing={
|
||||||
|
<>
|
||||||
|
{tabs.trailing ? tabs.trailing : null}
|
||||||
|
<FormRow.Arrow />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
// Based on: https://github.com/pyoncord/Bunny/blob/dev/src/lib/ui/settings/patches/shared.tsx
|
||||||
|
onPress={() => {
|
||||||
|
const Component = tabs.page;
|
||||||
|
|
||||||
|
navigation.navigate("BUNNY_CUSTOM_PAGE", {
|
||||||
|
title: tabs.title(),
|
||||||
|
render: () => <Component />,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function patchPanelUI(tabs: PinToSettingsTabs, patches: (() => void)[]) {
|
||||||
|
const { bunny } = window as any;
|
||||||
|
|
||||||
|
// panel settings were removed in version 264.5 💔
|
||||||
|
try {
|
||||||
|
patches.push(
|
||||||
|
after(
|
||||||
|
"default",
|
||||||
|
bunny.metro.findByNameLazy(
|
||||||
|
"UserSettingsOverviewWrapper",
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
(_, ret) => {
|
||||||
|
const UserSettingsOverview = findInReactTree(
|
||||||
|
ret.props.children,
|
||||||
|
n => n.type?.name === "UserSettingsOverview",
|
||||||
|
);
|
||||||
|
|
||||||
|
patches.push(
|
||||||
|
after(
|
||||||
|
"render",
|
||||||
|
UserSettingsOverview.type.prototype,
|
||||||
|
(_args, res) => {
|
||||||
|
const titles = [
|
||||||
|
intlProxy.BILLING_SETTINGS,
|
||||||
|
intlProxy.PREMIUM_SETTINGS,
|
||||||
|
];
|
||||||
|
|
||||||
|
const sections = findInReactTree(
|
||||||
|
res.props.children,
|
||||||
|
n => n?.children?.[1]?.type === FormSection,
|
||||||
|
)?.children;
|
||||||
|
|
||||||
|
if (sections) {
|
||||||
|
const index = sections.findIndex((c: any) => titles.includes(c?.props.label));
|
||||||
|
|
||||||
|
sections.splice(
|
||||||
|
-~index || 4,
|
||||||
|
0,
|
||||||
|
<Section tabs={tabs} />,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
logger.info("Panel UI patch failed graciously 💔");
|
||||||
|
}
|
||||||
|
}
|
||||||
105
nexxstuff/lib/pinToSettings/tabs.tsx
Normal file
105
nexxstuff/lib/pinToSettings/tabs.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
// Based on: https://github.com/pyoncord/Bunny/blob/dev/src/lib/ui/settings/patches/tabs.tsx
|
||||||
|
|
||||||
|
import { findByProps } from "@vendetta/metro";
|
||||||
|
import { after } from "@vendetta/patcher";
|
||||||
|
import { findInReactTree } from "@vendetta/utils";
|
||||||
|
|
||||||
|
import type { PinToSettingsTabs } from ".";
|
||||||
|
|
||||||
|
const { bunny } = window as any;
|
||||||
|
|
||||||
|
const { TableRowIcon } = findByProps("TableRowIcon");
|
||||||
|
|
||||||
|
const tabsNavigationRef = bunny.metro.findByPropsLazy("getRootNavigationRef");
|
||||||
|
const settingConstants = bunny.metro.findByPropsLazy("SETTING_RENDERER_CONFIG");
|
||||||
|
const createListModule = bunny.metro.findByPropsLazy("createList");
|
||||||
|
const SettingsOverviewScreen = bunny.metro.findByNameLazy(
|
||||||
|
"SettingsOverviewScreen",
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
export function patchTabsUI(tabs: PinToSettingsTabs, patches: (() => void)[]) {
|
||||||
|
const row = {
|
||||||
|
[tabs.key]: {
|
||||||
|
type: "pressable",
|
||||||
|
title: tabs.title,
|
||||||
|
icon: tabs.icon,
|
||||||
|
IconComponent: tabs.icon && (() => <TableRowIcon source={tabs.icon} />),
|
||||||
|
usePredicate: tabs.predicate,
|
||||||
|
useTrailing: tabs.trailing,
|
||||||
|
onPress: () => {
|
||||||
|
const navigation = tabsNavigationRef.getRootNavigationRef();
|
||||||
|
const Component = tabs.page;
|
||||||
|
|
||||||
|
navigation.navigate("BUNNY_CUSTOM_PAGE", {
|
||||||
|
title: tabs.title(),
|
||||||
|
render: () => <Component />,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
withArrow: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let rendererConfigValue = settingConstants.SETTING_RENDERER_CONFIG;
|
||||||
|
|
||||||
|
Object.defineProperty(settingConstants, "SETTING_RENDERER_CONFIG", {
|
||||||
|
enumerable: true,
|
||||||
|
configurable: true,
|
||||||
|
get: () => ({
|
||||||
|
...rendererConfigValue,
|
||||||
|
...row,
|
||||||
|
}),
|
||||||
|
set: v => (rendererConfigValue = v),
|
||||||
|
});
|
||||||
|
|
||||||
|
const firstRender = Symbol("pinToSettings meow meow");
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!createListModule) return;
|
||||||
|
patches.push(
|
||||||
|
after("createList", createListModule, (args) => {
|
||||||
|
// shrug??
|
||||||
|
if (!args[0][firstRender]) {
|
||||||
|
args[0][firstRender] = true;
|
||||||
|
|
||||||
|
const [config] = args;
|
||||||
|
const sections = config.sections;
|
||||||
|
|
||||||
|
const section = sections?.find((x: any) =>
|
||||||
|
["Bunny", "Revenge", "Kettu"].some(
|
||||||
|
mod => x.label === mod && x.title === mod,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (section?.settings) {
|
||||||
|
section.settings = [...section.settings, tabs.key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch {
|
||||||
|
if (!SettingsOverviewScreen) return;
|
||||||
|
patches.push(
|
||||||
|
after("default", SettingsOverviewScreen, (args, ret) => {
|
||||||
|
// shrug??
|
||||||
|
if (!args[0][firstRender]) {
|
||||||
|
args[0][firstRender] = true;
|
||||||
|
|
||||||
|
const { sections } = findInReactTree(
|
||||||
|
ret,
|
||||||
|
i => i.props?.sections,
|
||||||
|
).props;
|
||||||
|
const section = sections?.find((x: any) =>
|
||||||
|
["Bunny", "Revenge", "Kettu"].some(
|
||||||
|
mod => x.label === mod && x.title === mod,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (section?.settings) {
|
||||||
|
section.settings = [...section.settings, tabs.key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
75
nexxstuff/lib/redesign/index.tsx
Normal file
75
nexxstuff/lib/redesign/index.tsx
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { findByProps } from "@vendetta/metro";
|
||||||
|
import { React, ReactNative as RN } from "@vendetta/metro/common";
|
||||||
|
import { showToast } from "@vendetta/ui/toasts";
|
||||||
|
|
||||||
|
import type * as t from "./types"; // shamelessly stolen from Bunny
|
||||||
|
|
||||||
|
const NotFound = (prop: string, isFunction?: boolean) => () => {
|
||||||
|
const trigger = () => {
|
||||||
|
showToast(`${prop} not found! Search for PNF in Debug Logs!`);
|
||||||
|
console.warn(
|
||||||
|
`!! PNF ERROR !!\nMissing the redesign ${
|
||||||
|
isFunction ? "function" : "component"
|
||||||
|
}: ${prop}. Please bug @nexpid about this on Discord!`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isFunction) trigger();
|
||||||
|
else React.useEffect(trigger, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const findProp = (...props: string[]) => findByProps(...props)?.[props[0]];
|
||||||
|
const findPropPolyfill = (isFunction: boolean, ...props: string[]) =>
|
||||||
|
findProp(...props) ?? NotFound(props[0], isFunction);
|
||||||
|
|
||||||
|
// components
|
||||||
|
|
||||||
|
// buttons
|
||||||
|
export const Button = findPropPolyfill(false, "Button") as t.Button;
|
||||||
|
export const RowButton = findPropPolyfill(false, "RowButton") as t.RowButton;
|
||||||
|
export const FloatingActionButton = findPropPolyfill(
|
||||||
|
false,
|
||||||
|
"FloatingActionButton",
|
||||||
|
) as t.FAB;
|
||||||
|
export const PressableScale = findPropPolyfill(
|
||||||
|
false,
|
||||||
|
"PressableScale",
|
||||||
|
) as typeof RN.Pressable;
|
||||||
|
export const IconButton = findPropPolyfill(false, "IconButton") as t.IconButton;
|
||||||
|
export const ContextMenu = findPropPolyfill(
|
||||||
|
false,
|
||||||
|
"ContextMenu",
|
||||||
|
) as t.ContextMenu;
|
||||||
|
|
||||||
|
// inputs
|
||||||
|
const _Slider = findPropPolyfill(false, "Slider");
|
||||||
|
export const Slider =
|
||||||
|
(props => (
|
||||||
|
<RN.View style={{ marginHorizontal: 16, marginTop: 16, marginBottom: 12 }}>
|
||||||
|
<_Slider
|
||||||
|
{...props}
|
||||||
|
onValueChange={(val: any) => props.value !== val && props.onValueChange?.(val)}
|
||||||
|
/>
|
||||||
|
</RN.View>
|
||||||
|
)) as t.Slider;
|
||||||
|
export const TextInput = findPropPolyfill(false, "TextInput") as t.TextInput;
|
||||||
|
|
||||||
|
// tabs
|
||||||
|
export const Tabs = findPropPolyfill(false, "Tabs") as t.Tabs;
|
||||||
|
export const SegmentedControlPages = findPropPolyfill(
|
||||||
|
false,
|
||||||
|
"SegmentedControlPages",
|
||||||
|
) as t.Tabs;
|
||||||
|
|
||||||
|
// views
|
||||||
|
export const Card = findPropPolyfill(false, "Card") as t.Card;
|
||||||
|
export const Stack = findPropPolyfill(false, "Stack") as t.Stack;
|
||||||
|
|
||||||
|
// functions
|
||||||
|
|
||||||
|
export const useSegmentedControlState = findPropPolyfill(
|
||||||
|
true,
|
||||||
|
"useSegmentedControlState",
|
||||||
|
) as t.useSegmentedControlState;
|
||||||
175
nexxstuff/lib/redesign/types.ts
Normal file
175
nexxstuff/lib/redesign/types.ts
Normal file
|
|
@ -0,0 +1,175 @@
|
||||||
|
import type {
|
||||||
|
FlexAlignType,
|
||||||
|
ImageSourcePropType,
|
||||||
|
PressableProps,
|
||||||
|
TextInputProps,
|
||||||
|
ViewProps,
|
||||||
|
} from "react-native";
|
||||||
|
import type { SharedValue } from "react-native-reanimated";
|
||||||
|
|
||||||
|
export type ButtonVariant =
|
||||||
|
| "primary"
|
||||||
|
| "secondary"
|
||||||
|
| "tertiary"
|
||||||
|
| "primary-overlay"
|
||||||
|
| "secondary-overlay"
|
||||||
|
| "destructive"
|
||||||
|
| "active";
|
||||||
|
|
||||||
|
export interface PrimitiveButton {
|
||||||
|
onPress?: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
icon?: ImageSourcePropType | React.ReactNode;
|
||||||
|
style?: ViewProps["style"];
|
||||||
|
}
|
||||||
|
export interface PrimitiveButtonIcon {
|
||||||
|
source: ImageSourcePropType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Button =
|
||||||
|
& React.FC<
|
||||||
|
PrimitiveButton & {
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
text: string;
|
||||||
|
iconPosition?: "start" | "end";
|
||||||
|
grow?: boolean;
|
||||||
|
loading?: boolean;
|
||||||
|
onPressIn?: () => void;
|
||||||
|
onPressOut?: () => void;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
& {
|
||||||
|
Icon: React.FC<PrimitiveButtonIcon>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type RowButton =
|
||||||
|
& React.FC<
|
||||||
|
PrimitiveButton & {
|
||||||
|
icon?: ImageSourcePropType;
|
||||||
|
label: string;
|
||||||
|
subLabel?: string;
|
||||||
|
variant?: "primary" | "secondary";
|
||||||
|
trailing?: React.ReactNode;
|
||||||
|
disabled?: boolean;
|
||||||
|
arrow?: boolean;
|
||||||
|
/** Requires onPress to bet set */
|
||||||
|
onPressIn?: () => void;
|
||||||
|
/** Requires onPress to bet set */
|
||||||
|
onPressOut?: () => void;
|
||||||
|
}
|
||||||
|
>
|
||||||
|
& {
|
||||||
|
Icon: React.FC<
|
||||||
|
PrimitiveButtonIcon & {
|
||||||
|
variant?: "secondary" | "danger" | "blurple" | "boosting-pink";
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FAB = React.FC<{
|
||||||
|
icon: ImageSourcePropType;
|
||||||
|
onPress?: () => void;
|
||||||
|
text?: string;
|
||||||
|
positionBottom?: number;
|
||||||
|
state?: {
|
||||||
|
collapseText?: SharedValue;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type IconButton = React.FC<{
|
||||||
|
icon: ImageSourcePropType | React.ReactNode;
|
||||||
|
onPress?: () => void;
|
||||||
|
onLongPress?: () => void;
|
||||||
|
loading?: boolean;
|
||||||
|
disabled?: boolean;
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
style?: ViewProps["style"];
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type ContextMenu = React.FC<{
|
||||||
|
title?: string;
|
||||||
|
triggerOnLongPress?: boolean;
|
||||||
|
align?: "left" | "right" | "above" | "below";
|
||||||
|
items: {
|
||||||
|
label: string;
|
||||||
|
variant: "default" | "destructive";
|
||||||
|
action: () => void;
|
||||||
|
iconSource?: ImageSourcePropType;
|
||||||
|
// TODO add IconComponent
|
||||||
|
}[];
|
||||||
|
children: (data: {
|
||||||
|
ref: React.MutableRefObject<any>;
|
||||||
|
onPress: (() => void) | undefined;
|
||||||
|
onLongPress: (() => void) | undefined;
|
||||||
|
onAccessibilityAction: () => void;
|
||||||
|
accessibilityActions: { name: string; label: string }[];
|
||||||
|
}) => React.ReactNode | undefined;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type Slider = React.FC<{
|
||||||
|
value: number;
|
||||||
|
accessibilityLabel?: string;
|
||||||
|
accessibilityValue?: {
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
step: number;
|
||||||
|
onValueChange?: (value: number) => void;
|
||||||
|
minimumValue: number;
|
||||||
|
maximumValue: number;
|
||||||
|
onSlidingStart?: () => void;
|
||||||
|
onSlidingComplete?: () => void;
|
||||||
|
startIcon?: React.ReactNode;
|
||||||
|
endIcon?: React.ReactNode;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type TextInput = React.FC<
|
||||||
|
Omit<TextInputProps, "onChange"> & {
|
||||||
|
onChange?: (value: string) => void;
|
||||||
|
size?: "sm" | "md" | "lg";
|
||||||
|
label?: string;
|
||||||
|
description?: React.ReactNode;
|
||||||
|
focusable?: boolean;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
leadingPressableProps?: PressableProps;
|
||||||
|
leadingIcon?: React.FC;
|
||||||
|
leadingText?: string;
|
||||||
|
trailingPressableProps?: PressableProps;
|
||||||
|
trailingIcon?: React.FC;
|
||||||
|
trailingText?: string;
|
||||||
|
isClearable?: boolean;
|
||||||
|
status?: "error" | "default";
|
||||||
|
errorMessage?: string;
|
||||||
|
isCentered?: boolean;
|
||||||
|
grow?: boolean;
|
||||||
|
isRound?: boolean;
|
||||||
|
ref?: any;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
type controlState = symbol;
|
||||||
|
export type Tabs = React.FC<{
|
||||||
|
state: controlState;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type Card = React.FC<ViewProps>;
|
||||||
|
|
||||||
|
export type Stack = React.FC<
|
||||||
|
React.PropsWithChildren<{
|
||||||
|
direction?: "horizontal" | "vertical";
|
||||||
|
spacing?: number;
|
||||||
|
align?: FlexAlignType;
|
||||||
|
style?: ViewProps["style"];
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type useSegmentedControlState = (state: {
|
||||||
|
defaultIndex: number;
|
||||||
|
items: {
|
||||||
|
label: string;
|
||||||
|
id: string;
|
||||||
|
page: React.ReactNode;
|
||||||
|
}[];
|
||||||
|
pageWidth: number;
|
||||||
|
}) => controlState;
|
||||||
17
nexxstuff/lib/ui.ts
Normal file
17
nexxstuff/lib/ui.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { NavigationNative, React } from "@vendetta/metro/common";
|
||||||
|
|
||||||
|
export function managePage(
|
||||||
|
options: {
|
||||||
|
title?: string;
|
||||||
|
headerLeft?: React.FC;
|
||||||
|
headerRight?: React.FC;
|
||||||
|
},
|
||||||
|
navigation?: any,
|
||||||
|
...deps: any[]
|
||||||
|
) {
|
||||||
|
if (!navigation) navigation = NavigationNative.useNavigation();
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
navigation.setOptions(options);
|
||||||
|
}, [navigation, ...deps]);
|
||||||
|
}
|
||||||
294
nexxstuff/migration.tsx
Normal file
294
nexxstuff/migration.tsx
Normal file
|
|
@ -0,0 +1,294 @@
|
||||||
|
import { React, ReactNative } from "@vendetta/metro/common";
|
||||||
|
import { installPlugin, plugins, removePlugin } from "@vendetta/plugins";
|
||||||
|
import { createMMKVBackend } from "@vendetta/storage";
|
||||||
|
import { showConfirmationAlert } from "@vendetta/ui/alerts";
|
||||||
|
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||||
|
import { showToast } from "@vendetta/ui/toasts";
|
||||||
|
import { RichText } from "./components/RichText";
|
||||||
|
import Text from "./components/Text";
|
||||||
|
import { Reanimated, RNFileModule } from "./deps";
|
||||||
|
import { Stack } from "./lib/redesign";
|
||||||
|
|
||||||
|
const { Pressable } = ReactNative;
|
||||||
|
|
||||||
|
interface InvalidDomain {
|
||||||
|
match: RegExp;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const specialIDs = {
|
||||||
|
usrpfp: "userpfp",
|
||||||
|
} as Record<string, string>;
|
||||||
|
const allowedPluginIDs = [
|
||||||
|
PLUGINS_LIST,
|
||||||
|
Object.keys(specialIDs),
|
||||||
|
].flat();
|
||||||
|
|
||||||
|
const invalidDomains: InvalidDomain[] = [{
|
||||||
|
match: /vendetta\.nexpid\.xyz\/(?<plugin>.+)\/$/i,
|
||||||
|
label: "vendetta.nexpid.xyz",
|
||||||
|
}, {
|
||||||
|
match: /dev\.bunny\.nexpid\.xyz\/(?<plugin>.+)\/$/i,
|
||||||
|
label: "dev.bunny.nexpid.xyz",
|
||||||
|
}, {
|
||||||
|
match: /bunny\.nexpid\.xyz\/(?<plugin>.+)\/$/i,
|
||||||
|
label: "bunny.nexpid.xyz",
|
||||||
|
}];
|
||||||
|
|
||||||
|
const migrationLockFilePath = "nexpid/revengemigration.txt";
|
||||||
|
|
||||||
|
function MigrationModal(
|
||||||
|
{ affectedPlugin, domains }: {
|
||||||
|
affectedPlugin?: string;
|
||||||
|
domains: string[];
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
const jumper = Reanimated.useSharedValue(0);
|
||||||
|
const spinner = Reanimated.useSharedValue("0deg");
|
||||||
|
const squisherX = Reanimated.useSharedValue(1);
|
||||||
|
const squisherY = Reanimated.useSharedValue(1);
|
||||||
|
const [toggle, setToggle] = React.useState(true);
|
||||||
|
|
||||||
|
const duration = 300;
|
||||||
|
const fromY = -10;
|
||||||
|
const toY = 30;
|
||||||
|
const spinBetween = 11;
|
||||||
|
|
||||||
|
const squishConfig = { duration: 0 };
|
||||||
|
const squishVal = 1.25;
|
||||||
|
const releaseConfig = {
|
||||||
|
duration: duration * 2,
|
||||||
|
easing: Reanimated.Easing.out(Reanimated.Easing.quad),
|
||||||
|
};
|
||||||
|
const releaseVal = 0.9;
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!toggle) {
|
||||||
|
jumper.value = 0;
|
||||||
|
spinner.value = "0deg";
|
||||||
|
squisherX.value = 1;
|
||||||
|
squisherY.value = 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
jumper.value = Reanimated.withRepeat(
|
||||||
|
Reanimated.withSequence(
|
||||||
|
Reanimated.withTiming(toY, {
|
||||||
|
easing: Reanimated.Easing.out(Reanimated.Easing.quad),
|
||||||
|
duration,
|
||||||
|
}),
|
||||||
|
Reanimated.withTiming(fromY, {
|
||||||
|
easing: Reanimated.Easing.quad,
|
||||||
|
duration,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
spinner.value = Reanimated.withRepeat(
|
||||||
|
Reanimated.withSequence(
|
||||||
|
Reanimated.withTiming("0deg", { duration: (duration * 2) * spinBetween }),
|
||||||
|
Reanimated.withTiming("-360deg", {
|
||||||
|
duration: duration * 2,
|
||||||
|
easing: Reanimated.Easing.out(Reanimated.Easing.exp),
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
|
||||||
|
squisherX.value = Reanimated.withRepeat(
|
||||||
|
Reanimated.withSequence(
|
||||||
|
Reanimated.withTiming(squishVal, squishConfig),
|
||||||
|
Reanimated.withTiming(releaseVal, releaseConfig),
|
||||||
|
),
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
squisherY.value = Reanimated.withRepeat(
|
||||||
|
Reanimated.withSequence(
|
||||||
|
Reanimated.withTiming(1 / squishVal, squishConfig),
|
||||||
|
Reanimated.withTiming(1 / releaseVal, releaseConfig),
|
||||||
|
),
|
||||||
|
-1,
|
||||||
|
);
|
||||||
|
}, [toggle]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack direction="vertical" spacing={8}>
|
||||||
|
<Stack
|
||||||
|
direction="horizontal"
|
||||||
|
spacing={14}
|
||||||
|
style={{
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: 18,
|
||||||
|
marginHorizontal: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Pressable
|
||||||
|
style={{ width: 80, height: 80 }}
|
||||||
|
pointerEvents="box-only"
|
||||||
|
onPress={() => setToggle(!toggle)}
|
||||||
|
accessibilityRole="togglebutton"
|
||||||
|
accessibilityLabel="toggle jumping animation"
|
||||||
|
>
|
||||||
|
<Reanimated.default.Image
|
||||||
|
source={{
|
||||||
|
uri: "https://api.lanyard.rest/853550207039832084.webp?size=128",
|
||||||
|
width: 128,
|
||||||
|
height: 128,
|
||||||
|
}}
|
||||||
|
style={[{
|
||||||
|
borderRadius: 40,
|
||||||
|
width: 80,
|
||||||
|
height: 80,
|
||||||
|
position: "absolute",
|
||||||
|
bottom: 0,
|
||||||
|
}, {
|
||||||
|
bottom: jumper,
|
||||||
|
transform: [{ scaleX: squisherX }, { scaleY: squisherY }, {
|
||||||
|
rotate: spinner,
|
||||||
|
}],
|
||||||
|
}]}
|
||||||
|
/>
|
||||||
|
</Pressable>
|
||||||
|
<Stack direction="vertical" style={{ flex: 1 }}>
|
||||||
|
<Text
|
||||||
|
variant="heading-xl/extrabold"
|
||||||
|
color="TEXT_DEFAULT"
|
||||||
|
style={{ flex: 0 }}
|
||||||
|
>
|
||||||
|
Hey, it's me, nexpid!
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
variant="heading-md/medium"
|
||||||
|
color="TEXT_DEFAULT"
|
||||||
|
style={{ flex: 0 }}
|
||||||
|
>
|
||||||
|
You might know me. I made {affectedPlugin
|
||||||
|
? (
|
||||||
|
<>
|
||||||
|
a plugin you use, <RichText.Bold>{affectedPlugin}</RichText.Bold>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
: "some of the plugins you use"}!
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
<Text
|
||||||
|
variant="text-md/medium"
|
||||||
|
color="TEXT_DEFAULT"
|
||||||
|
>
|
||||||
|
{affectedPlugin
|
||||||
|
? "The plugin you use has"
|
||||||
|
: "Some of my plugins you use have"} one of these domains:
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
variant="text-md/bold"
|
||||||
|
color="TEXT_DEFAULT"
|
||||||
|
style={{ marginHorizontal: 10 }}
|
||||||
|
>
|
||||||
|
{domains.map(label => `• ${label}`).join("\n")}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
variant="text-md/medium"
|
||||||
|
color="TEXT_DEFAULT"
|
||||||
|
>
|
||||||
|
These domains are being deprecated and plugins on them won't receive any new updates. Please
|
||||||
|
use the new domain instead:
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
variant="text-lg/bold"
|
||||||
|
color="TEXT_DEFAULT"
|
||||||
|
style={{ textDecorationLine: "underline" }}
|
||||||
|
align="center"
|
||||||
|
>
|
||||||
|
revenge.nexpid.xyz
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// thanks to the shim, this only gets run once even if multiple plugins are installed
|
||||||
|
export async function runMigration() {
|
||||||
|
if (
|
||||||
|
await RNFileModule.fileExists(
|
||||||
|
`${RNFileModule.DocumentsDirPath}/${migrationLockFilePath}`,
|
||||||
|
)
|
||||||
|
) return;
|
||||||
|
|
||||||
|
const affected: {
|
||||||
|
id: string;
|
||||||
|
link: string;
|
||||||
|
name: string;
|
||||||
|
}[] = [];
|
||||||
|
const isOutdated = new Set<string>();
|
||||||
|
for (const plugin of Object.keys(plugins)) {
|
||||||
|
const parser = invalidDomains.find(x => x.match.test(plugin));
|
||||||
|
const id = parser && plugin.match(parser.match)?.groups?.plugin;
|
||||||
|
|
||||||
|
if (id && allowedPluginIDs.includes(id)) {
|
||||||
|
affected.push({
|
||||||
|
id,
|
||||||
|
link: plugin,
|
||||||
|
name: plugins[plugin].manifest.name,
|
||||||
|
});
|
||||||
|
isOutdated.add(parser.label);
|
||||||
|
|
||||||
|
if (plugin.includes("bn-plugins.github.io/vd-proxy")) {
|
||||||
|
isOutdated.add("proxied plugin links");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOutdated.size) {
|
||||||
|
showConfirmationAlert({
|
||||||
|
// @ts-expect-error Missing from typings
|
||||||
|
children: (
|
||||||
|
<MigrationModal
|
||||||
|
affectedPlugin={affected.length === 1 ? affected[0].name : undefined}
|
||||||
|
domains={[...isOutdated.values()]}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
confirmText: "Migrate now",
|
||||||
|
async onConfirm() {
|
||||||
|
showToast("Starting migration");
|
||||||
|
|
||||||
|
console.log("MIGRATION STARTS HERE, AFFECTED", affected);
|
||||||
|
try {
|
||||||
|
// take all plugins that are bad & make good :blush:
|
||||||
|
for (const { id, link, name } of affected) {
|
||||||
|
const newPlugin = `https://revenge.nexpid.xyz/${specialIDs[id] ?? id}/`;
|
||||||
|
console.log(
|
||||||
|
"FROM",
|
||||||
|
link,
|
||||||
|
"TO",
|
||||||
|
newPlugin,
|
||||||
|
);
|
||||||
|
|
||||||
|
await createMMKVBackend(newPlugin).set(await createMMKVBackend(link).get());
|
||||||
|
if (!plugins[newPlugin]) await installPlugin(newPlugin);
|
||||||
|
// returns a promise (bug in types)
|
||||||
|
await removePlugin(link);
|
||||||
|
showToast(`Migrated ${name}`);
|
||||||
|
}
|
||||||
|
console.log("MIGRATION ENDS HERE");
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Migration error", e);
|
||||||
|
return showToast(
|
||||||
|
`Got an error! ${e} (ping nexpid about this!!!)`,
|
||||||
|
getAssetIDByName("CircleXIcon-primary"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast(
|
||||||
|
"Migrated successfully! Please reload your client to apply changes",
|
||||||
|
getAssetIDByName("CircleCheckIcon-primary"),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
cancelText: "Remind me later",
|
||||||
|
secondaryConfirmText: "I understand (don't show again)",
|
||||||
|
onConfirmSecondary() {
|
||||||
|
RNFileModule.writeFile("documents", migrationLockFilePath, "revenged", "utf8");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
262
nexxstuff/types.tsx
Normal file
262
nexxstuff/types.tsx
Normal file
|
|
@ -0,0 +1,262 @@
|
||||||
|
import { findByProps, findByStoreName } from "@vendetta/metro";
|
||||||
|
import { FluxDispatcher, ReactNative as RN } from "@vendetta/metro/common";
|
||||||
|
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||||
|
import { showToast } from "@vendetta/ui/toasts";
|
||||||
|
import type { StyleSheet } from "react-native";
|
||||||
|
|
||||||
|
import { plugin } from "@vendetta";
|
||||||
|
import { before } from "@vendetta/patcher";
|
||||||
|
import type Modal from "./components/Modal";
|
||||||
|
import { RNChatModule } from "./deps";
|
||||||
|
import type { ChatRowTypeMessage } from "./typings";
|
||||||
|
|
||||||
|
const ThemeStore = findByStoreName("ThemeStore");
|
||||||
|
const { triggerHaptic } = findByProps("triggerHaptic");
|
||||||
|
|
||||||
|
const colorModule = findByProps("colors", "unsafe_rawColors");
|
||||||
|
const colorResolver = colorModule?.internal ?? colorModule?.meta;
|
||||||
|
|
||||||
|
const { useThemeContext } = findByProps("useThemeContext");
|
||||||
|
|
||||||
|
export const TextStyleSheet = findByProps("TextStyleSheet")
|
||||||
|
.TextStyleSheet as _TextStyleSheet;
|
||||||
|
|
||||||
|
export const Navigator = findByProps("Navigator")?.Navigator;
|
||||||
|
export const modalCloseButton = findByProps(
|
||||||
|
"getHeaderCloseButton",
|
||||||
|
)?.getHeaderCloseButton;
|
||||||
|
export const { popModal, pushModal } = findByProps("popModal", "pushModal");
|
||||||
|
|
||||||
|
export type Entries<T> = [keyof T, T[keyof T]];
|
||||||
|
|
||||||
|
// ...
|
||||||
|
export const getDiscordTheme = () => {
|
||||||
|
// getDefaultFallbackTheme is not exported :(
|
||||||
|
const { theme } = ThemeStore;
|
||||||
|
|
||||||
|
if (theme.startsWith("vd-theme-")) return theme.split("-")[3];
|
||||||
|
return theme;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolveCustomSemantic = (dark: any, light: any, inverted?: boolean) =>
|
||||||
|
getDiscordTheme() === "light" ? (inverted ? dark : light) : (inverted ? light : dark);
|
||||||
|
|
||||||
|
export const lerp = (og: string, target: string, perc: number) => {
|
||||||
|
const hex2rgb = (hex: string) =>
|
||||||
|
hex.match(/\w\w/g)?.map(x => Number.parseInt(x, 16)) ?? [0, 0, 0];
|
||||||
|
const rgb2hex = (rgb: number[]) => `#${rgb.map(x => x.toString(16).padStart(2, "0")).join("")}`;
|
||||||
|
|
||||||
|
const ogR = hex2rgb(og);
|
||||||
|
const targetR = hex2rgb(target);
|
||||||
|
|
||||||
|
const result = ogR.map((ogC, i) => Math.round(ogC + (targetR[i] - ogC) * perc));
|
||||||
|
|
||||||
|
return rgb2hex(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function resolveSemanticColor(
|
||||||
|
color: any,
|
||||||
|
theme: string = ThemeStore.theme,
|
||||||
|
) {
|
||||||
|
return (color && colorResolver?.resolveSemanticColor(theme, color)) || "#000000";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getUserAvatar(
|
||||||
|
user: {
|
||||||
|
discriminator: string;
|
||||||
|
avatar?: string;
|
||||||
|
id: string;
|
||||||
|
},
|
||||||
|
animated?: boolean,
|
||||||
|
): string {
|
||||||
|
const isPomelo = user.discriminator === "0";
|
||||||
|
|
||||||
|
return user.avatar
|
||||||
|
? `https://cdn.discordapp.com/avatars/${user.id}/${
|
||||||
|
animated && user.avatar.startsWith("a_")
|
||||||
|
? `${user.avatar}.gif`
|
||||||
|
: `${user.avatar}.png`
|
||||||
|
}`
|
||||||
|
: `https://cdn.discordapp.com/embed/avatars/${
|
||||||
|
isPomelo
|
||||||
|
? (Number.parseInt(user.id) >> 22) % 6
|
||||||
|
: Number.parseInt(user.discriminator) % 5
|
||||||
|
}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openModal(key: string, modal: typeof Modal) {
|
||||||
|
const empty = Symbol("empty");
|
||||||
|
if (!Navigator || !modalCloseButton) {
|
||||||
|
showToast(
|
||||||
|
`${
|
||||||
|
[
|
||||||
|
Navigator ? empty : "Navigator",
|
||||||
|
modalCloseButton ? empty : "modalCloseButton",
|
||||||
|
]
|
||||||
|
.filter(x => x !== empty)
|
||||||
|
.join(", ")
|
||||||
|
} is missing! Please try reinstalling your client.`,
|
||||||
|
getAssetIDByName("CircleXIcon-primary"),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pushModal({
|
||||||
|
key,
|
||||||
|
modal: {
|
||||||
|
key,
|
||||||
|
modal,
|
||||||
|
animation: "slide-up",
|
||||||
|
shouldPersistUnderModals: false,
|
||||||
|
closable: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function doHaptic(dur: number): Promise<void> {
|
||||||
|
triggerHaptic();
|
||||||
|
const interval = setInterval(() => triggerHaptic(), 1);
|
||||||
|
return new Promise(res => setTimeout(() => res(clearInterval(interval)), dur));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function androidifyColor(color: string, alpha = 255): number {
|
||||||
|
const [_, r, g, b] = color.match(/#([A-F0-9]{2})([A-F0-9]{2})([A-F0-9]{2})/i) ?? [];
|
||||||
|
if (!_) return 0;
|
||||||
|
|
||||||
|
return (
|
||||||
|
((alpha & 0xff) << 24)
|
||||||
|
| ((Number.parseInt(r, 16) & 0xff) << 16)
|
||||||
|
| ((Number.parseInt(g, 16) & 0xff) << 8)
|
||||||
|
| (Number.parseInt(b, 16) & 0xff)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function fluxSubscribe(
|
||||||
|
topic: string,
|
||||||
|
callback: (data: any) => void,
|
||||||
|
once?: boolean,
|
||||||
|
) {
|
||||||
|
const cback = (data: any) => (
|
||||||
|
callback(data), once && FluxDispatcher.unsubscribe(topic, cback)
|
||||||
|
);
|
||||||
|
FluxDispatcher.subscribe(topic, cback);
|
||||||
|
return () => FluxDispatcher.unsubscribe(topic, cback);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatBytes(bytes: number) {
|
||||||
|
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
||||||
|
if (bytes === 0) return "0 B";
|
||||||
|
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
|
return `${(bytes / 1024 ** i).toFixed(2)} ${sizes[i]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deepEquals(x: any, y: any) {
|
||||||
|
if (x === y) return true;
|
||||||
|
if (typeof x === "object" && typeof y === "object" && x && y) {
|
||||||
|
if (Object.keys(x).length !== Object.keys(y).length) return false;
|
||||||
|
|
||||||
|
for (const prop in x) {
|
||||||
|
if (Object.hasOwn(y, prop)) {
|
||||||
|
if (!deepEquals(x[prop], y[prop])) return false;
|
||||||
|
} else return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDuration(duration: number) {
|
||||||
|
const seconds = duration % 60;
|
||||||
|
const minutes = Math.floor(duration / 60) % 60;
|
||||||
|
const hours = Math.floor(duration / 3600);
|
||||||
|
|
||||||
|
if (hours > 0) {
|
||||||
|
return `${hours}:${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
return `${minutes}:${String(seconds).padStart(2, "0")}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createThemeContextStyleSheet<
|
||||||
|
T extends StyleSheet.NamedStyles<any>,
|
||||||
|
>(sheet: T) {
|
||||||
|
const themeContext = useThemeContext();
|
||||||
|
|
||||||
|
for (const key in sheet) {
|
||||||
|
// @ts-expect-error types
|
||||||
|
sheet[key] = new Proxy(RN.StyleSheet.flatten(sheet[key]), {
|
||||||
|
get(target, prop, receiver) {
|
||||||
|
const res = Reflect.get(target, prop, receiver);
|
||||||
|
return colorResolver.isSemanticColor(res)
|
||||||
|
? resolveSemanticColor(res, themeContext?.theme)
|
||||||
|
: res;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return sheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function patchRows(callback: (rows: ChatRowTypeMessage[]) => void) {
|
||||||
|
return before("updateRows", RNChatModule, (args) => {
|
||||||
|
const rows = JSON.parse(args[1]);
|
||||||
|
try {
|
||||||
|
callback(rows);
|
||||||
|
} catch (e: any) {
|
||||||
|
console.error(`[nexpid.patchRows][${plugin.id}]`, e.stack);
|
||||||
|
}
|
||||||
|
|
||||||
|
args[1] = JSON.stringify(rows);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
export type TextStyleSheetCase = "normal" | "medium" | "semibold" | "bold";
|
||||||
|
|
||||||
|
type TextStyleSheetHasExtraBoldCase =
|
||||||
|
| "heading-sm"
|
||||||
|
| "heading-md"
|
||||||
|
| "heading-lg"
|
||||||
|
| "heading-xl"
|
||||||
|
| "heading-xxl"
|
||||||
|
| "heading-deprecated-12";
|
||||||
|
export type TextStyleSheetHasCase =
|
||||||
|
| TextStyleSheetHasExtraBoldCase
|
||||||
|
| "text-xxs"
|
||||||
|
| "text-xs"
|
||||||
|
| "text-sm"
|
||||||
|
| "text-md"
|
||||||
|
| "text-lg"
|
||||||
|
| "redesign/message-preview"
|
||||||
|
| "redesign/channel-title";
|
||||||
|
|
||||||
|
export type TextStyleSheetVariant =
|
||||||
|
| `${TextStyleSheetHasCase}/${TextStyleSheetCase}`
|
||||||
|
| `${TextStyleSheetHasExtraBoldCase}/${TextStyleSheetCase | "extrabold"}`
|
||||||
|
| "eyebrow"
|
||||||
|
| "redesign/heading-18/bold"
|
||||||
|
| "display-sm"
|
||||||
|
| "display-md"
|
||||||
|
| "display-lg";
|
||||||
|
|
||||||
|
type _TextStyleSheet = Record<
|
||||||
|
TextStyleSheetVariant,
|
||||||
|
{
|
||||||
|
fontSize: number;
|
||||||
|
lineHeight: number;
|
||||||
|
textTransform: "none" | "capitalize" | "uppercase" | "lowercase";
|
||||||
|
fontFamily: string;
|
||||||
|
includeFontPadding: boolean;
|
||||||
|
letterSpacing?: number;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export interface SearchContext {
|
||||||
|
type: string;
|
||||||
|
[key: PropertyKey]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isObject(x: Record<any, any>) {
|
||||||
|
return typeof x === "object" && !Array.isArray(x);
|
||||||
|
}
|
||||||
177
nexxstuff/typings.d.ts
vendored
Normal file
177
nexxstuff/typings.d.ts
vendored
Normal file
|
|
@ -0,0 +1,177 @@
|
||||||
|
export interface DisplayProfileData {
|
||||||
|
displayProfile: {
|
||||||
|
userId: string;
|
||||||
|
guildId?: string;
|
||||||
|
banner?: string;
|
||||||
|
bio?: string;
|
||||||
|
pronouns: string;
|
||||||
|
accentColor?: number;
|
||||||
|
emoji?: any;
|
||||||
|
themeColors?: number[];
|
||||||
|
popoutAnimationParticleType?: any;
|
||||||
|
_profileThemesExperimentBucket?: number;
|
||||||
|
_guildMemberProfile?: GuildMemberProfile;
|
||||||
|
};
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface User {
|
||||||
|
hasFlag: (flag: any) => boolean;
|
||||||
|
isStaff: () => boolean;
|
||||||
|
isStaffPersonal: () => boolean;
|
||||||
|
hasAnyStaffLevel: () => boolean;
|
||||||
|
id: string;
|
||||||
|
username: string;
|
||||||
|
discriminator: string;
|
||||||
|
avatar?: string;
|
||||||
|
avatarDecoration?: string;
|
||||||
|
email?: string;
|
||||||
|
verified: boolean;
|
||||||
|
bot: boolean;
|
||||||
|
system: boolean;
|
||||||
|
mfaEnabled: boolean;
|
||||||
|
mobile: boolean;
|
||||||
|
desktop: boolean;
|
||||||
|
premiumType?: number;
|
||||||
|
flags: number;
|
||||||
|
publicFlags: number;
|
||||||
|
purchasedFlags: number;
|
||||||
|
premiumUsageFlags: number;
|
||||||
|
phone?: string;
|
||||||
|
nsfwAllowed?: boolean;
|
||||||
|
guildMemberAvatars: Record<string, string>;
|
||||||
|
hasBouncedEmail: boolean; // ???
|
||||||
|
personalConnectionId?: any; // ???
|
||||||
|
globalName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GuildMemberProfile {
|
||||||
|
userId: string;
|
||||||
|
guildId: string;
|
||||||
|
avatar?: string;
|
||||||
|
banner?: string;
|
||||||
|
accentColor?: number;
|
||||||
|
emoji?: any;
|
||||||
|
themeColors?: number[];
|
||||||
|
popoutAnimationParticleType?: any;
|
||||||
|
bio?: string;
|
||||||
|
pronouns?: string;
|
||||||
|
badges: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReactionEvent {
|
||||||
|
channelId: string;
|
||||||
|
messageId: string;
|
||||||
|
userId: string;
|
||||||
|
emoji: { name: string; id?: string; animated?: boolean };
|
||||||
|
burst: boolean;
|
||||||
|
colors: string[];
|
||||||
|
messageAuthorId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type VendettaSysColor = [
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
];
|
||||||
|
export type VendettaSysColors = Record<
|
||||||
|
"neutral1" | "neutral2" | "accent1" | "accent2" | "accent3",
|
||||||
|
VendettaSysColor
|
||||||
|
>;
|
||||||
|
|
||||||
|
type PlusColorResolvable =
|
||||||
|
| string
|
||||||
|
| [string, string | undefined, string | undefined, string | undefined];
|
||||||
|
export interface PlusStructureV0 {
|
||||||
|
icons?: Record<string, PlusColorResolvable>;
|
||||||
|
mentionLineColor?: PlusColorResolvable;
|
||||||
|
customOverlays?: boolean;
|
||||||
|
iconpack?: string;
|
||||||
|
version: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PlusStructure = PlusStructureV0;
|
||||||
|
|
||||||
|
export type ThemeWithPlus = Theme & {
|
||||||
|
data: ThemeDataWithPlus;
|
||||||
|
};
|
||||||
|
export type ThemeDataWithPlus = ThemeData & {
|
||||||
|
plus?: PlusStructure;
|
||||||
|
};
|
||||||
|
|
||||||
|
// chat row stuff
|
||||||
|
export type ContentRow = {
|
||||||
|
type: "text";
|
||||||
|
content: string;
|
||||||
|
} | {
|
||||||
|
type:
|
||||||
|
| "strong"
|
||||||
|
| "s"
|
||||||
|
| "italic"
|
||||||
|
| "em"
|
||||||
|
| "underline"
|
||||||
|
| "u"
|
||||||
|
| "strikethrough"
|
||||||
|
| "subtext";
|
||||||
|
content: ContentRow[];
|
||||||
|
} | {
|
||||||
|
type: "spoiler";
|
||||||
|
channelId: string;
|
||||||
|
content: ContentRow[];
|
||||||
|
} | {
|
||||||
|
type: "heading";
|
||||||
|
level: number;
|
||||||
|
content: ContentRow[];
|
||||||
|
} | {
|
||||||
|
type: "link";
|
||||||
|
target: string;
|
||||||
|
content: ContentRow[];
|
||||||
|
} | {
|
||||||
|
type: "list";
|
||||||
|
ordered: boolean;
|
||||||
|
start?: number;
|
||||||
|
items: ContentRow[];
|
||||||
|
} | {
|
||||||
|
type: "emoji";
|
||||||
|
content: string;
|
||||||
|
surrogate: string;
|
||||||
|
jumboable?: boolean;
|
||||||
|
} | {
|
||||||
|
type: "customEmoji";
|
||||||
|
id: string;
|
||||||
|
alt: string;
|
||||||
|
src: string;
|
||||||
|
frozenSrc: string;
|
||||||
|
jumboable?: boolean;
|
||||||
|
};
|
||||||
|
export type ContentRowIs<Type extends ContentRow["type"]> = Extract<ContentRow, { type: Type }>;
|
||||||
|
|
||||||
|
interface MessageEmbed {
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChatRowTypeMessage {
|
||||||
|
type: 1;
|
||||||
|
message: {
|
||||||
|
id: string;
|
||||||
|
authorId: string;
|
||||||
|
channelId: string;
|
||||||
|
guildId?: string;
|
||||||
|
content?: ContentRow[];
|
||||||
|
embeds?: MessageEmbed[];
|
||||||
|
codedLinks?: any[];
|
||||||
|
communicationDisabled?: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChatRow = ChatRowTypeMessage;
|
||||||
|
|
@ -4,6 +4,7 @@ import { semanticColors } from "@vendetta/ui";
|
||||||
import { getAssetIDByName } from "@vendetta/ui/assets";
|
import { getAssetIDByName } from "@vendetta/ui/assets";
|
||||||
import { showToast } from "@vendetta/ui/toasts";
|
import { showToast } from "@vendetta/ui/toasts";
|
||||||
import { findByProps } from "@vendetta/metro";
|
import { findByProps } from "@vendetta/metro";
|
||||||
|
import Text from "../../../../nexxstuff/components/Text";
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const [clickCounter, setClickCounter] = React.useState(0);
|
const [clickCounter, setClickCounter] = React.useState(0);
|
||||||
|
|
@ -87,24 +88,7 @@ export default function Header() {
|
||||||
textContainer: {
|
textContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
marginLeft: 4,
|
marginLeft: 4,
|
||||||
},
|
justifyContent: "center",
|
||||||
title: {
|
|
||||||
fontSize: 28,
|
|
||||||
fontWeight: "700",
|
|
||||||
color: semanticColors.MOBILE_TEXT_HEADING_PRIMARY,
|
|
||||||
marginBottom: 4,
|
|
||||||
textAlign: "left",
|
|
||||||
},
|
|
||||||
subtitle: {
|
|
||||||
fontSize: 14,
|
|
||||||
color: semanticColors.TEXT_MUTED,
|
|
||||||
textAlign: "left",
|
|
||||||
lineHeight: 20,
|
|
||||||
},
|
|
||||||
avatarPlaceholder: {
|
|
||||||
width: 48,
|
|
||||||
height: 48,
|
|
||||||
tintColor: semanticColors.INTERACTIVE_NORMAL,
|
|
||||||
},
|
},
|
||||||
animatedIndicator: {
|
animatedIndicator: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
|
|
@ -123,6 +107,11 @@ export default function Header() {
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight: "bold",
|
fontWeight: "bold",
|
||||||
},
|
},
|
||||||
|
avatarPlaceholder: {
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
tintColor: semanticColors.INTERACTIVE_NORMAL,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch user's avatar on component mount
|
// Fetch user's avatar on component mount
|
||||||
|
|
@ -257,10 +246,12 @@ export default function Header() {
|
||||||
</RN.Pressable>
|
</RN.Pressable>
|
||||||
|
|
||||||
<RN.View style={styles.textContainer}>
|
<RN.View style={styles.textContainer}>
|
||||||
<RN.Text style={styles.title}>Commands</RN.Text>
|
<Text variant="display-md" color="TEXT_DEFAULT" align="left">
|
||||||
<RN.Text style={styles.subtitle}>
|
Commands
|
||||||
|
</Text>
|
||||||
|
<Text variant="text-md/bold" color="TEXT_MUTED" align="left">
|
||||||
A collection of useful commands for Vendetta like clients
|
A collection of useful commands for Vendetta like clients
|
||||||
</RN.Text>
|
</Text>
|
||||||
</RN.View>
|
</RN.View>
|
||||||
</RN.View>
|
</RN.View>
|
||||||
</RN.View>
|
</RN.View>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue