Add threading to post feeds
parent
8da3124f3a
commit
ba837ad9af
|
@ -5,6 +5,13 @@ import {AtUri} from '../../third-party/uri'
|
||||||
import {RootStoreModel} from './root-store'
|
import {RootStoreModel} from './root-store'
|
||||||
import * as apilib from '../lib/api'
|
import * as apilib from '../lib/api'
|
||||||
import {cleanError} from '../../lib/strings'
|
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 {
|
export class FeedItemMyStateModel {
|
||||||
repost?: string
|
repost?: string
|
||||||
|
@ -19,6 +26,8 @@ export class FeedItemMyStateModel {
|
||||||
export class FeedItemModel implements GetTimeline.FeedItem {
|
export class FeedItemModel implements GetTimeline.FeedItem {
|
||||||
// ui state
|
// ui state
|
||||||
_reactKey: string = ''
|
_reactKey: string = ''
|
||||||
|
_isThreadParent: boolean = false
|
||||||
|
_isThreadChild: boolean = false
|
||||||
|
|
||||||
// data
|
// data
|
||||||
uri: string = ''
|
uri: string = ''
|
||||||
|
@ -46,11 +55,13 @@ export class FeedItemModel implements GetTimeline.FeedItem {
|
||||||
constructor(
|
constructor(
|
||||||
public rootStore: RootStoreModel,
|
public rootStore: RootStoreModel,
|
||||||
reactKey: string,
|
reactKey: string,
|
||||||
v: GetTimeline.FeedItem | GetAuthorFeed.FeedItem,
|
v: FeedItemWithThreadMeta,
|
||||||
) {
|
) {
|
||||||
makeAutoObservable(this, {rootStore: false})
|
makeAutoObservable(this, {rootStore: false})
|
||||||
this._reactKey = reactKey
|
this._reactKey = reactKey
|
||||||
this.copy(v)
|
this.copy(v)
|
||||||
|
this._isThreadParent = v._isThreadParent || false
|
||||||
|
this._isThreadChild = v._isThreadChild || false
|
||||||
}
|
}
|
||||||
|
|
||||||
copy(v: GetTimeline.FeedItem | GetAuthorFeed.FeedItem) {
|
copy(v: GetTimeline.FeedItem | GetAuthorFeed.FeedItem) {
|
||||||
|
@ -197,7 +208,9 @@ export class FeedModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
get nonReplyFeed() {
|
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) {
|
setHasNewLatest(v: boolean) {
|
||||||
|
@ -391,17 +404,18 @@ export class FeedModel {
|
||||||
this.loadMoreCursor = res.data.cursor
|
this.loadMoreCursor = res.data.cursor
|
||||||
this.hasMore = !!this.loadMoreCursor
|
this.hasMore = !!this.loadMoreCursor
|
||||||
let counter = this.feed.length
|
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)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -15,6 +15,8 @@ import {UserAvatar} from '../util/UserAvatar'
|
||||||
import {s, colors} from '../../lib/styles'
|
import {s, colors} from '../../lib/styles'
|
||||||
import {useStores} from '../../../state'
|
import {useStores} from '../../../state'
|
||||||
|
|
||||||
|
const TOP_REPLY_LINE_LENGTH = 12
|
||||||
|
|
||||||
export const FeedItem = observer(function FeedItem({
|
export const FeedItem = observer(function FeedItem({
|
||||||
item,
|
item,
|
||||||
}: {
|
}: {
|
||||||
|
@ -74,8 +76,22 @@ export const FeedItem = observer(function FeedItem({
|
||||||
return <View />
|
return <View />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const outerStyles = [
|
||||||
|
styles.outer,
|
||||||
|
item._isThreadChild ? styles.outerNoTop : undefined,
|
||||||
|
item._isThreadParent ? styles.outerNoBottom : undefined,
|
||||||
|
]
|
||||||
return (
|
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 && (
|
{item.repostedBy && (
|
||||||
<Link
|
<Link
|
||||||
style={styles.includeReason}
|
style={styles.includeReason}
|
||||||
|
@ -103,26 +119,31 @@ export const FeedItem = observer(function FeedItem({
|
||||||
)}
|
)}
|
||||||
<View style={styles.layout}>
|
<View style={styles.layout}>
|
||||||
<View style={styles.layoutAvi}>
|
<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
|
<UserAvatar
|
||||||
size={50}
|
size={item._isThreadChild ? 30 : 50}
|
||||||
displayName={item.author.displayName}
|
displayName={item.author.displayName}
|
||||||
handle={item.author.handle}
|
handle={item.author.handle}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.layoutContent}>
|
<View style={styles.layoutContent}>
|
||||||
<PostMeta
|
{!item._isThreadChild ? (
|
||||||
itemHref={itemHref}
|
<PostMeta
|
||||||
itemTitle={itemTitle}
|
itemHref={itemHref}
|
||||||
authorHref={authorHref}
|
itemTitle={itemTitle}
|
||||||
authorHandle={item.author.handle}
|
authorHref={authorHref}
|
||||||
authorDisplayName={item.author.displayName}
|
authorHandle={item.author.handle}
|
||||||
timestamp={item.indexedAt}
|
authorDisplayName={item.author.displayName}
|
||||||
isAuthor={item.author.did === store.me.did}
|
timestamp={item.indexedAt}
|
||||||
onDeletePost={onDeletePost}
|
isAuthor={item.author.did === store.me.did}
|
||||||
/>
|
onDeletePost={onDeletePost}
|
||||||
{replyHref !== '' && (
|
/>
|
||||||
|
) : undefined}
|
||||||
|
{!item._isThreadChild && replyHref !== '' && (
|
||||||
<View style={[s.flexRow, s.mb5, {alignItems: 'center'}]}>
|
<View style={[s.flexRow, s.mb5, {alignItems: 'center'}]}>
|
||||||
<Text style={[s.gray5, s.f15, s.mr2]}>Replying to</Text>
|
<Text style={[s.gray5, s.f15, s.mr2]}>Replying to</Text>
|
||||||
<Link href={replyHref} title="Parent post">
|
<Link href={replyHref} title="Parent post">
|
||||||
|
@ -165,6 +186,35 @@ const styles = StyleSheet.create({
|
||||||
backgroundColor: colors.white,
|
backgroundColor: colors.white,
|
||||||
padding: 10,
|
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: {
|
includeReason: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
paddingLeft: 60,
|
paddingLeft: 60,
|
||||||
|
|
Loading…
Reference in New Issue