Bluesky app fork with some witchin' additions 馃挮
at main 212 lines 6.0 kB view raw
1import {ScrollView, View} from 'react-native' 2import {moderateProfile, type ModerationOpts} from '@atproto/api' 3import {msg, Trans} from '@lingui/macro' 4import {useLingui} from '@lingui/react' 5import {useNavigation} from '@react-navigation/native' 6 7import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted' 8import {type NavigationProp} from '#/lib/routes/types' 9import {sanitizeDisplayName} from '#/lib/strings/display-names' 10import {sanitizeHandle} from '#/lib/strings/handles' 11import {logger} from '#/logger' 12import {useProfileShadow} from '#/state/cache/profile-shadow' 13import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' 14import {useModerationOpts} from '#/state/preferences/moderation-opts' 15import {useListConvosQuery} from '#/state/queries/messages/list-conversations' 16import {useSession} from '#/state/session' 17import {UserAvatar} from '#/view/com/util/UserAvatar' 18import {atoms as a, tokens, useTheme} from '#/alf' 19import {Button} from '#/components/Button' 20import {useDialogContext} from '#/components/Dialog' 21import {Text} from '#/components/Typography' 22import {useSimpleVerificationState} from '#/components/verification' 23import {VerificationCheck} from '#/components/verification/VerificationCheck' 24import type * as bsky from '#/types/bsky' 25 26export function RecentChats({postUri}: {postUri: string}) { 27 const control = useDialogContext() 28 const {currentAccount} = useSession() 29 const {data} = useListConvosQuery({status: 'accepted'}) 30 const convos = data?.pages[0]?.convos?.slice(0, 10) 31 const moderationOpts = useModerationOpts() 32 const navigation = useNavigation<NavigationProp>() 33 34 const onSelectChat = (convoId: string) => { 35 control.close(() => { 36 logger.metric('share:press:recentDm', {}, {statsig: true}) 37 navigation.navigate('MessagesConversation', { 38 conversation: convoId, 39 embed: postUri, 40 }) 41 }) 42 } 43 44 if (!moderationOpts) return null 45 46 return ( 47 <View 48 style={[a.relative, a.flex_1, {marginHorizontal: tokens.space.md * -1}]}> 49 <ScrollView 50 horizontal 51 style={[a.flex_1, a.pt_2xs, {minHeight: 98}]} 52 contentContainerStyle={[a.gap_sm, a.px_md]} 53 showsHorizontalScrollIndicator={false} 54 nestedScrollEnabled> 55 {convos && convos.length > 0 ? ( 56 convos.map(convo => { 57 const otherMember = convo.members.find( 58 member => member.did !== currentAccount?.did, 59 ) 60 61 if ( 62 !otherMember || 63 otherMember.handle === 'missing.invalid' || 64 convo.muted 65 ) 66 return null 67 68 return ( 69 <RecentChatItem 70 key={convo.id} 71 profile={otherMember} 72 onPress={() => onSelectChat(convo.id)} 73 moderationOpts={moderationOpts} 74 /> 75 ) 76 }) 77 ) : ( 78 <> 79 <ConvoSkeleton /> 80 <ConvoSkeleton /> 81 <ConvoSkeleton /> 82 <ConvoSkeleton /> 83 <ConvoSkeleton /> 84 </> 85 )} 86 </ScrollView> 87 {convos && convos.length === 0 && <NoConvos />} 88 </View> 89 ) 90} 91 92const WIDTH = 80 93 94function RecentChatItem({ 95 profile: profileUnshadowed, 96 onPress, 97 moderationOpts, 98}: { 99 profile: bsky.profile.AnyProfileView 100 onPress: () => void 101 moderationOpts: ModerationOpts 102}) { 103 const {_} = useLingui() 104 const t = useTheme() 105 106 const profile = useProfileShadow(profileUnshadowed) 107 108 const moderation = moderateProfile(profile, moderationOpts) 109 const name = sanitizeDisplayName( 110 profile.displayName || sanitizeHandle(profile.handle), 111 moderation.ui('displayName'), 112 ) 113 const verification = useSimpleVerificationState({profile}) 114 115 if (isBlockedOrBlocking(profile) || isMuted(profile)) { 116 return null 117 } 118 119 return ( 120 <Button 121 onPress={onPress} 122 label={_(msg`Send skeet to ${name}`)} 123 style={[ 124 a.flex_col, 125 {width: WIDTH}, 126 a.gap_sm, 127 a.justify_start, 128 a.align_center, 129 ]}> 130 <UserAvatar 131 avatar={profile.avatar} 132 size={WIDTH - 8} 133 type={profile.associated?.labeler ? 'labeler' : 'user'} 134 moderation={moderation.ui('avatar')} 135 /> 136 <View style={[a.flex_row, a.align_center, a.justify_center, a.w_full]}> 137 <Text 138 emoji 139 style={[a.text_xs, a.leading_snug, t.atoms.text_contrast_medium]} 140 numberOfLines={1}> 141 {name} 142 </Text> 143 {verification.showBadge && ( 144 <View style={[a.pl_2xs]}> 145 <VerificationCheck 146 width={10} 147 verifier={verification.role === 'verifier'} 148 /> 149 </View> 150 )} 151 </View> 152 </Button> 153 ) 154} 155 156function ConvoSkeleton() { 157 const t = useTheme() 158 const enableSquareButtons = useEnableSquareButtons() 159 return ( 160 <View 161 style={[ 162 a.flex_col, 163 {width: WIDTH, height: WIDTH + 15}, 164 a.gap_xs, 165 a.justify_start, 166 a.align_center, 167 ]}> 168 <View 169 style={[ 170 t.atoms.bg_contrast_50, 171 {width: WIDTH - 8, height: WIDTH - 8}, 172 enableSquareButtons ? a.rounded_sm : a.rounded_full, 173 ]} 174 /> 175 <View 176 style={[ 177 t.atoms.bg_contrast_50, 178 {width: WIDTH - 8, height: 10}, 179 a.rounded_xs, 180 ]} 181 /> 182 </View> 183 ) 184} 185 186function NoConvos() { 187 const t = useTheme() 188 189 return ( 190 <View 191 style={[ 192 a.absolute, 193 a.inset_0, 194 a.justify_center, 195 a.align_center, 196 a.px_2xl, 197 ]}> 198 <View 199 style={[a.absolute, a.inset_0, t.atoms.bg_contrast_25, {opacity: 0.5}]} 200 /> 201 <Text 202 style={[ 203 a.text_sm, 204 t.atoms.text_contrast_high, 205 a.text_center, 206 a.font_semi_bold, 207 ]}> 208 <Trans>Start a conversation, and it will appear here.</Trans> 209 </Text> 210 </View> 211 ) 212}