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 systemzio/stable
parent
f23e9978d8
commit
6616b2bff0
|
@ -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])
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
} 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 Installed', {
|
||||
version: newAppInfo.version,
|
||||
build: newAppInfo.build,
|
||||
})
|
||||
} else if (newAppInfo.version !== oldAppInfo.version) {
|
||||
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
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)) {
|
||||
|
|
|
@ -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],
|
||||
)
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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}>
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -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})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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') => {
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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])
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 (
|
||||
|
|
Loading…
Reference in New Issue