From ae3099dfca13f6651762f6ea9a3d2a14ebc99df4 Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Sun, 18 Dec 2022 18:54:05 -0600 Subject: [PATCH] Improve thread rendering --- src/state/models/feed-view.ts | 113 +++++++++------ src/state/models/post-thread-view.ts | 1 + src/view/com/post-thread/PostThread.tsx | 5 +- src/view/com/post-thread/PostThreadItem.tsx | 145 ++++++++++++-------- src/view/com/posts/FeedItem.tsx | 31 +++++ 5 files changed, 194 insertions(+), 101 deletions(-) diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts index f2832887aa..f8080d4b57 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 +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( diff --git a/src/state/models/post-thread-view.ts b/src/state/models/post-thread-view.ts index ebe5b730dc..64de7d2604 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 bbaa4efa2a..0df505a741 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 { 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 1a0c744d68..45fd861162 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 ( - - {!item.replyingTo && item.record.reply && ( - - )} - {item.replies?.length !== 0 && } - {item.replyingTo ? ( - - - - + + {!item.replyingTo && item.record.reply && ( + + )} + {item.replies?.length !== 0 && } + {item.replyingTo ? ( + + + + + + + {item.replyingTo.text} + + + ) : undefined} + + + + + + + + + + + + + - - {item.replyingTo.text} - + + {item._hasMore ? ( + + Load more + ) : 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 74edad3652..51f76904fb 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({ + {item._isThreadChildElided ? ( + + + + + + + + + View full thread + + ) : 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, + }, })