Bluesky app fork with some witchin' additions 馃挮
at readme-update 213 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 {useProfileShadow} from '#/state/cache/profile-shadow' 12import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons' 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 skeet 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 const enableSquareButtons = useEnableSquareButtons() 160 return ( 161 <View 162 style={[ 163 a.flex_col, 164 {width: WIDTH, height: WIDTH + 15}, 165 a.gap_xs, 166 a.justify_start, 167 a.align_center, 168 ]}> 169 <View 170 style={[ 171 t.atoms.bg_contrast_50, 172 {width: WIDTH - 8, height: WIDTH - 8}, 173 enableSquareButtons ? a.rounded_sm : a.rounded_full, 174 ]} 175 /> 176 <View 177 style={[ 178 t.atoms.bg_contrast_50, 179 {width: WIDTH - 8, height: 10}, 180 a.rounded_xs, 181 ]} 182 /> 183 </View> 184 ) 185} 186 187function NoConvos() { 188 const t = useTheme() 189 190 return ( 191 <View 192 style={[ 193 a.absolute, 194 a.inset_0, 195 a.justify_center, 196 a.align_center, 197 a.px_2xl, 198 ]}> 199 <View 200 style={[a.absolute, a.inset_0, t.atoms.bg_contrast_25, {opacity: 0.5}]} 201 /> 202 <Text 203 style={[ 204 a.text_sm, 205 t.atoms.text_contrast_high, 206 a.text_center, 207 a.font_semi_bold, 208 ]}> 209 <Trans>Start a conversation, and it will appear here.</Trans> 210 </Text> 211 </View> 212 ) 213}