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
This commit is contained in:
Paul Frazee 2023-11-15 17:17:50 -08:00 committed by GitHub
parent f23e9978d8
commit 6616b2bff0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 186 additions and 136 deletions

38
src/state/events.ts Normal file
View file

@ -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)
}

View file

@ -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)) {

View file

@ -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],
)

View file

@ -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>

View file

@ -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
}

View file

@ -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)
}