diff options
author | Paul Frazee <pfrazee@gmail.com> | 2022-07-25 14:21:48 -0500 |
---|---|---|
committer | Paul Frazee <pfrazee@gmail.com> | 2022-07-25 14:21:48 -0500 |
commit | 3794eca88e13c3c292b0f64b1acb1169ecbeb83d (patch) | |
tree | c073772f77d43cb13582db5999b01ae5adf7272a | |
parent | 7f04ac172e8ada1244de1df2064e32d32f1c2348 (diff) | |
download | voidsky-3794eca88e13c3c292b0f64b1acb1169ecbeb83d.tar.zst |
Add state updates after screen changes
-rw-r--r-- | src/state/models/feed-view.ts | 134 | ||||
-rw-r--r-- | src/state/models/post-thread-view.ts | 33 | ||||
-rw-r--r-- | src/view/com/feed/Feed.tsx | 4 | ||||
-rw-r--r-- | src/view/com/post-thread/PostThread.tsx | 11 | ||||
-rw-r--r-- | src/view/screens/stacks/Profile.tsx | 23 | ||||
-rw-r--r-- | src/view/screens/tabroots/Home.tsx | 14 | ||||
-rw-r--r-- | todos.txt | 4 |
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 - * |