mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React, {memo, useMemo} from 'react'
2import {StyleSheet, View} from 'react-native'
3import {
4 AppBskyFeedDefs,
5 AppBskyFeedPost,
6 AppBskyFeedThreadgate,
7 AtUri,
8 ModerationDecision,
9 RichText as RichTextAPI,
10} from '@atproto/api'
11import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
12import {msg, Plural, Trans} from '@lingui/macro'
13import {useLingui} from '@lingui/react'
14
15import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
16import {useLanguagePrefs} from '#/state/preferences'
17import {useOpenLink} from '#/state/preferences/in-app-browser'
18import {ThreadPost} from '#/state/queries/post-thread'
19import {useComposerControls} from '#/state/shell/composer'
20import {useMergedThreadgateHiddenReplies} from '#/state/threadgate-hidden-replies'
21import {MAX_POST_LINES} from 'lib/constants'
22import {usePalette} from 'lib/hooks/usePalette'
23import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
24import {makeProfileLink} from 'lib/routes/links'
25import {sanitizeDisplayName} from 'lib/strings/display-names'
26import {sanitizeHandle} from 'lib/strings/handles'
27import {countLines} from 'lib/strings/helpers'
28import {niceDate} from 'lib/strings/time'
29import {s} from 'lib/styles'
30import {isWeb} from 'platform/detection'
31import {useSession} from 'state/session'
32import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn'
33import {atoms as a} from '#/alf'
34import {AppModerationCause} from '#/components/Pills'
35import {RichText} from '#/components/RichText'
36import {ContentHider} from '../../../components/moderation/ContentHider'
37import {LabelsOnMyPost} from '../../../components/moderation/LabelsOnMe'
38import {PostAlerts} from '../../../components/moderation/PostAlerts'
39import {PostHider} from '../../../components/moderation/PostHider'
40import {WhoCanReply} from '../../../components/WhoCanReply'
41import {getTranslatorLink, isPostInLanguage} from '../../../locale/helpers'
42import {ErrorMessage} from '../util/error/ErrorMessage'
43import {Link, TextLink} from '../util/Link'
44import {formatCount} from '../util/numeric/format'
45import {PostCtrls} from '../util/post-ctrls/PostCtrls'
46import {PostEmbeds, PostEmbedViewContext} from '../util/post-embeds'
47import {PostMeta} from '../util/PostMeta'
48import {Text} from '../util/text/Text'
49import {PreviewableUserAvatar} from '../util/UserAvatar'
50
51export function PostThreadItem({
52 post,
53 record,
54 moderation,
55 treeView,
56 depth,
57 prevPost,
58 nextPost,
59 isHighlightedPost,
60 hasMore,
61 showChildReplyLine,
62 showParentReplyLine,
63 hasPrecedingItem,
64 overrideBlur,
65 onPostReply,
66 hideTopBorder,
67 threadgateRecord,
68}: {
69 post: AppBskyFeedDefs.PostView
70 record: AppBskyFeedPost.Record
71 moderation: ModerationDecision | undefined
72 treeView: boolean
73 depth: number
74 prevPost: ThreadPost | undefined
75 nextPost: ThreadPost | undefined
76 isHighlightedPost?: boolean
77 hasMore?: boolean
78 showChildReplyLine?: boolean
79 showParentReplyLine?: boolean
80 hasPrecedingItem: boolean
81 overrideBlur: boolean
82 onPostReply: (postUri: string | undefined) => void
83 hideTopBorder?: boolean
84 threadgateRecord?: AppBskyFeedThreadgate.Record
85}) {
86 const postShadowed = usePostShadow(post)
87 const richText = useMemo(
88 () =>
89 new RichTextAPI({
90 text: record.text,
91 facets: record.facets,
92 }),
93 [record],
94 )
95 if (postShadowed === POST_TOMBSTONE) {
96 return <PostThreadItemDeleted hideTopBorder={hideTopBorder} />
97 }
98 if (richText && moderation) {
99 return (
100 <PostThreadItemLoaded
101 // Safeguard from clobbering per-post state below:
102 key={postShadowed.uri}
103 post={postShadowed}
104 prevPost={prevPost}
105 nextPost={nextPost}
106 record={record}
107 richText={richText}
108 moderation={moderation}
109 treeView={treeView}
110 depth={depth}
111 isHighlightedPost={isHighlightedPost}
112 hasMore={hasMore}
113 showChildReplyLine={showChildReplyLine}
114 showParentReplyLine={showParentReplyLine}
115 hasPrecedingItem={hasPrecedingItem}
116 overrideBlur={overrideBlur}
117 onPostReply={onPostReply}
118 hideTopBorder={hideTopBorder}
119 threadgateRecord={threadgateRecord}
120 />
121 )
122 }
123 return null
124}
125
126function PostThreadItemDeleted({hideTopBorder}: {hideTopBorder?: boolean}) {
127 const pal = usePalette('default')
128 return (
129 <View
130 style={[
131 styles.outer,
132 pal.border,
133 pal.view,
134 s.p20,
135 s.flexRow,
136 hideTopBorder && styles.noTopBorder,
137 ]}>
138 <FontAwesomeIcon icon={['far', 'trash-can']} color={pal.colors.icon} />
139 <Text style={[pal.textLight, s.ml10]}>
140 <Trans>This post has been deleted.</Trans>
141 </Text>
142 </View>
143 )
144}
145
146let PostThreadItemLoaded = ({
147 post,
148 record,
149 richText,
150 moderation,
151 treeView,
152 depth,
153 prevPost,
154 nextPost,
155 isHighlightedPost,
156 hasMore,
157 showChildReplyLine,
158 showParentReplyLine,
159 hasPrecedingItem,
160 overrideBlur,
161 onPostReply,
162 hideTopBorder,
163 threadgateRecord,
164}: {
165 post: Shadow<AppBskyFeedDefs.PostView>
166 record: AppBskyFeedPost.Record
167 richText: RichTextAPI
168 moderation: ModerationDecision
169 treeView: boolean
170 depth: number
171 prevPost: ThreadPost | undefined
172 nextPost: ThreadPost | undefined
173 isHighlightedPost?: boolean
174 hasMore?: boolean
175 showChildReplyLine?: boolean
176 showParentReplyLine?: boolean
177 hasPrecedingItem: boolean
178 overrideBlur: boolean
179 onPostReply: (postUri: string | undefined) => void
180 hideTopBorder?: boolean
181 threadgateRecord?: AppBskyFeedThreadgate.Record
182}): React.ReactNode => {
183 const pal = usePalette('default')
184 const {_, i18n} = useLingui()
185 const langPrefs = useLanguagePrefs()
186 const {openComposer} = useComposerControls()
187 const [limitLines, setLimitLines] = React.useState(
188 () => countLines(richText?.text) >= MAX_POST_LINES,
189 )
190 const {currentAccount} = useSession()
191 const rootUri = record.reply?.root?.uri || post.uri
192 const postHref = React.useMemo(() => {
193 const urip = new AtUri(post.uri)
194 return makeProfileLink(post.author, 'post', urip.rkey)
195 }, [post.uri, post.author])
196 const itemTitle = _(msg`Post by ${post.author.handle}`)
197 const authorHref = makeProfileLink(post.author)
198 const authorTitle = post.author.handle
199 const isThreadAuthor = getThreadAuthor(post, record) === currentAccount?.did
200 const likesHref = React.useMemo(() => {
201 const urip = new AtUri(post.uri)
202 return makeProfileLink(post.author, 'post', urip.rkey, 'liked-by')
203 }, [post.uri, post.author])
204 const likesTitle = _(msg`Likes on this post`)
205 const repostsHref = React.useMemo(() => {
206 const urip = new AtUri(post.uri)
207 return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by')
208 }, [post.uri, post.author])
209 const repostsTitle = _(msg`Reposts of this post`)
210 const threadgateHiddenReplies = useMergedThreadgateHiddenReplies({
211 threadgateRecord,
212 })
213 const additionalPostAlerts: AppModerationCause[] = React.useMemo(() => {
214 const isPostHiddenByThreadgate = threadgateHiddenReplies.has(post.uri)
215 const isControlledByViewer = new AtUri(rootUri).host === currentAccount?.did
216 return isControlledByViewer && isPostHiddenByThreadgate
217 ? [
218 {
219 type: 'reply-hidden',
220 source: {type: 'user', did: currentAccount?.did},
221 priority: 6,
222 },
223 ]
224 : []
225 }, [post, currentAccount?.did, threadgateHiddenReplies, rootUri])
226 const quotesHref = React.useMemo(() => {
227 const urip = new AtUri(post.uri)
228 return makeProfileLink(post.author, 'post', urip.rkey, 'quotes')
229 }, [post.uri, post.author])
230 const quotesTitle = _(msg`Quotes of this post`)
231
232 const translatorUrl = getTranslatorLink(
233 record?.text || '',
234 langPrefs.primaryLanguage,
235 )
236 const needsTranslation = useMemo(
237 () =>
238 Boolean(
239 langPrefs.primaryLanguage &&
240 !isPostInLanguage(post, [langPrefs.primaryLanguage]),
241 ),
242 [post, langPrefs.primaryLanguage],
243 )
244
245 const onPressReply = React.useCallback(() => {
246 openComposer({
247 replyTo: {
248 uri: post.uri,
249 cid: post.cid,
250 text: record.text,
251 author: post.author,
252 embed: post.embed,
253 moderation,
254 },
255 onPost: onPostReply,
256 })
257 }, [openComposer, post, record, onPostReply, moderation])
258
259 const onPressShowMore = React.useCallback(() => {
260 setLimitLines(false)
261 }, [setLimitLines])
262
263 if (!record) {
264 return <ErrorMessage message={_(msg`Invalid or unsupported post record`)} />
265 }
266
267 if (isHighlightedPost) {
268 return (
269 <>
270 {rootUri !== post.uri && (
271 <View style={{paddingLeft: 16, flexDirection: 'row', height: 16}}>
272 <View style={{width: 38}}>
273 <View
274 style={[
275 styles.replyLine,
276 {
277 flexGrow: 1,
278 backgroundColor: pal.colors.replyLine,
279 },
280 ]}
281 />
282 </View>
283 </View>
284 )}
285
286 <View
287 testID={`postThreadItem-by-${post.author.handle}`}
288 style={[
289 styles.outer,
290 styles.outerHighlighted,
291 pal.border,
292 pal.view,
293 rootUri === post.uri && styles.outerHighlightedRoot,
294 hideTopBorder && styles.noTopBorder,
295 ]}
296 accessible={false}>
297 <View style={[styles.layout]}>
298 <View style={[styles.layoutAvi, {paddingBottom: 8}]}>
299 <PreviewableUserAvatar
300 size={42}
301 profile={post.author}
302 moderation={moderation.ui('avatar')}
303 type={post.author.associated?.labeler ? 'labeler' : 'user'}
304 />
305 </View>
306 <View style={styles.layoutContent}>
307 <View
308 style={[styles.meta, styles.metaExpandedLine1, {zIndex: 1}]}>
309 <Link style={s.flex1} href={authorHref} title={authorTitle}>
310 <Text
311 type="xl-bold"
312 style={[pal.text, a.self_start]}
313 numberOfLines={1}
314 lineHeight={1.2}>
315 {sanitizeDisplayName(
316 post.author.displayName ||
317 sanitizeHandle(post.author.handle),
318 moderation.ui('displayName'),
319 )}
320 </Text>
321 </Link>
322 </View>
323 <View style={styles.meta}>
324 <Link style={s.flex1} href={authorHref} title={authorTitle}>
325 <Text type="md" style={[pal.textLight]} numberOfLines={1}>
326 {sanitizeHandle(post.author.handle, '@')}
327 </Text>
328 </Link>
329 </View>
330 </View>
331 {currentAccount?.did !== post.author.did && (
332 <PostThreadFollowBtn did={post.author.did} />
333 )}
334 </View>
335 <View style={[s.pl10, s.pr10, s.pb10]}>
336 <LabelsOnMyPost post={post} />
337 <ContentHider
338 modui={moderation.ui('contentView')}
339 ignoreMute
340 style={styles.contentHider}
341 childContainerStyle={styles.contentHiderChild}>
342 <PostAlerts
343 modui={moderation.ui('contentView')}
344 size="lg"
345 includeMute
346 style={[a.pt_2xs, a.pb_sm]}
347 additionalCauses={additionalPostAlerts}
348 />
349 {richText?.text ? (
350 <View
351 style={[
352 styles.postTextContainer,
353 styles.postTextLargeContainer,
354 ]}>
355 <RichText
356 enableTags
357 selectable
358 value={richText}
359 style={[a.flex_1, a.text_xl]}
360 authorHandle={post.author.handle}
361 />
362 </View>
363 ) : undefined}
364 {post.embed && (
365 <View style={[a.pb_sm]}>
366 <PostEmbeds
367 embed={post.embed}
368 moderation={moderation}
369 viewContext={PostEmbedViewContext.ThreadHighlighted}
370 />
371 </View>
372 )}
373 </ContentHider>
374 <ExpandedPostDetails
375 post={post}
376 isThreadAuthor={isThreadAuthor}
377 translatorUrl={translatorUrl}
378 needsTranslation={needsTranslation}
379 />
380 {post.repostCount !== 0 ||
381 post.likeCount !== 0 ||
382 post.quoteCount !== 0 ? (
383 // Show this section unless we're *sure* it has no engagement.
384 <View style={[styles.expandedInfo, pal.border]}>
385 {post.repostCount != null && post.repostCount !== 0 ? (
386 <Link
387 style={styles.expandedInfoItem}
388 href={repostsHref}
389 title={repostsTitle}>
390 <Text
391 testID="repostCount-expanded"
392 type="lg"
393 style={pal.textLight}>
394 <Text type="xl-bold" style={pal.text}>
395 {formatCount(i18n, post.repostCount)}
396 </Text>{' '}
397 <Plural
398 value={post.repostCount}
399 one="repost"
400 other="reposts"
401 />
402 </Text>
403 </Link>
404 ) : null}
405 {post.quoteCount != null &&
406 post.quoteCount !== 0 &&
407 !post.viewer?.embeddingDisabled ? (
408 <Link
409 style={styles.expandedInfoItem}
410 href={quotesHref}
411 title={quotesTitle}>
412 <Text
413 testID="quoteCount-expanded"
414 type="lg"
415 style={pal.textLight}>
416 <Text type="xl-bold" style={pal.text}>
417 {formatCount(i18n, post.quoteCount)}
418 </Text>{' '}
419 <Plural
420 value={post.quoteCount}
421 one="quote"
422 other="quotes"
423 />
424 </Text>
425 </Link>
426 ) : null}
427 {post.likeCount != null && post.likeCount !== 0 ? (
428 <Link
429 style={styles.expandedInfoItem}
430 href={likesHref}
431 title={likesTitle}>
432 <Text
433 testID="likeCount-expanded"
434 type="lg"
435 style={pal.textLight}>
436 <Text type="xl-bold" style={pal.text}>
437 {formatCount(i18n, post.likeCount)}
438 </Text>{' '}
439 <Plural value={post.likeCount} one="like" other="likes" />
440 </Text>
441 </Link>
442 ) : null}
443 </View>
444 ) : null}
445 <View style={[s.pl10, s.pr10]}>
446 <PostCtrls
447 big
448 post={post}
449 record={record}
450 richText={richText}
451 onPressReply={onPressReply}
452 onPostReply={onPostReply}
453 logContext="PostThreadItem"
454 threadgateRecord={threadgateRecord}
455 />
456 </View>
457 </View>
458 </View>
459 </>
460 )
461 } else {
462 const isThreadedChild = treeView && depth > 0
463 const isThreadedChildAdjacentTop =
464 isThreadedChild && prevPost?.ctx.depth === depth && depth !== 1
465 const isThreadedChildAdjacentBot =
466 isThreadedChild && nextPost?.ctx.depth === depth
467 return (
468 <PostOuterWrapper
469 post={post}
470 depth={depth}
471 showParentReplyLine={!!showParentReplyLine}
472 treeView={treeView}
473 hasPrecedingItem={hasPrecedingItem}
474 hideTopBorder={hideTopBorder}>
475 <PostHider
476 testID={`postThreadItem-by-${post.author.handle}`}
477 href={postHref}
478 disabled={overrideBlur}
479 style={[pal.view]}
480 modui={moderation.ui('contentList')}
481 iconSize={isThreadedChild ? 26 : 38}
482 iconStyles={
483 isThreadedChild ? {marginRight: 4} : {marginLeft: 2, marginRight: 2}
484 }
485 profile={post.author}
486 interpretFilterAsBlur>
487 <View
488 style={{
489 flexDirection: 'row',
490 gap: 10,
491 paddingLeft: 8,
492 height: isThreadedChildAdjacentTop ? 8 : 16,
493 }}>
494 <View style={{width: 38}}>
495 {!isThreadedChild && showParentReplyLine && (
496 <View
497 style={[
498 styles.replyLine,
499 {
500 flexGrow: 1,
501 backgroundColor: pal.colors.replyLine,
502 marginBottom: 4,
503 },
504 ]}
505 />
506 )}
507 </View>
508 </View>
509
510 <View
511 style={[
512 styles.layout,
513 {
514 paddingBottom:
515 showChildReplyLine && !isThreadedChild
516 ? 0
517 : isThreadedChildAdjacentBot
518 ? 4
519 : 8,
520 },
521 ]}>
522 {/* If we are in threaded mode, the avatar is rendered in PostMeta */}
523 {!isThreadedChild && (
524 <View style={styles.layoutAvi}>
525 <PreviewableUserAvatar
526 size={38}
527 profile={post.author}
528 moderation={moderation.ui('avatar')}
529 type={post.author.associated?.labeler ? 'labeler' : 'user'}
530 />
531
532 {showChildReplyLine && (
533 <View
534 style={[
535 styles.replyLine,
536 {
537 flexGrow: 1,
538 backgroundColor: pal.colors.replyLine,
539 marginTop: 4,
540 },
541 ]}
542 />
543 )}
544 </View>
545 )}
546
547 <View
548 style={
549 isThreadedChild
550 ? styles.layoutContentThreaded
551 : styles.layoutContent
552 }>
553 <PostMeta
554 author={post.author}
555 moderation={moderation}
556 authorHasWarning={!!post.author.labels?.length}
557 timestamp={post.indexedAt}
558 postHref={postHref}
559 showAvatar={isThreadedChild}
560 avatarModeration={moderation.ui('avatar')}
561 avatarSize={28}
562 displayNameType="md-bold"
563 displayNameStyle={isThreadedChild && s.ml2}
564 style={
565 isThreadedChild && {
566 alignItems: 'center',
567 paddingBottom: isWeb ? 5 : 2,
568 }
569 }
570 />
571 <LabelsOnMyPost post={post} />
572 <PostAlerts
573 modui={moderation.ui('contentList')}
574 style={[a.pt_2xs, a.pb_2xs]}
575 additionalCauses={additionalPostAlerts}
576 />
577 {richText?.text ? (
578 <View style={styles.postTextContainer}>
579 <RichText
580 enableTags
581 value={richText}
582 style={[a.flex_1, a.text_md]}
583 numberOfLines={limitLines ? MAX_POST_LINES : undefined}
584 authorHandle={post.author.handle}
585 />
586 </View>
587 ) : undefined}
588 {limitLines ? (
589 <TextLink
590 text={_(msg`Show More`)}
591 style={pal.link}
592 onPress={onPressShowMore}
593 href="#"
594 />
595 ) : undefined}
596 {post.embed && (
597 <View style={[a.pb_xs]}>
598 <PostEmbeds
599 embed={post.embed}
600 moderation={moderation}
601 viewContext={PostEmbedViewContext.Feed}
602 />
603 </View>
604 )}
605 <PostCtrls
606 post={post}
607 record={record}
608 richText={richText}
609 onPressReply={onPressReply}
610 logContext="PostThreadItem"
611 threadgateRecord={threadgateRecord}
612 />
613 </View>
614 </View>
615 {hasMore ? (
616 <Link
617 style={[
618 styles.loadMore,
619 {
620 paddingLeft: treeView ? 8 : 70,
621 paddingTop: 0,
622 paddingBottom: treeView ? 4 : 12,
623 },
624 ]}
625 href={postHref}
626 title={itemTitle}
627 noFeedback>
628 <Text type="sm-medium" style={pal.textLight}>
629 <Trans>More</Trans>
630 </Text>
631 <FontAwesomeIcon
632 icon="angle-right"
633 color={pal.colors.textLight}
634 size={14}
635 />
636 </Link>
637 ) : undefined}
638 </PostHider>
639 </PostOuterWrapper>
640 )
641 }
642}
643PostThreadItemLoaded = memo(PostThreadItemLoaded)
644
645function PostOuterWrapper({
646 post,
647 treeView,
648 depth,
649 showParentReplyLine,
650 hasPrecedingItem,
651 hideTopBorder,
652 children,
653}: React.PropsWithChildren<{
654 post: AppBskyFeedDefs.PostView
655 treeView: boolean
656 depth: number
657 showParentReplyLine: boolean
658 hasPrecedingItem: boolean
659 hideTopBorder?: boolean
660}>) {
661 const {isMobile} = useWebMediaQueries()
662 const pal = usePalette('default')
663 if (treeView && depth > 0) {
664 return (
665 <View
666 style={[
667 pal.border,
668 styles.cursor,
669 {
670 flexDirection: 'row',
671 paddingHorizontal: isMobile ? 10 : 6,
672 borderTopWidth: depth === 1 ? StyleSheet.hairlineWidth : 0,
673 },
674 ]}>
675 {Array.from(Array(depth - 1)).map((_, n: number) => (
676 <View
677 key={`${post.uri}-padding-${n}`}
678 style={{
679 borderLeftWidth: 2,
680 borderLeftColor: pal.colors.border,
681 marginLeft: isMobile ? 6 : 12,
682 paddingLeft: isMobile ? 6 : 8,
683 }}
684 />
685 ))}
686 <View style={{flex: 1}}>{children}</View>
687 </View>
688 )
689 }
690 return (
691 <View
692 style={[
693 styles.outer,
694 pal.border,
695 showParentReplyLine && hasPrecedingItem && styles.noTopBorder,
696 hideTopBorder && styles.noTopBorder,
697 styles.cursor,
698 ]}>
699 {children}
700 </View>
701 )
702}
703
704function ExpandedPostDetails({
705 post,
706 isThreadAuthor,
707 needsTranslation,
708 translatorUrl,
709}: {
710 post: AppBskyFeedDefs.PostView
711 isThreadAuthor: boolean
712 needsTranslation: boolean
713 translatorUrl: string
714}) {
715 const pal = usePalette('default')
716 const {_, i18n} = useLingui()
717 const openLink = useOpenLink()
718 const isRootPost = !('reply' in post.record)
719
720 const onTranslatePress = React.useCallback(() => {
721 openLink(translatorUrl)
722 }, [openLink, translatorUrl])
723
724 return (
725 <View
726 style={[
727 a.flex_row,
728 a.align_center,
729 a.flex_wrap,
730 a.gap_xs,
731 s.mt2,
732 s.mb10,
733 ]}>
734 <Text style={[a.text_sm, pal.textLight]}>
735 {niceDate(i18n, post.indexedAt)}
736 </Text>
737 {isRootPost && (
738 <WhoCanReply post={post} isThreadAuthor={isThreadAuthor} />
739 )}
740 {needsTranslation && (
741 <>
742 <Text style={[a.text_sm, pal.textLight]}>·</Text>
743
744 <Text
745 style={[a.text_sm, pal.link]}
746 title={_(msg`Translate`)}
747 onPress={onTranslatePress}>
748 <Trans>Translate</Trans>
749 </Text>
750 </>
751 )}
752 </View>
753 )
754}
755
756function getThreadAuthor(
757 post: AppBskyFeedDefs.PostView,
758 record: AppBskyFeedPost.Record,
759): string {
760 if (!record.reply) {
761 return post.author.did
762 }
763 try {
764 return new AtUri(record.reply.root.uri).host
765 } catch {
766 return ''
767 }
768}
769
770const styles = StyleSheet.create({
771 outer: {
772 borderTopWidth: StyleSheet.hairlineWidth,
773 paddingLeft: 8,
774 },
775 outerHighlighted: {
776 borderTopWidth: 0,
777 paddingTop: 4,
778 paddingLeft: 8,
779 paddingRight: 8,
780 },
781 outerHighlightedRoot: {
782 borderTopWidth: StyleSheet.hairlineWidth,
783 paddingTop: 16,
784 },
785 noTopBorder: {
786 borderTopWidth: 0,
787 },
788 layout: {
789 flexDirection: 'row',
790 paddingHorizontal: 8,
791 },
792 layoutAvi: {},
793 layoutContent: {
794 flex: 1,
795 marginLeft: 10,
796 },
797 layoutContentThreaded: {
798 flex: 1,
799 },
800 meta: {
801 flexDirection: 'row',
802 paddingVertical: 2,
803 },
804 metaExpandedLine1: {
805 paddingVertical: 0,
806 },
807 alert: {
808 marginBottom: 6,
809 },
810 postTextContainer: {
811 flexDirection: 'row',
812 alignItems: 'center',
813 flexWrap: 'wrap',
814 paddingBottom: 4,
815 paddingRight: 10,
816 overflow: 'hidden',
817 },
818 postTextLargeContainer: {
819 paddingHorizontal: 0,
820 paddingRight: 0,
821 paddingBottom: 10,
822 },
823 translateLink: {
824 marginBottom: 6,
825 },
826 contentHider: {
827 marginBottom: 6,
828 },
829 contentHiderChild: {
830 marginTop: 6,
831 },
832 expandedInfo: {
833 flexDirection: 'row',
834 padding: 10,
835 borderTopWidth: StyleSheet.hairlineWidth,
836 borderBottomWidth: StyleSheet.hairlineWidth,
837 marginTop: 5,
838 marginBottom: 10,
839 },
840 expandedInfoItem: {
841 marginRight: 10,
842 },
843 loadMore: {
844 flexDirection: 'row',
845 alignItems: 'center',
846 justifyContent: 'flex-start',
847 gap: 4,
848 paddingHorizontal: 20,
849 },
850 replyLine: {
851 width: 2,
852 marginLeft: 'auto',
853 marginRight: 'auto',
854 },
855 cursor: {
856 // @ts-ignore web only
857 cursor: 'pointer',
858 },
859})