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

Configure Feed

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

at cope-settings-sync 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}