about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/state/models/feed-view.ts85
-rw-r--r--src/view/com/posts/FeedItem.tsx78
2 files changed, 137 insertions, 26 deletions
diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts
index 7b27b239b..c6b7c0dd4 100644
--- a/src/state/models/feed-view.ts
+++ b/src/state/models/feed-view.ts
@@ -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
+    }
+  }
+}
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index 6deb04209..4d7b307b1 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -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,