feat: nuke nexxlib
This commit is contained in:
parent
ddcef7c85b
commit
fc5eb71659
26 changed files with 0 additions and 2362 deletions
|
|
@ -1,82 +0,0 @@
|
|||
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,
|
||||
);
|
||||
};
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
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,
|
||||
},
|
||||
),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
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 }]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,227 +0,0 @@
|
|||
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",
|
||||
)!;
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
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;
|
||||
}
|
||||
|
|
@ -1,68 +0,0 @@
|
|||
// 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;
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
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();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -1,91 +0,0 @@
|
|||
// 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 💔");
|
||||
}
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
// 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];
|
||||
}
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
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;
|
||||
|
|
@ -1,175 +0,0 @@
|
|||
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;
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
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]);
|
||||
}
|
||||
|
|
@ -1,294 +0,0 @@
|
|||
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");
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -1,262 +0,0 @@
|
|||
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
177
nexxstuff/typings.d.ts
vendored
|
|
@ -1,177 +0,0 @@
|
|||
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;
|
||||
|
|
@ -7,10 +7,6 @@
|
|||
"esModuleInterop": true,
|
||||
"jsx": "react-native",
|
||||
"allowJs": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@nlib/*": ["nexxstuff/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"plugins",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue