about summary refs log tree commit diff
path: root/src/state/shell
diff options
context:
space:
mode:
Diffstat (limited to 'src/state/shell')
-rw-r--r--src/state/shell/color-mode.tsx2
-rw-r--r--src/state/shell/composer.tsx85
-rw-r--r--src/state/shell/drawer-open.tsx1
-rw-r--r--src/state/shell/index.tsx29
-rw-r--r--src/state/shell/logged-out.tsx37
-rw-r--r--src/state/shell/minimal-mode.tsx31
-rw-r--r--src/state/shell/onboarding.tsx123
-rw-r--r--src/state/shell/reminders.e2e.ts8
-rw-r--r--src/state/shell/reminders.ts29
-rw-r--r--src/state/shell/shell-layout.tsx41
-rw-r--r--src/state/shell/tick-every-minute.tsx20
11 files changed, 376 insertions, 30 deletions
diff --git a/src/state/shell/color-mode.tsx b/src/state/shell/color-mode.tsx
index 74379da37..c6a4b8a18 100644
--- a/src/state/shell/color-mode.tsx
+++ b/src/state/shell/color-mode.tsx
@@ -27,7 +27,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) {
       setState(persisted.get('colorMode'))
       updateDocument(persisted.get('colorMode'))
     })
-  }, [setStateWrapped])
+  }, [setState])
 
   return (
     <stateContext.Provider value={state}>
diff --git a/src/state/shell/composer.tsx b/src/state/shell/composer.tsx
new file mode 100644
index 000000000..bdf5e4a7a
--- /dev/null
+++ b/src/state/shell/composer.tsx
@@ -0,0 +1,85 @@
+import React from 'react'
+import {AppBskyEmbedRecord} from '@atproto/api'
+import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
+
+export interface ComposerOptsPostRef {
+  uri: string
+  cid: string
+  text: string
+  author: {
+    handle: string
+    displayName?: string
+    avatar?: string
+  }
+}
+export interface ComposerOptsQuote {
+  uri: string
+  cid: string
+  text: string
+  indexedAt: string
+  author: {
+    did: string
+    handle: string
+    displayName?: string
+    avatar?: string
+  }
+  embeds?: AppBskyEmbedRecord.ViewRecord['embeds']
+}
+export interface ComposerOpts {
+  replyTo?: ComposerOptsPostRef
+  onPost?: () => void
+  quote?: ComposerOptsQuote
+  mention?: string // handle of user to mention
+}
+
+type StateContext = ComposerOpts | undefined
+type ControlsContext = {
+  openComposer: (opts: ComposerOpts) => void
+  closeComposer: () => boolean
+}
+
+const stateContext = React.createContext<StateContext>(undefined)
+const controlsContext = React.createContext<ControlsContext>({
+  openComposer(_opts: ComposerOpts) {},
+  closeComposer() {
+    return false
+  },
+})
+
+export function Provider({children}: React.PropsWithChildren<{}>) {
+  const [state, setState] = React.useState<StateContext>()
+
+  const openComposer = useNonReactiveCallback((opts: ComposerOpts) => {
+    setState(opts)
+  })
+
+  const closeComposer = useNonReactiveCallback(() => {
+    let wasOpen = !!state
+    setState(undefined)
+    return wasOpen
+  })
+
+  const api = React.useMemo(
+    () => ({
+      openComposer,
+      closeComposer,
+    }),
+    [openComposer, closeComposer],
+  )
+
+  return (
+    <stateContext.Provider value={state}>
+      <controlsContext.Provider value={api}>
+        {children}
+      </controlsContext.Provider>
+    </stateContext.Provider>
+  )
+}
+
+export function useComposerState() {
+  return React.useContext(stateContext)
+}
+
+export function useComposerControls() {
+  return React.useContext(controlsContext)
+}
diff --git a/src/state/shell/drawer-open.tsx b/src/state/shell/drawer-open.tsx
index a2322f680..061ff53d7 100644
--- a/src/state/shell/drawer-open.tsx
+++ b/src/state/shell/drawer-open.tsx
@@ -8,6 +8,7 @@ const setContext = React.createContext<SetContext>((_: boolean) => {})
 
 export function Provider({children}: React.PropsWithChildren<{}>) {
   const [state, setState] = React.useState(false)
+
   return (
     <stateContext.Provider value={state}>
       <setContext.Provider value={setState}>{children}</setContext.Provider>
diff --git a/src/state/shell/index.tsx b/src/state/shell/index.tsx
index 1e01a4e7d..53f05055c 100644
--- a/src/state/shell/index.tsx
+++ b/src/state/shell/index.tsx
@@ -1,8 +1,12 @@
 import React from 'react'
+import {Provider as ShellLayoutProvder} from './shell-layout'
 import {Provider as DrawerOpenProvider} from './drawer-open'
 import {Provider as DrawerSwipableProvider} from './drawer-swipe-disabled'
 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 {
@@ -11,15 +15,26 @@ export {
 } from './drawer-swipe-disabled'
 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 (
-    <DrawerOpenProvider>
-      <DrawerSwipableProvider>
-        <MinimalModeProvider>
-          <ColorModeProvider>{children}</ColorModeProvider>
-        </MinimalModeProvider>
-      </DrawerSwipableProvider>
-    </DrawerOpenProvider>
+    <ShellLayoutProvder>
+      <DrawerOpenProvider>
+        <DrawerSwipableProvider>
+          <MinimalModeProvider>
+            <ColorModeProvider>
+              <OnboardingProvider>
+                <ComposerProvider>
+                  <TickEveryMinuteProvider>{children}</TickEveryMinuteProvider>
+                </ComposerProvider>
+              </OnboardingProvider>
+            </ColorModeProvider>
+          </MinimalModeProvider>
+        </DrawerSwipableProvider>
+      </DrawerOpenProvider>
+    </ShellLayoutProvder>
   )
 }
diff --git a/src/state/shell/logged-out.tsx b/src/state/shell/logged-out.tsx
new file mode 100644
index 000000000..19eaee76b
--- /dev/null
+++ b/src/state/shell/logged-out.tsx
@@ -0,0 +1,37 @@
+import React from 'react'
+
+type StateContext = {
+  showLoggedOut: boolean
+}
+
+const StateContext = React.createContext<StateContext>({
+  showLoggedOut: false,
+})
+const ControlsContext = React.createContext<{
+  setShowLoggedOut: (show: boolean) => void
+}>({
+  setShowLoggedOut: () => {},
+})
+
+export function Provider({children}: React.PropsWithChildren<{}>) {
+  const [showLoggedOut, setShowLoggedOut] = React.useState(false)
+
+  const state = React.useMemo(() => ({showLoggedOut}), [showLoggedOut])
+  const controls = React.useMemo(() => ({setShowLoggedOut}), [setShowLoggedOut])
+
+  return (
+    <StateContext.Provider value={state}>
+      <ControlsContext.Provider value={controls}>
+        {children}
+      </ControlsContext.Provider>
+    </StateContext.Provider>
+  )
+}
+
+export function useLoggedOutView() {
+  return React.useContext(StateContext)
+}
+
+export function useLoggedOutViewControls() {
+  return React.useContext(ControlsContext)
+}
diff --git a/src/state/shell/minimal-mode.tsx b/src/state/shell/minimal-mode.tsx
index 4909a9a65..2c2f60b52 100644
--- a/src/state/shell/minimal-mode.tsx
+++ b/src/state/shell/minimal-mode.tsx
@@ -1,16 +1,37 @@
 import React from 'react'
+import {
+  Easing,
+  SharedValue,
+  useSharedValue,
+  withTiming,
+} from 'react-native-reanimated'
 
-type StateContext = boolean
+type StateContext = SharedValue<number>
 type SetContext = (v: boolean) => void
 
-const stateContext = React.createContext<StateContext>(false)
+const stateContext = React.createContext<StateContext>({
+  value: 0,
+  addListener() {},
+  removeListener() {},
+  modify() {},
+})
 const setContext = React.createContext<SetContext>((_: boolean) => {})
 
 export function Provider({children}: React.PropsWithChildren<{}>) {
-  const [state, setState] = React.useState(false)
+  const mode = useSharedValue(0)
+  const setMode = React.useCallback(
+    (v: boolean) => {
+      'worklet'
+      mode.value = withTiming(v ? 1 : 0, {
+        duration: 400,
+        easing: Easing.bezier(0.25, 0.1, 0.25, 1),
+      })
+    },
+    [mode],
+  )
   return (
-    <stateContext.Provider value={state}>
-      <setContext.Provider value={setState}>{children}</setContext.Provider>
+    <stateContext.Provider value={mode}>
+      <setContext.Provider value={setMode}>{children}</setContext.Provider>
     </stateContext.Provider>
   )
 }
diff --git a/src/state/shell/onboarding.tsx b/src/state/shell/onboarding.tsx
new file mode 100644
index 000000000..6a18b461f
--- /dev/null
+++ b/src/state/shell/onboarding.tsx
@@ -0,0 +1,123 @@
+import React from 'react'
+import * as persisted from '#/state/persisted'
+import {track} from '#/lib/analytics/analytics'
+
+export const OnboardingScreenSteps = {
+  Welcome: 'Welcome',
+  RecommendedFeeds: 'RecommendedFeeds',
+  RecommendedFollows: 'RecommendedFollows',
+  Home: 'Home',
+} as const
+
+type OnboardingStep =
+  (typeof OnboardingScreenSteps)[keyof typeof OnboardingScreenSteps]
+const OnboardingStepsArray = Object.values(OnboardingScreenSteps)
+
+type Action =
+  | {type: 'set'; step: OnboardingStep}
+  | {type: 'next'; currentStep?: OnboardingStep}
+  | {type: 'start'}
+  | {type: 'finish'}
+  | {type: 'skip'}
+
+export type StateContext = persisted.Schema['onboarding'] & {
+  isComplete: boolean
+  isActive: boolean
+}
+export type DispatchContext = (action: Action) => void
+
+const stateContext = React.createContext<StateContext>(
+  compute(persisted.defaults.onboarding),
+)
+const dispatchContext = React.createContext<DispatchContext>((_: Action) => {})
+
+function reducer(state: StateContext, action: Action): StateContext {
+  switch (action.type) {
+    case 'set': {
+      if (OnboardingStepsArray.includes(action.step)) {
+        persisted.write('onboarding', {step: action.step})
+        return compute({...state, step: action.step})
+      }
+      return state
+    }
+    case 'next': {
+      const currentStep = action.currentStep || state.step
+      let nextStep = 'Home'
+      if (currentStep === 'Welcome') {
+        nextStep = 'RecommendedFeeds'
+      } else if (currentStep === 'RecommendedFeeds') {
+        nextStep = 'RecommendedFollows'
+      } else if (currentStep === 'RecommendedFollows') {
+        nextStep = 'Home'
+      }
+      persisted.write('onboarding', {step: nextStep})
+      return compute({...state, step: nextStep})
+    }
+    case 'start': {
+      track('Onboarding:Begin')
+      persisted.write('onboarding', {step: 'Welcome'})
+      return compute({...state, step: 'Welcome'})
+    }
+    case 'finish': {
+      track('Onboarding:Complete')
+      persisted.write('onboarding', {step: 'Home'})
+      return compute({...state, step: 'Home'})
+    }
+    case 'skip': {
+      track('Onboarding:Skipped')
+      persisted.write('onboarding', {step: 'Home'})
+      return compute({...state, step: 'Home'})
+    }
+    default: {
+      throw new Error('Invalid action')
+    }
+  }
+}
+
+export function Provider({children}: React.PropsWithChildren<{}>) {
+  const [state, dispatch] = React.useReducer(
+    reducer,
+    compute(persisted.get('onboarding')),
+  )
+
+  React.useEffect(() => {
+    return persisted.onUpdate(() => {
+      const next = persisted.get('onboarding').step
+      // TODO we've introduced a footgun
+      if (state.step !== next) {
+        dispatch({
+          type: 'set',
+          step: persisted.get('onboarding').step as OnboardingStep,
+        })
+      }
+    })
+  }, [state, dispatch])
+
+  return (
+    <stateContext.Provider value={state}>
+      <dispatchContext.Provider value={dispatch}>
+        {children}
+      </dispatchContext.Provider>
+    </stateContext.Provider>
+  )
+}
+
+export function useOnboardingState() {
+  return React.useContext(stateContext)
+}
+
+export function useOnboardingDispatch() {
+  return React.useContext(dispatchContext)
+}
+
+export function isOnboardingActive() {
+  return compute(persisted.get('onboarding')).isActive
+}
+
+function compute(state: persisted.Schema['onboarding']): StateContext {
+  return {
+    ...state,
+    isActive: state.step !== 'Home',
+    isComplete: state.step === 'Home',
+  }
+}
diff --git a/src/state/shell/reminders.e2e.ts b/src/state/shell/reminders.e2e.ts
index 6238ffa29..e8c12792a 100644
--- a/src/state/shell/reminders.e2e.ts
+++ b/src/state/shell/reminders.e2e.ts
@@ -1,10 +1,6 @@
-import {OnboardingModel} from '../models/discovery/onboarding'
-import {SessionModel} from '../models/session'
+export function init() {}
 
-export function shouldRequestEmailConfirmation(
-  _session: SessionModel,
-  _onboarding: OnboardingModel,
-) {
+export function shouldRequestEmailConfirmation() {
   return false
 }
 
diff --git a/src/state/shell/reminders.ts b/src/state/shell/reminders.ts
index d68a272ac..88d0a5d85 100644
--- a/src/state/shell/reminders.ts
+++ b/src/state/shell/reminders.ts
@@ -1,20 +1,27 @@
 import * as persisted from '#/state/persisted'
-import {OnboardingModel} from '../models/discovery/onboarding'
-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,
-  onboarding: OnboardingModel,
-) {
-  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 (onboarding.isActive) {
+  if (isOnboardingActive()) {
     return false
   }
   // only prompt once
@@ -25,7 +32,7 @@ export function shouldRequestEmailConfirmation(
   // 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/shell-layout.tsx b/src/state/shell/shell-layout.tsx
new file mode 100644
index 000000000..a58ba851c
--- /dev/null
+++ b/src/state/shell/shell-layout.tsx
@@ -0,0 +1,41 @@
+import React from 'react'
+import {SharedValue, useSharedValue} from 'react-native-reanimated'
+
+type StateContext = {
+  headerHeight: SharedValue<number>
+  footerHeight: SharedValue<number>
+}
+
+const stateContext = React.createContext<StateContext>({
+  headerHeight: {
+    value: 0,
+    addListener() {},
+    removeListener() {},
+    modify() {},
+  },
+  footerHeight: {
+    value: 0,
+    addListener() {},
+    removeListener() {},
+    modify() {},
+  },
+})
+
+export function Provider({children}: React.PropsWithChildren<{}>) {
+  const headerHeight = useSharedValue(0)
+  const footerHeight = useSharedValue(0)
+
+  const value = React.useMemo(
+    () => ({
+      headerHeight,
+      footerHeight,
+    }),
+    [headerHeight, footerHeight],
+  )
+
+  return <stateContext.Provider value={value}>{children}</stateContext.Provider>
+}
+
+export function useShellLayout() {
+  return React.useContext(stateContext)
+}
diff --git a/src/state/shell/tick-every-minute.tsx b/src/state/shell/tick-every-minute.tsx
new file mode 100644
index 000000000..c37221c90
--- /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<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)
+}