Bluesky app fork with some witchin' additions 馃挮
at 5ee667f307bc459ba53cdaabdad00a0ea1ee6846 212 lines 5.9 kB view raw
1import {ScrollView, View} from 'react-native' 2import {moderateProfile, type ModerationOpts} from '@atproto/api' 3import {msg} from '@lingui/core/macro' 4import {useLingui} from '@lingui/react' 5import {Trans} from '@lingui/react/macro' 6import {useNavigation} from '@react-navigation/native' 7 8import {isBlockedOrBlocking, isMuted} from '#/lib/moderation/blocked-and-muted' 9import {type NavigationProp} from '#/lib/routes/types' 10import {sanitizeDisplayName} from '#/lib/strings/display-names' 11import {sanitizeHandle} from '#/lib/strings/handles' 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 {useAnalytics} from '#/analytics' 24import type * as bsky from '#/types/bsky' 25 26export function RecentChats({postUri}: {postUri: string}) { 27 const ax = useAnalytics() 28 const control = useDialogContext() 29 const {currentAccount} = useSession() 30 const {data} = useListConvosQuery({status: 'accepted'}) 31 const convos = data?.pages[0]?.convos?.slice(0, 10) 32 const moderationOpts = useModerationOpts() 33 const navigation = useNavigation<NavigationProp>() 34 35 const onSelectChat = (convoId: string) => { 36 control.close(() => { 37 ax.metric('share:press:recentDm', {}) 38 navigation.navigate('MessagesConversation', { 39 conversation: convoId, 40 embed: postUri, 41 }) 42 }) 43 } 44 45 if (!moderationOpts) return null 46 47 return ( 48 <View 49 style={[a.relative, a.flex_1, {marginHorizontal: tokens.space.md * -1}]}> 50 <ScrollView 51 horizontal 52 style={[a.flex_1, a.pt_2xs, {minHeight: 98}]} 53 contentContainerStyle={[a.gap_sm, a.px_md]} 54 showsHorizontalScrollIndicator={false} 55 nestedScrollEnabled> 56 {convos && convos.length > 0 ? ( 57 convos.map(convo => { 58 const otherMember = convo.members.find( 59 member => member.did !== currentAccount?.did, 60 ) 61 62 if ( 63 !otherMember || 64 otherMember.handle === 'missing.invalid' || 65 convo.muted 66 ) 67 return null 68 69 return ( 70 <RecentChatItem 71 key={convo.id} 72 profile={otherMember} 73 onPress={() => onSelectChat(convo.id)} 74 moderationOpts={moderationOpts} 75 /> 76 ) 77 }) 78 ) : ( 79 <> 80 <ConvoSkeleton /> 81 <ConvoSkeleton /> 82 <ConvoSkeleton /> 83 <ConvoSkeleton /> 84 <ConvoSkeleton /> 85 </> 86 )} 87 </ScrollView> 88 {convos && convos.length === 0 && <NoConvos />} 89 </View> 90 ) 91} 92 93const WIDTH = 80 94 95function RecentChatItem({ 96 profile: profileUnshadowed, 97 onPress, 98 moderationOpts, 99}: { 100 profile: bsky.profile.AnyProfileView 101 onPress: () => void 102 moderationOpts: ModerationOpts 103}) { 104 const {_} = useLingui() 105 const t = useTheme() 106 107 const profile = useProfileShadow(profileUnshadowed) 108 109 const moderation = moderateProfile(profile, moderationOpts) 110 const name = sanitizeDisplayName( 111 profile.displayName || sanitizeHandle(profile.handle), 112 moderation.ui('displayName'), 113 ) 114 const verification = useSimpleVerificationState({profile}) 115 116 if (isBlockedOrBlocking(profile) || isMuted(profile)) { 117 return null 118 } 119 120 return ( 121 <Button 122 onPress={onPress} 123 label={_(msg`Send post to ${name}`)} 124 style={[ 125 a.flex_col, 126 {width: WIDTH}, 127 a.gap_sm, 128 a.justify_start, 129 a.align_center, 130 ]}> 131 <UserAvatar 132 avatar={profile.avatar} 133 size={WIDTH - 8} 134 type={profile.associated?.labeler ? 'labeler' : 'user'} 135 moderation={moderation.ui('avatar')} 136 /> 137 <View style={[a.flex_row, a.align_center, a.justify_center, a.w_full]}> 138 <Text 139 emoji 140 style={[a.text_xs, a.leading_snug, t.atoms.text_contrast_medium]} 141 numberOfLines={1}> 142 {name} 143 </Text> 144 {verification.showBadge && ( 145 <View style={[a.pl_2xs]}> 146 <VerificationCheck 147 width={10} 148 verifier={verification.role === 'verifier'} 149 /> 150 </View> 151 )} 152 </View> 153 </Button> 154 ) 155} 156 157function ConvoSkeleton() { 158 const t = useTheme() 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 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}