Push notification & session management cleanup (#92)
* Add some temporary logging to help suss out the session drop issue * Fix to session resumption: copy session tokens during a resumeSession attempt * Factor out notifee display into a lib and add to storybook * Tune the bg notifications fetch to only get what is needed * Fix: run account update inside a mobx action * Remove debugging logs for sessions * Fixes to bg notifications fetchzio/stable
parent
079e1dbe18
commit
5f18931915
|
@ -188,10 +188,10 @@ export function createFullHandle(name: string, domain: string): string {
|
|||
return `${name}.${domain}`
|
||||
}
|
||||
|
||||
export function enforceLen(str: string, len: number): string {
|
||||
export function enforceLen(str: string, len: number, ellipsis = false): string {
|
||||
str = str || ''
|
||||
if (str.length > len) {
|
||||
return str.slice(0, len)
|
||||
return str.slice(0, len) + (ellipsis ? '...' : '')
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import {RootStoreModel} from './root-store'
|
|||
import {FeedModel} from './feed-view'
|
||||
import {NotificationsViewModel} from './notifications-view'
|
||||
import {isObj, hasProp} from '../lib/type-guards'
|
||||
import {displayNotificationFromModel} from '../../view/lib/notifee'
|
||||
|
||||
export class MeModel {
|
||||
did: string = ''
|
||||
|
@ -125,19 +126,30 @@ export class MeModel {
|
|||
this.notificationCount = res.data.count
|
||||
notifee.setBadgeCount(this.notificationCount)
|
||||
if (newNotifications) {
|
||||
// trigger pre-emptive fetch on new notifications
|
||||
let oldMostRecent = this.notifications.mostRecentNotification
|
||||
this.notifications.refresh().then(() => {
|
||||
// if a new most recent notification is found, trigger a notification card
|
||||
const mostRecent = this.notifications.mostRecentNotification
|
||||
if (mostRecent && oldMostRecent?.uri !== mostRecent?.uri) {
|
||||
const notifeeOpts = mostRecent.toNotifeeOpts()
|
||||
if (notifeeOpts) {
|
||||
notifee.displayNotification(notifeeOpts)
|
||||
}
|
||||
}
|
||||
})
|
||||
this.notifications.refresh()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async bgFetchNotifications() {
|
||||
const res = await this.rootStore.api.app.bsky.notification.getCount()
|
||||
// NOTE we don't update this.notificationCount to avoid repaints during bg
|
||||
// this means `newNotifications` may not be accurate, so we rely on
|
||||
// `mostRecent` to determine if there really is a new notif to show -prf
|
||||
const newNotifications = this.notificationCount !== res.data.count
|
||||
notifee.setBadgeCount(res.data.count)
|
||||
this.rootStore.log.debug(
|
||||
`Background fetch received unread count = ${res.data.count}`,
|
||||
)
|
||||
if (newNotifications) {
|
||||
this.rootStore.log.debug(
|
||||
'Background fetch detected potentially a new notification',
|
||||
)
|
||||
const mostRecent = await this.notifications.getNewMostRecent()
|
||||
if (mostRecent) {
|
||||
this.rootStore.log.debug('Got the notification, triggering a push')
|
||||
displayNotificationFromModel(mostRecent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import {
|
|||
AppBskyFeedVote,
|
||||
AppBskyGraphAssertion,
|
||||
AppBskyGraphFollow,
|
||||
AppBskyEmbedImages,
|
||||
} from '@atproto/api'
|
||||
import {RootStoreModel} from './root-store'
|
||||
import {PostThreadViewModel} from './post-thread-view'
|
||||
|
@ -180,42 +179,6 @@ export class NotificationsViewItemModel {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
toNotifeeOpts() {
|
||||
let author = this.author.displayName || this.author.handle
|
||||
let title: string
|
||||
let body: string = ''
|
||||
if (this.isUpvote) {
|
||||
title = `${author} liked your post`
|
||||
body = this.additionalPost?.thread?.postRecord?.text || ''
|
||||
} else if (this.isRepost) {
|
||||
title = `${author} reposted your post`
|
||||
body = this.additionalPost?.thread?.postRecord?.text || ''
|
||||
} else if (this.isReply) {
|
||||
title = `${author} replied to your post`
|
||||
body = this.additionalPost?.thread?.postRecord?.text || ''
|
||||
} else if (this.isFollow) {
|
||||
title = `${author} followed you`
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
let ios
|
||||
if (
|
||||
AppBskyEmbedImages.isPresented(this.additionalPost?.thread?.post.embed) &&
|
||||
this.additionalPost?.thread?.post.embed.images[0]?.thumb
|
||||
) {
|
||||
ios = {
|
||||
attachments: [
|
||||
{url: this.additionalPost.thread.post.embed.images[0].thumb},
|
||||
],
|
||||
}
|
||||
}
|
||||
return {
|
||||
title,
|
||||
body,
|
||||
ios,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class NotificationsViewModel {
|
||||
|
@ -234,7 +197,7 @@ export class NotificationsViewModel {
|
|||
// data
|
||||
notifications: NotificationsViewItemModel[] = []
|
||||
|
||||
// this is used to trigger push notifications
|
||||
// this is used to help trigger push notifications
|
||||
mostRecentNotification: NotificationsViewItemModel | undefined
|
||||
|
||||
constructor(
|
||||
|
@ -246,6 +209,7 @@ export class NotificationsViewModel {
|
|||
{
|
||||
rootStore: false,
|
||||
params: false,
|
||||
mostRecentNotification: false,
|
||||
_loadPromise: false,
|
||||
_loadMorePromise: false,
|
||||
_updatePromise: false,
|
||||
|
@ -333,6 +297,24 @@ export class NotificationsViewModel {
|
|||
}
|
||||
}
|
||||
|
||||
async getNewMostRecent(): Promise<NotificationsViewItemModel | undefined> {
|
||||
let old = this.mostRecentNotification
|
||||
const res = await this.rootStore.api.app.bsky.notification.list({limit: 1})
|
||||
if (
|
||||
!res.data.notifications[0] ||
|
||||
old?.uri === res.data.notifications[0].uri
|
||||
) {
|
||||
return
|
||||
}
|
||||
this.mostRecentNotification = new NotificationsViewItemModel(
|
||||
this.rootStore,
|
||||
'mostRecent',
|
||||
res.data.notifications[0],
|
||||
)
|
||||
await this.mostRecentNotification.fetchAdditionalData()
|
||||
return this.mostRecentNotification
|
||||
}
|
||||
|
||||
// state transitions
|
||||
// =
|
||||
|
||||
|
@ -434,9 +416,6 @@ export class NotificationsViewModel {
|
|||
'mostRecent',
|
||||
res.data.notifications[0],
|
||||
)
|
||||
await this.mostRecentNotification.fetchAdditionalData()
|
||||
} else {
|
||||
this.mostRecentNotification = undefined
|
||||
}
|
||||
return this._appendAll(res, true)
|
||||
}
|
||||
|
|
|
@ -136,8 +136,7 @@ export class RootStoreModel {
|
|||
async onBgFetch(taskId: string) {
|
||||
this.log.debug(`Background fetch fired for task ${taskId}`)
|
||||
if (this.session.hasSession) {
|
||||
// grab notifications
|
||||
await this.me.fetchNotifications()
|
||||
await this.me.bgFetchNotifications()
|
||||
}
|
||||
BackgroundFetch.finish(taskId)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {makeAutoObservable} from 'mobx'
|
||||
import {makeAutoObservable, runInAction} from 'mobx'
|
||||
import {
|
||||
sessionClient as AtpApi,
|
||||
Session,
|
||||
|
@ -298,9 +298,19 @@ export class SessionModel {
|
|||
})
|
||||
try {
|
||||
const sess = await api.com.atproto.session.get()
|
||||
if (!sess.success || sess.data.did !== account.did) {
|
||||
if (
|
||||
!sess.success ||
|
||||
sess.data.did !== account.did ||
|
||||
!api.sessionManager.session
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// copy over the access tokens, as they may have refreshed during the .get() above
|
||||
runInAction(() => {
|
||||
account.refreshJwt = api.sessionManager.session?.refreshJwt
|
||||
account.accessJwt = api.sessionManager.session?.accessJwt
|
||||
})
|
||||
} catch (_e) {
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import notifee from '@notifee/react-native'
|
||||
import {AppBskyEmbedImages} from '@atproto/api'
|
||||
import {NotificationsViewItemModel} from '../../state/models/notifications-view'
|
||||
import {enforceLen} from '../../lib/strings'
|
||||
|
||||
export function displayNotification(
|
||||
title: string,
|
||||
body?: string,
|
||||
image?: string,
|
||||
) {
|
||||
const opts: {title: string; body?: string; ios?: any} = {title}
|
||||
if (body) {
|
||||
opts.body = enforceLen(body, 70, true)
|
||||
}
|
||||
if (image) {
|
||||
opts.ios = {
|
||||
attachments: [{url: image}],
|
||||
}
|
||||
}
|
||||
return notifee.displayNotification(opts)
|
||||
}
|
||||
|
||||
export function displayNotificationFromModel(
|
||||
notif: NotificationsViewItemModel,
|
||||
) {
|
||||
let author = notif.author.displayName || notif.author.handle
|
||||
let title: string
|
||||
let body: string = ''
|
||||
if (notif.isUpvote) {
|
||||
title = `${author} liked your post`
|
||||
body = notif.additionalPost?.thread?.postRecord?.text || ''
|
||||
} else if (notif.isRepost) {
|
||||
title = `${author} reposted your post`
|
||||
body = notif.additionalPost?.thread?.postRecord?.text || ''
|
||||
} else if (notif.isMention) {
|
||||
title = `${author} mentioned you`
|
||||
body = notif.additionalPost?.thread?.postRecord?.text || ''
|
||||
} else if (notif.isReply) {
|
||||
title = `${author} replied to your post`
|
||||
body = notif.additionalPost?.thread?.postRecord?.text || ''
|
||||
} else if (notif.isFollow) {
|
||||
title = `${author} followed you`
|
||||
} else {
|
||||
return
|
||||
}
|
||||
let image
|
||||
if (
|
||||
AppBskyEmbedImages.isPresented(notif.additionalPost?.thread?.post.embed) &&
|
||||
notif.additionalPost?.thread?.post.embed.images[0]?.thumb
|
||||
) {
|
||||
image = notif.additionalPost.thread.post.embed.images[0].thumb
|
||||
}
|
||||
return displayNotification(title, body, image)
|
||||
}
|
|
@ -5,6 +5,8 @@ import {ThemeProvider} from '../lib/ThemeContext'
|
|||
import {PaletteColorName} from '../lib/ThemeContext'
|
||||
import {usePalette} from '../lib/hooks/usePalette'
|
||||
import {s} from '../lib/styles'
|
||||
import {DEF_AVATAR} from '../lib/assets'
|
||||
import {displayNotification} from '../lib/notifee'
|
||||
|
||||
import {Text} from '../com/util/text/Text'
|
||||
import {ViewSelector} from '../com/util/ViewSelector'
|
||||
|
@ -17,7 +19,7 @@ import {RadioGroup} from '../com/util/forms/RadioGroup'
|
|||
import {ErrorScreen} from '../com/util/error/ErrorScreen'
|
||||
import {ErrorMessage} from '../com/util/error/ErrorMessage'
|
||||
|
||||
const MAIN_VIEWS = ['Base', 'Controls', 'Error']
|
||||
const MAIN_VIEWS = ['Base', 'Controls', 'Error', 'Notifs']
|
||||
|
||||
export const Debug = () => {
|
||||
const [colorScheme, setColorScheme] = React.useState<'light' | 'dark'>(
|
||||
|
@ -46,9 +48,9 @@ function DebugInner({
|
|||
const [currentView, setCurrentView] = React.useState<number>(0)
|
||||
const pal = usePalette('default')
|
||||
|
||||
const renderItem = item => {
|
||||
const renderItem = (item, i) => {
|
||||
return (
|
||||
<View>
|
||||
<View key={`view-${i}`}>
|
||||
<View style={[s.pt10, s.pl10, s.pr10]}>
|
||||
<ToggleButton
|
||||
type="default-light"
|
||||
|
@ -57,12 +59,14 @@ function DebugInner({
|
|||
label="Dark mode"
|
||||
/>
|
||||
</View>
|
||||
{item.currentView === 2 ? (
|
||||
<ErrorView key="error" />
|
||||
{item.currentView === 3 ? (
|
||||
<NotifsView />
|
||||
) : item.currentView === 2 ? (
|
||||
<ErrorView />
|
||||
) : item.currentView === 1 ? (
|
||||
<ControlsView key="controls" />
|
||||
<ControlsView />
|
||||
) : (
|
||||
<BaseView key="base" />
|
||||
<BaseView />
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
|
@ -168,6 +172,30 @@ function ErrorView() {
|
|||
)
|
||||
}
|
||||
|
||||
function NotifsView() {
|
||||
const trigger = () => {
|
||||
displayNotification(
|
||||
'Paul Frazee liked your post',
|
||||
"Hello world! This is a test of the notifications card. The text is long to see how that's handled.",
|
||||
)
|
||||
}
|
||||
const triggerImg = () => {
|
||||
displayNotification(
|
||||
'Paul Frazee liked your post',
|
||||
"Hello world! This is a test of the notifications card. The text is long to see how that's handled.",
|
||||
DEF_AVATAR,
|
||||
)
|
||||
}
|
||||
return (
|
||||
<View style={s.p10}>
|
||||
<View style={s.flexRow}>
|
||||
<Button onPress={trigger} label="Trigger" />
|
||||
<Button onPress={triggerImg} label="Trigger w/image" style={s.ml5} />
|
||||
</View>
|
||||
</View>
|
||||
)
|
||||
}
|
||||
|
||||
function PaletteView({palette}: {palette: PaletteColorName}) {
|
||||
const defaultPal = usePalette('default')
|
||||
const pal = usePalette(palette)
|
||||
|
|
Loading…
Reference in New Issue