Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 6d68a5bd212dd4eeee816828ffe4e27601cdd7f3 211 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 {useProfileShadow} from '#/state/cache/profile-shadow' 12import {useModerationOpts} from '#/state/preferences/moderation-opts' 13import {useListConvosQuery} from '#/state/queries/messages/list-conversations' 14import {useSession} from '#/state/session' 15import {UserAvatar} from '#/view/com/util/UserAvatar' 16import {atoms as a, tokens, useTheme} from '#/alf' 17import {Button} from '#/components/Button' 18import {useDialogContext} from '#/components/Dialog' 19import {Text} from '#/components/Typography' 20import {useSimpleVerificationState} from '#/components/verification' 21import {VerificationCheck} from '#/components/verification/VerificationCheck' 22import {useAnalytics} from '#/analytics' 23import type * as bsky from '#/types/bsky' 24 25export function RecentChats({postUri}: {postUri: string}) { 26 const ax = useAnalytics() 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 ax.metric('share:press:recentDm', {}) 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 post 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 return ( 159 <View 160 style={[ 161 a.flex_col, 162 {width: WIDTH, height: WIDTH + 15}, 163 a.gap_xs, 164 a.justify_start, 165 a.align_center, 166 ]}> 167 <View 168 style={[ 169 t.atoms.bg_contrast_50, 170 {width: WIDTH - 8, height: WIDTH - 8}, 171 a.rounded_full, 172 ]} 173 /> 174 <View 175 style={[ 176 t.atoms.bg_contrast_50, 177 {width: WIDTH - 8, height: 10}, 178 a.rounded_xs, 179 ]} 180 /> 181 </View> 182 ) 183} 184 185function NoConvos() { 186 const t = useTheme() 187 188 return ( 189 <View 190 style={[ 191 a.absolute, 192 a.inset_0, 193 a.justify_center, 194 a.align_center, 195 a.px_2xl, 196 ]}> 197 <View 198 style={[a.absolute, a.inset_0, t.atoms.bg_contrast_25, {opacity: 0.5}]} 199 /> 200 <Text 201 style={[ 202 a.text_sm, 203 t.atoms.text_contrast_high, 204 a.text_center, 205 a.font_semi_bold, 206 ]}> 207 <Trans>Start a conversation, and it will appear here.</Trans> 208 </Text> 209 </View> 210 ) 211}