revenge-bundle-next/src/plugins/start/user-badges/index.tsx

225 lines
6.8 KiB
TypeScript
Raw Normal View History

2025-07-13 16:30:20 +07:00
import { AlertActionCreators } from '@revenge-mod/discord/actions'
import { Design } from '@revenge-mod/discord/design'
import { byProps } from '@revenge-mod/modules/finders/filters'
import { getModule } from '@revenge-mod/modules/finders/get'
import { InternalPluginFlags, registerPlugin } from '@revenge-mod/plugins/_'
import { PluginFlags } from '@revenge-mod/plugins/constants'
import { afterJSX, beforeJSX } from '@revenge-mod/react/jsx-runtime'
import { findInReactFiber } from '@revenge-mod/utils/react'
import { Image } from 'react-native'
import { Badges, UsersWithBadges } from './constants'
2025-07-15 22:36:45 +07:00
import { styles, useBadgeStyles } from './styles'
2025-07-13 16:30:20 +07:00
import { afterReconciled } from './utils'
2025-07-15 22:36:45 +07:00
import type { FC, ReactElement } from 'react'
2025-07-13 16:30:20 +07:00
import type {
ImageProps,
ImageSourcePropType,
ImageStyle,
PressableProps,
StyleProp,
} from 'react-native'
2025-07-15 22:36:45 +07:00
import type { Badge, BadgeId } from './constants'
2025-07-13 16:30:20 +07:00
const DUMMY_SYMBOL = {} as unknown as string
2025-07-15 22:36:45 +07:00
interface ProfileBadgeProps {
id: string
userId: string
label: string
source: ImageSourcePropType
themeType: string
badgeSize: number
}
interface ProfileBadgeRowsProps {
userId: string
badges: Array<{
description: string
icon: string
id: string
link?: string
}>
themeType: string
showToastOnPress?: boolean
}
type ViewWithProfileBadges = ReactElement<{
children: Array<ReactElement<ProfileBadgeProps, FC<ProfileBadgeProps>>>
}>
2025-07-13 16:30:20 +07:00
registerPlugin(
{
id: 'revenge.user-badges',
name: 'User Badges',
description: 'Badges for Revenge contributors and sponsors.',
author: 'Revenge',
icon: 'ShieldUserIcon',
},
{
start({ cleanup }) {
const unsub = getModule(
2025-07-15 22:36:45 +07:00
byProps<{ ProfileBadgeRows: FC<ProfileBadgeRowsProps> }>(
'ProfileBadgeRows',
),
2025-07-13 16:30:20 +07:00
({ ProfileBadgeRows }) => {
unsub()
cleanup(
beforeJSX(ProfileBadgeRows, args => {
const [, props] = args
2025-07-15 22:36:45 +07:00
// Inject dummy badge for users with custom badges to ensure ProfileBadge components exist
2025-07-13 16:30:20 +07:00
if (
2025-07-15 22:36:45 +07:00
// has custom badges
2025-07-13 16:30:20 +07:00
UsersWithBadges[props.userId] &&
2025-07-15 22:36:45 +07:00
// but no actual badges
2025-07-13 16:30:20 +07:00
!props.badges.length
)
2025-07-15 22:36:45 +07:00
// @ts-expect-error: This will never be rendered
props.badges.unshift({ id: DUMMY_SYMBOL })
2025-07-13 16:30:20 +07:00
return args
}),
)
cleanup(
afterJSX(ProfileBadgeRows, fiber => {
2025-07-15 22:36:45 +07:00
patchProfileBadgeRows(fiber)
2025-07-13 16:30:20 +07:00
return fiber
}),
)
},
)
cleanup(unsub)
},
stop({ plugin }) {
plugin.flags |= PluginFlags.ReloadRequired
},
},
PluginFlags.Enabled,
// Essential because this is a perk
InternalPluginFlags.Internal | InternalPluginFlags.Essential,
)
function patchProfileBadgeRows(
2025-07-15 22:36:45 +07:00
element: ReactElement<ProfileBadgeRowsProps, FC<ProfileBadgeRowsProps>>,
): void {
const unpatch = afterReconciled(element, fiber => {
2025-07-13 16:30:20 +07:00
unpatch()
2025-07-15 22:36:45 +07:00
const container = findInReactFiber(
2025-07-13 16:30:20 +07:00
fiber as ReactElement,
isViewContainingProfileBadgeElements,
)
2025-07-15 22:36:45 +07:00
if (!container) return fiber
const [{ type: ProfileBadge, props }] = container.props.children
const userBadges = UsersWithBadges[props.userId]
2025-07-13 16:30:20 +07:00
2025-07-15 22:36:45 +07:00
if (userBadges) {
for (const badgeId of userBadges) {
const badge = Badges[badgeId]
if (!badge) continue
const badgeElement = (
<ProfileBadge
{...props}
key={badgeId}
id={badgeId}
label={badge.label}
source={badge.icon}
/>
)
2025-07-13 16:30:20 +07:00
2025-07-15 22:36:45 +07:00
patchProfileBadge(badgeElement, badgeId, badge)
container.props.children.push(badgeElement)
2025-07-13 16:30:20 +07:00
}
2025-07-15 22:36:45 +07:00
// Remove dummy badge if it exists
if (props.id === DUMMY_SYMBOL) container.props.children.shift()
2025-07-13 16:30:20 +07:00
}
return fiber
})
}
function patchProfileBadge(
2025-07-15 22:36:45 +07:00
element: ReactElement<ProfileBadgeProps, FC<ProfileBadgeProps>>,
id: BadgeId,
2025-07-13 16:30:20 +07:00
badge: Badge,
2025-07-15 22:36:45 +07:00
): void {
const { bnw, showDialog } = badge
2025-07-15 22:36:45 +07:00
// Return early if we don't actually need to patch this badge
if (!bnw && !showDialog) return
2025-07-13 16:30:20 +07:00
2025-07-15 22:36:45 +07:00
const unpatch = afterReconciled(element, fiber => {
unpatch()
2025-07-13 16:30:20 +07:00
2025-07-15 22:36:45 +07:00
// This will not result in a conditional hook error because badges do not change flags during runtime
const badgeStyles = useBadgeStyles()
2025-07-13 16:30:20 +07:00
2025-07-15 22:36:45 +07:00
if (showDialog) {
const pressable = findInReactFiber(
fiber as ReactElement,
(node): node is ReactElement<PressableProps> =>
node.type?.render,
)
if (pressable)
pressable.props.onPress = () =>
openBadgeDialog(id, badge, badgeStyles)
}
2025-07-13 16:30:20 +07:00
2025-07-15 22:36:45 +07:00
if (bnw) {
const image = findInReactFiber(
fiber as ReactElement,
(node): node is ReactElement<ImageProps> => node.type === Image,
)
if (image) {
const styles = image.props.style as Extract<
StyleProp<ImageStyle>,
any[]
>
styles.push(badgeStyles.tinted)
2025-07-13 16:30:20 +07:00
}
2025-07-15 22:36:45 +07:00
}
2025-07-13 16:30:20 +07:00
2025-07-15 22:36:45 +07:00
return fiber
})
2025-07-13 16:30:20 +07:00
}
2025-07-15 22:36:45 +07:00
const { AlertActionButton, AlertModal, Stack, Text } = Design
2025-07-13 16:30:20 +07:00
function openBadgeDialog(
2025-07-15 22:36:45 +07:00
id: BadgeId,
{ label, description, bnw, icon }: Badge,
2025-07-15 22:36:45 +07:00
badgeStyles: ReturnType<typeof useBadgeStyles>,
): void {
2025-07-13 16:30:20 +07:00
AlertActionCreators.openAlert(
2025-07-15 22:36:45 +07:00
`REVENGE_PROFILE_BADGE-${id}`,
<AlertModal
2025-07-13 16:30:20 +07:00
title={
2025-07-15 22:36:45 +07:00
<Stack style={styles.stack}>
2025-07-13 16:30:20 +07:00
<Image
source={icon}
2025-07-15 22:36:45 +07:00
style={[styles.display, bnw && badgeStyles.tinted]}
2025-07-13 16:30:20 +07:00
/>
2025-07-15 22:36:45 +07:00
<Text variant="heading-lg/bold" color="header-primary">
2025-07-13 16:30:20 +07:00
{label}
2025-07-15 22:36:45 +07:00
</Text>
</Stack>
2025-07-13 16:30:20 +07:00
}
content={description}
2025-07-15 22:36:45 +07:00
actions={<AlertActionButton text="Okay" />}
2025-07-13 16:30:20 +07:00
/>,
)
}
const isViewContainingProfileBadgeElements = (
node: any,
2025-07-15 22:36:45 +07:00
): node is ViewWithProfileBadges =>
node.props?.children?.[0]?.type.name === 'ProfileBadge'