about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorPaul Frazee <pfrazee@gmail.com>2022-07-20 11:07:39 -0500
committerPaul Frazee <pfrazee@gmail.com>2022-07-20 11:07:39 -0500
commit19c694bc601c2b5d494d635134ffe9ca3fdc7774 (patch)
tree4d9a42e58c2f94dd59bcdd36177ff733fd63d9dc /src
parent8131158c0e2f45765e5ee953487161302c731792 (diff)
downloadvoidsky-19c694bc601c2b5d494d635134ffe9ca3fdc7774.tar.zst
Update feed component to use flatlist for lazy-loading scrolling; update feed-view model to match loading needs
Diffstat (limited to 'src')
-rw-r--r--src/state/models/feed-view.ts121
-rw-r--r--src/view/com/Feed.tsx28
-rw-r--r--src/view/screens/Home.tsx2
3 files changed, 124 insertions, 27 deletions
diff --git a/src/state/models/feed-view.ts b/src/state/models/feed-view.ts
index 1fc507a70..4b915a766 100644
--- a/src/state/models/feed-view.ts
+++ b/src/state/models/feed-view.ts
@@ -25,15 +25,22 @@ export class FeedViewItemModel implements bsky.FeedView.FeedItem {
 }
 
 export class FeedViewModel implements bsky.FeedView.Response {
-  state = 'idle'
+  isLoading = false
+  isRefreshing = false
+  hasLoaded = false
   error = ''
   params: bsky.FeedView.Params
   feed: FeedViewItemModel[] = []
+  _loadMorePromise: Promise<void> | undefined
 
   constructor(public rootStore: RootStoreModel, params: bsky.FeedView.Params) {
     makeAutoObservable(
       this,
-      {rootStore: false, params: false},
+      {
+        rootStore: false,
+        params: false,
+        _loadMorePromise: false,
+      },
       {autoBind: true},
     )
     this.params = params
@@ -47,52 +54,126 @@ export class FeedViewModel implements bsky.FeedView.Response {
     return this.error !== ''
   }
 
-  get isLoading() {
-    return this.state === 'loading'
+  get isEmpty() {
+    return this.hasLoaded && !this.hasContent
   }
 
-  get isEmpty() {
-    return !this.hasContent && !this.hasError && !this.isLoading
+  get loadMoreCursor() {
+    if (this.hasContent) {
+      return this.feed[this.feed.length - 1].indexedAt
+    }
+    return undefined
   }
 
-  async fetch() {
+  // public api
+  // =
+
+  async setup() {
+    if (this._loadMorePromise) {
+      return this._loadMorePromise
+    }
     if (this.hasContent) {
-      await this.updateContent()
+      await this._refresh()
     } else {
-      await this.initialLoad()
+      await this._initialLoad()
+    }
+  }
+
+  async refresh() {
+    if (this._loadMorePromise) {
+      return this._loadMorePromise
     }
+    await this._refresh()
   }
 
-  async initialLoad() {
-    this.state = 'loading'
+  async loadMore() {
+    if (this._loadMorePromise) {
+      return this._loadMorePromise
+    }
+    this._loadMorePromise = this._loadMore()
+    await this._loadMorePromise
+    this._loadMorePromise = undefined
+  }
+
+  // state transitions
+  // =
+
+  private _xLoading(isRefreshing = false) {
+    this.isLoading = true
+    this.isRefreshing = isRefreshing
     this.error = ''
+  }
+
+  private _xIdle(err: string = '') {
+    this.isLoading = false
+    this.isRefreshing = false
+    this.hasLoaded = true
+    this.error = err
+  }
+
+  // loader functions
+  // =
+
+  private async _initialLoad() {
+    console.log('_initialLoad()')
+    this._xLoading()
+    await new Promise(r => setTimeout(r, 1e3)) // DEBUG
     try {
       const res = (await this.rootStore.api.mainPds.view(
         'blueskyweb.xyz:FeedView',
         this.params,
       )) as bsky.FeedView.Response
       this._replaceAll(res)
-      runInAction(() => {
-        this.state = 'idle'
-      })
+      this._xIdle()
     } catch (e: any) {
-      runInAction(() => {
-        this.state = 'error'
-        this.error = `Failed to load feed: ${e.toString()}`
+      this._xIdle(`Failed to load feed: ${e.toString()}`)
+    }
+  }
+
+  private async _loadMore() {
+    console.log('_loadMore()')
+    this._xLoading()
+    await new Promise(r => setTimeout(r, 1e3)) // DEBUG
+    try {
+      const params = Object.assign({}, this.params, {
+        before: this.loadMoreCursor,
       })
+      const res = (await this.rootStore.api.mainPds.view(
+        'blueskyweb.xyz:FeedView',
+        params,
+      )) as bsky.FeedView.Response
+      this._appendAll(res)
+      this._xIdle()
+    } catch (e: any) {
+      this._xIdle(`Failed to load feed: ${e.toString()}`)
     }
   }
 
-  async updateContent() {
+  private async _refresh() {
+    console.log('_refresh()')
+    this._xLoading(true)
     // TODO: refetch and update items
+    await new Promise(r => setTimeout(r, 1e3)) // DEBUG
+    this._xIdle()
   }
 
   private _replaceAll(res: bsky.FeedView.Response) {
     this.feed.length = 0
     let counter = 0
     for (const item of res.feed) {
-      // TODO: validate .record
-      this.feed.push(new FeedViewItemModel(`item-${counter++}`, item))
+      this._append(counter++, item)
+    }
+  }
+
+  private _appendAll(res: bsky.FeedView.Response) {
+    let counter = this.feed.length
+    for (const item of res.feed) {
+      this._append(counter++, item)
     }
   }
+
+  private _append(keyId: number, item: bsky.FeedView.FeedItem) {
+    // TODO: validate .record
+    this.feed.push(new FeedViewItemModel(`item-${keyId}`, item))
+  }
 }
diff --git a/src/view/com/Feed.tsx b/src/view/com/Feed.tsx
index 2cba0610a..507f0edde 100644
--- a/src/view/com/Feed.tsx
+++ b/src/view/com/Feed.tsx
@@ -1,16 +1,32 @@
 import React from 'react'
-import {observer} from 'mobx-react-lite'
-import {Text, View} from 'react-native'
-import {FeedViewModel} from '../../state/models/feed-view'
+import {observer, Observer} from 'mobx-react-lite'
+import {Text, View, FlatList} from 'react-native'
+import {FeedViewModel, FeedViewItemModel} from '../../state/models/feed-view'
 import {FeedItem} from './FeedItem'
 
 export const Feed = observer(function Feed({feed}: {feed: FeedViewModel}) {
+  const renderItem = ({item}: {item: FeedViewItemModel}) => (
+    <Observer>{() => <FeedItem item={item} />}</Observer>
+  )
+  const onRefresh = () => {
+    feed.refresh().catch(err => console.error('Failed to refresh', err))
+  }
+  const onEndReached = () => {
+    feed.loadMore().catch(err => console.error('Failed to load more', err))
+  }
   return (
     <View>
-      {feed.isLoading && <Text>Loading...</Text>}
+      {feed.isLoading && !feed.isRefreshing && <Text>Loading...</Text>}
       {feed.hasError && <Text>{feed.error}</Text>}
-      {feed.hasContent &&
-        feed.feed.map(item => <FeedItem key={item.key} item={item} />)}
+      {feed.hasContent && (
+        <FlatList
+          data={feed.feed}
+          renderItem={renderItem}
+          refreshing={feed.isRefreshing}
+          onRefresh={onRefresh}
+          onEndReached={onEndReached}
+        />
+      )}
       {feed.isEmpty && <Text>This feed is empty!</Text>}
     </View>
   )
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index affd042d1..6685eb794 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -9,7 +9,7 @@ export function Home(/*{navigation}: RootTabsScreenProps<'Home'>*/) {
   const store = useStores()
   useEffect(() => {
     console.log('Fetching home feed')
-    store.homeFeed.fetch()
+    store.homeFeed.setup()
   }, [store.homeFeed])
   return (
     <Shell>