about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.native.tsx4
-rw-r--r--src/lib/api/debug-appview-proxy-header.ts39
-rw-r--r--src/lib/bg-scheduler.ts18
-rw-r--r--src/lib/bg-scheduler.web.ts13
-rw-r--r--src/lib/notifee.ts82
-rw-r--r--src/lib/notifications/notifications.ts101
-rw-r--r--src/platform/detection.ts1
-rw-r--r--src/state/models/feeds/notifications.ts30
-rw-r--r--src/state/models/root-store.ts59
-rw-r--r--src/view/screens/Debug.tsx7
-rw-r--r--src/view/screens/Settings.tsx2
11 files changed, 126 insertions, 230 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx
index a02ca62c8..ad37aa099 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -12,7 +12,7 @@ import {s} from 'lib/styles'
 import * as view from './view/index'
 import {RootStoreModel, setupState, RootStoreProvider} from './state'
 import {Shell} from './view/shell'
-import * as notifee from 'lib/notifee'
+import * as notifications from 'lib/notifications/notifications'
 import * as analytics from 'lib/analytics/analytics'
 import * as Toast from './view/com/util/Toast'
 import {handleLink} from './Navigation'
@@ -30,7 +30,7 @@ const App = observer(() => {
     setupState().then(store => {
       setRootStore(store)
       analytics.init(store)
-      notifee.init(store)
+      notifications.init(store)
       SplashScreen.hideAsync()
       Linking.getInitialURL().then((url: string | null) => {
         if (url) {
diff --git a/src/lib/api/debug-appview-proxy-header.ts b/src/lib/api/debug-appview-proxy-header.ts
index 39890b7c3..7571bd37f 100644
--- a/src/lib/api/debug-appview-proxy-header.ts
+++ b/src/lib/api/debug-appview-proxy-header.ts
@@ -8,23 +8,30 @@
  * version of the app.
  */
 
-import {useState, useCallback} from 'react'
+import {useState, useCallback, useEffect} from 'react'
 import {BskyAgent} from '@atproto/api'
 import {isWeb} from 'platform/detection'
+import * as Storage from 'lib/storage'
 
 export function useDebugHeaderSetting(agent: BskyAgent): [boolean, () => void] {
-  const [enabled, setEnabled] = useState<boolean>(isEnabled())
+  const [enabled, setEnabled] = useState<boolean>(false)
 
-  const toggle = useCallback(() => {
-    if (!isWeb || typeof window === 'undefined') {
-      return
+  useEffect(() => {
+    async function check() {
+      if (await isEnabled()) {
+        setEnabled(true)
+      }
     }
+    check()
+  }, [])
+
+  const toggle = useCallback(() => {
     if (!enabled) {
-      localStorage.setItem('set-header-x-appview-proxy', 'yes')
+      Storage.saveString('set-header-x-appview-proxy', 'yes')
       agent.api.xrpc.setHeader('x-appview-proxy', 'true')
       setEnabled(true)
     } else {
-      localStorage.removeItem('set-header-x-appview-proxy')
+      Storage.remove('set-header-x-appview-proxy')
       agent.api.xrpc.unsetHeader('x-appview-proxy')
       setEnabled(false)
     }
@@ -34,30 +41,24 @@ export function useDebugHeaderSetting(agent: BskyAgent): [boolean, () => void] {
 }
 
 export function setDebugHeader(agent: BskyAgent, enabled: boolean) {
-  if (!isWeb || typeof window === 'undefined') {
-    return
-  }
   if (enabled) {
-    localStorage.setItem('set-header-x-appview-proxy', 'yes')
+    Storage.saveString('set-header-x-appview-proxy', 'yes')
     agent.api.xrpc.setHeader('x-appview-proxy', 'true')
   } else {
-    localStorage.removeItem('set-header-x-appview-proxy')
+    Storage.remove('set-header-x-appview-proxy')
     agent.api.xrpc.unsetHeader('x-appview-proxy')
   }
 }
 
-export function applyDebugHeader(agent: BskyAgent) {
+export async function applyDebugHeader(agent: BskyAgent) {
   if (!isWeb) {
     return
   }
-  if (isEnabled()) {
+  if (await isEnabled()) {
     agent.api.xrpc.setHeader('x-appview-proxy', 'true')
   }
 }
 
-function isEnabled() {
-  if (!isWeb || typeof window === 'undefined') {
-    return false
-  }
-  return localStorage.getItem('set-header-x-appview-proxy') === 'yes'
+async function isEnabled() {
+  return (await Storage.loadString('set-header-x-appview-proxy')) === 'yes'
 }
diff --git a/src/lib/bg-scheduler.ts b/src/lib/bg-scheduler.ts
deleted file mode 100644
index db3f2d7fd..000000000
--- a/src/lib/bg-scheduler.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import BackgroundFetch, {
-  BackgroundFetchStatus,
-} from 'react-native-background-fetch'
-
-export function configure(
-  handler: (taskId: string) => Promise<void>,
-  timeoutHandler: (taskId: string) => void,
-): Promise<BackgroundFetchStatus> {
-  return BackgroundFetch.configure(
-    {minimumFetchInterval: 15},
-    handler,
-    timeoutHandler,
-  )
-}
-
-export function finish(taskId: string) {
-  return BackgroundFetch.finish(taskId)
-}
diff --git a/src/lib/bg-scheduler.web.ts b/src/lib/bg-scheduler.web.ts
deleted file mode 100644
index 91ec9428f..000000000
--- a/src/lib/bg-scheduler.web.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-type BackgroundFetchStatus = 0 | 1 | 2
-
-export async function configure(
-  _handler: (taskId: string) => Promise<void>,
-  _timeoutHandler: (taskId: string) => Promise<void>,
-): Promise<BackgroundFetchStatus> {
-  // TODO
-  return 0
-}
-
-export function finish(_taskId: string) {
-  // TODO
-}
diff --git a/src/lib/notifee.ts b/src/lib/notifee.ts
deleted file mode 100644
index 485d79aed..000000000
--- a/src/lib/notifee.ts
+++ /dev/null
@@ -1,82 +0,0 @@
-import notifee, {EventType} from '@notifee/react-native'
-import {AppBskyEmbedImages, AtUri} from '@atproto/api'
-import {RootStoreModel} from 'state/models/root-store'
-import {NotificationsFeedItemModel} from 'state/models/feeds/notifications'
-import {enforceLen} from 'lib/strings/helpers'
-import {sanitizeDisplayName} from './strings/display-names'
-import {resetToTab} from '../Navigation'
-
-export function init(store: RootStoreModel) {
-  store.onUnreadNotifications(count => notifee.setBadgeCount(count))
-  store.onPushNotification(displayNotificationFromModel)
-  store.onSessionLoaded(() => {
-    // request notifications permission once the user has logged in
-    notifee.requestPermission()
-  })
-  notifee.onForegroundEvent(async ({type}: {type: EventType}) => {
-    store.log.debug('Notifee foreground event', {type})
-    if (type === EventType.PRESS) {
-      store.log.debug('User pressed a notifee, opening notifications')
-      resetToTab('NotificationsTab')
-    }
-  })
-  notifee.onBackgroundEvent(async _e => {}) // notifee requires this but we handle it with onForegroundEvent
-}
-
-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(
-  notification: NotificationsFeedItemModel,
-) {
-  let author = sanitizeDisplayName(
-    notification.author.displayName || notification.author.handle,
-  )
-  let title: string
-  let body: string = ''
-  if (notification.isLike) {
-    title = `${author} liked your post`
-    body = notification.additionalPost?.thread?.postRecord?.text || ''
-  } else if (notification.isRepost) {
-    title = `${author} reposted your post`
-    body = notification.additionalPost?.thread?.postRecord?.text || ''
-  } else if (notification.isMention) {
-    title = `${author} mentioned you`
-    body = notification.additionalPost?.thread?.postRecord?.text || ''
-  } else if (notification.isReply) {
-    title = `${author} replied to your post`
-    body = notification.additionalPost?.thread?.postRecord?.text || ''
-  } else if (notification.isFollow) {
-    title = 'New follower!'
-    body = `${author} has followed you`
-  } else if (notification.isCustomFeedLike) {
-    title = `${author} liked your custom feed`
-    body = `${new AtUri(notification.subjectUri).rkey}`
-  } else {
-    return
-  }
-  let image
-  if (
-    AppBskyEmbedImages.isView(
-      notification.additionalPost?.thread?.post.embed,
-    ) &&
-    notification.additionalPost?.thread?.post.embed.images[0]?.thumb
-  ) {
-    image = notification.additionalPost.thread.post.embed.images[0].thumb
-  }
-  return displayNotification(title, body, image)
-}
diff --git a/src/lib/notifications/notifications.ts b/src/lib/notifications/notifications.ts
new file mode 100644
index 000000000..b517b40bf
--- /dev/null
+++ b/src/lib/notifications/notifications.ts
@@ -0,0 +1,101 @@
+import * as Notifications from 'expo-notifications'
+import {RootStoreModel} from '../../state'
+import {resetToTab} from '../../Navigation'
+import {devicePlatform, isIOS} from 'platform/detection'
+
+// TODO prod did = did:web:api.bsky.app
+
+export function init(store: RootStoreModel) {
+  store.onUnreadNotifications(count => Notifications.setBadgeCountAsync(count))
+
+  store.onSessionLoaded(async () => {
+    // request notifications permission once the user has logged in
+    const perms = await Notifications.getPermissionsAsync()
+    if (!perms.granted) {
+      await Notifications.requestPermissionsAsync()
+    }
+
+    // register the push token with the server
+    const token = await getPushToken()
+    if (token) {
+      try {
+        await store.agent.api.app.bsky.notification.registerPush({
+          serviceDid: 'did:web:api.staging.bsky.dev',
+          platform: devicePlatform,
+          token: token.data,
+          appId: 'xyz.blueskyweb.app',
+        })
+        store.log.debug('Notifications: Sent push token (init)', {
+          type: token.type,
+          token: token.data,
+        })
+      } catch (error) {
+        store.log.error('Notifications: Failed to set push token', error)
+      }
+    }
+
+    // listens for new changes to the push token
+    // In rare situations, a push token may be changed by the push notification service while the app is running. When a token is rolled, the old one becomes invalid and sending notifications to it will fail. A push token listener will let you handle this situation gracefully by registering the new token with your backend right away.
+    Notifications.addPushTokenListener(async ({data: t, type}) => {
+      store.log.debug('Notifications: Push token changed', {t, type})
+      if (t) {
+        try {
+          await store.agent.api.app.bsky.notification.registerPush({
+            serviceDid: 'did:web:api.staging.bsky.dev',
+            platform: devicePlatform,
+            token: t,
+            appId: 'xyz.blueskyweb.app',
+          })
+          store.log.debug('Notifications: Sent push token (event)', {
+            type,
+            token: t,
+          })
+        } catch (error) {
+          store.log.error('Notifications: Failed to set push token', error)
+        }
+      }
+    })
+  })
+
+  // handle notifications that are tapped on, regardless of whether the app is in the foreground or background
+  Notifications.addNotificationReceivedListener(event => {
+    store.log.debug('Notifications: received', event)
+    if (event.request.trigger.type === 'push') {
+      let payload
+      if (isIOS) {
+        payload = event.request.trigger.payload
+      } else {
+        // TODO: handle android payload deeplink
+      }
+      if (payload) {
+        store.log.debug('Notifications: received payload', payload)
+        // TODO: deeplink notif here
+      }
+    }
+  })
+
+  const sub = Notifications.addNotificationResponseReceivedListener(
+    response => {
+      store.log.debug(
+        'Notifications: response received',
+        response.actionIdentifier,
+      )
+      if (
+        response.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER
+      ) {
+        store.log.debug(
+          'User pressed a notification, opening notifications tab',
+        )
+        resetToTab('NotificationsTab')
+      }
+    },
+  )
+
+  return () => {
+    sub.remove()
+  }
+}
+
+export function getPushToken() {
+  return Notifications.getDevicePushTokenAsync()
+}
diff --git a/src/platform/detection.ts b/src/platform/detection.ts
index 3069c9be2..41ca20e5d 100644
--- a/src/platform/detection.ts
+++ b/src/platform/detection.ts
@@ -5,6 +5,7 @@ import {dedupArray} from 'lib/functions'
 export const isIOS = Platform.OS === 'ios'
 export const isAndroid = Platform.OS === 'android'
 export const isNative = isIOS || isAndroid
+export const devicePlatform = isIOS ? 'ios' : isAndroid ? 'android' : 'web'
 export const isWeb = !isNative
 export const isMobileWebMediaQuery = 'only screen and (max-width: 1230px)'
 export const isMobileWeb =
diff --git a/src/state/models/feeds/notifications.ts b/src/state/models/feeds/notifications.ts
index f52853070..a9836d38f 100644
--- a/src/state/models/feeds/notifications.ts
+++ b/src/state/models/feeds/notifications.ts
@@ -478,36 +478,6 @@ export class NotificationsFeedModel {
     }
   }
 
-  /**
-   * Used in background fetch to trigger notifications
-   */
-  async getNewMostRecent(): Promise<NotificationsFeedItemModel | undefined> {
-    let old = this.mostRecentNotificationUri
-    const res = await this.rootStore.agent.listNotifications({
-      limit: 1,
-    })
-    if (!res.data.notifications[0] || old === res.data.notifications[0].uri) {
-      return
-    }
-    this.mostRecentNotificationUri = res.data.notifications[0].uri
-    const notif = new NotificationsFeedItemModel(
-      this.rootStore,
-      'mostRecent',
-      res.data.notifications[0],
-    )
-    const addedUri = notif.additionalDataUri
-    if (addedUri) {
-      const postsRes = await this.rootStore.agent.app.bsky.feed.getPosts({
-        uris: [addedUri],
-      })
-      const post = postsRes.data.posts[0]
-      notif.setAdditionalData(post)
-      this.rootStore.posts.set(post.uri, post)
-    }
-    const filtered = this._filterNotifications([notif])
-    return filtered[0]
-  }
-
   // state transitions
   // =
 
diff --git a/src/state/models/root-store.ts b/src/state/models/root-store.ts
index 6ced8090a..1d6d3a0d0 100644
--- a/src/state/models/root-store.ts
+++ b/src/state/models/root-store.ts
@@ -6,7 +6,6 @@ import {makeAutoObservable} from 'mobx'
 import {BskyAgent} from '@atproto/api'
 import {createContext, useContext} from 'react'
 import {DeviceEventEmitter, EmitterSubscription} from 'react-native'
-import * as BgScheduler from 'lib/bg-scheduler'
 import {z} from 'zod'
 import {isObj, hasProp} from 'lib/type-guards'
 import {LogModel} from './log'
@@ -16,7 +15,6 @@ import {HandleResolutionsCache} from './cache/handle-resolutions'
 import {ProfilesCache} from './cache/profiles-view'
 import {PostsCache} from './cache/posts'
 import {LinkMetasCache} from './cache/link-metas'
-import {NotificationsFeedItemModel} from './feeds/notifications'
 import {MeModel} from './me'
 import {InvitedUsers} from './invited-users'
 import {PreferencesModel} from './ui/preferences'
@@ -61,7 +59,6 @@ export class RootStoreModel {
       serialize: false,
       hydrate: false,
     })
-    this.initBgFetch()
   }
 
   setAppInfo(info: AppInfo) {
@@ -249,62 +246,6 @@ export class RootStoreModel {
   emitUnreadNotifications(count: number) {
     DeviceEventEmitter.emit('unread-notifications', count)
   }
-
-  // a notification has been queued for push
-  onPushNotification(
-    handler: (notif: NotificationsFeedItemModel) => void,
-  ): EmitterSubscription {
-    return DeviceEventEmitter.addListener('push-notification', handler)
-  }
-  emitPushNotification(notif: NotificationsFeedItemModel) {
-    DeviceEventEmitter.emit('push-notification', notif)
-  }
-
-  // background fetch
-  // =
-  // - we use this to poll for unread notifications, which is not "ideal" behavior but
-  //   gives us a solution for push-notifications that work against any pds
-
-  initBgFetch() {
-    // NOTE
-    // background fetch runs every 15 minutes *at most* and will get slowed down
-    // based on some heuristics run by iOS, meaning it is not a reliable form of delivery
-    // -prf
-    BgScheduler.configure(
-      this.onBgFetch.bind(this),
-      this.onBgFetchTimeout.bind(this),
-    ).then(status => {
-      this.log.debug(`Background fetch initiated, status: ${status}`)
-    })
-  }
-
-  async onBgFetch(taskId: string) {
-    this.log.debug(`Background fetch fired for task ${taskId}`)
-    if (this.session.hasSession) {
-      const res = await this.agent.countUnreadNotifications()
-      const hasNewNotifs = this.me.notifications.unreadCount !== res.data.count
-      this.emitUnreadNotifications(res.data.count)
-      this.log.debug(
-        `Background fetch received unread count = ${res.data.count}`,
-      )
-      if (hasNewNotifs) {
-        this.log.debug(
-          'Background fetch detected potentially a new notification',
-        )
-        const mostRecent = await this.me.notifications.getNewMostRecent()
-        if (mostRecent) {
-          this.log.debug('Got the notification, triggering a push')
-          this.emitPushNotification(mostRecent)
-        }
-      }
-    }
-    BgScheduler.finish(taskId)
-  }
-
-  onBgFetchTimeout(taskId: string) {
-    this.log.debug(`Background fetch timed out for task ${taskId}`)
-    BgScheduler.finish(taskId)
-  }
 }
 
 const throwawayInst = new RootStoreModel(
diff --git a/src/view/screens/Debug.tsx b/src/view/screens/Debug.tsx
index 852025324..0e0464200 100644
--- a/src/view/screens/Debug.tsx
+++ b/src/view/screens/Debug.tsx
@@ -5,9 +5,7 @@ import {ViewHeader} from '../com/util/ViewHeader'
 import {ThemeProvider, PaletteColorName} from 'lib/ThemeContext'
 import {usePalette} from 'lib/hooks/usePalette'
 import {s} from 'lib/styles'
-import {displayNotification} from 'lib/notifee'
 import * as Toast from 'view/com/util/Toast'
-
 import {Text} from '../com/util/text/Text'
 import {ViewSelector} from '../com/util/ViewSelector'
 import {EmptyState} from '../com/util/EmptyState'
@@ -177,10 +175,7 @@ function ErrorView() {
 
 function NotifsView() {
   const triggerPush = () => {
-    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.",
-    )
+    // TODO: implement local notification for testing
   }
   const triggerToast = () => {
     Toast.show('The task has been completed')
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index 478fcaa09..f1d4767f3 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -505,7 +505,7 @@ export const SettingsScreen = withAuthRequired(
               System log
             </Text>
           </TouchableOpacity>
-          {isDesktopWeb ? (
+          {isDesktopWeb || __DEV__ ? (
             <ToggleButton
               type="default-light"
               label="Experiment: Use AppView Proxy"