Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 169 lines 4.7 kB view raw
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}