about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/App.native.tsx14
-rw-r--r--src/App.web.tsx7
-rw-r--r--src/lib/analytics/analytics.tsx78
-rw-r--r--src/lib/notifications/notifications.ts14
-rw-r--r--src/state/events.ts38
-rw-r--r--src/state/models/ui/shell.ts12
-rw-r--r--src/state/session/index.tsx9
-rw-r--r--src/state/shell/index.tsx7
-rw-r--r--src/state/shell/reminders.ts22
-rw-r--r--src/state/shell/tick-every-minute.tsx20
-rw-r--r--src/view/com/feeds/FeedPage.tsx23
-rw-r--r--src/view/com/profile/ProfileSubpageHeader.tsx3
-rw-r--r--src/view/com/util/TimeElapsed.tsx11
-rw-r--r--src/view/screens/Home.tsx7
-rw-r--r--src/view/screens/Notifications.tsx15
-rw-r--r--src/view/screens/Profile.tsx8
-rw-r--r--src/view/screens/Search/Search.tsx12
-rw-r--r--src/view/shell/Drawer.tsx8
-rw-r--r--src/view/shell/bottom-bar/BottomBar.tsx5
-rw-r--r--src/view/shell/desktop/LeftNav.tsx7
20 files changed, 185 insertions, 135 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx
index 60bce0578..ffa8b338e 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -11,6 +11,8 @@ import {QueryClientProvider} from '@tanstack/react-query'
 import 'view/icons'
 
 import {init as initPersistedState} from '#/state/persisted'
+import {init as initReminders} from '#/state/shell/reminders'
+import {listenSessionDropped} from './state/events'
 import {useColorMode} from 'state/shell'
 import {ThemeProvider} from 'lib/ThemeContext'
 import {s} from 'lib/styles'
