Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
119
fork

Configure Feed

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

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