about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--.eslintrc.js1
-rw-r--r--.gitignore5
-rw-r--r--app.json8
-rw-r--r--eas.json14
-rw-r--r--package.json5
-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
-rw-r--r--tsconfig.check.json2
-rw-r--r--yarn.lock122
18 files changed, 253 insertions, 260 deletions
diff --git a/.eslintrc.js b/.eslintrc.js
index c5b7e861f..a6881033a 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -17,6 +17,7 @@ module.exports = {
     '.husky',
     'patches',
     '*.html',
+    'bskyweb',
   ],
   overrides: [
     {
diff --git a/.gitignore b/.gitignore
index 2fa850bf7..66658f8e4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -96,4 +96,7 @@ ios/
 
 # environment variables
 .env
-.env.*
\ No newline at end of file
+.env.*
+
+# Firebase (Android) Google services
+google-services.json
\ No newline at end of file
diff --git a/app.json b/app.json
index 4ee73752a..55250fb76 100644
--- a/app.json
+++ b/app.json
@@ -25,11 +25,7 @@
       },
       "infoPlist": {
         "UIBackgroundModes": [
-          "fetch",
-          "processing"
-        ],
-        "BGTaskSchedulerPermittedIdentifiers": [
-          "com.transistorsoft.fetch"
+          "remote-notification"
         ],
         "NSCameraUsageDescription": "Used for profile pictures, posts, and other kinds of content.",
         "NSMicrophoneUsageDescription": "Used for posts and other kinds of content.",
@@ -48,6 +44,7 @@
         "foregroundImage": "./assets/adaptive-icon.png",
         "backgroundColor": "#ffffff"
       },
+      "googleServicesFile": "./google-services.json",
       "package": "xyz.blueskyweb.app",
       "intentFilters": [
         {
@@ -73,7 +70,6 @@
     },
     "plugins": [
       "expo-localization",
-      "react-native-background-fetch",
       "sentry-expo",
       [
         "expo-build-properties",
diff --git a/eas.json b/eas.json
index 60c8be378..30c3cce4b 100644
--- a/eas.json
+++ b/eas.json
@@ -9,20 +9,28 @@
       "distribution": "internal",
       "ios": {
         "simulator": true,
-        "resourceClass": "medium"
+        "resourceClass": "large"
+      },
+      "channel": "development"
+    },
+    "development-device": {
+      "developmentClient": true,
+      "distribution": "internal",
+      "ios": {
+        "resourceClass": "large"
       },
       "channel": "development"
     },
     "preview": {
       "distribution": "internal",
       "ios": {
-        "resourceClass": "medium"
+        "resourceClass": "large"
       },
       "channel": "preview"
     },
     "production": {
       "ios": {
-        "resourceClass": "medium"
+        "resourceClass": "large"
       },
       "channel": "production"
     }
diff --git a/package.json b/package.json
index 5eb4780ab..57ad831a3 100644
--- a/package.json
+++ b/package.json
@@ -24,7 +24,7 @@
     "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all"
   },
   "dependencies": {
-    "@atproto/api": "^0.6.5",
+    "@atproto/api": "^0.6.6",
     "@bam.tech/react-native-image-resizer": "^3.0.4",
     "@braintree/sanitize-url": "^6.0.2",
     "@expo/html-elements": "^0.4.2",
@@ -36,7 +36,6 @@
     "@gorhom/bottom-sheet": "^4.4.7",
     "@mattermost/react-native-paste-input": "^0.6.4",
     "@miblanchard/react-native-slider": "^2.2.0",
-    "@notifee/react-native": "^7.4.0",
     "@react-native-async-storage/async-storage": "^1.17.6",
     "@react-native-camera-roll/camera-roll": "^5.2.2",
     "@react-native-clipboard/clipboard": "^1.10.0",
@@ -83,6 +82,7 @@
     "expo-image-picker": "^14.1.1",
     "expo-localization": "~14.1.1",
     "expo-media-library": "~15.2.3",
+    "expo-notifications": "~0.18.1",
     "expo-sharing": "~11.2.2",
     "expo-splash-screen": "~0.18.2",
     "expo-status-bar": "~1.4.4",
@@ -114,7 +114,6 @@
     "react-dom": "^18.2.0",
     "react-native": "0.71.8",
     "react-native-appstate-hook": "^1.0.6",
-    "react-native-background-fetch": "^4.1.8",
     "react-native-draggable-flatlist": "^4.0.1",
     "react-native-drawer-layout": "^3.2.0",
     "react-native-fs": "^2.20.0",
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"
diff --git a/tsconfig.check.json b/tsconfig.check.json
index f2e29bbb7..b90fc83e0 100644
--- a/tsconfig.check.json
+++ b/tsconfig.check.json
@@ -1,4 +1,4 @@
 {
   "extends": "./tsconfig.json",
-  "exclude": ["__e2e__", "dist"],
+  "include": ["src"]
 }
diff --git a/yarn.lock b/yarn.lock
index 1a9fed10b..e53c4626a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -40,10 +40,10 @@
     tlds "^1.234.0"
     typed-emitter "^2.1.0"
 
-"@atproto/api@^0.6.5":
-  version "0.6.5"
-  resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.6.5.tgz#496a011b7e8fbf2af32a30cec07a021aa6ef3f4b"
-  integrity sha512-u6NVkYpdUU5jKGxio2FIRmok0LL+eqqMzihm9LDfydZ4Pi4NqfrOrWw0H1WA7zO3vH9AaxnLSMTwSEAkRRb2FA==
+"@atproto/api@^0.6.6":
+  version "0.6.6"
+  resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.6.6.tgz#c1bfdb6bc7dee9cdba1901cde0081c2d422d7c29"
+  integrity sha512-j+yNTjllVxuTc4bAegghTopju7MdhczLXWvWIli40uXwCzQ3JjS1mFr/47eETtysib2phWYQvfhtCrqQq6AAig==
   dependencies:
     "@atproto/common-web" "*"
     "@atproto/uri" "*"
@@ -3089,7 +3089,7 @@
     semver "7.3.2"
     tempy "0.3.0"
 
-"@expo/image-utils@0.3.23":
+"@expo/image-utils@0.3.23", "@expo/image-utils@^0.3.18":
   version "0.3.23"
   resolved "https://registry.yarnpkg.com/@expo/image-utils/-/image-utils-0.3.23.tgz#f14fd7e1f5ff6f8e4911a41e27dd274470665c3f"
   integrity sha512-nhUVvW0TrRE4jtWzHQl8TR4ox7kcmrc2I0itaeJGjxF5A54uk7avgA0wRt7jP1rdvqQo1Ke1lXyLYREdhN9tPw==
@@ -3362,6 +3362,11 @@
   resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
   integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==
 
+"@ide/backoff@^1.0.0":
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/@ide/backoff/-/backoff-1.0.0.tgz#466842c25bd4a4833e0642fab41ccff064010176"
+  integrity sha512-F0YfUDjvT+Mtt/R4xdl2X0EYCHMMiJqNLdxHD++jDT5ydEFIyqbCHh51Qx2E211dgZprPKhV7sHmnXKpLuvc5g==
+
 "@ipld/car@^3.2.3":
   version "3.2.4"
   resolved "https://registry.yarnpkg.com/@ipld/car/-/car-3.2.4.tgz#115951ba2255ec51d865773a074e422c169fb01c"
@@ -3982,11 +3987,6 @@
     "@nodelib/fs.scandir" "2.1.5"
     fastq "^1.6.0"
 
-"@notifee/react-native@^7.4.0":
-  version "7.8.0"
-  resolved "https://registry.yarnpkg.com/@notifee/react-native/-/react-native-7.8.0.tgz#2990883753990f3585aa0cb5becc5cbdbcd87a43"
-  integrity sha512-sx8h62U4FrR4pqlbN1rkgPsdamDt9Tad0zgfO6VtP6rNJq/78k8nxUnh0xIX3WPDcCV8KAzdYCE7+UNvhF1CpQ==
-
 "@npmcli/fs@^1.0.0":
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.1.1.tgz#72f719fe935e687c56a4faecf3c03d06ba593257"
@@ -7290,6 +7290,16 @@ asn1.js@^5.0.1:
     minimalistic-assert "^1.0.0"
     safer-buffer "^2.1.0"
 
+assert@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/assert/-/assert-2.0.0.tgz#95fc1c616d48713510680f2eaf2d10dd22e02d32"
+  integrity sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A==
+  dependencies:
+    es6-object-assign "^1.1.0"
+    is-nan "^1.2.1"
+    object-is "^1.0.1"
+    util "^0.12.0"
+
 assign-symbols@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/assign-symbols/-/assign-symbols-1.0.0.tgz#59667f41fadd4f20ccbc2bb96b8d4f7f78ec0367"
@@ -7664,6 +7674,11 @@ babel-preset-react-app@^10.0.1:
     babel-plugin-macros "^3.1.0"
     babel-plugin-transform-react-remove-prop-types "^0.4.24"
 
+badgin@^1.1.5:
+  version "1.2.3"
+  resolved "https://registry.yarnpkg.com/badgin/-/badgin-1.2.3.tgz#994b5f519827d7d5422224825b2c8faea2bc43ad"
+  integrity sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==
+
 balanced-match@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
@@ -9772,6 +9787,11 @@ es-to-primitive@^1.2.1:
     is-date-object "^1.0.1"
     is-symbol "^1.0.2"
 
+es6-object-assign@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c"
+  integrity sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw==
+
 escalade@^3.1.1:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
@@ -10252,7 +10272,7 @@ expect@^29.0.0, expect@^29.5.0:
     jest-message-util "^29.5.0"
     jest-util "^29.5.0"
 
-expo-application@~5.1.1:
+expo-application@~5.1.0, expo-application@~5.1.1:
   version "5.1.1"
   resolved "https://registry.yarnpkg.com/expo-application/-/expo-application-5.1.1.tgz#5206bf0cf89cb0e32d1f5037a0481e5c86b951ab"
   integrity sha512-aDatTcTTCdTbHw8h4/Tq2ilc6InM5ntF9xWCJdOcnUEcglxxGphVI/lzJKBaBF6mJECA8mEOjpVg2EGxOctTwg==
@@ -10425,6 +10445,21 @@ expo-modules-core@1.2.7:
     compare-versions "^3.4.0"
     invariant "^2.2.4"
 
+expo-notifications@~0.18.1:
+  version "0.18.1"
+  resolved "https://registry.yarnpkg.com/expo-notifications/-/expo-notifications-0.18.1.tgz#c726bee7b6691d5f154b874afeda3b4561571cfc"
+  integrity sha512-lOEiuPE6ubkS5u7Nj/57gkmUGD/MxsRTC6bg9SGJqXIitBQZk3Tmv9y8bjTrn71n7DsrH8K7xCZTbVwr+kLQGg==
+  dependencies:
+    "@expo/image-utils" "^0.3.18"
+    "@ide/backoff" "^1.0.0"
+    abort-controller "^3.0.0"
+    assert "^2.0.0"
+    badgin "^1.1.5"
+    expo-application "~5.1.0"
+    expo-constants "~14.2.0"
+    fs-extra "^9.1.0"
+    uuid "^3.4.0"
+
 expo-pwa@0.0.125:
   version "0.0.125"
   resolved "https://registry.yarnpkg.com/expo-pwa/-/expo-pwa-0.0.125.tgz#fb5a66f21e7c9a51cdfa76d692b48bd116e6e002"
@@ -11951,6 +11986,14 @@ is-accessor-descriptor@^1.0.0:
   dependencies:
     kind-of "^6.0.0"
 
+is-arguments@^1.0.4:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b"
+  integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==
+  dependencies:
+    call-bind "^1.0.2"
+    has-tostringtag "^1.0.0"
+
 is-array-buffer@^3.0.1, is-array-buffer@^3.0.2:
   version "3.0.2"
   resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.2.tgz#f2653ced8412081638ecb0ebbd0c41c6e0aecbbe"
@@ -12121,6 +12164,13 @@ is-generator-fn@^2.0.0:
   resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118"
   integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==
 
+is-generator-function@^1.0.7:
+  version "1.0.10"
+  resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72"
+  integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==
+  dependencies:
+    has-tostringtag "^1.0.0"
+
 is-glob@^2.0.0:
   version "2.0.1"
   resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-2.0.1.tgz#d096f926a3ded5600f3fdfd91198cb0888c2d863"
@@ -12152,6 +12202,14 @@ is-module@^1.0.0:
   resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
   integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==
 
+is-nan@^1.2.1:
+  version "1.3.2"
+  resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d"
+  integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==
+  dependencies:
+    call-bind "^1.0.0"
+    define-properties "^1.1.3"
+
 is-negative-zero@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150"
@@ -12292,6 +12350,13 @@ is-typed-array@^1.1.10, is-typed-array@^1.1.9:
     gopd "^1.0.1"
     has-tostringtag "^1.0.0"
 
+is-typed-array@^1.1.3:
+  version "1.1.12"
+  resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.12.tgz#d0bab5686ef4a76f7a73097b95470ab199c57d4a"
+  integrity sha512-Z14TF2JNG8Lss5/HMqt0//T9JeHXttXy5pH/DBU4vi98ozO2btxzq9MwYDZYnKwU8nRsz/+GVFVRDq3DkVuSPg==
+  dependencies:
+    which-typed-array "^1.1.11"
+
 is-typedarray@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a"
@@ -15149,6 +15214,14 @@ object-inspect@^1.12.3, object-inspect@^1.9.0:
   resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.3.tgz#ba62dffd67ee256c8c086dfae69e016cd1f198b9"
   integrity sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==
 
+object-is@^1.0.1:
+  version "1.1.5"
+  resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac"
+  integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==
+  dependencies:
+    call-bind "^1.0.2"
+    define-properties "^1.1.3"
+
 object-keys@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e"
@@ -16985,11 +17058,6 @@ react-native-appstate-hook@^1.0.6:
   resolved "https://registry.yarnpkg.com/react-native-appstate-hook/-/react-native-appstate-hook-1.0.6.tgz#cbc16e7b89cfaea034cabd999f00e99053cabd06"
   integrity sha512-0hPVyf5yLxCSVrrNEuGqN1ZnSSj3Ye2gZex0NtcK/AHYwMc0rXWFNZjBKOoZSouspqu3hXBbQ6NOUSTzrME1AQ==
 
-react-native-background-fetch@^4.1.8:
-  version "4.1.10"
-  resolved "https://registry.yarnpkg.com/react-native-background-fetch/-/react-native-background-fetch-4.1.10.tgz#12c7e85140af67fb05edb7cd9960e4f09a457797"
-  integrity sha512-Ug54vTctZuD/c06ZLk/VyvFdhw/hCVVOHYR5heyMqc6FlT/m9fVhFWyl4uH3JmPCzmWDVR3fO28CzrGpKOrusw==
-
 react-native-codegen@^0.71.5:
   version "0.71.5"
   resolved "https://registry.yarnpkg.com/react-native-codegen/-/react-native-codegen-0.71.5.tgz#454a42a891cd4ca5fc436440d301044dc1349c14"
@@ -19704,6 +19772,17 @@ util.promisify@~1.0.0:
     has-symbols "^1.0.1"
     object.getownpropertydescriptors "^2.1.0"
 
+util@^0.12.0:
+  version "0.12.5"
+  resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc"
+  integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==
+  dependencies:
+    inherits "^2.0.3"
+    is-arguments "^1.0.4"
+    is-generator-function "^1.0.7"
+    is-typed-array "^1.1.3"
+    which-typed-array "^1.1.2"
+
 utila@~0.4:
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/utila/-/utila-0.4.0.tgz#8a16a05d445657a3aea5eecc5b12a4fa5379772c"
@@ -20104,6 +20183,17 @@ which-module@^2.0.0:
   resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409"
   integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==
 
+which-typed-array@^1.1.11, which-typed-array@^1.1.2:
+  version "1.1.11"
+  resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.11.tgz#99d691f23c72aab6768680805a271b69761ed61a"
+  integrity sha512-qe9UWWpkeG5yzZ0tNYxDmd7vo58HDBc39mZ0xWWpolAGADdFOzkfamWLDxkOWcvHQKVmdTyQdLD4NOfjLWTKew==
+  dependencies:
+    available-typed-arrays "^1.0.5"
+    call-bind "^1.0.2"
+    for-each "^0.3.3"
+    gopd "^1.0.1"
+    has-tostringtag "^1.0.0"
+
 which-typed-array@^1.1.9:
   version "1.1.9"
   resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6"