forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {useCallback, useEffect} from 'react'
2import {type ScrollView, View} from 'react-native'
3import Animated, {useAnimatedRef, useSharedValue} from 'react-native-reanimated'
4import {moderateProfile} from '@atproto/api'
5import {useLingui} from '@lingui/react/macro'
6
7import {HITSLOP_10} from '#/lib/constants'
8import {sanitizeDisplayName} from '#/lib/strings/display-names'
9import {sanitizeHandle} from '#/lib/strings/handles'
10import {useModerationOpts} from '#/state/preferences/moderation-opts'
11import {DraggableScrollView} from '#/view/com/pager/DraggableScrollView'
12import {atoms as a, useTheme} from '#/alf'
13import {Button} from '#/components/Button'
14import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times'
15import * as ProfileCard from '#/components/ProfileCard'
16import {Text} from '#/components/Typography'
17import type * as bsky from '#/types/bsky'
18
19type Props = {
20 testID?: string
21 profiles: bsky.profile.AnyProfileView[]
22 onRemove?: (did: string) => void
23}
24
25export function ChatProfileTabs({testID, profiles, onRemove}: Props) {
26 const t = useTheme()
27 const scrollElRef = useAnimatedRef<ScrollView>()
28 const contentSize = useSharedValue(0)
29 const scrollX = useSharedValue(0)
30
31 useEffect(() => {
32 requestAnimationFrame(() => {
33 // Scroll to the end of the list when `profiles` changes.
34 scrollElRef.current?.scrollToEnd({animated: true})
35 })
36 }, [profiles, scrollElRef])
37
38 return (
39 <View testID={testID} accessibilityRole="list" style={[t.atoms.bg]}>
40 <DraggableScrollView
41 ref={scrollElRef}
42 testID={`${testID}-selector`}
43 horizontal={true}
44 showsHorizontalScrollIndicator={false}
45 onScroll={e => {
46 scrollX.set(Math.round(e.nativeEvent.contentOffset.x))
47 }}>
48 <Animated.View
49 style={[
50 a.flex_row,
51 a.flex_grow,
52 a.gap_sm,
53 a.align_center,
54 a.justify_start,
55 ]}
56 onLayout={e => {
57 contentSize.set(e.nativeEvent.layout.width)
58 }}>
59 {profiles.map((profile, index) => (
60 <Tab
61 key={profile.did}
62 testID={testID}
63 index={index}
64 profile={profile}
65 total={profiles.length}
66 onRemove={onRemove}
67 />
68 ))}
69 </Animated.View>
70 </DraggableScrollView>
71 </View>
72 )
73}
74
75function Tab({
76 testID,
77 index,
78 profile,
79 total,
80 onRemove,
81}: {
82 testID?: string
83 index: number
84 profile: bsky.profile.AnyProfileView
85 total: number
86 onRemove?: (did: string) => void
87}) {
88 const t = useTheme()
89 const {t: l} = useLingui()
90 const moderationOpts = useModerationOpts()
91
92 const moderation = moderateProfile(profile, moderationOpts!)
93 const displayName = sanitizeDisplayName(
94 profile.displayName || sanitizeHandle(profile.handle),
95 moderation.ui('displayName'),
96 )
97
98 const onPressItem = useCallback(
99 (did: string) => {
100 onRemove?.(did)
101 },
102 [onRemove],
103 )
104
105 return (
106 <View
107 testID={`${testID}-selector-${profile.did}`}
108 style={[
109 a.flex_row,
110 a.align_center,
111 a.border,
112 a.justify_center,
113 a.rounded_lg,
114 a.pl_xs,
115 a.pr_sm,
116 a.py_xs,
117 t.atoms.border_contrast_low,
118 t.atoms.bg,
119 index === 0 ? a.ml_lg : index === total - 1 ? a.mr_lg : null,
120 ]}>
121 {moderationOpts ? (
122 <>
123 <ProfileCard.Avatar
124 profile={profile}
125 moderationOpts={moderationOpts}
126 size={24}
127 disabledPreview
128 />
129 <View style={[a.flex_row, a.align_center, a.max_w_full, a.ml_xs]}>
130 <Text
131 emoji
132 style={[
133 a.text_sm,
134 a.font_normal,
135 a.leading_snug,
136 a.self_start,
137 a.flex_shrink,
138 t.atoms.text,
139 ]}
140 numberOfLines={1}>
141 {displayName}
142 </Text>
143 </View>
144 </>
145 ) : (
146 <>
147 <ProfileCard.AvatarPlaceholder size={24} />
148 <ProfileCard.NamePlaceholder />
149 </>
150 )}
151 <Button
152 hitSlop={HITSLOP_10}
153 label={l`Remove ${displayName} from group chat`}
154 style={[a.ml_xs]}
155 onPress={() => onPressItem(profile.did)}>
156 {({hovered, pressed, focused}) => (
157 <XIcon
158 size="sm"
159 style={[
160 hovered || pressed || focused
161 ? t.atoms.text
162 : t.atoms.text_contrast_high,
163 ]}
164 />
165 )}
166 </Button>
167 </View>
168 )
169}