Skip to content

Commit

Permalink
Shell behaviors update (react-query refactor) (#1915)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
pfrazee authored Nov 16, 2023
1 parent f23e997 commit 6616b2b
Show file tree
Hide file tree
Showing 20 changed files with 185 additions and 135 deletions.
14 changes: 9 additions & 5 deletions src/App.native.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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])
Expand Down
7 changes: 5 additions & 2 deletions src/App.web.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -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])
Expand Down
78 changes: 40 additions & 38 deletions src/lib/analytics/analytics.tsx
Original file line number Diff line number Diff line change
@@ -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',
Expand All @@ -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
Expand All @@ -45,29 +47,26 @@ 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()
}
})

// NOTE
// 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
Expand All @@ -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
Expand All @@ -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
}
14 changes: 7 additions & 7 deletions src/lib/notifications/notifications.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand All @@ -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',
Expand Down Expand Up @@ -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',
Expand Down
38 changes: 38 additions & 0 deletions src/state/events.ts
Original file line number Diff line number Diff line change
@@ -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)
}
12 changes: 1 addition & 11 deletions src/state/models/ui/shell.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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()
}

Expand Down Expand Up @@ -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)) {
Expand Down
9 changes: 7 additions & 2 deletions src/state/session/index.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React from 'react'
import {DeviceEventEmitter} from 'react-native'
import {BskyAgent, AtpPersistSessionHandler} from '@atproto/api'

import {networkRetry} from '#/lib/async/retry'
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

Expand Down Expand Up @@ -98,7 +98,9 @@ function createPersistSessionHandler(
logger.DebugContext.session,
)

if (expired) DeviceEventEmitter.emit('session-dropped')
if (expired) {
emitSessionDropped()
}

persistSessionCallback({
expired,
Expand Down Expand Up @@ -180,6 +182,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {

setState(s => ({...s, agent}))
upsertAccount(account)
emitSessionLoaded(account, agent)

logger.debug(
`session: created account`,
Expand Down Expand Up @@ -230,6 +233,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {

setState(s => ({...s, agent}))
upsertAccount(account)
emitSessionLoaded(account, agent)

logger.debug(
`session: logged in`,
Expand Down Expand Up @@ -291,6 +295,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {

setState(s => ({...s, agent}))
upsertAccount(account)
emitSessionLoaded(account, agent)
},
[upsertAccount],
)
Expand Down
7 changes: 6 additions & 1 deletion src/state/shell/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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 (
Expand All @@ -24,7 +27,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
<MinimalModeProvider>
<ColorModeProvider>
<OnboardingProvider>
<ComposerProvider>{children}</ComposerProvider>
<ComposerProvider>
<TickEveryMinuteProvider>{children}</TickEveryMinuteProvider>
</ComposerProvider>
</OnboardingProvider>
</ColorModeProvider>
</MinimalModeProvider>
Expand Down
Loading

0 comments on commit 6616b2b

Please sign in to comment.