about summary refs log tree commit diff
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2022-07-25 14:21:48 -0500
committerPaul Frazee <pfrazee@gmail.com>2022-07-25 14:21:48 -0500
commit3794eca88e13c3c292b0f64b1acb1169ecbeb83d (patch)
treec073772f77d43cb13582db5999b01ae5adf7272a
parent7f04ac172e8ada1244de1df2064e32d32f1c2348 (diff)
downloadvoidsky-3794eca88e13c3c292b0f64b1acb1169ecbeb83d.tar.zst
Add state updates after screen changes
-rw-r--r--src/state/models/feed-view.ts134
-rw-r--r--src/state/models/post-thread-view.ts33
-rw-r--r--src/view/com/feed/Feed.tsx4
-rw-r--r--src/view/com/post-thread/PostThread.tsx11
-rw-r--r--src/view/screens/stacks/Profile.tsx23
-rw-r--r--src/view/screens/tabroots/Home.tsx14
-rw-r--r--todos.txt4
7 files changed, 171 insertions, 52 deletions
diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts
index 2eced3dc9..5264aa27e 100644
--- a/src/state/models/feed-view.ts
+++ b/src/state/models/feed-view.ts
@@ -1,6 +1,5 @@
 import {makeAutoObservable, runInAction} from 'mobx'
 import {bsky} from '@adxp/mock-api'
-import _omit from 'lodash.omit'
 import {RootStoreModel} from './root-store'
 import * as apilib from '../lib/api'
 
@@ -39,9 +38,22 @@ export class FeedViewItemModel implements bsky.FeedView.FeedItem {
   ) {
     makeAutoObservable(this, {rootStore: false})
     this._reactKey = reactKey
-    Object.assign(this, _omit(v, 'myState'))
+    this.copy(v)
+  }
+
+  copy(v: bsky.FeedView.FeedItem) {
+    this.uri = v.uri
+    this.author = v.author
+    this.repostedBy = v.repostedBy
+    this.record = v.record
+    this.embed = v.embed
+    this.replyCount = v.replyCount
+    this.repostCount = v.repostCount
+    this.likeCount = v.likeCount
+    this.indexedAt = v.indexedAt
     if (v.myState) {
-      Object.assign(this.myState, v.myState)
+      this.myState.hasLiked = v.myState.hasLiked
+      this.myState.hasReposted = v.myState.hasReposted
     }
   }
 
@@ -85,7 +97,9 @@ export class FeedViewModel implements bsky.FeedView.Response {
   hasLoaded = false
   error = ''
   params: bsky.FeedView.Params
+  _loadPromise: Promise<void> | undefined
   _loadMorePromise: Promise<void> | undefined
+  _updatePromise: Promise<void> | undefined
 
   // data
   feed: FeedViewItemModel[] = []
@@ -97,6 +111,7 @@ export class FeedViewModel implements bsky.FeedView.Response {
         rootStore: false,
         params: false,
         _loadMorePromise: false,
+        _updatePromise: false,
       },
       {autoBind: true},
     )
