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]}>&middot;</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})