Bluesky app fork with some witchin' additions 馃挮
at main 209 lines 7.1 kB view raw
1import {memo, useCallback} from 'react' 2import {type StyleProp, View, type ViewStyle} from 'react-native' 3import {type AppBskyActorDefs, type ModerationDecision} from '@atproto/api' 4import {msg} from '@lingui/macro' 5import {useLingui} from '@lingui/react' 6import {useQueryClient} from '@tanstack/react-query' 7 8import {useActorStatus} from '#/lib/actor-status' 9import {makeProfileLink} from '#/lib/routes/links' 10import {forceLTR} from '#/lib/strings/bidi' 11import {NON_BREAKING_SPACE} from '#/lib/strings/constants' 12import {sanitizeDisplayName} from '#/lib/strings/display-names' 13import {sanitizeHandle} from '#/lib/strings/handles' 14import {sanitizePronouns} from '#/lib/strings/pronouns' 15import {niceDate} from '#/lib/strings/time' 16import {useProfileShadow} from '#/state/cache/profile-shadow' 17import {precacheProfile} from '#/state/queries/profile' 18import {atoms as a, platform, useTheme, web} from '#/alf' 19import {WebOnlyInlineLinkText} from '#/components/Link' 20import {ProfileHoverCard} from '#/components/ProfileHoverCard' 21import {Text} from '#/components/Typography' 22import {useSimpleVerificationState} from '#/components/verification' 23import {VerificationCheck} from '#/components/verification/VerificationCheck' 24import {IS_ANDROID} from '#/env' 25import {TimeElapsed} from './TimeElapsed' 26import {PreviewableUserAvatar} from './UserAvatar' 27 28interface PostMetaOpts { 29 author: AppBskyActorDefs.ProfileViewBasic 30 moderation: ModerationDecision | undefined 31 postHref: string 32 timestamp: string 33 showAvatar?: boolean 34 showPronouns?: boolean 35 avatarSize?: number 36 onOpenAuthor?: () => void 37 style?: StyleProp<ViewStyle> 38} 39 40let PostMeta = (opts: PostMetaOpts): React.ReactNode => { 41 const t = useTheme() 42 const {i18n, _} = useLingui() 43 44 const author = useProfileShadow(opts.author) 45 const displayName = author.displayName || author.handle 46 const handle = author.handle 47 // remove dumb typing when you update the atproto api package!! 48 const pronouns = (author as {pronouns?: string})?.pronouns 49 const profileLink = makeProfileLink(author) 50 const queryClient = useQueryClient() 51 const onOpenAuthor = opts.onOpenAuthor 52 const onBeforePressAuthor = useCallback(() => { 53 precacheProfile(queryClient, author) 54 onOpenAuthor?.() 55 }, [queryClient, author, onOpenAuthor]) 56 const onBeforePressPost = useCallback(() => { 57 precacheProfile(queryClient, author) 58 }, [queryClient, author]) 59 60 const timestampLabel = niceDate(i18n, opts.timestamp) 61 const verification = useSimpleVerificationState({profile: author}) 62 const {isActive: live} = useActorStatus(author) 63 64 return ( 65 <View 66 style={[ 67 IS_ANDROID ? a.flex_1 : a.flex_shrink, 68 a.flex_row, 69 a.align_center, 70 a.pb_xs, 71 a.gap_xs, 72 a.z_20, 73 opts.style, 74 ]}> 75 {opts.showAvatar && ( 76 <View style={[a.self_center, a.mr_2xs]}> 77 <PreviewableUserAvatar 78 size={opts.avatarSize || 16} 79 profile={author} 80 moderation={opts.moderation?.ui('avatar')} 81 type={author.associated?.labeler ? 'labeler' : 'user'} 82 live={live} 83 hideLiveBadge 84 /> 85 </View> 86 )} 87 <View style={[a.flex_row, a.align_end, a.flex_shrink]}> 88 <ProfileHoverCard did={author.did}> 89 <View style={[a.flex_row, a.align_end, a.flex_shrink]}> 90 <WebOnlyInlineLinkText 91 emoji 92 numberOfLines={1} 93 to={profileLink} 94 label={_(msg`View profile`)} 95 disableMismatchWarning 96 onPress={onBeforePressAuthor} 97 style={[ 98 a.text_md, 99 a.font_semi_bold, 100 t.atoms.text, 101 a.leading_tight, 102 a.flex_shrink_0, 103 {maxWidth: '70%'}, 104 ]}> 105 {forceLTR( 106 sanitizeDisplayName( 107 displayName, 108 opts.moderation?.ui('displayName'), 109 ), 110 )} 111 </WebOnlyInlineLinkText> 112 {verification.showBadge && ( 113 <View 114 style={[ 115 a.pl_2xs, 116 a.self_center, 117 { 118 marginTop: platform({web: 1, ios: 0, android: -1}), 119 }, 120 ]}> 121 <VerificationCheck 122 width={platform({android: 13, default: 12})} 123 verifier={verification.role === 'verifier'} 124 /> 125 </View> 126 )} 127 <WebOnlyInlineLinkText 128 emoji 129 numberOfLines={1} 130 to={profileLink} 131 label={_(msg`View profile`)} 132 disableMismatchWarning 133 disableUnderline 134 onPress={onBeforePressAuthor} 135 style={[ 136 a.text_md, 137 t.atoms.text_contrast_medium, 138 {lineHeight: 1.17}, 139 {flexShrink: 10}, 140 ]}> 141 {NON_BREAKING_SPACE + sanitizeHandle(handle, '@')} 142 </WebOnlyInlineLinkText> 143 {opts.showPronouns && pronouns && ( 144 <WebOnlyInlineLinkText 145 emoji 146 numberOfLines={1} 147 to={profileLink} 148 label={_(msg`View Profile`)} 149 disableMismatchWarning 150 disableUnderline 151 onPress={onBeforePressAuthor} 152 style={[ 153 t.atoms.text_contrast_low, 154 a.pl_2xs, 155 a.text_md, 156 {lineHeight: 1.17}, 157 {flexShrink: 5}, 158 ]}> 159 {NON_BREAKING_SPACE + sanitizePronouns(pronouns)} 160 </WebOnlyInlineLinkText> 161 )} 162 </View> 163 </ProfileHoverCard> 164 165 <TimeElapsed timestamp={opts.timestamp}> 166 {({timeElapsed}) => ( 167 <WebOnlyInlineLinkText 168 to={opts.postHref} 169 label={timestampLabel} 170 title={timestampLabel} 171 disableMismatchWarning 172 disableUnderline 173 onPress={onBeforePressPost} 174 style={[ 175 a.pl_xs, 176 a.text_md, 177 a.leading_tight, 178 IS_ANDROID && a.flex_grow, 179 a.text_right, 180 t.atoms.text_contrast_medium, 181 web({ 182 whiteSpace: 'nowrap', 183 }), 184 ]}> 185 {!opts.showPronouns && ( 186 <> 187 {!IS_ANDROID && ( 188 <Text 189 style={[ 190 a.text_md, 191 a.leading_tight, 192 t.atoms.text_contrast_medium, 193 ]} 194 accessible={false}> 195 &middot;{' '} 196 </Text> 197 )} 198 {timeElapsed} 199 </> 200 )} 201 </WebOnlyInlineLinkText> 202 )} 203 </TimeElapsed> 204 </View> 205 </View> 206 ) 207} 208PostMeta = memo(PostMeta) 209export {PostMeta}