Improve thread rendering
parent
69b86255c6
commit
ae3099dfca
|
@ -17,6 +17,7 @@ let _idCounter = 0
|
|||
type FeedItem = GetTimeline.FeedItem | GetAuthorFeed.FeedItem
|
||||
type FeedItemWithThreadMeta = FeedItem & {
|
||||
_isThreadParent?: boolean
|
||||
_isThreadChildElided?: boolean
|
||||
_isThreadChild?: boolean
|
||||
}
|
||||
|
||||
|
@ -34,6 +35,7 @@ export class FeedItemModel implements GetTimeline.FeedItem {
|
|||
// ui state
|
||||
_reactKey: string = ''
|
||||
_isThreadParent: boolean = false
|
||||
_isThreadChildElided: boolean = false
|
||||
_isThreadChild: boolean = false
|
||||
|
||||
// data
|
||||
|
@ -70,6 +72,7 @@ export class FeedItemModel implements GetTimeline.FeedItem {
|
|||
this.copy(v)
|
||||
this._isThreadParent = v._isThreadParent || false
|
||||
this._isThreadChild = v._isThreadChild || false
|
||||
this._isThreadChildElided = v._isThreadChildElided || false
|
||||
}
|
||||
|
||||
copy(v: GetTimeline.FeedItem | GetAuthorFeed.FeedItem) {
|
||||
|
@ -469,15 +472,7 @@ export class FeedModel {
|
|||
this.loadMoreCursor = res.data.cursor
|
||||
this.hasMore = !!this.loadMoreCursor
|
||||
|
||||
// HACK 1
|
||||
// rearrange the posts to represent threads
|
||||
// (should be done on the server)
|
||||
// -prf
|
||||
// HACK 2
|
||||
// deduplicate posts on the home feed
|
||||
// (should be done on the server)
|
||||
// -prf
|
||||
const reorgedFeed = preprocessFeed(res.data.feed, this.feedType === 'home')
|
||||
const reorgedFeed = preprocessFeed(res.data.feed)
|
||||
|
||||
const promises = []
|
||||
const toAppend: FeedItemModel[] = []
|
||||
|
@ -569,38 +564,78 @@ export class FeedModel {
|
|||
}
|
||||
}
|
||||
|
||||
function preprocessFeed(
|
||||
feed: FeedItem[],
|
||||
dedup: boolean,
|
||||
): FeedItemWithThreadMeta[] {
|
||||
// DEBUG
|
||||
// this has been temporarily disabled to see if it's the cause of some bugs
|
||||
// if the issues go away, we know this was the cause
|
||||
// -prf
|
||||
return feed
|
||||
// const reorg: FeedItemWithThreadMeta[] = []
|
||||
// for (let i = feed.length - 1; i >= 0; i--) {
|
||||
// const item = feed[i] as FeedItemWithThreadMeta
|
||||
interface Slice {
|
||||
index: number
|
||||
length: number
|
||||
}
|
||||
function preprocessFeed(feed: FeedItem[]): FeedItemWithThreadMeta[] {
|
||||
const reorg: FeedItemWithThreadMeta[] = []
|
||||
|
||||
// if (dedup) {
|
||||
// if (reorg.find(item2 => item2.uri === item.uri)) {
|
||||
// continue
|
||||
// }
|
||||
// }
|
||||
// phase one: identify threads and reorganize them into the feed so
|
||||
// that they are in order and marked as part of a thread
|
||||
for (let i = feed.length - 1; i >= 0; i--) {
|
||||
const item = feed[i] as FeedItemWithThreadMeta
|
||||
|
||||
// const selfReplyUri = getSelfReplyUri(item)
|
||||
// if (selfReplyUri) {
|
||||
// const parentIndex = reorg.findIndex(item2 => item2.uri === selfReplyUri)
|
||||
// if (parentIndex !== -1 && !reorg[parentIndex]._isThreadParent) {
|
||||
// reorg[parentIndex]._isThreadParent = true
|
||||
// item._isThreadChild = true
|
||||
// reorg.splice(parentIndex + 1, 0, item)
|
||||
// continue
|
||||
// }
|
||||
// }
|
||||
// reorg.unshift(item)
|
||||
// }
|
||||
// return reorg
|
||||
const selfReplyUri = getSelfReplyUri(item)
|
||||
if (selfReplyUri) {
|
||||
const parentIndex = reorg.findIndex(item2 => item2.uri === selfReplyUri)
|
||||
if (parentIndex !== -1 && !reorg[parentIndex]._isThreadParent) {
|
||||
reorg[parentIndex]._isThreadParent = true
|
||||
item._isThreadChild = true
|
||||
reorg.splice(parentIndex + 1, 0, item)
|
||||
continue
|
||||
}
|
||||
}
|
||||
reorg.unshift(item)
|
||||
}
|
||||
|
||||
// phase two: identify the positions of the threads
|
||||
let activeSlice = -1
|
||||
let threadSlices: Slice[] = []
|
||||
for (let i = 0; i < reorg.length; i++) {
|
||||
const item = reorg[i] as FeedItemWithThreadMeta
|
||||
if (activeSlice === -1) {
|
||||
if (item._isThreadParent) {
|
||||
activeSlice = i
|
||||
}
|
||||
} else {
|
||||
if (!item._isThreadChild) {
|
||||
threadSlices.push({index: activeSlice, length: i - activeSlice})
|
||||
activeSlice = -1
|
||||
}
|
||||
}
|
||||
}
|
||||
if (activeSlice !== -1) {
|
||||
threadSlices.push({index: activeSlice, length: reorg.length - activeSlice})
|
||||
}
|
||||
|
||||
// phase three: reorder the feed so that the timestamp of the
|
||||
// last post in a thread establishes its ordering
|
||||
for (const slice of threadSlices) {
|
||||
const removed: FeedItemWithThreadMeta[] = reorg.splice(
|
||||
slice.index,
|
||||
slice.length,
|
||||
)
|
||||
const targetDate = new Date(removed[removed.length - 1].indexedAt)
|
||||
const newIndex = reorg.findIndex(
|
||||
item => new Date(item.indexedAt) < targetDate,
|
||||
)
|
||||
reorg.splice(newIndex, 0, ...removed)
|
||||
slice.index = newIndex
|
||||
}
|
||||
|
||||
// phase four: compress any threads that are longer than 3 posts
|
||||
let removedCount = 0
|
||||
for (const slice of threadSlices) {
|
||||
if (slice.length > 3) {
|
||||
reorg.splice(slice.index - removedCount + 1, slice.length - 3)
|
||||
reorg[slice.index - removedCount]._isThreadChildElided = true
|
||||
console.log(reorg[slice.index - removedCount])
|
||||
removedCount += slice.length - 3
|
||||
}
|
||||
}
|
||||
|
||||
return reorg
|
||||
}
|
||||
|
||||
function getSelfReplyUri(
|
||||
|
|
|
@ -48,6 +48,7 @@ export class PostThreadViewPostModel implements GetPostThread.Post {
|
|||
_reactKey: string = ''
|
||||
_depth = 0
|
||||
_isHighlightedPost = false
|
||||
_hasMore = false
|
||||
|
||||
// data
|
||||
$type: string = ''
|
||||
|
|
|
@ -90,14 +90,17 @@ export const PostThread = observer(function PostThread({
|
|||
|
||||
function* flattenThread(
|
||||
post: PostThreadViewPostModel,
|
||||
isAscending = false,
|
||||
): Generator<PostThreadViewPostModel, void> {
|
||||
if (post.parent) {
|
||||
yield* flattenThread(post.parent)
|
||||
yield* flattenThread(post.parent, true)
|
||||
}
|
||||
yield post
|
||||
if (post.replies?.length) {
|
||||
for (const reply of post.replies) {
|
||||
yield* flattenThread(reply)
|
||||
}
|
||||
} else if (!isAscending && !post.parent && post.replyCount > 0) {
|
||||
post._hasMore = true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -226,71 +226,82 @@ export const PostThreadItem = observer(function PostThreadItem({
|
|||
)
|
||||
} else {
|
||||
return (
|
||||
<Link style={styles.outer} href={itemHref} title={itemTitle} noFeedback>
|
||||
{!item.replyingTo && item.record.reply && (
|
||||
<View style={styles.parentReplyLine} />
|
||||
)}
|
||||
{item.replies?.length !== 0 && <View style={styles.childReplyLine} />}
|
||||
{item.replyingTo ? (
|
||||
<View style={styles.replyingTo}>
|
||||
<View style={styles.replyingToLine} />
|
||||
<View style={styles.replyingToAvatar}>
|
||||
<UserAvatar
|
||||
handle={item.replyingTo.author.handle}
|
||||
displayName={item.replyingTo.author.displayName}
|
||||
avatar={item.replyingTo.author.avatar}
|
||||
size={30}
|
||||
<>
|
||||
<Link style={styles.outer} href={itemHref} title={itemTitle} noFeedback>
|
||||
{!item.replyingTo && item.record.reply && (
|
||||
<View style={styles.parentReplyLine} />
|
||||
)}
|
||||
{item.replies?.length !== 0 && <View style={styles.childReplyLine} />}
|
||||
{item.replyingTo ? (
|
||||
<View style={styles.replyingTo}>
|
||||
<View style={styles.replyingToLine} />
|
||||
<View style={styles.replyingToAvatar}>
|
||||
<UserAvatar
|
||||
handle={item.replyingTo.author.handle}
|
||||
displayName={item.replyingTo.author.displayName}
|
||||
avatar={item.replyingTo.author.avatar}
|
||||
size={30}
|
||||
/>
|
||||
</View>
|
||||
<Text style={styles.replyingToText} numberOfLines={2}>
|
||||
{item.replyingTo.text}
|
||||
</Text>
|
||||
</View>
|
||||
) : undefined}
|
||||
<View style={styles.layout}>
|
||||
<View style={styles.layoutAvi}>
|
||||
<Link href={authorHref} title={authorTitle}>
|
||||
<UserAvatar
|
||||
size={50}
|
||||
displayName={item.author.displayName}
|
||||
handle={item.author.handle}
|
||||
avatar={item.author.avatar}
|
||||
/>
|
||||
</Link>
|
||||
</View>
|
||||
<View style={styles.layoutContent}>
|
||||
<PostMeta
|
||||
itemHref={itemHref}
|
||||
itemTitle={itemTitle}
|
||||
authorHref={authorHref}
|
||||
authorHandle={item.author.handle}
|
||||
authorDisplayName={item.author.displayName}
|
||||
timestamp={item.indexedAt}
|
||||
isAuthor={item.author.did === store.me.did}
|
||||
onCopyPostText={onCopyPostText}
|
||||
onDeletePost={onDeletePost}
|
||||
/>
|
||||
<View style={styles.postTextContainer}>
|
||||
<RichText
|
||||
text={record.text}
|
||||
entities={record.entities}
|
||||
style={[styles.postText]}
|
||||
/>
|
||||
</View>
|
||||
<PostEmbeds embed={item.embed} style={{marginBottom: 10}} />
|
||||
<PostCtrls
|
||||
replyCount={item.replyCount}
|
||||
repostCount={item.repostCount}
|
||||
upvoteCount={item.upvoteCount}
|
||||
isReposted={!!item.myState.repost}
|
||||
isUpvoted={!!item.myState.upvote}
|
||||
onPressReply={onPressReply}
|
||||
onPressToggleRepost={onPressToggleRepost}
|
||||
onPressToggleUpvote={onPressToggleUpvote}
|
||||
/>
|
||||
</View>
|
||||
<Text style={styles.replyingToText} numberOfLines={2}>
|
||||
{item.replyingTo.text}
|
||||
</Text>
|
||||
</View>
|
||||
</Link>
|
||||
{item._hasMore ? (
|
||||
<Link
|
||||
style={styles.loadMore}
|
||||
href={itemHref}
|
||||
title={itemTitle}
|
||||
noFeedback>
|
||||
<Text style={styles.loadMoreText}>Load more</Text>
|
||||
</Link>
|
||||
) : undefined}
|
||||
<View style={styles.layout}>
|
||||
<View style={styles.layoutAvi}>
|
||||
<Link href={authorHref} title={authorTitle}>
|
||||
<UserAvatar
|
||||
size={50}
|
||||
displayName={item.author.displayName}
|
||||
handle={item.author.handle}
|
||||
avatar={item.author.avatar}
|
||||
/>
|
||||
</Link>
|
||||
</View>
|
||||
<View style={styles.layoutContent}>
|
||||
<PostMeta
|
||||
itemHref={itemHref}
|
||||
itemTitle={itemTitle}
|
||||
authorHref={authorHref}
|
||||
authorHandle={item.author.handle}
|
||||
authorDisplayName={item.author.displayName}
|
||||
timestamp={item.indexedAt}
|
||||
isAuthor={item.author.did === store.me.did}
|
||||
onCopyPostText={onCopyPostText}
|
||||
onDeletePost={onDeletePost}
|
||||
/>
|
||||
<View style={styles.postTextContainer}>
|
||||
<RichText
|
||||
text={record.text}
|
||||
entities={record.entities}
|
||||
style={[styles.postText]}
|
||||
/>
|
||||
</View>
|
||||
<PostEmbeds embed={item.embed} style={{marginBottom: 10}} />
|
||||
<PostCtrls
|
||||
replyCount={item.replyCount}
|
||||
repostCount={item.repostCount}
|
||||
upvoteCount={item.upvoteCount}
|
||||
isReposted={!!item.myState.repost}
|
||||
isUpvoted={!!item.myState.upvote}
|
||||
onPressReply={onPressReply}
|
||||
onPressToggleRepost={onPressToggleRepost}
|
||||
onPressToggleUpvote={onPressToggleUpvote}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
}
|
||||
})
|
||||
|
@ -398,4 +409,16 @@ const styles = StyleSheet.create({
|
|||
expandedInfoItem: {
|
||||
marginRight: 10,
|
||||
},
|
||||
loadMore: {
|
||||
paddingLeft: 28,
|
||||
paddingVertical: 10,
|
||||
backgroundColor: colors.white,
|
||||
borderRadius: 6,
|
||||
margin: 2,
|
||||
marginBottom: 0,
|
||||
},
|
||||
loadMoreText: {
|
||||
fontSize: 17,
|
||||
color: colors.blue3,
|
||||
},
|
||||
})
|
||||
|
|
|
@ -2,6 +2,7 @@ import React, {useMemo, useState} from 'react'
|
|||
import {observer} from 'mobx-react-lite'
|
||||
import {StyleSheet, Text, View} from 'react-native'
|
||||
import Clipboard from '@react-native-clipboard/clipboard'
|
||||
import Svg, {Circle} from 'react-native-svg'
|
||||
import {AtUri} from '../../../third-party/uri'
|
||||
import * as PostType from '../../../third-party/api/src/client/types/app/bsky/feed/post'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
|
@ -207,6 +208,22 @@ export const FeedItem = observer(function FeedItem({
|
|||
</View>
|
||||
</View>
|
||||
</Link>
|
||||
{item._isThreadChildElided ? (
|
||||
<Link
|
||||
style={styles.viewFullThread}
|
||||
href={itemHref}
|
||||
title={itemTitle}
|
||||
noFeedback>
|
||||
<View style={styles.viewFullThreadDots}>
|
||||
<Svg width="4" height="30">
|
||||
<Circle x="2" y="5" r="1.5" fill={colors.gray3} />
|
||||
<Circle x="2" y="11" r="1.5" fill={colors.gray3} />
|
||||
<Circle x="2" y="17" r="1.5" fill={colors.gray3} />
|
||||
</Svg>
|
||||
</View>
|
||||
<Text style={styles.viewFullThreadText}>View full thread</Text>
|
||||
</Link>
|
||||
) : undefined}
|
||||
</>
|
||||
)
|
||||
})
|
||||
|
@ -281,4 +298,18 @@ const styles = StyleSheet.create({
|
|||
postEmbeds: {
|
||||
marginBottom: 10,
|
||||
},
|
||||
viewFullThread: {
|
||||
backgroundColor: colors.white,
|
||||
paddingTop: 4,
|
||||
paddingLeft: 72,
|
||||
},
|
||||
viewFullThreadDots: {
|
||||
position: 'absolute',
|
||||
left: 35,
|
||||
top: 0,
|
||||
},
|
||||
viewFullThreadText: {
|
||||
color: colors.blue3,
|
||||
fontSize: 16,
|
||||
},
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue