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/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 {ProfileBadges} from '#/components/ProfileBadges'
20import {ProfileHoverCard} from '#/components/ProfileHoverCard'
21import {Text} from '#/components/Typography'
22import {IS_ANDROID} from '#/env'
23import {useActorStatus} from '#/features/liveNow'
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 linkDisabled?: boolean
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 unstableCacheProfileView(queryClient, author)
54 onOpenAuthor?.()
55 }, [queryClient, author, onOpenAuthor])
56 const onBeforePressPost = useCallback(() => {
57 unstableCacheProfileView(queryClient, author)
58 }, [queryClient, author])
59
60 const timestampLabel = niceDate(i18n, opts.timestamp)
61 const {isActive: live} = useActorStatus(author)
62
63 const MaybeLinkText = opts.linkDisabled ? Text : WebOnlyInlineLinkText
64
65 return (
66 <View
67 style={[
68 IS_ANDROID ? a.flex_1 : a.flex_shrink,
69 a.flex_row,
70 a.align_center,
71 a.pb_xs,
72 a.gap_xs,
73 a.z_20,
74 opts.style,
75 ]}>
76 {opts.showAvatar && (
77 <View style={[a.self_center, a.mr_2xs]}>
78 <PreviewableUserAvatar
79 size={opts.avatarSize || 16}
80 profile={author}
81 moderation={opts.moderation?.ui('avatar')}
82 type={author.associated?.labeler ? 'labeler' : 'user'}
83 live={live}
84 hideLiveBadge
85 disableNavigation={opts.linkDisabled}
86 />
87 </View>
88 )}
89 <View style={[a.flex_row, a.align_end, a.flex_shrink]}>
90 <ProfileHoverCard did={author.did}>
91 <View style={[a.flex_row, a.align_end, a.flex_shrink]}>
92 <MaybeLinkText
93 emoji
94 numberOfLines={1}
95 to={profileLink}
96 label={_(msg`View profile`)}
97 disableMismatchWarning
98 onPress={opts.linkDisabled ? undefined : onBeforePressAuthor}
99 style={[
100 a.text_md,
101 a.font_semi_bold,
102 t.atoms.text,
103 a.leading_tight,
104 a.flex_shrink,
105 ]}>
106 {forceLTR(
107 sanitizeDisplayName(
108 displayName,
109 opts.moderation?.ui('displayName'),
110 ),
111 )}
112 </MaybeLinkText>
113 <ProfileBadges
114 profile={author}
115 size="sm"
116 pdsInteractive={false}
117 style={[
118 a.pl_2xs,
119 a.self_center,
120 {
121 marginTop: platform({web: 1, ios: 0, android: -1}),
122 },
123 ]}
124 />
125 <MaybeLinkText
126 emoji
127 numberOfLines={1}
128 to={profileLink}
129 label={_(msg`View profile`)}
130 disableMismatchWarning
131 disableUnderline
132 onPress={opts.linkDisabled ? undefined : onBeforePressAuthor}
133 style={[
134 a.text_md,
135 t.atoms.text_contrast_medium,
136 {lineHeight: 1.17},
137 {flexBasis: '30%'},
138 a.flex_grow,
139 a.flex_shrink_0,
140 web({maxWidth: 'max-content'}),
141 ]}>
142 {NON_BREAKING_SPACE + sanitizeHandle(handle, '@')}
143 </MaybeLinkText>
144 {opts.showPronouns && pronouns && (
145 <WebOnlyInlineLinkText
146 emoji
147 numberOfLines={1}
148 to={profileLink}
149 label={_(msg`View Profile`)}
150 disableMismatchWarning
151 disableUnderline
152 onPress={onBeforePressAuthor}
153 style={[
154 t.atoms.text_contrast_low,
155 a.pl_2xs,
156 a.text_md,
157 {lineHeight: 1.17},
158 {flexShrink: 5},
159 ]}>
160 {NON_BREAKING_SPACE + sanitizePronouns(pronouns)}
161 </WebOnlyInlineLinkText>
162 )}
163 </View>
164 </ProfileHoverCard>
165
166 <TimeElapsed timestamp={opts.timestamp}>
167 {({timeElapsed}) => (
168 <MaybeLinkText
169 to={opts.postHref}
170 label={timestampLabel}
171 title={timestampLabel}
172 disableMismatchWarning
173 disableUnderline
174 onPress={opts.linkDisabled ? undefined : onBeforePressPost}
175 style={[
176 a.pl_xs,
177 a.text_md,
178 a.leading_tight,
179 IS_ANDROID && a.flex_grow,
180 a.text_right,
181 t.atoms.text_contrast_medium,
182 web({
183 whiteSpace: 'nowrap',
184 }),
185 ]}>
186 {!opts.showPronouns && (
187 <>
188 {!IS_ANDROID && (
189 <Text
190 style={[
191 a.text_md,
192 a.leading_tight,
193 t.atoms.text_contrast_medium,
194 ]}
195 accessible={false}>
196 ·{' '}
197 </Text>
198 )}
199 {timeElapsed}
200 </>
201 )}
202 </MaybeLinkText>
203 )}
204 </TimeElapsed>
205 </View>
206 </View>
207 )
208}
209PostMeta = memo(PostMeta)
210export {PostMeta}