about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/state/models/feed-view.ts119
-rw-r--r--src/view/com/posts/FeedItem.tsx49
-rw-r--r--src/view/com/util/UserAvatar.tsx6
-rw-r--r--src/view/com/util/UserBanner.tsx6
4 files changed, 140 insertions, 40 deletions
diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts
index f5dce9e05..65248c575 100644
--- a/src/state/models/feed-view.ts
+++ b/src/state/models/feed-view.ts
@@ -1,6 +1,8 @@
 import {makeAutoObservable, runInAction} from 'mobx'
+import {Record as PostRecord} from '../../third-party/api/src/client/types/app/bsky/feed/post'
 import * as GetTimeline from '../../third-party/api/src/client/types/app/bsky/feed/getTimeline'
 import * as GetAuthorFeed from '../../third-party/api/src/client/types/app/bsky/feed/getAuthorFeed'
+import {PostThreadViewModel} from './post-thread-view'
 import {AtUri} from '../../third-party/uri'
 import {RootStoreModel} from './root-store'
 import * as apilib from '../lib/api'
@@ -43,10 +45,6 @@ export class FeedItemModel implements GetTimeline.FeedItem {
   repostedBy?: GetTimeline.Actor
   trendedBy?: GetTimeline.Actor
   record: Record<string, unknown> = {}
-  embed?:
-    | GetTimeline.RecordEmbed
-    | GetTimeline.ExternalEmbed
-    | GetTimeline.UnknownEmbed
   replyCount: number = 0
   repostCount: number = 0
   upvoteCount: number = 0
@@ -54,6 +52,9 @@ export class FeedItemModel implements GetTimeline.FeedItem {
   indexedAt: string = ''
   myState = new FeedItemMyStateModel()
 
+  // additional data
+  additionalParentPost?: PostThreadViewModel
+
   constructor(
     public rootStore: RootStoreModel,
     reactKey: string,
@@ -73,7 +74,6 @@ export class FeedItemModel implements GetTimeline.FeedItem {
     this.repostedBy = v.repostedBy
     this.trendedBy = v.trendedBy
     this.record = v.record
-    this.embed = v.embed
     this.replyCount = v.replyCount
     this.repostCount = v.repostCount
     this.upvoteCount = v.upvoteCount
@@ -156,6 +156,29 @@ export class FeedItemModel implements GetTimeline.FeedItem {
       rkey: new AtUri(this.uri).rkey,
     })
   }
+
+  get needsAdditionalData() {
+    if (
+      (this.record as PostRecord).reply?.parent?.uri &&
+      !this._isThreadChild
+    ) {
+      return !this.additionalParentPost
+    }
+    return false
+  }
+
+  async fetchAdditionalData() {
+    if (!this.needsAdditionalData) {
+      return
+    }
+    this.additionalParentPost = new PostThreadViewModel(this.rootStore, {
+      uri: (this.record as PostRecord).reply?.parent.uri,
+      depth: 0,
+    })
+    await this.additionalParentPost.setup().catch(e => {
+      console.error('Failed to load post needed by notification', e)
+    })
+  }
 }
 
 export class FeedModel {
@@ -345,7 +368,7 @@ export class FeedModel {
     this._xLoading(isRefreshing)
     try {
       const res = await this._getFeed({limit: PAGE_SIZE})
-      this._replaceAll(res)
+      await this._replaceAll(res)
       this._xIdle()
     } catch (e: any) {
       this._xIdle(e.toString())
@@ -356,7 +379,7 @@ export class FeedModel {
     this._xLoading()
     try {
       const res = await this._getFeed({limit: PAGE_SIZE})
-      this._prependAll(res)
+      await this._prependAll(res)
       this._xIdle()
     } catch (e: any) {
       this._xIdle(e.toString())
@@ -373,7 +396,7 @@ export class FeedModel {
         before: this.loadMoreCursor,
         limit: PAGE_SIZE,
       })
-      this._appendAll(res)
+      await this._appendAll(res)
       this._xIdle()
     } catch (e: any) {
       this._xIdle(`Failed to load feed: ${e.toString()}`)
@@ -407,13 +430,17 @@ export class FeedModel {
     }
   }
 
-  private _replaceAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
-    this.feed.length = 0
+  private async _replaceAll(
+    res: GetTimeline.Response | GetAuthorFeed.Response,
+  ) {
     this.pollCursor = res.data.feed[0]?.uri
-    this._appendAll(res)
+    return this._appendAll(res, true)
   }
 
-  private _appendAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
+  private async _appendAll(
+    res: GetTimeline.Response | GetAuthorFeed.Response,
+    replace = false,
+  ) {
     this.loadMoreCursor = res.data.cursor
     this.hasMore = !!this.loadMoreCursor
     let counter = this.feed.length
@@ -428,40 +455,64 @@ export class FeedModel {
     // -prf
     const reorgedFeed = preprocessFeed(res.data.feed, this.feedType === 'home')
 
+    const promises = []
+    const toAppend: FeedItemModel[] = []
     for (const item of reorgedFeed) {
-      this._append(counter++, item)
+      const itemModel = new FeedItemModel(
+        this.rootStore,
+        `item-${counter++}`,
+        item,
+      )
+      if (itemModel.needsAdditionalData) {
+        promises.push(
+          itemModel.fetchAdditionalData().catch(e => {
+            console.error('Failure during feed-view _appendAll()', e)
+          }),
+        )
+      }
+      toAppend.push(itemModel)
     }
+    await Promise.all(promises)
+    runInAction(() => {
+      if (replace) {
+        this.feed = toAppend
+      } else {
+        this.feed = this.feed.concat(toAppend)
+      }
+    })
   }
 
-  private _append(
-    keyId: number,
-    item: GetTimeline.FeedItem | GetAuthorFeed.FeedItem,
+  private async _prependAll(
+    res: GetTimeline.Response | GetAuthorFeed.Response,
   ) {
-    // TODO: validate .record
-    this.feed.push(new FeedItemModel(this.rootStore, `item-${keyId}`, item))
-  }
-
-  private _prependAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
     this.pollCursor = res.data.feed[0]?.uri
     let counter = this.feed.length
-    const toPrepend = []
+
+    const promises = []
+    const toPrepend: FeedItemModel[] = []
     for (const item of res.data.feed) {
       if (this.feed.find(item2 => item2.uri === item.uri)) {
         break // stop here - we've hit a post we already have
       }
-      toPrepend.unshift(item) // reverse the order
-    }
-    for (const item of toPrepend) {
-      this._prepend(counter++, item)
-    }
-  }
 
-  private _prepend(
-    keyId: number,
-    item: GetTimeline.FeedItem | GetAuthorFeed.FeedItem,
-  ) {
-    // TODO: validate .record
-    this.feed.unshift(new FeedItemModel(this.rootStore, `item-${keyId}`, item))
+      const itemModel = new FeedItemModel(
+        this.rootStore,
+        `item-${counter++}`,
+        item,
+      )
+      if (itemModel.needsAdditionalData) {
+        promises.push(
+          itemModel.fetchAdditionalData().catch(e => {
+            console.error('Failure during feed-view _prependAll()', e)
+          }),
+        )
+      }
+      toPrepend.push(itemModel)
+    }
+    await Promise.all(promises)
+    runInAction(() => {
+      this.feed = toPrepend.concat(this.feed)
+    })
   }
 
   private _updateAll(res: GetTimeline.Response | GetAuthorFeed.Response) {
diff --git a/src/view/com/posts/FeedItem.tsx b/src/view/com/posts/FeedItem.tsx
index b34fe239d..0e3a58090 100644
--- a/src/view/com/posts/FeedItem.tsx
+++ b/src/view/com/posts/FeedItem.tsx
@@ -18,6 +18,7 @@ import {s, colors} from '../../lib/styles'
 import {useStores} from '../../../state'
 
 const TOP_REPLY_LINE_LENGTH = 12
+const REPLYING_TO_LINE_LENGTH = 8
 
 export const FeedItem = observer(function FeedItem({
   item,
@@ -129,6 +130,25 @@ export const FeedItem = observer(function FeedItem({
           </Text>
         </Link>
       )}
+      {item.additionalParentPost ? (
+        <View style={styles.replyingTo}>
+          <View style={styles.replyingToLine} />
+          <View style={styles.replyingToAvatar}>
+            <UserAvatar
+              handle={item.additionalParentPost?.thread?.author.handle}
+              displayName={
+                item.additionalParentPost?.thread?.author.displayName
+              }
+              size={32}
+            />
+          </View>
+          <View style={styles.replyingToTextContainer}>
+            <Text style={styles.replyingToText} numberOfLines={2}>
+              {item.additionalParentPost?.thread?.record.text}
+            </Text>
+          </View>
+        </View>
+      ) : undefined}
       <View style={styles.layout}>
         <View style={styles.layoutAvi}>
           <Link
@@ -237,6 +257,35 @@ const styles = StyleSheet.create({
     marginRight: 4,
     color: colors.gray4,
   },
+  replyingToLine: {
+    position: 'absolute',
+    left: 24,
+    bottom: -1 * REPLYING_TO_LINE_LENGTH + 6,
+    height: REPLYING_TO_LINE_LENGTH,
+    borderLeftWidth: 2,
+    borderLeftColor: colors.gray2,
+  },
+  replyingTo: {
+    flexDirection: 'row',
+    backgroundColor: colors.white,
+    paddingBottom: 8,
+    paddingRight: 24,
+  },
+  replyingToAvatar: {
+    marginLeft: 9,
+    marginRight: 19,
+    marginTop: 1,
+  },
+  replyingToTextContainer: {
+    flex: 1,
+    flexDirection: 'row',
+    height: 34,
+    alignItems: 'center',
+  },
+  replyingToText: {
+    flex: 1,
+    color: colors.gray5,
+  },
   layout: {
     flexDirection: 'row',
   },
diff --git a/src/view/com/util/UserAvatar.tsx b/src/view/com/util/UserAvatar.tsx
index 2ed161253..2b2388473 100644
--- a/src/view/com/util/UserAvatar.tsx
+++ b/src/view/com/util/UserAvatar.tsx
@@ -21,7 +21,7 @@ export function UserAvatar({
   size: number
   handle: string
   displayName: string | undefined
-  userAvatar: string | null
+  userAvatar: string | null | undefined
   setUserAvatar?: React.Dispatch<React.SetStateAction<string | null>>
 }) {
   const initials = getInitials(displayName || handle)
@@ -92,7 +92,7 @@ export function UserAvatar({
   // setUserAvatar is only passed as prop on the EditProfile component
   return setUserAvatar != null && IMAGES_ENABLED ? (
     <TouchableOpacity onPress={handleEditAvatar}>
-      {userAvatar != null ? (
+      {userAvatar ? (
         <Image style={styles.avatarImage} source={{uri: userAvatar}} />
       ) : (
         renderSvg(size, initials)
@@ -105,7 +105,7 @@ export function UserAvatar({
         />
       </View>
     </TouchableOpacity>
-  ) : userAvatar != null ? (
+  ) : userAvatar ? (
     <Image
       style={styles.avatarImage}
       resizeMode="stretch"
diff --git a/src/view/com/util/UserBanner.tsx b/src/view/com/util/UserBanner.tsx
index c0421fe12..684e984bd 100644
--- a/src/view/com/util/UserBanner.tsx
+++ b/src/view/com/util/UserBanner.tsx
@@ -17,7 +17,7 @@ export function UserBanner({
   setUserBanner,
 }: {
   handle: string
-  userBanner: string | null
+  userBanner: string | null | undefined
   setUserBanner?: React.Dispatch<React.SetStateAction<string | null>>
 }) {
   const gradient = getGradient(handle)
@@ -81,7 +81,7 @@ export function UserBanner({
   // setUserBanner is only passed as prop on the EditProfile component
   return setUserBanner != null && IMAGES_ENABLED ? (
     <TouchableOpacity onPress={handleEditBanner}>
-      {userBanner != null ? (
+      {userBanner ? (
         <Image style={styles.bannerImage} source={{uri: userBanner}} />
       ) : (
         renderSvg()
@@ -94,7 +94,7 @@ export function UserBanner({
         />
       </View>
     </TouchableOpacity>
-  ) : userBanner != null ? (
+  ) : userBanner ? (
     <Image
       style={styles.bannerImage}
       resizeMode="stretch"