Files
bsky-social-app/src/components/dms/ChatProfileTabs.tsx
T
2026-08-13 12:26:23 -07:00

170 lines
4.6 KiB
TypeScript

import {useCallback, useEffect} from 'react'
import {type ScrollView, View} from 'react-native'
import Animated, {useAnimatedRef, useSharedValue} from 'react-native-reanimated'
import {moderateProfile} from '@bsky/sdk/moderation'
import {useLingui} from '@lingui/react/macro'
import {HITSLOP_10} from '#/lib/constants'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {DraggableScrollView} from '#/view/com/pager/DraggableScrollView'
import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
import * as ProfileCard from '#/components/ProfileCard'
import {Text} from '#/components/Typography'
import type * as bsky from '#/types/bsky'
type Props = {
testID?: string
profiles: bsky.profile.AnyProfileView[]
onRemove?: (did: string) => void
}
export function ChatProfileTabs({testID, profiles, onRemove}: Props) {
const t = useTheme()
const scrollElRef = useAnimatedRef<ScrollView>()
const contentSize = useSharedValue(0)
const scrollX = useSharedValue(0)
useEffect(() => {
requestAnimationFrame(() => {
// Scroll to the end of the list when `profiles` changes.
scrollElRef.current?.scrollToEnd({animated: true})
})
}, [profiles, scrollElRef])
return (
<View testID={testID} accessibilityRole="list" style={[t.atoms.bg]}>
<DraggableScrollView
ref={scrollElRef}
testID={`${testID}-selector`}
horizontal={true}
showsHorizontalScrollIndicator={false}
onScroll={e => {
scrollX.set(Math.round(e.nativeEvent.contentOffset.x))
}}>
<Animated.View
style={[
a.flex_row,
a.flex_grow,
a.gap_sm,
a.align_center,
a.justify_start,
]}
onLayout={e => {
contentSize.set(e.nativeEvent.layout.width)
}}>
{profiles.map((profile, index) => (
<Tab
key={profile.did}
testID={testID}
index={index}
profile={profile}
total={profiles.length}
onRemove={onRemove}
/>
))}
</Animated.View>
</DraggableScrollView>
</View>
)
}
function Tab({
testID,
index,
profile,
total,
onRemove,
}: {
testID?: string
index: number
profile: bsky.profile.AnyProfileView
total: number
onRemove?: (did: string) => void
}) {
const t = useTheme()
const {t: l} = useLingui()
const moderationOpts = useModerationOpts()
const moderation = moderateProfile(profile, moderationOpts!)
const displayName = sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'),
)
const onPressItem = useCallback(
(did: string) => {
onRemove?.(did)
},
[onRemove],
)
return (
<View
testID={`${testID}-selector-${profile.did}`}
style={[
a.flex_row,
a.align_center,
a.border,
a.justify_center,
a.rounded_lg,
a.pl_xs,
a.pr_sm,
a.py_xs,
t.atoms.border_contrast_low,
t.atoms.bg,
index === 0 ? a.ml_lg : index === total - 1 ? a.mr_lg : null,
]}>
{moderationOpts ? (
<>
<ProfileCard.Avatar
profile={profile}
moderationOpts={moderationOpts}
size={24}
disabledPreview
/>
<View style={[a.flex_row, a.align_center, a.max_w_full, a.ml_xs]}>
<Text
emoji
style={[
a.text_sm,
a.font_normal,
a.leading_snug,
a.self_start,
a.flex_shrink,
t.atoms.text,
]}
numberOfLines={1}>
{displayName}
</Text>
</View>
</>
) : (
<>
<ProfileCard.AvatarPlaceholder size={24} />
<ProfileCard.NamePlaceholder />
</>
)}
<Button
hitSlop={HITSLOP_10}
label={l`Remove ${displayName} from group chat`}
style={[a.ml_xs]}
onPress={() => onPressItem(profile.did)}>
{({hovered, pressed, focused}) => (
<XIcon
size="sm"
style={[
hovered || pressed || focused
? t.atoms.text
: t.atoms.text_contrast_high,
]}
/>
)}
</Button>
</View>
)
}