forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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 ·{' '}
196 </Text>
197 )}
198 {timeElapsed}
199 </>
200 )}
201 </WebOnlyInlineLinkText>
202 )}
203 </TimeElapsed>
204 </View>
205 </View>
206 )
207}
208PostMeta = memo(PostMeta)
209export {PostMeta}