Use a persistent notifications model to improve load times of the notifications view

This commit is contained in:
Paul Frazee 2022-11-28 14:19:49 -06:00
parent 1aa1f77049
commit 9051aecdcb
4 changed files with 49 additions and 41 deletions

View file

@ -1,6 +1,7 @@
import {makeAutoObservable, runInAction} from 'mobx' import {makeAutoObservable, runInAction} from 'mobx'
import {RootStoreModel} from './root-store' import {RootStoreModel} from './root-store'
import {MembershipsViewModel} from './memberships-view' import {MembershipsViewModel} from './memberships-view'
import {NotificationsViewModel} from './notifications-view'
export class MeModel { export class MeModel {
did?: string did?: string
@ -9,9 +10,11 @@ export class MeModel {
description?: string description?: string
notificationCount: number = 0 notificationCount: number = 0
memberships?: MembershipsViewModel memberships?: MembershipsViewModel
notifications: NotificationsViewModel
constructor(public rootStore: RootStoreModel) { constructor(public rootStore: RootStoreModel) {
makeAutoObservable(this, {rootStore: false}, {autoBind: true}) makeAutoObservable(this, {rootStore: false}, {autoBind: true})
this.notifications = new NotificationsViewModel(this.rootStore, {})
} }
clear() { clear() {
@ -43,7 +46,12 @@ export class MeModel {
this.memberships = new MembershipsViewModel(this.rootStore, { this.memberships = new MembershipsViewModel(this.rootStore, {
actor: this.did, actor: this.did,
}) })
await this.memberships?.setup() await this.memberships?.setup().catch(e => {
console.error('Failed to setup memberships model', e)
})
await this.notifications.setup().catch(e => {
console.error('Failed to setup notifications model', e)
})
} else { } else {
this.clear() this.clear()
} }
@ -56,7 +64,12 @@ export class MeModel {
async fetchStateUpdate() { async fetchStateUpdate() {
const res = await this.rootStore.api.app.bsky.notification.getCount() const res = await this.rootStore.api.app.bsky.notification.getCount()
runInAction(() => { runInAction(() => {
const newNotifications = this.notificationCount !== res.data.count
this.notificationCount = res.data.count this.notificationCount = res.data.count
if (newNotifications) {
// trigger pre-emptive fetch on new notifications
this.notifications.refresh()
}
}) })
} }

View file

@ -203,7 +203,6 @@ export class NotificationsViewModel {
await this._pendingWork() await this._pendingWork()
this._loadPromise = this._initialLoad(isRefreshing) this._loadPromise = this._initialLoad(isRefreshing)
await this._loadPromise await this._loadPromise
this._updateReadState()
this._loadPromise = undefined this._loadPromise = undefined
} }
@ -240,6 +239,20 @@ export class NotificationsViewModel {
this._updatePromise = undefined this._updatePromise = undefined
} }
/**
* Update read/unread state
*/
async updateReadState() {
try {
await this.rootStore.api.app.bsky.notification.updateSeen({
seenAt: new Date().toISOString(),
})
this.rootStore.me.clearNotificationCount()
} catch (e) {
console.log('Failed to update notifications read state', e)
}
}
// state transitions // state transitions
// = // =
@ -329,11 +342,10 @@ export class NotificationsViewModel {
} }
private async _replaceAll(res: ListNotifications.Response) { private async _replaceAll(res: ListNotifications.Response) {
this.notifications.length = 0 return this._appendAll(res, true)
return this._appendAll(res)
} }
private async _appendAll(res: ListNotifications.Response) { private async _appendAll(res: ListNotifications.Response, replace = false) {
this.loadMoreCursor = res.data.cursor this.loadMoreCursor = res.data.cursor
this.hasMore = !!this.loadMoreCursor this.hasMore = !!this.loadMoreCursor
let counter = this.notifications.length let counter = this.notifications.length
@ -357,7 +369,11 @@ export class NotificationsViewModel {
) )
}) })
runInAction(() => { runInAction(() => {
this.notifications = this.notifications.concat(itemModels) if (replace) {
this.notifications = itemModels
} else {
this.notifications = this.notifications.concat(itemModels)
}
}) })
} }
@ -374,17 +390,6 @@ export class NotificationsViewModel {
} }
} }
} }
private async _updateReadState() {
try {
await this.rootStore.api.app.bsky.notification.updateSeen({
seenAt: new Date().toISOString(),
})
this.rootStore.me.clearNotificationCount()
} catch (e) {
console.log('Failed to update notifications read state', e)
}
}
} }
function groupNotifications( function groupNotifications(

View file

@ -32,7 +32,7 @@ export const Feed = observer(function Feed({
} }
return ( return (
<View style={{flex: 1}}> <View style={{flex: 1}}>
{view.isLoading && !view.isRefreshing && !view.hasContent && ( {view.isLoading && !view.isRefreshing && (
<NotificationFeedLoadingPlaceholder /> <NotificationFeedLoadingPlaceholder />
)} )}
{view.hasError && ( {view.hasError && (
@ -43,7 +43,7 @@ export const Feed = observer(function Feed({
onPressTryAgain={onPressTryAgain} onPressTryAgain={onPressTryAgain}
/> />
)} )}
{view.hasContent && ( {view.hasLoaded && (
<FlatList <FlatList
data={view.notifications} data={view.notifications}
keyExtractor={item => item._reactKey} keyExtractor={item => item._reactKey}
@ -53,7 +53,7 @@ export const Feed = observer(function Feed({
onEndReached={onEndReached} onEndReached={onEndReached}
/> />
)} )}
{view.isEmpty && ( {view.hasLoaded && view.isEmpty && (
<EmptyState icon="bell" message="No notifications yet!" /> <EmptyState icon="bell" message="No notifications yet!" />
)} )}
</View> </View>

View file

@ -7,43 +7,33 @@ import {NotificationsViewModel} from '../../state/models/notifications-view'
import {ScreenParams} from '../routes' import {ScreenParams} from '../routes'
export const Notifications = ({navIdx, visible}: ScreenParams) => { export const Notifications = ({navIdx, visible}: ScreenParams) => {
const [hasSetup, setHasSetup] = useState<boolean>(false)
const [notesView, setNotesView] = useState<
NotificationsViewModel | undefined
>()
const store = useStores() const store = useStores()
useEffect(() => { useEffect(() => {
let aborted = false
if (!visible) { if (!visible) {
return return
} }
console.log('Updating notifications feed')
store.me.refreshMemberships() // needed for the invite notifications store.me.refreshMemberships() // needed for the invite notifications
if (hasSetup) { store.me.notifications
console.log('Updating notifications feed') .update()
notesView?.update() .catch(e => {
} else { console.error('Error while updating notifications feed', e)
store.nav.setTitle(navIdx, 'Notifications')
const newNotesView = new NotificationsViewModel(store, {})
setNotesView(newNotesView)
newNotesView.setup().then(() => {
if (aborted) return
setHasSetup(true)
}) })
} .then(() => {
return () => { store.me.notifications.updateReadState()
aborted = true })
} store.nav.setTitle(navIdx, 'Notifications')
}, [visible, store]) }, [visible, store])
const onPressTryAgain = () => { const onPressTryAgain = () => {
notesView?.refresh() store.me.notifications.refresh()
} }
return ( return (
<View style={{flex: 1}}> <View style={{flex: 1}}>
<ViewHeader title="Notifications" /> <ViewHeader title="Notifications" />
{notesView && <Feed view={notesView} onPressTryAgain={onPressTryAgain} />} <Feed view={store.me.notifications} onPressTryAgain={onPressTryAgain} />
</View> </View>
) )
} }