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