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

Configure Feed

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

at main 205 lines 5.7 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 {ProfileBadges} from '#/components/ProfileBadges' 22import {Text} from '#/components/Typography' 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 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 <ProfileBadges profile={profile} size="xs" style={[a.pl_2xs]} /> 144 </View> 145 </Button> 146 ) 147} 148 149function ConvoSkeleton() { 150 const t = useTheme() 151 const enableSquareButtons = useEnableSquareButtons() 152 return ( 153 <View 154 style={[ 155 a.flex_col, 156 {width: WIDTH, height: WIDTH + 15}, 157 a.gap_xs, 158 a.justify_start, 159 a.align_center, 160 ]}> 161 <View 162 style={[ 163 t.atoms.bg_contrast_50, 164 {width: WIDTH - 8, height: WIDTH - 8}, 165 enableSquareButtons ? a.rounded_sm : a.rounded_full, 166 ]} 167 /> 168 <View 169 style={[ 170 t.atoms.bg_contrast_50, 171 {width: WIDTH - 8, height: 10}, 172 a.rounded_xs, 173 ]} 174 /> 175 </View> 176 ) 177} 178 179function NoConvos() { 180 const t = useTheme() 181 182 return ( 183 <View 184 style={[ 185 a.absolute, 186 a.inset_0, 187 a.justify_center, 188 a.align_center, 189 a.px_2xl, 190 ]}> 191 <View 192 style={[a.absolute, a.inset_0, t.atoms.bg_contrast_25, {opacity: 0.5}]} 193 /> 194 <Text 195 style={[ 196 a.text_sm, 197 t.atoms.text_contrast_high, 198 a.text_center, 199 a.font_semi_bold, 200 ]}> 201 <Trans>Start a conversation, and it will appear here.</Trans> 202 </Text> 203 </View> 204 ) 205}