Merge remote-tracking branch 'origin/main' into 3p-moderators
* origin/main: (69 commits) Update .po files use `showControls` to show/hide live text icon on ios (#2982) Fix dim mode unread notif color Make dim theme dim (#2966) Add handle validation to create account UI (#2959) Normalize relative day (#2874) increase timeout to 15s (#2958) use `useOpenLink` hook for links in ALF (#2975) Rename Home Feed Prefs to Following Feed Prefs (#2965) Refactor feed header components (#2964) patch react-navigation to fix history bug (#2955) Use EAS managed build number, run build/submit on GH Actions (#2841) Fix `numberOfLines` not updating on iOS 15 (#2956) Fix UITextView line height adjustment for DynamicType, always use the max width for the view (#2916) Navigate back from a deleted post's route (#2948) Add optional close callback to Dialog (#2947) Fix flash when pressing into just-created post (#2945) Last usage (#2944) Update blogpost URL in ExportCarDialog.tsx (#2939) Prefer full posts for post thread placeholder (#2943) ...
This commit is contained in:
@@ -25,6 +25,8 @@ import {useSetTitle} from 'lib/hooks/useSetTitle'
|
||||
import {
|
||||
ThreadNode,
|
||||
ThreadPost,
|
||||
ThreadNotFound,
|
||||
ThreadBlocked,
|
||||
usePostThreadQuery,
|
||||
sortThread,
|
||||
} from '#/state/queries/post-thread'
|
||||
@@ -41,26 +43,40 @@ import {
|
||||
usePreferencesQuery,
|
||||
} from '#/state/queries/preferences'
|
||||
import {useSession} from '#/state/session'
|
||||
import {isAndroid, isNative} from '#/platform/detection'
|
||||
import {logger} from '#/logger'
|
||||
import {isAndroid, isNative, isWeb} from '#/platform/detection'
|
||||
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
||||
|
||||
const MAINTAIN_VISIBLE_CONTENT_POSITION = {minIndexForVisible: 1}
|
||||
// FlatList maintainVisibleContentPosition breaks if too many items
|
||||
// are prepended. This seems to be an optimal number based on *shrug*.
|
||||
const PARENTS_CHUNK_SIZE = 15
|
||||
|
||||
const MAINTAIN_VISIBLE_CONTENT_POSITION = {
|
||||
// We don't insert any elements before the root row while loading.
|
||||
// So the row we want to use as the scroll anchor is the first row.
|
||||
minIndexForVisible: 0,
|
||||
}
|
||||
|
||||
const TOP_COMPONENT = {_reactKey: '__top_component__'}
|
||||
const REPLY_PROMPT = {_reactKey: '__reply__'}
|
||||
const DELETED = {_reactKey: '__deleted__'}
|
||||
const BLOCKED = {_reactKey: '__blocked__'}
|
||||
const CHILD_SPINNER = {_reactKey: '__child_spinner__'}
|
||||
const LOAD_MORE = {_reactKey: '__load_more__'}
|
||||
const BOTTOM_COMPONENT = {_reactKey: '__bottom_component__'}
|
||||
|
||||
type YieldedItem =
|
||||
| ThreadPost
|
||||
type YieldedItem = ThreadPost | ThreadBlocked | ThreadNotFound
|
||||
type RowItem =
|
||||
| YieldedItem
|
||||
// TODO: TS doesn't actually enforce it's one of these, it only enforces matching shape.
|
||||
| typeof TOP_COMPONENT
|
||||
| typeof REPLY_PROMPT
|
||||
| typeof DELETED
|
||||
| typeof BLOCKED
|
||||
| typeof CHILD_SPINNER
|
||||
| typeof LOAD_MORE
|
||||
| typeof BOTTOM_COMPONENT
|
||||
|
||||
type ThreadSkeletonParts = {
|
||||
parents: YieldedItem[]
|
||||
highlightedPost: ThreadNode
|
||||
replies: YieldedItem[]
|
||||
}
|
||||
|
||||
export function PostThread({
|
||||
uri,
|
||||
@@ -153,61 +169,92 @@ function PostThreadLoaded({
|
||||
const {isMobile, isTabletOrMobile} = useWebMediaQueries()
|
||||
const ref = useRef<ListMethods>(null)
|
||||
const highlightedPostRef = useRef<View | null>(null)
|
||||
const needsScrollAdjustment = useRef<boolean>(
|
||||
!isNative || // web always uses scroll adjustment
|
||||
(thread.type === 'post' && !thread.ctx.isParentLoading), // native only does it when not loading from placeholder
|
||||
const [maxParents, setMaxParents] = React.useState(
|
||||
isWeb ? Infinity : PARENTS_CHUNK_SIZE,
|
||||
)
|
||||
const [maxVisible, setMaxVisible] = React.useState(100)
|
||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||
const [maxReplies, setMaxReplies] = React.useState(100)
|
||||
const treeView = React.useMemo(
|
||||
() => !!threadViewPrefs.lab_treeViewEnabled && hasBranchingReplies(thread),
|
||||
[threadViewPrefs, thread],
|
||||
)
|
||||
|
||||
// construct content
|
||||
const posts = React.useMemo(() => {
|
||||
let arr = Array.from(
|
||||
flattenThreadSkeleton(
|
||||
// On native, this is going to start out `true`. We'll toggle it to `false` after the initial render if flushed.
|
||||
// This ensures that the first render contains no parents--even if they are already available in the cache.
|
||||
// We need to delay showing them so that we can use maintainVisibleContentPosition to keep the main post on screen.
|
||||
// On the web this is not necessary because we can synchronously adjust the scroll in onContentSizeChange instead.
|
||||
const [deferParents, setDeferParents] = React.useState(isNative)
|
||||
|
||||
const skeleton = React.useMemo(
|
||||
() =>
|
||||
createThreadSkeleton(
|
||||
sortThread(thread, threadViewPrefs),
|
||||
hasSession,
|
||||
treeView,
|
||||
),
|
||||
)
|
||||
if (arr.length > maxVisible) {
|
||||
arr = arr.slice(0, maxVisible).concat([LOAD_MORE])
|
||||
}
|
||||
if (arr.indexOf(CHILD_SPINNER) === -1) {
|
||||
arr.push(BOTTOM_COMPONENT)
|
||||
[thread, threadViewPrefs, hasSession, treeView],
|
||||
)
|
||||
|
||||
// construct content
|
||||
const posts = React.useMemo(() => {
|
||||
const {parents, highlightedPost, replies} = skeleton
|
||||
let arr: RowItem[] = []
|
||||
if (highlightedPost.type === 'post') {
|
||||
const isRoot =
|
||||
!highlightedPost.parent && !highlightedPost.ctx.isParentLoading
|
||||
if (isRoot) {
|
||||
// No parents to load.
|
||||
arr.push(TOP_COMPONENT)
|
||||
} else {
|
||||
if (highlightedPost.ctx.isParentLoading || deferParents) {
|
||||
// We're loading parents of the highlighted post.
|
||||
// In this case, we don't render anything above the post.
|
||||
// If you add something here, you'll need to update both
|
||||
// maintainVisibleContentPosition and onContentSizeChange
|
||||
// to "hold onto" the correct row instead of the first one.
|
||||
} else {
|
||||
// Everything is loaded
|
||||
let startIndex = Math.max(0, parents.length - maxParents)
|
||||
if (startIndex === 0) {
|
||||
arr.push(TOP_COMPONENT)
|
||||
} else {
|
||||
// When progressively revealing parents, rendering a placeholder
|
||||
// here will cause scrolling jumps. Don't add it unless you test it.
|
||||
// QT'ing this thread is a great way to test all the scrolling hacks:
|
||||
// https://bsky.app/profile/www.mozzius.dev/post/3kjqhblh6qk2o
|
||||
}
|
||||
for (let i = startIndex; i < parents.length; i++) {
|
||||
arr.push(parents[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
arr.push(highlightedPost)
|
||||
if (!highlightedPost.post.viewer?.replyDisabled) {
|
||||
arr.push(REPLY_PROMPT)
|
||||
}
|
||||
if (highlightedPost.ctx.isChildLoading) {
|
||||
arr.push(CHILD_SPINNER)
|
||||
} else {
|
||||
for (let i = 0; i < replies.length; i++) {
|
||||
arr.push(replies[i])
|
||||
if (i === maxReplies) {
|
||||
arr.push(LOAD_MORE)
|
||||
break
|
||||
}
|
||||
}
|
||||
arr.push(BOTTOM_COMPONENT)
|
||||
}
|
||||
}
|
||||
return arr
|
||||
}, [thread, treeView, maxVisible, threadViewPrefs, hasSession])
|
||||
}, [skeleton, deferParents, maxParents, maxReplies])
|
||||
|
||||
/**
|
||||
* NOTE
|
||||
* Scroll positioning
|
||||
*
|
||||
* This callback is run if needsScrollAdjustment.current == true, which is...
|
||||
* - On web: always
|
||||
* - On native: when the placeholder cache is not being used
|
||||
*
|
||||
* It then only runs when viewing a reply, and the goal is to scroll the
|
||||
* reply into view.
|
||||
*
|
||||
* On native, if the placeholder cache is being used then maintainVisibleContentPosition
|
||||
* is a more effective solution, so we use that. Otherwise, typically we're loading from
|
||||
* the react-query cache, so we just need to immediately scroll down to the post.
|
||||
*
|
||||
* On desktop, maintainVisibleContentPosition isn't supported so we just always use
|
||||
* this technique.
|
||||
*
|
||||
* -prf
|
||||
*/
|
||||
const onContentSizeChange = React.useCallback(() => {
|
||||
// This is only used on the web to keep the post in view when its parents load.
|
||||
// On native, we rely on `maintainVisibleContentPosition` instead.
|
||||
const didAdjustScrollWeb = useRef<boolean>(false)
|
||||
const onContentSizeChangeWeb = React.useCallback(() => {
|
||||
// only run once
|
||||
if (!needsScrollAdjustment.current) {
|
||||
if (didAdjustScrollWeb.current) {
|
||||
return
|
||||
}
|
||||
|
||||
// wait for loading to finish
|
||||
if (thread.type === 'post' && !!thread.parent) {
|
||||
function onMeasure(pageY: number) {
|
||||
@@ -216,36 +263,41 @@ function PostThreadLoaded({
|
||||
offset: pageY,
|
||||
})
|
||||
}
|
||||
if (isNative) {
|
||||
highlightedPostRef.current?.measure(
|
||||
(_x, _y, _width, _height, _pageX, pageY) => {
|
||||
onMeasure(pageY)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
// Measure synchronously to avoid a layout jump.
|
||||
const domNode = highlightedPostRef.current
|
||||
if (domNode) {
|
||||
const pageY = (domNode as any as Element).getBoundingClientRect().top
|
||||
onMeasure(pageY)
|
||||
}
|
||||
// Measure synchronously to avoid a layout jump.
|
||||
const domNode = highlightedPostRef.current
|
||||
if (domNode) {
|
||||
const pageY = (domNode as any as Element).getBoundingClientRect().top
|
||||
onMeasure(pageY)
|
||||
}
|
||||
needsScrollAdjustment.current = false
|
||||
didAdjustScrollWeb.current = true
|
||||
}
|
||||
}, [thread])
|
||||
|
||||
const onPTR = React.useCallback(async () => {
|
||||
setIsPTRing(true)
|
||||
try {
|
||||
await onRefresh()
|
||||
} catch (err) {
|
||||
logger.error('Failed to refresh posts thread', {message: err})
|
||||
// On native, we reveal parents in chunks. Although they're all already
|
||||
// loaded and FlatList already has its own virtualization, unfortunately FlatList
|
||||
// has a bug that causes the content to jump around if too many items are getting
|
||||
// prepended at once. It also jumps around if items get prepended during scroll.
|
||||
// To work around this, we prepend rows after scroll bumps against the top and rests.
|
||||
const needsBumpMaxParents = React.useRef(false)
|
||||
const onStartReached = React.useCallback(() => {
|
||||
if (maxParents < skeleton.parents.length) {
|
||||
needsBumpMaxParents.current = true
|
||||
}
|
||||
setIsPTRing(false)
|
||||
}, [setIsPTRing, onRefresh])
|
||||
}, [maxParents, skeleton.parents.length])
|
||||
const bumpMaxParentsIfNeeded = React.useCallback(() => {
|
||||
if (!isNative) {
|
||||
return
|
||||
}
|
||||
if (needsBumpMaxParents.current) {
|
||||
needsBumpMaxParents.current = false
|
||||
setMaxParents(n => n + PARENTS_CHUNK_SIZE)
|
||||
}
|
||||
}, [])
|
||||
const onMomentumScrollEnd = bumpMaxParentsIfNeeded
|
||||
const onScrollToTop = bumpMaxParentsIfNeeded
|
||||
|
||||
const renderItem = React.useCallback(
|
||||
({item, index}: {item: YieldedItem; index: number}) => {
|
||||
({item, index}: {item: RowItem; index: number}) => {
|
||||
if (item === TOP_COMPONENT) {
|
||||
return isTabletOrMobile ? (
|
||||
<ViewHeader
|
||||
@@ -258,7 +310,7 @@ function PostThreadLoaded({
|
||||
{!isMobile && <ComposePrompt onPressCompose={onPressReply} />}
|
||||
</View>
|
||||
)
|
||||
} else if (item === DELETED) {
|
||||
} else if (isThreadNotFound(item)) {
|
||||
return (
|
||||
<View style={[pal.border, pal.viewLight, styles.itemContainer]}>
|
||||
<Text type="lg-bold" style={pal.textLight}>
|
||||
@@ -266,7 +318,7 @@ function PostThreadLoaded({
|
||||
</Text>
|
||||
</View>
|
||||
)
|
||||
} else if (item === BLOCKED) {
|
||||
} else if (isThreadBlocked(item)) {
|
||||
return (
|
||||
<View style={[pal.border, pal.viewLight, styles.itemContainer]}>
|
||||
<Text type="lg-bold" style={pal.textLight}>
|
||||
@@ -277,7 +329,7 @@ function PostThreadLoaded({
|
||||
} else if (item === LOAD_MORE) {
|
||||
return (
|
||||
<Pressable
|
||||
onPress={() => setMaxVisible(n => n + 50)}
|
||||
onPress={() => setMaxReplies(n => n + 50)}
|
||||
style={[pal.border, pal.view, styles.itemContainer]}
|
||||
accessibilityLabel={_(msg`Load more posts`)}
|
||||
accessibilityHint="">
|
||||
@@ -321,9 +373,12 @@ function PostThreadLoaded({
|
||||
const next = isThreadPost(posts[index - 1])
|
||||
? (posts[index - 1] as ThreadPost)
|
||||
: undefined
|
||||
const hasUnrevealedParents =
|
||||
index === 0 && maxParents < skeleton.parents.length
|
||||
return (
|
||||
<View
|
||||
ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined}>
|
||||
ref={item.ctx.isHighlightedPost ? highlightedPostRef : undefined}
|
||||
onLayout={deferParents ? () => setDeferParents(false) : undefined}>
|
||||
<PostThreadItem
|
||||
post={item.post}
|
||||
record={item.record}
|
||||
@@ -335,7 +390,9 @@ function PostThreadLoaded({
|
||||
hasMore={item.ctx.hasMore}
|
||||
showChildReplyLine={item.ctx.showChildReplyLine}
|
||||
showParentReplyLine={item.ctx.showParentReplyLine}
|
||||
hasPrecedingItem={!!prev?.ctx.showChildReplyLine}
|
||||
hasPrecedingItem={
|
||||
!!prev?.ctx.showChildReplyLine || hasUnrevealedParents
|
||||
}
|
||||
onPostReply={onRefresh}
|
||||
/>
|
||||
</View>
|
||||
@@ -356,7 +413,10 @@ function PostThreadLoaded({
|
||||
pal.colors.border,
|
||||
posts,
|
||||
onRefresh,
|
||||
deferParents,
|
||||
treeView,
|
||||
skeleton.parents.length,
|
||||
maxParents,
|
||||
_,
|
||||
],
|
||||
)
|
||||
@@ -365,17 +425,15 @@ function PostThreadLoaded({
|
||||
<List
|
||||
ref={ref}
|
||||
data={posts}
|
||||
initialNumToRender={!isNative ? posts.length : undefined}
|
||||
maintainVisibleContentPosition={
|
||||
!needsScrollAdjustment.current
|
||||
? MAINTAIN_VISIBLE_CONTENT_POSITION
|
||||
: undefined
|
||||
}
|
||||
keyExtractor={item => item._reactKey}
|
||||
renderItem={renderItem}
|
||||
refreshing={isPTRing}
|
||||
onRefresh={onPTR}
|
||||
onContentSizeChange={onContentSizeChange}
|
||||
onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb}
|
||||
onStartReached={onStartReached}
|
||||
onMomentumScrollEnd={onMomentumScrollEnd}
|
||||
onScrollToTop={onScrollToTop}
|
||||
maintainVisibleContentPosition={
|
||||
isNative ? MAINTAIN_VISIBLE_CONTENT_POSITION : undefined
|
||||
}
|
||||
style={s.hContentRegion}
|
||||
// @ts-ignore our .web version only -prf
|
||||
desktopFixedHeight
|
||||
@@ -487,41 +545,68 @@ function isThreadPost(v: unknown): v is ThreadPost {
|
||||
return !!v && typeof v === 'object' && 'type' in v && v.type === 'post'
|
||||
}
|
||||
|
||||
function* flattenThreadSkeleton(
|
||||
function isThreadNotFound(v: unknown): v is ThreadNotFound {
|
||||
return !!v && typeof v === 'object' && 'type' in v && v.type === 'not-found'
|
||||
}
|
||||
|
||||
function isThreadBlocked(v: unknown): v is ThreadBlocked {
|
||||
return !!v && typeof v === 'object' && 'type' in v && v.type === 'blocked'
|
||||
}
|
||||
|
||||
function createThreadSkeleton(
|
||||
node: ThreadNode,
|
||||
hasSession: boolean,
|
||||
treeView: boolean,
|
||||
isTraversingReplies: boolean = false,
|
||||
): ThreadSkeletonParts {
|
||||
return {
|
||||
parents: Array.from(flattenThreadParents(node, hasSession)),
|
||||
highlightedPost: node,
|
||||
replies: Array.from(flattenThreadReplies(node, hasSession, treeView)),
|
||||
}
|
||||
}
|
||||
|
||||
function* flattenThreadParents(
|
||||
node: ThreadNode,
|
||||
hasSession: boolean,
|
||||
): Generator<YieldedItem, void> {
|
||||
if (node.type === 'post') {
|
||||
if (!node.ctx.isParentLoading) {
|
||||
if (node.parent) {
|
||||
yield* flattenThreadSkeleton(node.parent, hasSession, treeView, false)
|
||||
} else if (!isTraversingReplies) {
|
||||
yield TOP_COMPONENT
|
||||
}
|
||||
if (node.parent) {
|
||||
yield* flattenThreadParents(node.parent, hasSession)
|
||||
}
|
||||
if (!hasSession && node.ctx.depth > 0 && hasPwiOptOut(node)) {
|
||||
if (!node.ctx.isHighlightedPost) {
|
||||
yield node
|
||||
}
|
||||
} else if (node.type === 'not-found') {
|
||||
yield node
|
||||
} else if (node.type === 'blocked') {
|
||||
yield node
|
||||
}
|
||||
}
|
||||
|
||||
function* flattenThreadReplies(
|
||||
node: ThreadNode,
|
||||
hasSession: boolean,
|
||||
treeView: boolean,
|
||||
): Generator<YieldedItem, void> {
|
||||
if (node.type === 'post') {
|
||||
if (!hasSession && hasPwiOptOut(node)) {
|
||||
return
|
||||
}
|
||||
yield node
|
||||
if (node.ctx.isHighlightedPost && !node.post.viewer?.replyDisabled) {
|
||||
yield REPLY_PROMPT
|
||||
if (!node.ctx.isHighlightedPost) {
|
||||
yield node
|
||||
}
|
||||
if (node.replies?.length) {
|
||||
for (const reply of node.replies) {
|
||||
yield* flattenThreadSkeleton(reply, hasSession, treeView, true)
|
||||
yield* flattenThreadReplies(reply, hasSession, treeView)
|
||||
if (!treeView && !node.ctx.isHighlightedPost) {
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (node.ctx.isChildLoading) {
|
||||
yield CHILD_SPINNER
|
||||
}
|
||||
} else if (node.type === 'not-found') {
|
||||
yield DELETED
|
||||
yield node
|
||||
} else if (node.type === 'blocked') {
|
||||
yield BLOCKED
|
||||
yield node
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {PostThreadFollowBtn} from 'view/com/post-thread/PostThreadFollowBtn'
|
||||
import {Link, TextLink} from '../util/Link'
|
||||
import {RichText} from '../util/text/RichText'
|
||||
import {RichText} from '#/components/RichText'
|
||||
import {Text} from '../util/text/Text'
|
||||
import {PreviewableUserAvatar} from '../util/UserAvatar'
|
||||
import {s} from 'lib/styles'
|
||||
@@ -27,7 +27,6 @@ import {PostHider} from '../util/moderation/PostHider'
|
||||
import {ContentHider} from '../util/moderation/ContentHider'
|
||||
import {PostAlerts} from '../util/moderation/PostAlerts'
|
||||
import {LabelsOnMyPost} from '../util/moderation/LabelsOnMe'
|
||||
import {PostSandboxWarning} from '../util/PostSandboxWarning'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {usePalette} from 'lib/hooks/usePalette'
|
||||
import {formatCount} from '../util/numeric/format'
|
||||
@@ -44,6 +43,8 @@ import {Shadow, usePostShadow, POST_TOMBSTONE} from '#/state/cache/post-shadow'
|
||||
import {ThreadPost} from '#/state/queries/post-thread'
|
||||
import {useSession} from 'state/session'
|
||||
import {WhoCanReply} from '../threadgate/WhoCanReply'
|
||||
import {LoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||
import {atoms as a} from '#/alf'
|
||||
|
||||
export function PostThreadItem({
|
||||
post,
|
||||
@@ -164,8 +165,6 @@ let PostThreadItemLoaded = ({
|
||||
() => countLines(richText?.text) >= MAX_POST_LINES,
|
||||
)
|
||||
const {currentAccount} = useSession()
|
||||
const hasEngagement = post.likeCount || post.repostCount
|
||||
|
||||
const rootUri = record.reply?.root?.uri || post.uri
|
||||
const postHref = React.useMemo(() => {
|
||||
const urip = new AtUri(post.uri)
|
||||
@@ -247,7 +246,6 @@ let PostThreadItemLoaded = ({
|
||||
testID={`postThreadItem-by-${post.author.handle}`}
|
||||
style={[styles.outer, styles.outerHighlighted, pal.border, pal.view]}
|
||||
accessible={false}>
|
||||
<PostSandboxWarning />
|
||||
<View style={[styles.layout]}>
|
||||
<View style={[styles.layoutAvi, {paddingBottom: 8}]}>
|
||||
<PreviewableUserAvatar
|
||||
@@ -305,10 +303,8 @@ let PostThreadItemLoaded = ({
|
||||
styles.postTextLargeContainer,
|
||||
]}>
|
||||
<RichText
|
||||
type="post-text-lg"
|
||||
richText={richText}
|
||||
lineHeight={1.3}
|
||||
style={s.flex1}
|
||||
value={richText}
|
||||
style={[a.flex_1, a.text_xl]}
|
||||
selectable
|
||||
/>
|
||||
</View>
|
||||
@@ -322,9 +318,16 @@ let PostThreadItemLoaded = ({
|
||||
translatorUrl={translatorUrl}
|
||||
needsTranslation={needsTranslation}
|
||||
/>
|
||||
{hasEngagement ? (
|
||||
{post.repostCount !== 0 || post.likeCount !== 0 ? (
|
||||
// Show this section unless we're *sure* it has no engagement.
|
||||
<View style={[styles.expandedInfo, pal.border]}>
|
||||
{post.repostCount ? (
|
||||
{post.repostCount == null && post.likeCount == null && (
|
||||
// If we're still loading and not sure, assume this post has engagement.
|
||||
// This lets us avoid a layout shift for the common case (embedded post with likes/reposts).
|
||||
// TODO: embeds should include metrics to avoid us having to guess.
|
||||
<LoadingPlaceholder width={50} height={20} />
|
||||
)}
|
||||
{post.repostCount != null && post.repostCount !== 0 ? (
|
||||
<Link
|
||||
style={styles.expandedInfoItem}
|
||||
href={repostsHref}
|
||||
@@ -339,10 +342,8 @@ let PostThreadItemLoaded = ({
|
||||
{pluralize(post.repostCount, 'repost')}
|
||||
</Text>
|
||||
</Link>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
{post.likeCount ? (
|
||||
) : null}
|
||||
{post.likeCount != null && post.likeCount !== 0 ? (
|
||||
<Link
|
||||
style={styles.expandedInfoItem}
|
||||
href={likesHref}
|
||||
@@ -357,13 +358,9 @@ let PostThreadItemLoaded = ({
|
||||
{pluralize(post.likeCount, 'like')}
|
||||
</Text>
|
||||
</Link>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
) : null}
|
||||
</View>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
) : null}
|
||||
<View style={[s.pl10, s.pr10, s.pb5]}>
|
||||
<PostCtrls
|
||||
big
|
||||
@@ -403,8 +400,6 @@ let PostThreadItemLoaded = ({
|
||||
? {marginRight: 4}
|
||||
: {marginLeft: 2, marginRight: 2}
|
||||
}>
|
||||
<PostSandboxWarning />
|
||||
|
||||
<View
|
||||
style={{
|
||||
flexDirection: 'row',
|
||||
@@ -419,7 +414,7 @@ let PostThreadItemLoaded = ({
|
||||
styles.replyLine,
|
||||
{
|
||||
flexGrow: 1,
|
||||
backgroundColor: pal.colors.border,
|
||||
backgroundColor: pal.colors.replyLine,
|
||||
marginBottom: 4,
|
||||
},
|
||||
]}
|
||||
@@ -440,6 +435,7 @@ let PostThreadItemLoaded = ({
|
||||
: 8,
|
||||
},
|
||||
]}>
|
||||
{/* If we are in threaded mode, the avatar is rendered in PostMeta */}
|
||||
{!isThreadedChild && (
|
||||
<View style={styles.layoutAvi}>
|
||||
<PreviewableUserAvatar
|
||||
@@ -456,7 +452,7 @@ let PostThreadItemLoaded = ({
|
||||
styles.replyLine,
|
||||
{
|
||||
flexGrow: 1,
|
||||
backgroundColor: pal.colors.border,
|
||||
backgroundColor: pal.colors.replyLine,
|
||||
marginTop: 4,
|
||||
},
|
||||
]}
|
||||
@@ -465,7 +461,12 @@ let PostThreadItemLoaded = ({
|
||||
</View>
|
||||
)}
|
||||
|
||||
<View style={styles.layoutContent}>
|
||||
<View
|
||||
style={
|
||||
isThreadedChild
|
||||
? styles.layoutContentThreaded
|
||||
: styles.layoutContent
|
||||
}>
|
||||
<PostMeta
|
||||
author={post.author}
|
||||
authorHasWarning={!!post.author.labels?.length}
|
||||
@@ -486,10 +487,8 @@ let PostThreadItemLoaded = ({
|
||||
{richText?.text ? (
|
||||
<View style={styles.postTextContainer}>
|
||||
<RichText
|
||||
type="post-text"
|
||||
richText={richText}
|
||||
style={[pal.text, s.flex1]}
|
||||
lineHeight={1.3}
|
||||
value={richText}
|
||||
style={[a.flex_1, a.text_md]}
|
||||
numberOfLines={limitLines ? MAX_POST_LINES : undefined}
|
||||
/>
|
||||
</View>
|
||||
@@ -664,6 +663,10 @@ const styles = StyleSheet.create({
|
||||
flex: 1,
|
||||
marginLeft: 10,
|
||||
},
|
||||
layoutContentThreaded: {
|
||||
flex: 1,
|
||||
paddingRight: 10,
|
||||
},
|
||||
meta: {
|
||||
flexDirection: 'row',
|
||||
paddingVertical: 2,
|
||||
|
||||
Reference in New Issue
Block a user