my fork of the bluesky client

Shell behaviors update (react-query refactor) (#1915)

* Move tick-every-minute into a hook/context

* Move soft-reset event out of the shell model

* Update soft-reset listener on new search page

* Implement session-loaded and session-dropped events

* Update analytics and push-notifications to use new session system

authored by

Paul Frazee and committed by
GitHub
6616b2bf f23e9978

+185 -135
+9 -5
src/App.native.tsx
··· 11 11 import 'view/icons' 12 12 13 13 import {init as initPersistedState} from '#/state/persisted' 14 + import {init as initReminders} from '#/state/shell/reminders' 15 + import {listenSessionDropped} from './state/events' 14 16 import {useColorMode} from 'state/shell' 15 17 import {ThemeProvider} from 'lib/ThemeContext' 16 18 import {s} from 'lib/styles' ··· 53 55 useEffect(() => { 54 56 setupState().then(store => { 55 57 setRootStore(store) 56 - analytics.init(store) 57 - notifications.init(store, queryClient) 58 - store.onSessionDropped(() => { 59 - Toast.show('Sorry! Your session expired. Please log in again.') 60 - }) 61 58 }) 62 59 }, []) 63 60 64 61 useEffect(() => { 62 + initReminders() 63 + analytics.init() 64 + notifications.init(queryClient) 65 + listenSessionDropped(() => { 66 + Toast.show('Sorry! Your session expired. Please log in again.') 67 + }) 68 + 65 69 const account = persisted.get('session').currentAccount 66 70 resumeSession(account) 67 71 }, [resumeSession])
+5 -2
src/App.web.tsx
··· 9 9 import 'view/icons' 10 10 11 11 import {init as initPersistedState} from '#/state/persisted' 12 + import {init as initReminders} from '#/state/shell/reminders' 12 13 import {useColorMode} from 'state/shell' 13 14 import * as analytics from 'lib/analytics/analytics' 14 15 import {RootStoreModel, setupState, RootStoreProvider} from './state' ··· 44 45 useEffect(() => { 45 46 setupState().then(store => { 46 47 setRootStore(store) 47 - analytics.init(store) 48 48 }) 49 - dynamicActivate(defaultLocale) // async import of locale data 50 49 }, []) 51 50 52 51 useEffect(() => { 52 + initReminders() 53 + analytics.init() 54 + dynamicActivate(defaultLocale) // async import of locale data 55 + 53 56 const account = persisted.get('session').currentAccount 54 57 resumeSession(account) 55 58 }, [resumeSession])
+40 -38
src/lib/analytics/analytics.tsx
··· 1 1 import React from 'react' 2 2 import {AppState, AppStateStatus} from 'react-native' 3 + import AsyncStorage from '@react-native-async-storage/async-storage' 3 4 import { 4 5 createClient, 5 6 AnalyticsProvider, 6 7 useAnalytics as useAnalyticsOrig, 7 8 ClientMethods, 8 9 } from '@segment/analytics-react-native' 9 - import {RootStoreModel, AppInfo} from 'state/models/root-store' 10 - import {useStores} from 'state/models/root-store' 10 + import {AppInfo} from 'state/models/root-store' 11 + import {useSession} from '#/state/session' 11 12 import {sha256} from 'js-sha256' 12 13 import {ScreenEvent, TrackEvent} from './types' 13 14 import {logger} from '#/logger' 15 + import {listenSessionLoaded} from '#/state/events' 14 16 15 17 const segmentClient = createClient({ 16 18 writeKey: '8I6DsgfiSLuoONyaunGoiQM7A6y2ybdI', ··· 21 23 export const track = segmentClient?.track?.bind?.(segmentClient) as TrackEvent 22 24 23 25 export function useAnalytics() { 24 - const store = useStores() 26 + const {hasSession} = useSession() 25 27 const methods: ClientMethods = useAnalyticsOrig() 26 28 return React.useMemo(() => { 27 - if (store.session.hasSession) { 29 + if (hasSession) { 28 30 return { 29 31 screen: methods.screen as ScreenEvent, // ScreenEvents defines all the possible screen names 30 32 track: methods.track as TrackEvent, // TrackEvents defines all the possible track events and their properties ··· 45 47 alias: () => Promise<void>, 46 48 reset: () => Promise<void>, 47 49 } 48 - }, [store, methods]) 50 + }, [hasSession, methods]) 49 51 } 50 52 51 - export function init(store: RootStoreModel) { 52 - store.onSessionLoaded(() => { 53 - const sess = store.session.currentSession 54 - if (sess) { 55 - if (sess.did) { 56 - const did_hashed = sha256(sess.did) 57 - segmentClient.identify(did_hashed, {did_hashed}) 58 - logger.debug('Ping w/hash') 59 - } else { 60 - logger.debug('Ping w/o hash') 61 - segmentClient.identify() 62 - } 53 + export function init() { 54 + listenSessionLoaded(account => { 55 + if (account.did) { 56 + const did_hashed = sha256(account.did) 57 + segmentClient.identify(did_hashed, {did_hashed}) 58 + logger.debug('Ping w/hash') 59 + } else { 60 + logger.debug('Ping w/o hash') 61 + segmentClient.identify() 63 62 } 64 63 }) 65 64 ··· 67 66 // this is a copy of segment's own lifecycle event tracking 68 67 // we handle it manually to ensure that it never fires while the app is backgrounded 69 68 // -prf 70 - segmentClient.isReady.onChange(() => { 69 + segmentClient.isReady.onChange(async () => { 71 70 if (AppState.currentState !== 'active') { 72 71 logger.debug('Prevented a metrics ping while the app was backgrounded') 73 72 return ··· 78 77 return 79 78 } 80 79 81 - const oldAppInfo = store.appInfo 80 + const oldAppInfo = await readAppInfo() 82 81 const newAppInfo = context.app as AppInfo 83 - store.setAppInfo(newAppInfo) 82 + writeAppInfo(newAppInfo) 84 83 logger.debug('Recording app info', {new: newAppInfo, old: oldAppInfo}) 85 84 86 85 if (typeof oldAppInfo === 'undefined') { 87 - if (store.session.hasSession) { 88 - segmentClient.track('Application Installed', { 89 - version: newAppInfo.version, 90 - build: newAppInfo.build, 91 - }) 92 - } 86 + segmentClient.track('Application Installed', { 87 + version: newAppInfo.version, 88 + build: newAppInfo.build, 89 + }) 93 90 } else if (newAppInfo.version !== oldAppInfo.version) { 94 - if (store.session.hasSession) { 95 - segmentClient.track('Application Updated', { 96 - version: newAppInfo.version, 97 - build: newAppInfo.build, 98 - previous_version: oldAppInfo.version, 99 - previous_build: oldAppInfo.build, 100 - }) 101 - } 102 - } 103 - if (store.session.hasSession) { 104 - segmentClient.track('Application Opened', { 105 - from_background: false, 91 + segmentClient.track('Application Updated', { 106 92 version: newAppInfo.version, 107 93 build: newAppInfo.build, 94 + previous_version: oldAppInfo.version, 95 + previous_build: oldAppInfo.build, 108 96 }) 109 97 } 98 + segmentClient.track('Application Opened', { 99 + from_background: false, 100 + version: newAppInfo.version, 101 + build: newAppInfo.build, 102 + }) 110 103 }) 111 104 112 105 let lastState: AppStateStatus = AppState.currentState ··· 130 123 <AnalyticsProvider client={segmentClient}>{children}</AnalyticsProvider> 131 124 ) 132 125 } 126 + 127 + async function writeAppInfo(value: AppInfo) { 128 + await AsyncStorage.setItem('BSKY_APP_INFO', JSON.stringify(value)) 129 + } 130 + 131 + async function readAppInfo(): Promise<Partial<AppInfo> | undefined> { 132 + const rawData = await AsyncStorage.getItem('BSKY_APP_INFO') 133 + return rawData ? JSON.parse(rawData) : undefined 134 + }
+7 -7
src/lib/notifications/notifications.ts
··· 1 1 import * as Notifications from 'expo-notifications' 2 2 import {QueryClient} from '@tanstack/react-query' 3 - import {RootStoreModel} from '../../state' 4 3 import {resetToTab} from '../../Navigation' 5 4 import {devicePlatform, isIOS} from 'platform/detection' 6 5 import {track} from 'lib/analytics/analytics' 7 6 import {logger} from '#/logger' 8 7 import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed' 8 + import {listenSessionLoaded} from '#/state/events' 9 9 10 10 const SERVICE_DID = (serviceUrl?: string) => 11 11 serviceUrl?.includes('staging') 12 12 ? 'did:web:api.staging.bsky.dev' 13 13 : 'did:web:api.bsky.app' 14 14 15 - export function init(store: RootStoreModel, queryClient: QueryClient) { 16 - store.onSessionLoaded(async () => { 15 + export function init(queryClient: QueryClient) { 16 + listenSessionLoaded(async (account, agent) => { 17 17 // request notifications permission once the user has logged in 18 18 const perms = await Notifications.getPermissionsAsync() 19 19 if (!perms.granted) { ··· 24 24 const token = await getPushToken() 25 25 if (token) { 26 26 try { 27 - await store.agent.api.app.bsky.notification.registerPush({ 28 - serviceDid: SERVICE_DID(store.session.data?.service), 27 + await agent.api.app.bsky.notification.registerPush({ 28 + serviceDid: SERVICE_DID(account.service), 29 29 platform: devicePlatform, 30 30 token: token.data, 31 31 appId: 'xyz.blueskyweb.app', ··· 53 53 ) 54 54 if (t) { 55 55 try { 56 - await store.agent.api.app.bsky.notification.registerPush({ 57 - serviceDid: SERVICE_DID(store.session.data?.service), 56 + await agent.api.app.bsky.notification.registerPush({ 57 + serviceDid: SERVICE_DID(account.service), 58 58 platform: devicePlatform, 59 59 token: t, 60 60 appId: 'xyz.blueskyweb.app',
+38
src/state/events.ts
··· 1 + import EventEmitter from 'eventemitter3' 2 + import {BskyAgent} from '@atproto/api' 3 + import {SessionAccount} from './session' 4 + 5 + type UnlistenFn = () => void 6 + 7 + const emitter = new EventEmitter() 8 + 9 + // a "soft reset" typically means scrolling to top and loading latest 10 + // but it can depend on the screen 11 + export function emitSoftReset() { 12 + emitter.emit('soft-reset') 13 + } 14 + export function listenSoftReset(fn: () => void): UnlistenFn { 15 + emitter.on('soft-reset', fn) 16 + return () => emitter.off('soft-reset', fn) 17 + } 18 + 19 + export function emitSessionLoaded( 20 + sessionAccount: SessionAccount, 21 + agent: BskyAgent, 22 + ) { 23 + emitter.emit('session-loaded', sessionAccount, agent) 24 + } 25 + export function listenSessionLoaded( 26 + fn: (sessionAccount: SessionAccount, agent: BskyAgent) => void, 27 + ): UnlistenFn { 28 + emitter.on('session-loaded', fn) 29 + return () => emitter.off('session-loaded', fn) 30 + } 31 + 32 + export function emitSessionDropped() { 33 + emitter.emit('session-dropped') 34 + } 35 + export function listenSessionDropped(fn: () => void): UnlistenFn { 36 + emitter.on('session-dropped', fn) 37 + return () => emitter.off('session-dropped', fn) 38 + }
+1 -11
src/state/models/ui/shell.ts
··· 1 1 import {AppBskyActorDefs} from '@atproto/api' 2 2 import {RootStoreModel} from '../root-store' 3 - import {makeAutoObservable, runInAction} from 'mobx' 3 + import {makeAutoObservable} from 'mobx' 4 4 import { 5 5 shouldRequestEmailConfirmation, 6 6 setEmailConfirmationRequested, ··· 40 40 export class ShellUiModel { 41 41 isLightboxActive = false 42 42 activeLightbox: ProfileImageLightbox | ImagesLightbox | null = null 43 - tickEveryMinute = Date.now() 44 43 45 44 constructor(public rootStore: RootStoreModel) { 46 45 makeAutoObservable(this, { 47 46 rootStore: false, 48 47 }) 49 48 50 - this.setupClock() 51 49 this.setupLoginModals() 52 50 } 53 51 ··· 81 79 closeLightbox() { 82 80 this.isLightboxActive = false 83 81 this.activeLightbox = null 84 - } 85 - 86 - setupClock() { 87 - setInterval(() => { 88 - runInAction(() => { 89 - this.tickEveryMinute = Date.now() 90 - }) 91 - }, 60_000) 92 82 } 93 83 94 84 setupLoginModals() {
+7 -2
src/state/session/index.tsx
··· 1 1 import React from 'react' 2 - import {DeviceEventEmitter} from 'react-native' 3 2 import {BskyAgent, AtpPersistSessionHandler} from '@atproto/api' 4 3 5 4 import {networkRetry} from '#/lib/async/retry' ··· 7 6 import * as persisted from '#/state/persisted' 8 7 import {PUBLIC_BSKY_AGENT} from '#/state/queries' 9 8 import {IS_PROD} from '#/lib/constants' 9 + import {emitSessionLoaded, emitSessionDropped} from '../events' 10 10 11 11 export type SessionAccount = persisted.PersistedAccount 12 12 ··· 98 98 logger.DebugContext.session, 99 99 ) 100 100 101 - if (expired) DeviceEventEmitter.emit('session-dropped') 101 + if (expired) { 102 + emitSessionDropped() 103 + } 102 104 103 105 persistSessionCallback({ 104 106 expired, ··· 180 182 181 183 setState(s => ({...s, agent})) 182 184 upsertAccount(account) 185 + emitSessionLoaded(account, agent) 183 186 184 187 logger.debug( 185 188 `session: created account`, ··· 230 233 231 234 setState(s => ({...s, agent})) 232 235 upsertAccount(account) 236 + emitSessionLoaded(account, agent) 233 237 234 238 logger.debug( 235 239 `session: logged in`, ··· 291 295 292 296 setState(s => ({...s, agent})) 293 297 upsertAccount(account) 298 + emitSessionLoaded(account, agent) 294 299 }, 295 300 [upsertAccount], 296 301 )
+6 -1
src/state/shell/index.tsx
··· 6 6 import {Provider as ColorModeProvider} from './color-mode' 7 7 import {Provider as OnboardingProvider} from './onboarding' 8 8 import {Provider as ComposerProvider} from './composer' 9 + import {Provider as TickEveryMinuteProvider} from './tick-every-minute' 9 10 10 11 export {useIsDrawerOpen, useSetDrawerOpen} from './drawer-open' 11 12 export { ··· 15 16 export {useMinimalShellMode, useSetMinimalShellMode} from './minimal-mode' 16 17 export {useColorMode, useSetColorMode} from './color-mode' 17 18 export {useOnboardingState, useOnboardingDispatch} from './onboarding' 19 + export {useComposerState, useComposerControls} from './composer' 20 + export {useTickEveryMinute} from './tick-every-minute' 18 21 19 22 export function Provider({children}: React.PropsWithChildren<{}>) { 20 23 return ( ··· 24 27 <MinimalModeProvider> 25 28 <ColorModeProvider> 26 29 <OnboardingProvider> 27 - <ComposerProvider>{children}</ComposerProvider> 30 + <ComposerProvider> 31 + <TickEveryMinuteProvider>{children}</TickEveryMinuteProvider> 32 + </ComposerProvider> 28 33 </OnboardingProvider> 29 34 </ColorModeProvider> 30 35 </MinimalModeProvider>
+16 -6
src/state/shell/reminders.ts
··· 1 1 import * as persisted from '#/state/persisted' 2 - import {SessionModel} from '../models/session' 3 2 import {toHashCode} from 'lib/strings/helpers' 4 3 import {isOnboardingActive} from './onboarding' 4 + import {SessionAccount} from '../session' 5 + import {listenSessionLoaded} from '../events' 6 + import {unstable__openModal} from '../modals' 5 7 6 - export function shouldRequestEmailConfirmation(session: SessionModel) { 7 - const sess = session.currentSession 8 - if (!sess) { 8 + export function init() { 9 + listenSessionLoaded(account => { 10 + if (shouldRequestEmailConfirmation(account)) { 11 + unstable__openModal({name: 'verify-email', showReminder: true}) 12 + setEmailConfirmationRequested() 13 + } 14 + }) 15 + } 16 + 17 + export function shouldRequestEmailConfirmation(account: SessionAccount) { 18 + if (!account) { 9 19 return false 10 20 } 11 - if (sess.emailConfirmed) { 21 + if (account.emailConfirmed) { 12 22 return false 13 23 } 14 24 if (isOnboardingActive()) { ··· 22 32 // shard the users into 2 day of the week buckets 23 33 // (this is to avoid a sudden influx of email updates when 24 34 // this feature rolls out) 25 - const code = toHashCode(sess.did) % 7 35 + const code = toHashCode(account.did) % 7 26 36 if (code !== today.getDay() && code !== (today.getDay() + 1) % 7) { 27 37 return false 28 38 }
+20
src/state/shell/tick-every-minute.tsx
··· 1 + import React from 'react' 2 + 3 + type StateContext = number 4 + 5 + const stateContext = React.createContext<StateContext>(0) 6 + 7 + export function Provider({children}: React.PropsWithChildren<{}>) { 8 + const [tick, setTick] = React.useState(Date.now()) 9 + React.useEffect(() => { 10 + const i = setInterval(() => { 11 + setTick(Date.now()) 12 + }, 60_000) 13 + return () => clearInterval(i) 14 + }, []) 15 + return <stateContext.Provider value={tick}>{children}</stateContext.Provider> 16 + } 17 + 18 + export function useTickEveryMinute() { 19 + return React.useContext(stateContext) 20 + }
+5 -18
src/view/com/feeds/FeedPage.tsx
··· 14 14 import {colors, s} from 'lib/styles' 15 15 import React from 'react' 16 16 import {FlatList, View, useWindowDimensions} from 'react-native' 17 - import {useStores} from 'state/index' 18 17 import {Feed} from '../posts/Feed' 19 18 import {TextLink} from '../util/Link' 20 19 import {FAB} from '../util/fab/FAB' ··· 23 22 import {useLingui} from '@lingui/react' 24 23 import {useSession} from '#/state/session' 25 24 import {useComposerControls} from '#/state/shell/composer' 25 + import {listenSoftReset, emitSoftReset} from '#/state/events' 26 26 27 27 const POLL_FREQ = 30e3 // 30sec 28 28 ··· 41 41 renderEmptyState: () => JSX.Element 42 42 renderEndOfFeed?: () => JSX.Element 43 43 }) { 44 - const store = useStores() 45 44 const {isSandbox} = useSession() 46 45 const pal = usePalette('default') 47 46 const {_} = useLingui() ··· 73 72 if (!isPageFocused || !isScreenFocused) { 74 73 return 75 74 } 76 - const softResetSub = store.onScreenSoftReset(onSoftReset) 77 75 screen('Feed') 78 - return () => { 79 - softResetSub.remove() 80 - } 81 - }, [store, onSoftReset, screen, feed, isPageFocused, isScreenFocused]) 76 + return listenSoftReset(onSoftReset) 77 + }, [onSoftReset, screen, isPageFocused, isScreenFocused]) 82 78 83 79 const onPressCompose = React.useCallback(() => { 84 80 track('HomeScreen:PressCompose') ··· 125 121 )} 126 122 </> 127 123 } 128 - onPress={() => store.emitScreenSoftReset()} 124 + onPress={emitSoftReset} 129 125 /> 130 126 <TextLink 131 127 type="title-lg" ··· 144 140 ) 145 141 } 146 142 return <></> 147 - }, [ 148 - isDesktop, 149 - pal.view, 150 - pal.text, 151 - pal.textLight, 152 - store, 153 - hasNew, 154 - _, 155 - isSandbox, 156 - ]) 143 + }, [isDesktop, pal.view, pal.text, pal.textLight, hasNew, _, isSandbox]) 157 144 158 145 return ( 159 146 <View testID={testID} style={s.h100pct}>
+2 -1
src/view/com/profile/ProfileSubpageHeader.tsx
··· 20 20 import {useLingui} from '@lingui/react' 21 21 import {msg} from '@lingui/macro' 22 22 import {useSetDrawerOpen} from '#/state/shell' 23 + import {emitSoftReset} from '#/state/events' 23 24 24 25 export const ProfileSubpageHeader = observer(function HeaderImpl({ 25 26 isLoading, ··· 145 146 href={href} 146 147 style={[pal.text, {fontWeight: 'bold'}]} 147 148 text={title || ''} 148 - onPress={() => store.emitScreenSoftReset()} 149 + onPress={emitSoftReset} 149 150 numberOfLines={4} 150 151 /> 151 152 )}
+5 -6
src/view/com/util/TimeElapsed.tsx
··· 1 1 import React from 'react' 2 - import {observer} from 'mobx-react-lite' 3 2 import {ago} from 'lib/strings/time' 4 - import {useStores} from 'state/index' 3 + import {useTickEveryMinute} from '#/state/shell' 5 4 6 5 // FIXME(dan): Figure out why the false positives 7 6 /* eslint-disable react/prop-types */ 8 7 9 - export const TimeElapsed = observer(function TimeElapsed({ 8 + export function TimeElapsed({ 10 9 timestamp, 11 10 children, 12 11 }: { 13 12 timestamp: string 14 13 children: ({timeElapsed}: {timeElapsed: string}) => JSX.Element 15 14 }) { 16 - const stores = useStores() 15 + const tick = useTickEveryMinute() 17 16 const [timeElapsed, setTimeAgo] = React.useState(ago(timestamp)) 18 17 19 18 React.useEffect(() => { 20 19 setTimeAgo(ago(timestamp)) 21 - }, [timestamp, setTimeAgo, stores.shell.tickEveryMinute]) 20 + }, [timestamp, setTimeAgo, tick]) 22 21 23 22 return children({timeElapsed}) 24 - }) 23 + }
+3 -4
src/view/screens/Home.tsx
··· 9 9 import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState' 10 10 import {FeedsTabBar} from '../com/pager/FeedsTabBar' 11 11 import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' 12 - import {useStores} from 'state/index' 13 12 import {FeedPage} from 'view/com/feeds/FeedPage' 14 13 import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell' 15 14 import {usePreferencesQuery} from '#/state/queries/preferences' 15 + import {emitSoftReset} from '#/state/events' 16 16 17 17 type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'> 18 18 export const HomeScreen = withAuthRequired( 19 19 observer(function HomeScreenImpl({}: Props) { 20 - const store = useStores() 21 20 const setMinimalShellMode = useSetMinimalShellMode() 22 21 const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() 23 22 const pagerRef = React.useRef<PagerRef>(null) ··· 74 73 ) 75 74 76 75 const onPressSelected = React.useCallback(() => { 77 - store.emitScreenSoftReset() 78 - }, [store]) 76 + emitSoftReset() 77 + }, []) 79 78 80 79 const onPageScrollStateChanged = React.useCallback( 81 80 (state: 'idle' | 'dragging' | 'settling') => {
+5 -10
src/view/screens/Notifications.tsx
··· 11 11 import {Feed} from '../com/notifications/Feed' 12 12 import {TextLink} from 'view/com/util/Link' 13 13 import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn' 14 - import {useStores} from 'state/index' 15 14 import {useOnMainScroll} from 'lib/hooks/useOnMainScroll' 16 15 import {usePalette} from 'lib/hooks/usePalette' 17 16 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' ··· 21 20 import {useSetMinimalShellMode} from '#/state/shell' 22 21 import {useUnreadNotifications} from '#/state/queries/notifications/unread' 23 22 import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed' 23 + import {listenSoftReset, emitSoftReset} from '#/state/events' 24 24 25 25 type Props = NativeStackScreenProps< 26 26 NotificationsTabNavigatorParams, ··· 28 28 > 29 29 export const NotificationsScreen = withAuthRequired( 30 30 function NotificationsScreenImpl({}: Props) { 31 - const store = useStores() 32 31 const setMinimalShellMode = useSetMinimalShellMode() 33 32 const [onMainScroll, isScrolledDown, resetMainScroll] = useOnMainScroll() 34 33 const scrollElRef = React.useRef<FlatList>(null) ··· 57 56 React.useCallback(() => { 58 57 setMinimalShellMode(false) 59 58 logger.debug('NotificationsScreen: Updating feed') 60 - const softResetSub = store.onScreenSoftReset(onPressLoadLatest) 61 59 screen('Notifications') 62 - 63 - return () => { 64 - softResetSub.remove() 65 - } 66 - }, [store, screen, onPressLoadLatest, setMinimalShellMode]), 60 + return listenSoftReset(onPressLoadLatest) 61 + }, [screen, onPressLoadLatest, setMinimalShellMode]), 67 62 ) 68 63 69 64 const ListHeaderComponent = React.useCallback(() => { ··· 100 95 )} 101 96 </> 102 97 } 103 - onPress={() => store.emitScreenSoftReset()} 98 + onPress={emitSoftReset} 104 99 /> 105 100 </View> 106 101 ) 107 102 } 108 103 return <></> 109 - }, [isDesktop, pal, store, hasNew]) 104 + }, [isDesktop, pal, hasNew]) 110 105 111 106 return ( 112 107 <View testID="notificationsScreen" style={s.hContentRegion}>
+3 -5
src/view/screens/Profile.tsx
··· 12 12 import {Feed} from 'view/com/posts/Feed' 13 13 import {ProfileLists} from '../com/lists/ProfileLists' 14 14 import {ProfileFeedgens} from '../com/feeds/ProfileFeedgens' 15 - import {useStores} from 'state/index' 16 15 import {ProfileHeader} from '../com/profile/ProfileHeader' 17 16 import {PagerWithHeader} from 'view/com/pager/PagerWithHeader' 18 17 import {ErrorScreen} from '../com/util/error/ErrorScreen' ··· 37 36 import {LoadLatestBtn} from '../com/util/load-latest/LoadLatestBtn' 38 37 import {useQueryClient} from '@tanstack/react-query' 39 38 import {useComposerControls} from '#/state/shell/composer' 39 + import {listenSoftReset} from '#/state/events' 40 40 41 41 type Props = NativeStackScreenProps<CommonNavigatorParams, 'Profile'> 42 42 export const ProfileScreen = withAuthRequired(function ProfileScreenImpl({ ··· 126 126 hideBackButton: boolean 127 127 }) { 128 128 const profile = useProfileShadow(profileUnshadowed, dataUpdatedAt) 129 - const store = useStores() 130 129 const {currentAccount} = useSession() 131 130 const setMinimalShellMode = useSetMinimalShellMode() 132 131 const {openComposer} = useComposerControls() ··· 169 168 React.useCallback(() => { 170 169 setMinimalShellMode(false) 171 170 screen('Profile') 172 - const softResetSub = store.onScreenSoftReset(() => { 171 + return listenSoftReset(() => { 173 172 viewSelectorRef.current?.scrollToTop() 174 173 }) 175 - return () => softResetSub.remove() 176 - }, [store, viewSelectorRef, setMinimalShellMode, screen]), 174 + }, [viewSelectorRef, setMinimalShellMode, screen]), 177 175 ) 178 176 179 177 useFocusEffect(
+3 -9
src/view/screens/Search/Search.tsx
··· 42 42 import {useModerationOpts} from '#/state/queries/preferences' 43 43 import {SearchResultCard} from '#/view/shell/desktop/Search' 44 44 import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell' 45 - import {useStores} from '#/state' 46 45 import {isWeb} from '#/platform/detection' 46 + import {listenSoftReset} from '#/state/events' 47 47 48 48 function Loader() { 49 49 const pal = usePalette('default') ··· 421 421 const moderationOpts = useModerationOpts() 422 422 const search = useActorAutocompleteFn() 423 423 const setMinimalShellMode = useSetMinimalShellMode() 424 - const store = useStores() 425 424 const {isTablet} = useWebMediaQueries() 426 425 427 426 const searchDebounceTimeout = React.useRef<NodeJS.Timeout | undefined>( ··· 490 489 491 490 useFocusEffect( 492 491 React.useCallback(() => { 493 - const softResetSub = store.onScreenSoftReset(onSoftReset) 494 - 495 492 setMinimalShellMode(false) 496 - 497 - return () => { 498 - softResetSub.remove() 499 - } 500 - }, [store, onSoftReset, setMinimalShellMode]), 493 + return listenSoftReset(onSoftReset) 494 + }, [onSoftReset, setMinimalShellMode]), 501 495 ) 502 496 503 497 return (
+4 -4
src/view/shell/Drawer.tsx
··· 50 50 import {useSession, SessionAccount} from '#/state/session' 51 51 import {useProfileQuery} from '#/state/queries/profile' 52 52 import {useUnreadNotifications} from '#/state/queries/notifications/unread' 53 + import {emitSoftReset} from '#/state/events' 53 54 54 55 export function DrawerProfileCard({ 55 56 account, ··· 103 104 export const DrawerContent = observer(function DrawerContentImpl() { 104 105 const theme = useTheme() 105 106 const pal = usePalette('default') 106 - const store = useStores() 107 107 const {_} = useLingui() 108 108 const setDrawerOpen = useSetDrawerOpen() 109 109 const navigation = useNavigation<NavigationProp>() ··· 124 124 if (isWeb) { 125 125 // hack because we have flat navigator for web and MyProfile does not exist on the web navigator -ansh 126 126 if (tab === 'MyProfile') { 127 - navigation.navigate('Profile', {name: store.me.handle}) 127 + navigation.navigate('Profile', {name: currentAccount!.handle}) 128 128 } else { 129 129 // @ts-ignore must be Home, Search, Notifications, or MyProfile 130 130 navigation.navigate(tab) ··· 132 132 } else { 133 133 const tabState = getTabState(state, tab) 134 134 if (tabState === TabState.InsideAtRoot) { 135 - store.emitScreenSoftReset() 135 + emitSoftReset() 136 136 } else if (tabState === TabState.Inside) { 137 137 navigation.dispatch(StackActions.popToTop()) 138 138 } else { ··· 141 141 } 142 142 } 143 143 }, 144 - [store, track, navigation, setDrawerOpen], 144 + [track, navigation, setDrawerOpen, currentAccount], 145 145 ) 146 146 147 147 const onPressHome = React.useCallback(() => onPressTab('Home'), [onPressTab])
+3 -2
src/view/shell/bottom-bar/BottomBar.tsx
··· 29 29 import {useModalControls} from '#/state/modals' 30 30 import {useShellLayout} from '#/state/shell/shell-layout' 31 31 import {useUnreadNotifications} from '#/state/queries/notifications/unread' 32 + import {emitSoftReset} from '#/state/events' 32 33 33 34 type TabOptions = 'Home' | 'Search' | 'Notifications' | 'MyProfile' | 'Feeds' 34 35 ··· 53 54 const state = navigation.getState() 54 55 const tabState = getTabState(state, tab) 55 56 if (tabState === TabState.InsideAtRoot) { 56 - store.emitScreenSoftReset() 57 + emitSoftReset() 57 58 } else if (tabState === TabState.Inside) { 58 59 navigation.dispatch(StackActions.popToTop()) 59 60 } else { 60 61 navigation.navigate(`${tab}Tab`) 61 62 } 62 63 }, 63 - [store, track, navigation], 64 + [track, navigation], 64 65 ) 65 66 const onPressHome = React.useCallback(() => onPressTab('Home'), [onPressTab]) 66 67 const onPressSearch = React.useCallback(
+3 -4
src/view/shell/desktop/LeftNav.tsx
··· 16 16 import {Link} from 'view/com/util/Link' 17 17 import {LoadingPlaceholder} from 'view/com/util/LoadingPlaceholder' 18 18 import {usePalette} from 'lib/hooks/usePalette' 19 - import {useStores} from 'state/index' 20 19 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' 21 20 import {s, colors} from 'lib/styles' 22 21 import { ··· 46 45 import {useUnreadNotifications} from '#/state/queries/notifications/unread' 47 46 import {useComposerControls} from '#/state/shell/composer' 48 47 import {useFetchHandle} from '#/state/queries/handle' 48 + import {emitSoftReset} from '#/state/events' 49 49 50 50 const ProfileCard = observer(function ProfileCardImpl() { 51 51 const {currentAccount} = useSession() ··· 126 126 }: NavItemProps) { 127 127 const pal = usePalette('default') 128 128 const {currentAccount} = useSession() 129 - const store = useStores() 130 129 const {isDesktop, isTablet} = useWebMediaQueries() 131 130 const [pathName] = React.useMemo(() => router.matchPath(href), [href]) 132 131 const currentRouteInfo = useNavigationState(state => { ··· 149 148 } 150 149 e.preventDefault() 151 150 if (isCurrent) { 152 - store.emitScreenSoftReset() 151 + emitSoftReset() 153 152 } else { 154 153 onPress() 155 154 } 156 155 }, 157 - [onPress, isCurrent, store], 156 + [onPress, isCurrent], 158 157 ) 159 158 160 159 return (