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