diff --git a/src/App.native.tsx b/src/App.native.tsx index 60bce0578d..ffa8b338e5 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 b734aea087..8e22f6480d 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 71bb8569a8..4b955b3655 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, reset: () => Promise, } - }, [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<{}>) { {children} ) } + +async function writeAppInfo(value: AppInfo) { + await AsyncStorage.setItem('BSKY_APP_INFO', JSON.stringify(value)) +} + +async function readAppInfo(): Promise | 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 d46479a05b..2320e1c7b4 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 0000000000..5441aafefc --- /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 310d4f0f91..223c20625c 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 e01e841f68..b8422553cd 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 63c3763d10..53f05055c3 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<{}>) { - {children} + + {children} + diff --git a/src/state/shell/reminders.ts b/src/state/shell/reminders.ts index e7ee7a5fe2..88d0a5d85f 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 0000000000..c37221c908 --- /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(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 {children} +} + +export function useTickEveryMinute() { + return React.useContext(stateContext) +} diff --git a/src/view/com/feeds/FeedPage.tsx b/src/view/com/feeds/FeedPage.tsx index 562b1c1415..e7dcf09b83 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} /> - }, [ - isDesktop, - pal.view, - pal.text, - pal.textLight, - store, - hasNew, - _, - isSandbox, - ]) + }, [isDesktop, pal.view, pal.text, pal.textLight, hasNew, _, isSandbox]) return ( diff --git a/src/view/com/profile/ProfileSubpageHeader.tsx b/src/view/com/profile/ProfileSubpageHeader.tsx index 251d3141b2..e1b587beb7 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 0765f65b26..dad46448c2 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 fdd764e441..c297f86503 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 export const HomeScreen = withAuthRequired( observer(function HomeScreenImpl({}: Props) { - const store = useStores() const setMinimalShellMode = useSetMinimalShellMode() const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled() const pagerRef = React.useRef(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 e0f2340732..970882f122 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(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} /> ) } return <> - }, [isDesktop, pal, store, hasNew]) + }, [isDesktop, pal, hasNew]) return ( diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx index 17ea4498c5..fdabc690ef 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 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 8fadfe8643..54e042d044 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( @@ -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 8a84a07c66..c5dcb150cf 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() @@ -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 81552635f3..de1dc17032 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 8bc1d49a0e..3ec68872e8 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 (