Add threading to post feeds

zio/stable
Paul Frazee 2022-11-23 10:39:28 -06:00
parent 8da3124f3a
commit ba837ad9af
2 changed files with 137 additions and 26 deletions

View File

@ -5,6 +5,13 @@ import {AtUri} from '../../third-party/uri'
import {RootStoreModel} from './root-store'
import * as apilib from '../lib/api'
import {cleanError} from '../../lib/strings'
import {isObj, hasProp} from '../lib/type-guards'
type FeedItem = GetTimeline.FeedItem | GetAuthorFeed.FeedItem
type FeedItemWithThreadMeta = FeedItem & {
_isThreadParent?: boolean
_isThreadChild?: boolean
}
export class FeedItemMyStateModel {
repost?: string
@ -19,6 +26,8 @@ export class FeedItemMyStateModel {
export class FeedItemModel implements GetTimeline.FeedItem {
// ui state
_reactKey: string = ''
_isThreadParent: boolean = false
_isThreadChild: boolean = false
// data
uri: string = ''
@ -46,11 +55,13 @@ export class FeedItemModel implements GetTimeline.FeedItem {
constructor(
public rootStore: RootStoreModel,
reactKey: string,
v: GetTimeline.FeedItem | GetAuthorFeed.FeedItem,
v: FeedItemWithThreadMeta,
) {
makeAutoObservable(this, {rootStore: false})
this._reactKey = reactKey
this.copy(v)
this._isThreadParent = v._isThreadParent || false
this._isThreadChild = v._isThreadChild || false
}
copy(v: GetTimeline.FeedItem | GetAuthorFeed.FeedItem) {
@ -197,7 +208,9 @@ export class FeedModel {
}
get nonReplyFeed() {
return this.feed.filter(post => !post.record.reply)
return this.feed.filter(
post => !post.record.reply || post._isThreadParent || post._isThreadChild,
)
}
setHasNewLatest(v: boolean) {
@ -391,17 +404,18 @@ export class FeedModel {
this.loadMoreCursor = res.data.cursor
this.hasMore = !!this.loadMoreCursor
let counter = this.feed.length
for (const item of res.data.feed) {
// HACK
// deduplicate posts on the home feed
// (should be done on the server)
// -prf
if (this.feedType === 'home') {
if (this.feed.find(item2 => item2.uri === item.uri)) {
continue
}
}
// 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')
for (const item of reorgedFeed) {
this._append(counter++, item)
}
}
@ -465,3 +479,50 @@ export class FeedModel {
}
}
}
function preprocessFeed(
feed: FeedItem[],
dedup: boolean,
): FeedItemWithThreadMeta[] {
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
}
function getSelfReplyUri(
item: GetTimeline.FeedItem | GetAuthorFeed.FeedItem,
): string | undefined {
if (
isObj(item.record) &&
hasProp(item.record, 'reply') &&
isObj(item.record.reply) &&
hasProp(item.record.reply, 'parent') &&
isObj(item.record.reply.parent) &&
hasProp(item.record.reply.parent, 'uri') &&
typeof item.record.reply.parent.uri === 'string'
) {
if (new AtUri(item.record.reply.parent.uri).host === item.author.did) {
return item.record.reply.parent.uri
}
}
}

View File

@ -15,6 +15,8 @@ import {UserAvatar} from '../util/UserAvatar'
import {s, colors} from '../../lib/styles'
import {useStores} from '../../../state'
const TOP_REPLY_LINE_LENGTH = 12
export const FeedItem = observer(function FeedItem({
item,
}: {
@ -74,8 +76,22 @@ export const FeedItem = observer(function FeedItem({
return <View />
}
const outerStyles = [
styles.outer,
item._isThreadChild ? styles.outerNoTop : undefined,
item._isThreadParent ? styles.outerNoBottom : undefined,
]
return (
<Link style={styles.outer} href={itemHref} title={itemTitle}>
<Link style={outerStyles} href={itemHref} title={itemTitle}>
{item._isThreadChild && <View style={styles.topReplyLine} />}
{item._isThreadParent && (
<View
style={[
styles.bottomReplyLine,
item._isThreadChild ? styles.bottomReplyLineSmallAvi : undefined,
]}
/>
)}
{item.repostedBy && (
<Link
style={styles.includeReason}
@ -103,26 +119,31 @@ export const FeedItem = observer(function FeedItem({
)}
<View style={styles.layout}>
<View style={styles.layoutAvi}>
<Link href={authorHref} title={item.author.handle}>
<Link
href={authorHref}
title={item.author.handle}
style={item._isThreadChild ? {marginLeft: 10} : undefined}>
<UserAvatar
size={50}
size={item._isThreadChild ? 30 : 50}
displayName={item.author.displayName}
handle={item.author.handle}
/>
</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}
onDeletePost={onDeletePost}
/>
{replyHref !== '' && (
{!item._isThreadChild ? (
<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}
onDeletePost={onDeletePost}
/>
) : undefined}
{!item._isThreadChild && replyHref !== '' && (
<View style={[s.flexRow, s.mb5, {alignItems: 'center'}]}>
<Text style={[s.gray5, s.f15, s.mr2]}>Replying to</Text>
<Link href={replyHref} title="Parent post">
@ -165,6 +186,35 @@ const styles = StyleSheet.create({
backgroundColor: colors.white,
padding: 10,
},
outerNoTop: {
marginTop: 1,
borderTopLeftRadius: 0,
borderTopRightRadius: 0,
},
outerNoBottom: {
marginBottom: 0,
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
},
topReplyLine: {
position: 'absolute',
left: 34,
top: -1 * TOP_REPLY_LINE_LENGTH + 10,
height: TOP_REPLY_LINE_LENGTH,
borderLeftWidth: 2,
borderLeftColor: colors.gray2,
},
bottomReplyLine: {
position: 'absolute',
left: 34,
top: 70,
bottom: 0,
borderLeftWidth: 2,
borderLeftColor: colors.gray2,
},
bottomReplyLineSmallAvi: {
top: 50,
},
includeReason: {
flexDirection: 'row',
paddingLeft: 60,