diff options
-rw-r--r-- | src/state/models/feed-view.ts | 117 | ||||
-rw-r--r-- | src/state/models/post-thread-view.ts | 1 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThread.tsx | 5 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThreadItem.tsx | 143 | ||||
-rw-r--r-- | src/view/com/posts/FeedItem.tsx | 31 |
5 files changed, 195 insertions, 102 deletions
diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts index f2832887a..f8080d4b5 100644 --- a/src/state/models/feed-view.ts +++ b/src/state/models/feed-view.ts @@ -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 - - // if (dedup) { - // if (reorg.find(item2 => item2.uri === item.uri)) { - // continue - // } - // } - - // 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 +interface Slice { + index: number + length: number +} +function preprocessFeed(feed: FeedItem[]): FeedItemWithThreadMeta[] { + const reorg: FeedItemWithThreadMeta[] = [] + + // 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) + } + + // 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( diff --git a/src/state/models/post-thread-view.ts b/src/state/models/post-thread-view.ts index ebe5b730d..64de7d260 100644 --- a/src/state/models/post-thread-view.ts +++ b/src/state/models/post-thread-view.ts @@ -48,6 +48,7 @@ export class PostThreadViewPostModel implements GetPostThread.Post { _reactKey: string = '' _depth = 0 _isHighlightedPost = false + _hasMore = false // data $type: string = '' diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx index bbaa4efa2..0df505a74 100644 --- a/src/view/com/post-thread/PostThread.tsx +++ b/src/view/com/post-thread/PostThread.tsx @@ -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 } } diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx index 1a0c744d6..45fd86116 100644 --- a/src/view/com/post-thread/PostThreadItem.tsx +++ b/src/view/com/post-thread/PostThreadItem.tsx @@ -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> - <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} + ) : 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} /> - </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 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> - <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> + </Link> + {item._hasMore ? ( + <Link + style={styles.loadMore} + href={itemHref} + title={itemTitle} + noFeedback> + <Text style={styles.loadMoreText}>Load more</Text> + </Link> + ) : undefined} + </> ) } }) @@ -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, + }, }) diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx index 74edad365..51f76904f 100644 --- a/src/view/com/posts/FeedItem.tsx +++ b/src/view/com/posts/FeedItem.tsx @@ -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, + }, }) |