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 {niceDate} from '#/lib/strings/time'
15import {useProfileShadow} from '#/state/cache/profile-shadow'
16import {precacheProfile} from '#/state/queries/profile'
17import {atoms as a, platform, useTheme, web} from '#/alf'
18import {WebOnlyInlineLinkText} from '#/components/Link'
19import {ProfileHoverCard} from '#/components/ProfileHoverCard'
20import {Text} from '#/components/Typography'
21import {useSimpleVerificationState} from '#/components/verification'
22import {VerificationCheck} from '#/components/verification/VerificationCheck'
23import {IS_ANDROID} from '#/env'
24import {TimeElapsed} from './TimeElapsed'
25import {PreviewableUserAvatar} from './UserAvatar'
26
27interface PostMetaOpts {
28 author: AppBskyActorDefs.ProfileViewBasic
29 moderation: ModerationDecision | undefined
30 postHref: string
31 timestamp: string
32 showAvatar?: boolean
33 avatarSize?: number
34 onOpenAuthor?: () => void
35 style?: StyleProp<ViewStyle>
36}
37
38let PostMeta = (opts: PostMetaOpts): React.ReactNode => {
39 const t = useTheme()
40 const {i18n, _} = useLingui()
41
42 const author = useProfileShadow(opts.author)
43 const displayName = author.displayName || author.handle
44 const handle = author.handle
45 const profileLink = makeProfileLink(author)
46 const queryClient = useQueryClient()
47 const onOpenAuthor = opts.onOpenAuthor
48 const onBeforePressAuthor = useCallback(() => {
49 precacheProfile(queryClient, author)
50 onOpenAuthor?.()
51 }, [queryClient, author, onOpenAuthor])
52 const onBeforePressPost = useCallback(() => {
53 precacheProfile(queryClient, author)
54 }, [queryClient, author])
55
56 const timestampLabel = niceDate(i18n, opts.timestamp)
57 const verification = useSimpleVerificationState({profile: author})
58 const {isActive: live} = useActorStatus(author)
59
60 return (
61 <View
62 style={[
63 IS_ANDROID ? a.flex_1 : a.flex_shrink,
64 a.flex_row,
65 a.align_center,
66 a.pb_xs,
67 a.gap_xs,
68 a.z_20,
69 opts.style,
70 ]}>
71 {opts.showAvatar && (
72 <View style={[a.self_center, a.mr_2xs]}>
73 <PreviewableUserAvatar
74 size={opts.avatarSize || 16}
75 profile={author}
76 moderation={opts.moderation?.ui('avatar')}
77 type={author.associated?.labeler ? 'labeler' : 'user'}
78 live={live}
79 hideLiveBadge
80 />
81 </View>
82 )}
83 <View style={[a.flex_row, a.align_end, a.flex_shrink]}>
84 <ProfileHoverCard did={author.did}>
85 <View style={[a.flex_row, a.align_end, a.flex_shrink]}>
86 <WebOnlyInlineLinkText
87 emoji
88 numberOfLines={1}
89 to={profileLink}
90 label={_(msg`View profile`)}
91 disableMismatchWarning
92 onPress={onBeforePressAuthor}
93 style={[
94 a.text_md,
95 a.font_semi_bold,
96 t.atoms.text,
97 a.leading_tight,
98 a.flex_shrink_0,
99 {maxWidth: '70%'},
100 ]}>
101 {forceLTR(
102 sanitizeDisplayName(
103 displayName,
104 opts.moderation?.ui('displayName'),
105 ),
106 )}
107 </WebOnlyInlineLinkText>
108 {verification.showBadge && (
109 <View
110 style={[
111 a.pl_2xs,
112 a.self_center,
113 {
114 marginTop: platform({web: 1, ios: 0, android: -1}),
115 },
116 ]}>
117 <VerificationCheck
118 width={platform({android: 13, default: 12})}
119 verifier={verification.role === 'verifier'}
120 />
121 </View>
122 )}
123 <WebOnlyInlineLinkText
124 emoji
125 numberOfLines={1}
126 to={profileLink}
127 label={_(msg`View profile`)}
128 disableMismatchWarning
129 disableUnderline
130 onPress={onBeforePressAuthor}
131 style={[
132 a.text_md,
133 t.atoms.text_contrast_medium,
134 {lineHeight:1.17},
135 {flexShrink: 10},
136 ]}>
137 {NON_BREAKING_SPACE + sanitizeHandle(handle, '@')}
138 </WebOnlyInlineLinkText>
139 </View>
140 </ProfileHoverCard>
141
142 <TimeElapsed timestamp={opts.timestamp}>
143 {({timeElapsed}) => (
144 <WebOnlyInlineLinkText
145 to={opts.postHref}
146 label={timestampLabel}
147 title={timestampLabel}
148 disableMismatchWarning
149 disableUnderline
150 onPress={onBeforePressPost}
151 style={[
152 a.pl_xs,
153 a.text_md,
154 a.leading_tight,
155 IS_ANDROID && a.flex_grow,
156 a.text_right,
157 t.atoms.text_contrast_medium,
158 web({
159 whiteSpace: 'nowrap',
160 }),
161 ]}>
162 {!IS_ANDROID && (
163 <Text
164 style={[
165 a.text_md,
166 a.leading_tight,
167 t.atoms.text_contrast_medium,
168 ]}
169 accessible={false}>
170 ·{' '}
171 </Text>
172 )}
173 {timeElapsed}
174 </WebOnlyInlineLinkText>
175 )}
176 </TimeElapsed>
177 </View>
178 </View>
179 )
180}
181PostMeta = memo(PostMeta)
182export {PostMeta}