about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/lib/strings.ts4
-rw-r--r--src/state/models/me.ts36
-rw-r--r--src/state/models/notifications-view.ts61
-rw-r--r--src/state/models/root-store.ts3
-rw-r--r--src/state/models/session.ts14
-rw-r--r--src/view/lib/notifee.ts54
-rw-r--r--src/view/screens/Debug.tsx42
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)