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