@@ -125,33 +140,52 @@ export class FeedViewModel implements bsky.FeedView.Response {
   // public api
   // =
 
-  async setup() {
-    if (this._loadMorePromise) {
-      return this._loadMorePromise
-    }
-    if (this.hasContent) {
-      await this._refresh()
-    } else {
-      await this._initialLoad()
+  /**
+   * Load for first render
+   */
+  async setup(isRefreshing = false) {
+    if (this._loadPromise) {
+      return this._loadPromise
     }
+    await this._pendingWork()
+    this._loadPromise = this._initialLoad(isRefreshing)
+    await this._loadPromise
+    this._loadPromise = undefined
   }
 
+  /**
+   * Reset and load
+   */
   async refresh() {
-    if (this._loadMorePromise) {
-      return this._loadMorePromise
-    }
-    await this._refresh()
+    return this.setup(true)
   }
 
+  /**
+   * Load more posts to the end of the feed
+   */
   async loadMore() {
     if (this._loadMorePromise) {
       return this._loadMorePromise
     }
+    await this._pendingWork()
     this._loadMorePromise = this._loadMore()
     await this._loadMorePromise
     this._loadMorePromise = undefined
   }
 
+  /**
+   * Update content in-place
+   */
+  async update() {
+    if (this._updatePromise) {
+      return this._updatePromise
+    }
+    await this._pendingWork()
+    this._updatePromise = this._update()
+    await this._updatePromise
+    this._updatePromise = undefined
+  }
+
   // state transitions
   // =
 
@@ -171,9 +205,21 @@ export class FeedViewModel implements bsky.FeedView.Response {
   // loader functions
   // =
 
-  private async _initialLoad() {
-    this._xLoading()
-    await new Promise(r => setTimeout(r, 1e3)) // DEBUG
+  private async _pendingWork() {
+    if (this._loadPromise) {
+      await this._loadPromise
+    }
+    if (this._loadMorePromise) {
+      await this._loadMorePromise
+    }
+    if (this._updatePromise) {
+      await this._updatePromise
+    }
+  }
+
+  private async _initialLoad(isRefreshing = false) {
+    this._xLoading(isRefreshing)
+    await new Promise(r => setTimeout(r, 250)) // DEBUG
     try {
       const res = (await this.rootStore.api.mainPds.view(
         'blueskyweb.xyz:FeedView',
@@ -188,7 +234,7 @@ export class FeedViewModel implements bsky.FeedView.Response {
 
   private async _loadMore() {
     this._xLoading()
-    await new Promise(r => setTimeout(r, 1e3)) // DEBUG
+    await new Promise(r => setTimeout(r, 250)) // DEBUG
     try {
       const params = Object.assign({}, this.params, {
         before: this.loadMoreCursor,
@@ -204,19 +250,37 @@ export class FeedViewModel implements bsky.FeedView.Response {
     }
   }
 
-  private async _refresh() {
-    this._xLoading(true)
-    // TODO: refetch and update items
-    await new Promise(r => setTimeout(r, 1e3)) // DEBUG
-    this._xIdle()
+  private async _update() {
+    this._xLoading()
+    await new Promise(r => setTimeout(r, 250)) // DEBUG
+    let numToFetch = this.feed.length
+    let cursor = undefined
+    try {
+      do {
+        const res = (await this.rootStore.api.mainPds.view(
+          'blueskyweb.xyz:FeedView',
+          {
+            before: cursor,
+            limit: Math.min(numToFetch, 100),
+          },
+        )) as bsky.FeedView.Response
+        if (res.feed.length === 0) {
+          break // sanity check
+        }
+        this._updateAll(res)
+        numToFetch -= res.feed.length
+        cursor = this.feed[res.feed.length - 1].indexedAt
+        console.log(numToFetch, cursor, res.feed.length)
+      } while (numToFetch > 0)
+      this._xIdle()
+    } catch (e: any) {
+      this._xIdle(`Failed to update feed: ${e.toString()}`)
+    }
   }
 
   private _replaceAll(res: bsky.FeedView.Response) {
     this.feed.length = 0
-    let counter = 0
-    for (const item of res.feed) {
-      this._append(counter++, item)
-    }
+    this._appendAll(res)
   }
 
   private _appendAll(res: bsky.FeedView.Response) {
@@ -230,4 +294,18 @@ export class FeedViewModel implements bsky.FeedView.Response {
     // TODO: validate .record
     this.feed.push(new FeedViewItemModel(this.rootStore, `item-${keyId}`, item))
   }
+
+  private _updateAll(res: bsky.FeedView.Response) {
+    for (const item of res.feed) {
+      const existingItem = this.feed.find(
+        // this find function has a key subtley- the indexedAt comparison
+        // the reason for this is reposts: they set the URI of the original post, not of the repost record
+        // the indexedAt time will be for the repost however, so we use that to help us
+        item2 => item.uri === item2.uri && item.indexedAt === item2.indexedAt,
+      )
+      if (existingItem) {
+        existingItem.copy(item)
+      }
+    }
+  }
 }
diff --git a/src/state/models/post-thread-view.ts b/src/state/models/post-thread-view.ts
index ef3a49e9e..f3603ec49 100644
--- a/src/state/models/post-thread-view.ts
+++ b/src/state/models/post-thread-view.ts
@@ -163,19 +163,35 @@ export class PostThreadViewModel {
   // public api
   // =
 
+  /**
+   * Load for first render
+   */
   async setup() {
     if (!this.resolvedUri) {
       await this._resolveUri()
     }
     if (this.hasContent) {
-      await this._refresh()
+      await this.update()
     } else {
-      await this._initialLoad()
+      await this._load()
     }
   }
 
+  /**
+   * Reset and load
+   */
   async refresh() {
-    await this._refresh()
+    await this._load(true)
+  }
+
+  /**
+   * Update content in-place
+   */
+  async update() {
+    // NOTE: it currently seems that a full load-and-replace works fine for this
+    //       if the UI loses its place or has jarring re-arrangements, replace this
+    //       with a more in-place update
+    this._load()
   }
 
   // state transitions
@@ -207,8 +223,8 @@ export class PostThreadViewModel {
     })
   }
 
-  private async _initialLoad() {
-    this._xLoading()
+  private async _load(isRefreshing = false) {
+    this._xLoading(isRefreshing)
     try {
       const res = (await this.rootStore.api.mainPds.view(
         'blueskyweb.xyz:PostThreadView',
@@ -221,13 +237,6 @@ export class PostThreadViewModel {
     }
   }
 
-  private async _refresh() {
-    this._xLoading(true)
-    // TODO: refetch and update items
-    await new Promise(r => setTimeout(r, 250)) // DEBUG
-    this._xIdle()
-  }
-
   private _replaceAll(res: bsky.PostThreadView.Response) {
     // TODO: validate .record
     const keyGen = reactKeyGenerator()
diff --git a/src/view/com/feed/Feed.tsx b/src/view/com/feed/Feed.tsx
index fe9d350d1..c666fc05e 100644
--- a/src/view/com/feed/Feed.tsx
+++ b/src/view/com/feed/Feed.tsx
@@ -27,7 +27,9 @@ export const Feed = observer(function Feed({
   }
   return (
     <View>
-      {feed.isLoading && !feed.isRefreshing && <Text>Loading...</Text>}
+      {feed.isLoading && !feed.isRefreshing && !feed.hasContent && (
+        <Text>Loading...</Text>
+      )}
       {feed.hasError && <Text>{feed.error}</Text>}
       {feed.hasContent && (
         <FlatList
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index 8f70e1493..bc9562ea1 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -1,6 +1,7 @@
 import React, {useState, useEffect} from 'react'
 import {observer} from 'mobx-react-lite'
 import {ActivityIndicator, FlatList, Text, View} from 'react-native'
+import {useFocusEffect} from '@react-navigation/native'
 import {OnNavigateContent} from '../../routes/types'
 import {
   PostThreadViewModel,
@@ -9,6 +10,8 @@ import {
 import {useStores} from '../../../state'
 import {PostThreadItem} from './PostThreadItem'
 
+const UPDATE_DELAY = 2e3 // wait 2s before refetching the thread for updates
+
 export const PostThread = observer(function PostThread({
   uri,
   onNavigateContent,
@@ -18,6 +21,7 @@ export const PostThread = observer(function PostThread({
 }) {
   const store = useStores()
   const [view, setView] = useState<PostThreadViewModel | undefined>()
+  const [lastUpdate, setLastUpdate] = useState<number>(Date.now())
 
   useEffect(() => {
     if (view?.params.uri === uri) {
@@ -30,6 +34,13 @@ export const PostThread = observer(function PostThread({
     newView.setup().catch(err => console.error('Failed to fetch thread', err))
   }, [uri, view?.params.uri, store])
 
+  useFocusEffect(() => {
+    if (Date.now() - lastUpdate > UPDATE_DELAY) {
+      view?.update()
+      setLastUpdate(Date.now())
+    }
+  })
+
   // loading
   // =
   if (
diff --git a/src/view/screens/stacks/Profile.tsx b/src/view/screens/stacks/Profile.tsx
index ccdaed4a4..033d3c273 100644
--- a/src/view/screens/stacks/Profile.tsx
+++ b/src/view/screens/stacks/Profile.tsx
@@ -11,19 +11,32 @@ export const Profile = ({
   route,
 }: RootTabsScreenProps<'Profile'>) => {
   const store = useStores()
+  const [hasSetup, setHasSetup] = useState<string>('')
   const [feedView, setFeedView] = useState<FeedViewModel | undefined>()
 
   useEffect(() => {
-    if (feedView?.params.author === route.params.name) {
-      console.log('Profile feed view')
+    const author = route.params.name
+    if (feedView?.params.author === author) {
       return // no change needed? or trigger refresh?
     }
-    console.log('Fetching profile feed view', route.params.name)
-    const newFeedView = new FeedViewModel(store, {author: route.params.name})
+    console.log('Fetching profile feed', author)
+    const newFeedView = new FeedViewModel(store, {author})
     setFeedView(newFeedView)
-    newFeedView.setup().catch(err => console.error('Failed to fetch feed', err))
+    newFeedView
+      .setup()
+      .catch(err => console.error('Failed to fetch feed', err))
+      .then(() => setHasSetup(author))
   }, [route.params.name, feedView?.params.author, store])
 
+  useEffect(() => {
+    return navigation.addListener('focus', () => {
+      if (hasSetup === feedView?.params.author) {
+        console.log('Updating profile feed', hasSetup)
+        feedView?.update()
+      }
+    })
+  }, [navigation, feedView, hasSetup])
+
   const onNavigateContent = (screen: string, props: Record<string, string>) => {
     // @ts-ignore it's up to the callers to supply correct params -prf
     navigation.push(screen, props)
diff --git a/src/view/screens/tabroots/Home.tsx b/src/view/screens/tabroots/Home.tsx
index 446a5a7e9..a9c952473 100644
--- a/src/view/screens/tabroots/Home.tsx
+++ b/src/view/screens/tabroots/Home.tsx
@@ -1,4 +1,4 @@
-import React, {useEffect, useLayoutEffect} from 'react'
+import React, {useState, useEffect, useLayoutEffect} from 'react'
 import {Image, StyleSheet, TouchableOpacity, View} from 'react-native'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {Shell} from '../../shell'
@@ -8,10 +8,11 @@ import {useStores} from '../../../state'
 import {AVIS} from '../../lib/assets'
 
 export function Home({navigation}: RootTabsScreenProps<'HomeTab'>) {
+  const [hasSetup, setHasSetup] = useState<boolean>(false)
   const store = useStores()
   useEffect(() => {
     console.log('Fetching home feed')
-    store.homeFeed.setup()
+    store.homeFeed.setup().then(() => setHasSetup(true))
   }, [store.homeFeed])
 
   const onNavigateContent = (screen: string, props: Record<string, string>) => {
@@ -19,6 +20,15 @@ export function Home({navigation}: RootTabsScreenProps<'HomeTab'>) {
     navigation.navigate(screen, props)
   }
 
+  useEffect(() => {
+    return navigation.addListener('focus', () => {
+      if (hasSetup) {
+        console.log('Updating home feed')
+        store.homeFeed.update()
+      }
+    })
+  }, [navigation, store.homeFeed, hasSetup])
+
   useLayoutEffect(() => {
     navigation.setOptions({
       headerShown: true,
diff --git a/todos.txt b/todos.txt
index e170e38e9..65c054ba6 100644
--- a/todos.txt
+++ b/todos.txt
@@ -1,17 +1,13 @@
 Paul's todo list
 
 - Feed view
-  - Refresh
   - Share btn
 - Thread view
-  - Refresh
   - Share btn
 - Profile view
-  - Refresh
   - Follow / Unfollow
   - Badges
 - Composer
-  - Refresh original view after reply
   - Check on navigation stack during a bunch of replies
 - Search view
   - *