@@ -53,15 +55,17 @@ const InnerApp = observer(function AppImpl() {
   useEffect(() => {
     setupState().then(store => {
       setRootStore(store)
-      analytics.init(store)
-      notifications.init(store, queryClient)
-      store.onSessionDropped(() => {
-        Toast.show('Sorry! Your session expired. Please log in again.')
-      })
     })
   }, [])
 
   useEffect(() => {
+    initReminders()
+    analytics.init()
+    notifications.init(queryClient)
+    listenSessionDropped(() => {
+      Toast.show('Sorry! Your session expired. Please log in again.')
+    })
+
     const account = persisted.get('session').currentAccount
     resumeSession(account)
   }, [resumeSession])
diff --git a/src/App.web.tsx b/src/App.web.tsx
index b734aea08..8e22f6480 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -9,6 +9,7 @@ import {RootSiblingParent} from 'react-native-root-siblings'
 import 'view/icons'
 
 import {init as initPersistedState} from '#/state/persisted'
+import {init as initReminders} from '#/state/shell/reminders'
 import {useColorMode} from 'state/shell'
 import * as analytics from 'lib/analytics/analytics'
 import {RootStoreModel, setupState, RootStoreProvider} from './state'
@@ -44,12 +45,14 @@ const InnerApp = observer(function AppImpl() {
   useEffect(() => {
     setupState().then(store => {
       setRootStore(store)
-      analytics.init(store)
     })
-    dynamicActivate(defaultLocale) // async import of locale data
   }, [])
 
   useEffect(() => {
+    initReminders()
+    analytics.init()
+    dynamicActivate(defaultLocale) // async import of locale data
+
     const account = persisted.get('session').currentAccount
     resumeSession(account)
   }, [resumeSession])
diff --git a/src/lib/analytics/analytics.tsx b/src/lib/analytics/analytics.tsx
index 71bb8569a..4b955b365 100644
--- a/src/lib/analytics/analytics.tsx
+++ b/src/lib/analytics/analytics.tsx
@@ -1,16 +1,18 @@
 import React from 'react'
 import {AppState, AppStateStatus} from 'react-native'
+import AsyncStorage from '@react-native-async-storage/async-storage'
 import {
   createClient,
   AnalyticsProvider,
   useAnalytics as useAnalyticsOrig,
   ClientMethods,
 } from '@segment/analytics-react-native'
-import {RootStoreModel, AppInfo} from 'state/models/root-store'
-import {useStores} from 'state/models/root-store'
+import {AppInfo} from 'state/models/root-store'
+import {useSession} from '#/state/session'
 import {sha256} from 'js-sha256'
 import {ScreenEvent, TrackEvent} from './types'
 import {logger} from '#/logger'
+import {listenSessionLoaded} from '#/state/events'
 
 const segmentClient = createClient({
   writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI',
@@ -21,10 +23,10 @@ const segmentClient = createClient({
 export const track = segmentClient?.track?.bind?.(segmentClient) as TrackEvent
 
 export function useAnalytics() {
-  const store = useStores()
+  const {hasSession} = useSession()
   const methods: ClientMethods = useAnalyticsOrig()
   return React.useMemo(() => {
-    if (store.session.hasSession) {
+    if (hasSession) {
       return {
         screen: methods.screen as ScreenEvent, // ScreenEvents defines all the possible screen names
         track: methods.track as TrackEvent, // TrackEvents defines all the possible track events and their properties
@@ -45,21 +47,18 @@ export function useAnalytics() {
       alias: () => Promise<void>,
       reset: () => Promise<void>,
     }
-  }, [store, methods])
+  }, [hasSession, methods])
 }
 
-export function init(store: RootStoreModel) {
-  store.onSessionLoaded(() => {
-    const sess = store.session.currentSession
-    if (sess) {
-      if (sess.did) {
-        const did_hashed = sha256(sess.did)
-        segmentClient.identify(did_hashed, {did_hashed})
-        logger.debug('Ping w/hash')
-      } else {
-        logger.debug('Ping w/o hash')
-        segmentClient.identify()
-      }
+export function init() {
+  listenSessionLoaded(account => {
+    if (account.did) {
+      const did_hashed = sha256(account.did)
+      segmentClient.identify(did_hashed, {did_hashed})
+      logger.debug('Ping w/hash')
+    } else {
+      logger.debug('Ping w/o hash')
+      segmentClient.identify()
     }
   })
 
@@ -67,7 +66,7 @@ export function init(store: RootStoreModel) {
   // this is a copy of segment's own lifecycle event tracking
   // we handle it manually to ensure that it never fires while the app is backgrounded
   // -prf
-  segmentClient.isReady.onChange(() => {
+  segmentClient.isReady.onChange(async () => {
     if (AppState.currentState !== 'active') {
       logger.debug('Prevented a metrics ping while the app was backgrounded')
       return
@@ -78,35 +77,29 @@ export function init(store: RootStoreModel) {
       return
     }
 
-    const oldAppInfo = store.appInfo
+    const oldAppInfo = await readAppInfo()
     const newAppInfo = context.app as AppInfo
-    store.setAppInfo(newAppInfo)
+    writeAppInfo(newAppInfo)
     logger.debug('Recording app info', {new: newAppInfo, old: oldAppInfo})
 
     if (typeof oldAppInfo === 'undefined') {
-      if (store.session.hasSession) {
-        segmentClient.track('Application Installed', {
-          version: newAppInfo.version,
-          build: newAppInfo.build,
-        })
-      }
+      segmentClient.track('Application Installed', {
+        version: newAppInfo.version,
+        build: newAppInfo.build,
+      })
     } else if (newAppInfo.version !== oldAppInfo.version) {
-      if (store.session.hasSession) {
-        segmentClient.track('Application Updated', {
-          version: newAppInfo.version,
-          build: newAppInfo.build,
-          previous_version: oldAppInfo.version,
-          previous_build: oldAppInfo.build,
-        })
-      }
-    }
-    if (store.session.hasSession) {
-      segmentClient.track('Application Opened', {
-        from_background: false,
+      segmentClient.track('Application Updated', {
         version: newAppInfo.version,
         build: newAppInfo.build,
+        previous_version: oldAppInfo.version,
+        previous_build: oldAppInfo.build,
       })
     }
+    segmentClient.track('Application Opened', {
+      from_background: false,
+      version: newAppInfo.version,
+      build: newAppInfo.build,
+    })
   })
 
   let lastState: AppStateStatus = AppState.currentState
@@ -130,3 +123,12 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
     <AnalyticsProvider client={segmentClient}>{children}</AnalyticsProvider>
   )
 }
+
+async function writeAppInfo(value: AppInfo) {
+  await AsyncStorage.setItem('BSKY_APP_INFO', JSON.stringify(value))
+}
+
+async function readAppInfo(): Promise<Partial<AppInfo> | undefined> {
+  const rawData = await AsyncStorage.getItem('BSKY_APP_INFO')
+  return rawData ? JSON.parse(rawData) : undefined
+}
diff --git a/src/lib/notifications/notifications.ts b/src/lib/notifications/notifications.ts
index d46479a05..2320e1c7b 100644
--- a/src/lib/notifications/notifications.ts
+++ b/src/lib/notifications/notifications.ts
@@ -1,19 +1,19 @@
 import * as Notifications from 'expo-notifications'
 import {QueryClient} from '@tanstack/react-query'
-import {RootStoreModel} from '../../state'
 import {resetToTab} from '../../Navigation'
 import {devicePlatform, isIOS} from 'platform/detection'
 import {track} from 'lib/analytics/analytics'
 import {logger} from '#/logger'
 import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed'
+import {listenSessionLoaded} from '#/state/events'
 
 const SERVICE_DID = (serviceUrl?: string) =>
   serviceUrl?.includes('staging')
     ? 'did:web:api.staging.bsky.dev'
     : 'did:web:api.bsky.app'
 
-export function init(store: RootStoreModel, queryClient: QueryClient) {
-  store.onSessionLoaded(async () => {
+export function init(queryClient: QueryClient) {
+  listenSessionLoaded(async (account, agent) => {
     // request notifications permission once the user has logged in
     const perms = await Notifications.getPermissionsAsync()
     if (!perms.granted) {
@@ -24,8 +24,8 @@ export function init(store: RootStoreModel, queryClient: QueryClient) {
     const token = await getPushToken()
     if (token) {
       try {
-        await store.agent.api.app.bsky.notification.registerPush({
-          serviceDid: SERVICE_DID(store.session.data?.service),
+        await agent.api.app.bsky.notification.registerPush({
+          serviceDid: SERVICE_DID(account.service),
           platform: devicePlatform,
           token: token.data,
           appId: 'xyz.blueskyweb.app',
@@ -53,8 +53,8 @@ export function init(store: RootStoreModel, queryClient: QueryClient) {
       )
       if (t) {
         try {
-          await store.agent.api.app.bsky.notification.registerPush({
-            serviceDid: SERVICE_DID(store.session.data?.service),
+          await agent.api.app.bsky.notification.registerPush({
+            serviceDid: SERVICE_DID(account.service),
             platform: devicePlatform,
             token: t,
             appId: 'xyz.blueskyweb.app',
diff --git a/src/state/events.ts b/src/state/events.ts
new file mode 100644
index 000000000..5441aafef
--- /dev/null
+++ b/src/state/events.ts
@@ -0,0 +1,38 @@
+import EventEmitter from 'eventemitter3'
+import {BskyAgent} from '@atproto/api'
+import {SessionAccount} from './session'
+
+type UnlistenFn = () => void
+
+const emitter = new EventEmitter()
+
+// a "soft reset" typically means scrolling to top and loading latest
+// but it can depend on the screen
+export function emitSoftReset() {
+  emitter.emit('soft-reset')
+}
+export function listenSoftReset(fn: () => void): UnlistenFn {
+  emitter.on('soft-reset', fn)
+  return () => emitter.off('soft-reset', fn)
+}
+
+export function emitSessionLoaded(
+  sessionAccount: SessionAccount,
+  agent: BskyAgent,
+) {
+  emitter.emit('session-loaded', sessionAccount, agent)
+}
+export function listenSessionLoaded(
+  fn: (sessionAccount: SessionAccount, agent: BskyAgent) => void,
+): UnlistenFn {
+  emitter.on('session-loaded', fn)
+  return () => emitter.off('session-loaded', fn)
+}
+
+export function emitSessionDropped() {
+  emitter.emit('session-dropped')
+}
+export function listenSessionDropped(fn: () => void): UnlistenFn {
+  emitter.on('session-dropped', fn)
+  return () => emitter.off('session-dropped', fn)
+}
diff --git a/src/state/models/ui/shell.ts b/src/state/models/ui/shell.ts
index 310d4f0f9..223c20625 100644
--- a/src/state/models/ui/shell.ts
+++ b/src/state/models/ui/shell.ts
@@ -1,6 +1,6 @@
 import {AppBskyActorDefs} from '@atproto/api'
 import {RootStoreModel} from '../root-store'
-import {makeAutoObservable, runInAction} from 'mobx'
+import {makeAutoObservable} from 'mobx'
 import {
   shouldRequestEmailConfirmation,
   setEmailConfirmationRequested,
@@ -40,14 +40,12 @@ export class ImagesLightbox implements LightboxModel {
 export class ShellUiModel {
   isLightboxActive = false
   activeLightbox: ProfileImageLightbox | ImagesLightbox | null = null
-  tickEveryMinute = Date.now()
 
   constructor(public rootStore: RootStoreModel) {
     makeAutoObservable(this, {
       rootStore: false,
     })
 
-    this.setupClock()
     this.setupLoginModals()
   }
 
@@ -83,14 +81,6 @@ export class ShellUiModel {
     this.activeLightbox = null
   }
 
-  setupClock() {
-    setInterval(() => {
-      runInAction(() => {
-        this.tickEveryMinute = Date.now()
-      })
-    }, 60_000)
-  }
-
   setupLoginModals() {
     this.rootStore.onSessionReady(() => {
       if (shouldRequestEmailConfirmation(this.rootStore.session)) {
diff --git a/src/state/session/index.tsx b/src/state/session/index.tsx
index e01e841f6..b8422553c 100644
--- a/src/state/session/index.tsx
+++ b/src/state/session/index.tsx
@@ -1,5 +1,4 @@
 import React from 'react'
-import {DeviceEventEmitter} from 'react-native'
 import {BskyAgent, AtpPersistSessionHandler} from '@atproto/api'
 
 import {networkRetry} from '#/lib/async/retry'
@@ -7,6 +6,7 @@ import {logger} from '#/logger'
 import * as persisted from '#/state/persisted'
 import {PUBLIC_BSKY_AGENT} from '#/state/queries'
 import {IS_PROD} from '#/lib/constants'
+import {emitSessionLoaded, emitSessionDropped} from '../events'
 
 export type SessionAccount = persisted.PersistedAccount
 
@@ -98,7 +98,9 @@ function createPersistSessionHandler(
       logger.DebugContext.session,
     )
 
-    if (expired) DeviceEventEmitter.emit('session-dropped')
+    if (expired) {
+      emitSessionDropped()
+    }
 
     persistSessionCallback({
       expired,
@@ -180,6 +182,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
 
       setState(s => ({...s, agent}))
       upsertAccount(account)
+      emitSessionLoaded(account, agent)
 
       logger.debug(
         `session: created account`,
@@ -230,6 +233,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
 
       setState(s => ({...s, agent}))
       upsertAccount(account)
+      emitSessionLoaded(account, agent)
 
       logger.debug(
         `session: logged in`,
@@ -291,6 +295,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
 
       setState(s => ({...s, agent}))
       upsertAccount(account)
+      emitSessionLoaded(account, agent)
     },
     [upsertAccount],
   )
diff --git a/src/state/shell/index.tsx b/src/state/shell/index.tsx
index 63c3763d1..53f05055c 100644
--- a/src/state/shell/index.tsx
+++ b/src/state/shell/index.tsx
@@ -6,6 +6,7 @@ import {Provider as MinimalModeProvider} from './minimal-mode'
 import {Provider as ColorModeProvider} from './color-mode'
 import {Provider as OnboardingProvider} from './onboarding'
 import {Provider as ComposerProvider} from './composer'
+import {Provider as TickEveryMinuteProvider} from './tick-every-minute'
 
 export {useIsDrawerOpen, useSetDrawerOpen} from './drawer-open'
 export {
@@ -15,6 +16,8 @@ export {
 export {useMinimalShellMode, useSetMinimalShellMode} from './minimal-mode'
 export {useColorMode, useSetColorMode} from './color-mode'
 export {useOnboardingState, useOnboardingDispatch} from './onboarding'
+export {useComposerState, useComposerControls} from './composer'
+export {useTickEveryMinute} from './tick-every-minute'
 
 export function Provider({children}: React.PropsWithChildren<{}>) {
   return (
@@ -24,7 +27,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
           <MinimalModeProvider>
             <ColorModeProvider>
               <OnboardingProvider>
-                <ComposerProvider>{children}</ComposerProvider>
+                <ComposerProvider>
+                  <TickEveryMinuteProvider>{children}</TickEveryMinuteProvider>
+                </ComposerProvider>
               </OnboardingProvider>
             </ColorModeProvider>
           </MinimalModeProvider>
diff --git a/src/state/shell/reminders.ts b/src/state/shell/reminders.ts
index e7ee7a5fe..88d0a5d85 100644
--- a/src/state/shell/reminders.ts
+++ b/src/state/shell/reminders.ts
@@ -1,14 +1,24 @@
 import * as persisted from '#/state/persisted'
-import {SessionModel} from '../models/session'
 import {toHashCode} from 'lib/strings/helpers'
 import {isOnboardingActive} from './onboarding'
+import {SessionAccount} from '../session'
+import {listenSessionLoaded} from '../events'
+import {unstable__openModal} from '../modals'
 
-export function shouldRequestEmailConfirmation(session: SessionModel) {
-  const sess = session.currentSession
-  if (!sess) {
+export function init() {
+  listenSessionLoaded(account => {
+    if (shouldRequestEmailConfirmation(account)) {
+      unstable__openModal({name: 'verify-email', showReminder: true})
+      setEmailConfirmationRequested()
+    }
+  })
+}
+
+export function shouldRequestEmailConfirmation(account: SessionAccount) {
+  if (!account) {
     return false
   }
-  if (sess.emailConfirmed) {
+  if (account.emailConfirmed) {
     return false
   }
   if (isOnboardingActive()) {
@@ -22,7 +32,7 @@ export function shouldRequestEmailConfirmation(session: SessionModel) {
   // shard the users into 2 day of the week buckets
   // (this is to avoid a sudden influx of email updates when
   // this feature rolls out)
-  const code = toHashCode(sess.did) % 7
+  const code = toHashCode(account.did) % 7
   if (code !== today.getDay() && code !== (today.getDay() + 1) % 7) {
     return false
   }
diff --git a/src/state/shell/tick-every-minute.tsx b/src/state/shell/tick-every-minute.tsx
new file mode 100644
index 000000000..c37221c90
--- /dev/null
+++ b/src/state/shell/tick-every-minute.tsx
@@ -0,0 +1,20 @@
+import React from 'react'
+
+type StateContext = number
+
+const stateContext = React.createContext<StateContext>(0)
+
+export function Provider({children}: React.PropsWithChildren<{}>) {
+  const [tick, setTick] = React.useState(Date.now())
+  React.useEffect(() => {
+    const i = setInterval(() => {
+      setTick(Date.now())
+    }, 60_000)
+    return () => clearInterval(i)
+  }, [])
+  return <stateContext.Provider value={tick}>{children}</stateContext.Provider>
+}
+
+export function useTickEveryMinute() {
+  return React.useContext(stateContext)
+}
diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx
index 562b1c141..e7dcf09b8 100644
--- a/src/view/com/feeds/FeedPage.tsx
+++ b/src/view/com/feeds/FeedPage.tsx
@@ -14,7 +14,6 @@ import {ComposeIcon2} from 'lib/icons'
 import {colors, s} from 'lib/styles'
 import React from 'react'
 import {FlatList, View, useWindowDimensions} from 'react-native'
-import {useStores} from 'state/index'
 import {Feed} from '../posts/Feed'
 import {TextLink} from '../util/Link'
 import {FAB} from '../util/fab/FAB'
@@ -23,6 +22,7 @@ import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 import {useSession} from '#/state/session'
 import {useComposerControls} from '#/state/shell/composer'
+import {listenSoftReset, emitSoftReset} from '#/state/events'
 
 const POLL_FREQ = 30e3 // 30sec
 
@@ -41,7 +41,6 @@ export function FeedPage({
   renderEmptyState: () => JSX.Element
   renderEndOfFeed?: () => JSX.Element
 }) {
-  const store = useStores()
   const {isSandbox} = useSession()
   const pal = usePalette('default')
   const {_} = useLingui()
@@ -73,12 +72,9 @@ export function FeedPage({
     if (!isPageFocused || !isScreenFocused) {
       return
     }
-    const softResetSub = store.onScreenSoftReset(onSoftReset)
     screen('Feed')
-    return () => {
-      softResetSub.remove()
-    }
-  }, [store, onSoftReset, screen, feed, isPageFocused, isScreenFocused])
+    return listenSoftReset(onSoftReset)
+  }, [onSoftReset, screen, isPageFocused, isScreenFocused])
 
   const onPressCompose = React.useCallback(() => {
     track('HomeScreen:PressCompose')
@@ -125,7 +121,7 @@ export function FeedPage({
                 )}
               </>
             }
-            onPress={() => store.emitScreenSoftReset()}
+            onPress={emitSoftReset}
           />
           <TextLink
             type="title-lg"
@@ -144,16 +140,7 @@ export function FeedPage({
       )
     }
     return <></>
-  }, [
-    isDesktop,
-    pal.view,
-    pal.text,
-    pal.textLight,
-    store,
-    hasNew,
-    _,
-    isSandbox,
-  ])
+  }, [isDesktop, pal.view, pal.text, pal.textLight, hasNew, _, isSandbox])
 
   return (
     <View testID={testID} style={s.h100pct}>
diff --git a/src/view/com/profile/ProfileSubpageHeader.tsx b/src/view/com/profile/ProfileSubpageHeader.tsx
index 251d3141b..e1b587beb 100644
--- a/src/view/com/profile/ProfileSubpageHeader.tsx
+++ b/src/view/com/profile/ProfileSubpageHeader.tsx
@@ -20,6 +20,7 @@ import {ImagesLightbox} from 'state/models/ui/shell'
 import {useLingui} from '@lingui/react'
 import {msg} from '@lingui/macro'
 import {useSetDrawerOpen} from '#/state/shell'
+import {emitSoftReset} from '#/state/events'
 
 export const ProfileSubpageHeader = observer(function HeaderImpl({
   isLoading,
@@ -145,7 +146,7 @@ export const ProfileSubpageHeader = observer(function HeaderImpl({
               href={href}
               style={[pal.text, {fontWeight: 'bold'}]}
               text={title || ''}
-              onPress={() => store.emitScreenSoftReset()}
+              onPress={emitSoftReset}
               numberOfLines={4}
             />
           )}
diff --git a/src/view/com/util/TimeElapsed.tsx b/src/view/com/util/TimeElapsed.tsx
index 0765f65b2..dad46448c 100644
--- a/src/view/com/util/TimeElapsed.tsx
+++ b/src/view/com/util/TimeElapsed.tsx
@@ -1,24 +1,23 @@
 import React from 'react'
-import {observer} from 'mobx-react-lite'
 import {ago} from 'lib/strings/time'
-import {useStores} from 'state/index'
+import {useTickEveryMinute} from '#/state/shell'
 
 // FIXME(dan): Figure out why the false positives
 /* eslint-disable react/prop-types */
 
-export const TimeElapsed = observer(function TimeElapsed({
+export function TimeElapsed({
   timestamp,
   children,
 }: {
   timestamp: string
   children: ({timeElapsed}: {timeElapsed: string}) => JSX.Element
 }) {
-  const stores = useStores()
+  const tick = useTickEveryMinute()
   const [timeElapsed, setTimeAgo] = React.useState(ago(timestamp))
 
   React.useEffect(() => {
     setTimeAgo(ago(timestamp))
-  }, [timestamp, setTimeAgo, stores.shell.tickEveryMinute])
+  }, [timestamp, setTimeAgo, tick])
 
   return children({timeElapsed})
-})
+}
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index fdd764e44..c297f8650 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -9,15 +9,14 @@ import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed'
 import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState'
 import {FeedsTabBar} from '../com/pager/FeedsTabBar'
 import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
-import {useStores} from 'state/index'
 import {FeedPage} from 'view/com/feeds/FeedPage'
 import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell'
 import {usePreferencesQuery} from '#/state/queries/preferences'
+import {emitSoftReset} from '#/state/events'
 
 type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
 export const HomeScreen = withAuthRequired(
   observer(function HomeScreenImpl({}: Props) {
-    const store = useStores()
     const setMinimalShellMode = useSetMinimalShellMode()
     const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
     const pagerRef = React.useRef<PagerRef>(null)
@@ -74,8 +73,8 @@ export const HomeScreen = withAuthRequired(
     )
 
     const onPressSelected = React.useCallback(() => {
-      store.emitScreenSoftReset()
-    }, [store])
+      emitSoftReset()
+    }, [])
 
     const onPageScrollStateChanged = React.useCallback(
       (state: 'idle' | 'dragging' | 'settling') => {
diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx
index e0f234073..970882f12 100644
--- a/src/view/screens/Notifications.tsx
+++ b/src/view/screens/Notifications.tsx
@@ -11,7 +11,6 @@ import {ViewHeader} from '../com/util/ViewHeader'
 import {Feed} from '../com/notifications/Feed'
 import {TextLink} from 'view/com/util/Link'
 import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
-import {useStores} from 'state/index'
 import {useOnMainScroll} from 'lib/hooks/useOnMainScroll'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -21,6 +20,7 @@ import {logger} from '#/logger'
 import {useSetMinimalShellMode} from '#/state/shell'
 import {useUnreadNotifications} from '#/state/queries/notifications/unread'
 import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed'
+import {listenSoftReset, emitSoftReset} from '#/state/events'
 
 type Props = NativeStackScreenProps<
   NotificationsTabNavigatorParams,
@@ -28,7 +28,6 @@ type Props = NativeStackScreenProps<
 >
 export const NotificationsScreen = withAuthRequired(
   function NotificationsScreenImpl({}: Props) {
-    const store = useStores()
     const setMinimalShellMode = useSetMinimalShellMode()
     const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll()
     const scrollElRef = React.useRef<FlatList>(null)
@@ -57,13 +56,9 @@ export const NotificationsScreen = withAuthRequired(
       React.useCallback(() => {
         setMinimalShellMode(false)
         logger.debug('NotificationsScreen: Updating feed')
-        const softResetSub = store.onScreenSoftReset(onPressLoadLatest)
         screen('Notifications')
-
-        return () => {
-          softResetSub.remove()
-        }
-      }, [store, screen, onPressLoadLatest, setMinimalShellMode]),
+        return listenSoftReset(onPressLoadLatest)
+      }, [screen, onPressLoadLatest, setMinimalShellMode]),
     )
 
     const ListHeaderComponent = React.useCallback(() => {
@@ -100,13 +95,13 @@ export const NotificationsScreen = withAuthRequired(
                   )}
                 </>
               }
-              onPress={() => store.emitScreenSoftReset()}
+              onPress={emitSoftReset}
             />
           </View>
         )
       }
       return <></>
-    }, [isDesktop, pal, store, hasNew])
+    }, [isDesktop, pal, hasNew])
 
     return (
       <View testID="notificationsScreen" style={s.hContentRegion}>
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 17ea4498c..fdabc690e 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -12,7 +12,6 @@ import {ScreenHider} from 'view/com/util/moderation/ScreenHider'
 import {Feed} from 'view/com/posts/Feed'
 import {ProfileLists} from '../com/lists/ProfileLists'
 import {ProfileFeedgens} from '../com/feeds/ProfileFeedgens'
-import {useStores} from 'state/index'
 import {ProfileHeader} from '../com/profile/ProfileHeader'
 import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
 import {ErrorScreen} from '../com/util/error/ErrorScreen'
@@ -37,6 +36,7 @@ import {cleanError} from '#/lib/strings/errors'
 import {LoadLatestBtn} from '../com/util/load-latest/LoadLatestBtn'
 import {useQueryClient} from '@tanstack/react-query'
 import {useComposerControls} from '#/state/shell/composer'
+import {listenSoftReset} from '#/state/events'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'>
 export const ProfileScreen = withAuthRequired(function ProfileScreenImpl({
@@ -126,7 +126,6 @@ function ProfileScreenLoaded({
   hideBackButton: boolean
 }) {
   const profile = useProfileShadow(profileUnshadowed, dataUpdatedAt)
-  const store = useStores()
   const {currentAccount} = useSession()
   const setMinimalShellMode = useSetMinimalShellMode()
   const {openComposer} = useComposerControls()
@@ -169,11 +168,10 @@ function ProfileScreenLoaded({
     React.useCallback(() => {
       setMinimalShellMode(false)
       screen('Profile')
-      const softResetSub = store.onScreenSoftReset(() => {
+      return listenSoftReset(() => {
         viewSelectorRef.current?.scrollToTop()
       })
-      return () => softResetSub.remove()
-    }, [store, viewSelectorRef, setMinimalShellMode, screen]),
+    }, [viewSelectorRef, setMinimalShellMode, screen]),
   )
 
   useFocusEffect(
diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx
index 8fadfe864..54e042d04 100644
--- a/src/view/screens/Search/Search.tsx
+++ b/src/view/screens/Search/Search.tsx
@@ -42,8 +42,8 @@ import {MagnifyingGlassIcon} from '#/lib/icons'
 import {useModerationOpts} from '#/state/queries/preferences'
 import {SearchResultCard} from '#/view/shell/desktop/Search'
 import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell'
-import {useStores} from '#/state'
 import {isWeb} from '#/platform/detection'
+import {listenSoftReset} from '#/state/events'
 
 function Loader() {
   const pal = usePalette('default')
@@ -421,7 +421,6 @@ export function SearchScreenMobile(
   const moderationOpts = useModerationOpts()
   const search = useActorAutocompleteFn()
   const setMinimalShellMode = useSetMinimalShellMode()
-  const store = useStores()
   const {isTablet} = useWebMediaQueries()
 
   const searchDebounceTimeout = React.useRef<NodeJS.Timeout | undefined>(
@@ -490,14 +489,9 @@ export function SearchScreenMobile(
 
   useFocusEffect(
     React.useCallback(() => {
-      const softResetSub = store.onScreenSoftReset(onSoftReset)
-
       setMinimalShellMode(false)
-
-      return () => {
-        softResetSub.remove()
-      }
-    }, [store, onSoftReset, setMinimalShellMode]),
+      return listenSoftReset(onSoftReset)
+    }, [onSoftReset, setMinimalShellMode]),
   )
 
   return (
diff --git a/src/view/shell/Drawer.tsx b/src/view/shell/Drawer.tsx
index 8a84a07c6..c5dcb150c 100644
--- a/src/view/shell/Drawer.tsx
+++ b/src/view/shell/Drawer.tsx
@@ -50,6 +50,7 @@ import {useModalControls} from '#/state/modals'
 import {useSession, SessionAccount} from '#/state/session'
 import {useProfileQuery} from '#/state/queries/profile'
 import {useUnreadNotifications} from '#/state/queries/notifications/unread'
+import {emitSoftReset} from '#/state/events'
 
 export function DrawerProfileCard({
   account,
@@ -103,7 +104,6 @@ export function DrawerProfileCard({
 export const DrawerContent = observer(function DrawerContentImpl() {
   const theme = useTheme()
   const pal = usePalette('default')
-  const store = useStores()
   const {_} = useLingui()
   const setDrawerOpen = useSetDrawerOpen()
   const navigation = useNavigation<NavigationProp>()
@@ -124,7 +124,7 @@ export const DrawerContent = observer(function DrawerContentImpl() {
       if (isWeb) {
         // hack because we have flat navigator for web and MyProfile does not exist on the web navigator -ansh
         if (tab === 'MyProfile') {
-          navigation.navigate('Profile', {name: store.me.handle})
+          navigation.navigate('Profile', {name: currentAccount!.handle})
         } else {
           // @ts-ignore must be Home, Search, Notifications, or MyProfile
           navigation.navigate(tab)
@@ -132,7 +132,7 @@ export const DrawerContent = observer(function DrawerContentImpl() {
       } else {
         const tabState = getTabState(state, tab)
         if (tabState === TabState.InsideAtRoot) {
-          store.emitScreenSoftReset()
+          emitSoftReset()
         } else if (tabState === TabState.Inside) {
           navigation.dispatch(StackActions.popToTop())
         } else {
@@ -141,7 +141,7 @@ export const DrawerContent = observer(function DrawerContentImpl() {
         }
       }
     },
-    [store, track, navigation, setDrawerOpen],
+    [track, navigation, setDrawerOpen, currentAccount],
   )
 
   const onPressHome = React.useCallback(() => onPressTab('Home'), [onPressTab])
diff --git a/src/view/shell/bottom-bar/BottomBar.tsx b/src/view/shell/bottom-bar/BottomBar.tsx
index 81552635f..de1dc1703 100644
--- a/src/view/shell/bottom-bar/BottomBar.tsx
+++ b/src/view/shell/bottom-bar/BottomBar.tsx
@@ -29,6 +29,7 @@ import {msg} from '@lingui/macro'
 import {useModalControls} from '#/state/modals'
 import {useShellLayout} from '#/state/shell/shell-layout'
 import {useUnreadNotifications} from '#/state/queries/notifications/unread'
+import {emitSoftReset} from '#/state/events'
 
 type TabOptions = 'Home' | 'Search' | 'Notifications' | 'MyProfile' | 'Feeds'
 
@@ -53,14 +54,14 @@ export const BottomBar = observer(function BottomBarImpl({
       const state = navigation.getState()
       const tabState = getTabState(state, tab)
       if (tabState === TabState.InsideAtRoot) {
-        store.emitScreenSoftReset()
+        emitSoftReset()
       } else if (tabState === TabState.Inside) {
         navigation.dispatch(StackActions.popToTop())
       } else {
         navigation.navigate(`${tab}Tab`)
       }
     },
-    [store, track, navigation],
+    [track, navigation],
   )
   const onPressHome = React.useCallback(() => onPressTab('Home'), [onPressTab])
   const onPressSearch = React.useCallback(
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index 8bc1d49a0..3ec68872e 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -16,7 +16,6 @@ import {UserAvatar} from 'view/com/util/UserAvatar'
 import {Link} from 'view/com/util/Link'
 import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder'
 import {usePalette} from 'lib/hooks/usePalette'
-import {useStores} from 'state/index'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {s, colors} from 'lib/styles'
 import {
@@ -46,6 +45,7 @@ import {useSession} from '#/state/session'
 import {useUnreadNotifications} from '#/state/queries/notifications/unread'
 import {useComposerControls} from '#/state/shell/composer'
 import {useFetchHandle} from '#/state/queries/handle'
+import {emitSoftReset} from '#/state/events'
 
 const ProfileCard = observer(function ProfileCardImpl() {
   const {currentAccount} = useSession()
@@ -126,7 +126,6 @@ const NavItem = observer(function NavItemImpl({
 }: NavItemProps) {
   const pal = usePalette('default')
   const {currentAccount} = useSession()
-  const store = useStores()
   const {isDesktop, isTablet} = useWebMediaQueries()
   const [pathName] = React.useMemo(() => router.matchPath(href), [href])
   const currentRouteInfo = useNavigationState(state => {
@@ -149,12 +148,12 @@ const NavItem = observer(function NavItemImpl({
       }
       e.preventDefault()
       if (isCurrent) {
-        store.emitScreenSoftReset()
+        emitSoftReset()
       } else {
         onPress()
       }
     },
-    [onPress, isCurrent, store],
+    [onPress, isCurrent],
   )
 
   return (