diff options
author | Paul Frazee <pfrazee@gmail.com> | 2023-01-25 11:31:09 -0600 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-01-25 11:31:09 -0600 |
commit | 5f189319157dfed67b331145cdfd322515022b93 (patch) | |
tree | 1a87f8c7dd46552ba137ff898eb972024e413fdd /src | |
parent | 079e1dbe180e7a1a5a58cdab90f154157ed07d23 (diff) | |
download | voidsky-5f189319157dfed67b331145cdfd322515022b93.tar.zst |
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 fetch
Diffstat (limited to 'src')
-rw-r--r-- | src/lib/strings.ts | 4 | ||||
-rw-r--r-- | src/state/models/me.ts | 36 | ||||
-rw-r--r-- | src/state/models/notifications-view.ts | 61 | ||||
-rw-r--r-- | src/state/models/root-store.ts | 3 | ||||
-rw-r--r-- | src/state/models/session.ts | 14 | ||||
-rw-r--r-- | src/view/lib/notifee.ts | 54 | ||||
-rw-r--r-- | src/view/screens/Debug.tsx | 42 |
7 files changed, 148 insertions, 66 deletions
diff --git a/src/lib/strings.ts b/src/lib/strings.ts index cb79e8824..8b93fa933 100644 --- a/src/lib/strings.ts +++ b/src/lib/strings.ts @@ -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 } diff --git a/src/state/models/me.ts b/src/state/models/me.ts index da46c60cf..0d0c1d1de 100644 --- a/src/state/models/me.ts +++ b/src/state/models/me.ts @@ -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) + } + } + } } diff --git a/src/state/models/notifications-view.ts b/src/state/models/notifications-view.ts index 34bb57f6e..93b6a398f 100644 --- a/src/state/models/notifications-view.ts +++ b/src/state/models/notifications-view.ts @@ -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) } diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts index 55dbbcfee..c4798ad0b 100644 --- a/src/state/models/root-store.ts +++ b/src/state/models/root-store.ts @@ -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) } diff --git a/src/state/models/session.ts b/src/state/models/session.ts index 77c1fb595..bc0a9123f 100644 --- a/src/state/models/session.ts +++ b/src/state/models/session.ts @@ -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 } diff --git a/src/view/lib/notifee.ts b/src/view/lib/notifee.ts new file mode 100644 index 000000000..5e1917381 --- /dev/null +++ b/src/view/lib/notifee.ts @@ -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) +} diff --git a/src/view/screens/Debug.tsx b/src/view/screens/Debug.tsx index 9365724a0..865f62dc6 100644 --- a/src/view/screens/Debug.tsx +++ b/src/view/screens/Debug.tsx @@ -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) |