Implement progress-guide controller
This commit is contained in:
+5
-1
@@ -45,6 +45,7 @@ import {
|
|||||||
import {readLastActiveAccount} from '#/state/session/util'
|
import {readLastActiveAccount} from '#/state/session/util'
|
||||||
import {Provider as ShellStateProvider} from '#/state/shell'
|
import {Provider as ShellStateProvider} from '#/state/shell'
|
||||||
import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
|
import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
|
||||||
|
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
|
||||||
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||||
import {TestCtrls} from '#/view/com/testing/TestCtrls'
|
import {TestCtrls} from '#/view/com/testing/TestCtrls'
|
||||||
@@ -119,10 +120,13 @@ function InnerApp() {
|
|||||||
<BackgroundNotificationPreferencesProvider>
|
<BackgroundNotificationPreferencesProvider>
|
||||||
<MutedThreadsProvider>
|
<MutedThreadsProvider>
|
||||||
<TourProvider>
|
<TourProvider>
|
||||||
<GestureHandlerRootView style={s.h100pct}>
|
<ProgressGuideProvider>
|
||||||
|
<GestureHandlerRootView
|
||||||
|
style={s.h100pct}>
|
||||||
<TestCtrls />
|
<TestCtrls />
|
||||||
<Shell />
|
<Shell />
|
||||||
</GestureHandlerRootView>
|
</GestureHandlerRootView>
|
||||||
|
</ProgressGuideProvider>
|
||||||
</TourProvider>
|
</TourProvider>
|
||||||
</MutedThreadsProvider>
|
</MutedThreadsProvider>
|
||||||
</BackgroundNotificationPreferencesProvider>
|
</BackgroundNotificationPreferencesProvider>
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import {
|
|||||||
import {readLastActiveAccount} from '#/state/session/util'
|
import {readLastActiveAccount} from '#/state/session/util'
|
||||||
import {Provider as ShellStateProvider} from '#/state/shell'
|
import {Provider as ShellStateProvider} from '#/state/shell'
|
||||||
import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
|
import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out'
|
||||||
|
import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide'
|
||||||
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed'
|
||||||
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
import {Provider as StarterPackProvider} from '#/state/shell/starter-pack'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
@@ -104,7 +105,9 @@ function InnerApp() {
|
|||||||
<MutedThreadsProvider>
|
<MutedThreadsProvider>
|
||||||
<SafeAreaProvider>
|
<SafeAreaProvider>
|
||||||
<TourProvider>
|
<TourProvider>
|
||||||
|
<ProgressGuideProvider>
|
||||||
<Shell />
|
<Shell />
|
||||||
|
</ProgressGuideProvider>
|
||||||
</TourProvider>
|
</TourProvider>
|
||||||
</SafeAreaProvider>
|
</SafeAreaProvider>
|
||||||
</MutedThreadsProvider>
|
</MutedThreadsProvider>
|
||||||
|
|||||||
@@ -1,58 +1,61 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
ProgressGuideAction,
|
||||||
|
ProgressGuideName,
|
||||||
|
useProgressGuide,
|
||||||
|
useProgressGuideControls,
|
||||||
|
} from '#/state/shell/progress-guide'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Button, ButtonIcon} from '../Button'
|
import {Button, ButtonIcon} from '#/components/Button'
|
||||||
import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '../icons/Times'
|
import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times'
|
||||||
import {Text} from '../Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {ProgressGuideTask} from './Task'
|
import {ProgressGuideTask} from './Task'
|
||||||
|
|
||||||
export function ProgressGuideList() {
|
export function ProgressGuideList({style}: {style: StyleProp<ViewStyle>}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const guide = useProgressGuide(ProgressGuideName.Like10Posts)
|
||||||
|
const {captureAction, endProgressGuide} = useProgressGuideControls()
|
||||||
|
|
||||||
const [numLikes, setNumLikes] = React.useState<number>(0)
|
|
||||||
const [numFollows, setNumFollows] = React.useState<number>(0)
|
|
||||||
|
|
||||||
// DEBUG
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const i = setInterval(() => {
|
const i = setInterval(() => captureAction(ProgressGuideAction.Like), 2e3)
|
||||||
setNumLikes(v => (v >= 10 ? 0 : v + 1))
|
return () => clearInterval(i)
|
||||||
setNumFollows(v => (v >= 7 ? 0 : v + 1))
|
}, [captureAction])
|
||||||
}, 1000)
|
|
||||||
return () => {
|
|
||||||
clearInterval(i)
|
|
||||||
}
|
|
||||||
}, [setNumLikes])
|
|
||||||
|
|
||||||
|
if (guide?.guide === ProgressGuideName.Like10Posts) {
|
||||||
return (
|
return (
|
||||||
<View style={[a.flex_col, a.gap_md]}>
|
<View style={[a.flex_col, a.gap_md, style]}>
|
||||||
<View style={[a.flex_row, a.align_center]}>
|
<View style={[a.flex_row, a.align_center]}>
|
||||||
<Text
|
<Text
|
||||||
style={[t.atoms.text_contrast_medium, a.font_semibold, a.text_xs]}>
|
style={[
|
||||||
<Trans>GET STARTED</Trans>
|
t.atoms.text_contrast_medium,
|
||||||
|
a.font_semibold,
|
||||||
|
a.text_xs,
|
||||||
|
{textTransform: 'uppercase'},
|
||||||
|
]}>
|
||||||
|
<Trans>Get started</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="tiny"
|
size="tiny"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
label={_(msg`Dismiss get started`)}>
|
label={_(msg`Dismiss get started`)}
|
||||||
|
onPress={endProgressGuide}>
|
||||||
<ButtonIcon icon={Times} />
|
<ButtonIcon icon={Times} />
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
<ProgressGuideTask
|
<ProgressGuideTask
|
||||||
current={numLikes}
|
current={guide.numLikes}
|
||||||
total={10}
|
total={10}
|
||||||
title={_(msg`Like 10 posts`)}
|
title={_(msg`Like 10 posts`)}
|
||||||
subtitle={_(msg`Teach Discover what you like.`)}
|
subtitle={_(msg`Teach Discover what you like.`)}
|
||||||
/>
|
/>
|
||||||
<ProgressGuideTask
|
|
||||||
current={numFollows}
|
|
||||||
total={7}
|
|
||||||
title={_(msg`Follow 7 people`)}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, {useImperativeHandle} from 'react'
|
import React, {useImperativeHandle} from 'react'
|
||||||
import {Keyboard, Pressable, View} from 'react-native'
|
import {Pressable, useWindowDimensions, View} from 'react-native'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
Easing,
|
Easing,
|
||||||
runOnJS,
|
runOnJS,
|
||||||
@@ -11,6 +11,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
|||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {isWeb} from '#/platform/detection'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {Portal} from '#/components/Portal'
|
import {Portal} from '#/components/Portal'
|
||||||
import {AnimatedCheck, AnimatedCheckRef} from '../anim/AnimatedCheck'
|
import {AnimatedCheck, AnimatedCheckRef} from '../anim/AnimatedCheck'
|
||||||
@@ -39,6 +40,7 @@ export const ProgressGuideToast = React.forwardRef<
|
|||||||
const opacity = useSharedValue(0)
|
const opacity = useSharedValue(0)
|
||||||
const animatedCheckRef = React.useRef<AnimatedCheckRef | null>(null)
|
const animatedCheckRef = React.useRef<AnimatedCheckRef | null>(null)
|
||||||
const timeoutRef = React.useRef<NodeJS.Timeout | undefined>()
|
const timeoutRef = React.useRef<NodeJS.Timeout | undefined>()
|
||||||
|
const winDim = useWindowDimensions()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Methods
|
* Methods
|
||||||
@@ -97,6 +99,20 @@ export const ProgressGuideToast = React.forwardRef<
|
|||||||
[open, close],
|
[open, close],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const containerStyle = React.useMemo(() => {
|
||||||
|
let left = 10
|
||||||
|
let right = 10
|
||||||
|
if (isWeb && winDim.width > 400) {
|
||||||
|
left = right = (winDim.width - 380) / 2
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
position: isWeb ? 'fixed' : 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left,
|
||||||
|
right,
|
||||||
|
}
|
||||||
|
}, [winDim.width])
|
||||||
|
|
||||||
const animatedStyle = useAnimatedStyle(() => ({
|
const animatedStyle = useAnimatedStyle(() => ({
|
||||||
transform: [{translateY: translateY.value}],
|
transform: [{translateY: translateY.value}],
|
||||||
opacity: opacity.value,
|
opacity: opacity.value,
|
||||||
@@ -105,15 +121,12 @@ export const ProgressGuideToast = React.forwardRef<
|
|||||||
return (
|
return (
|
||||||
isOpen && (
|
isOpen && (
|
||||||
<Portal>
|
<Portal>
|
||||||
<View
|
|
||||||
// iOS
|
|
||||||
accessibilityViewIsModal
|
|
||||||
// Android
|
|
||||||
importantForAccessibility="yes"
|
|
||||||
style={[a.absolute, a.inset_0]}
|
|
||||||
onTouchMove={() => Keyboard.dismiss()}>
|
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[a.absolute, {top: 0, left: 10, right: 10}, animatedStyle]}>
|
style={[
|
||||||
|
// @ts-ignore position: fixed is web only
|
||||||
|
containerStyle,
|
||||||
|
animatedStyle,
|
||||||
|
]}>
|
||||||
<Pressable
|
<Pressable
|
||||||
style={[
|
style={[
|
||||||
t.atoms.bg,
|
t.atoms.bg,
|
||||||
@@ -125,6 +138,13 @@ export const ProgressGuideToast = React.forwardRef<
|
|||||||
a.rounded_md,
|
a.rounded_md,
|
||||||
a.px_lg,
|
a.px_lg,
|
||||||
a.py_md,
|
a.py_md,
|
||||||
|
a.shadow_sm,
|
||||||
|
{
|
||||||
|
shadowRadius: 8,
|
||||||
|
shadowOpacity: 0.1,
|
||||||
|
shadowOffset: {width: 0, height: 2},
|
||||||
|
elevation: 8,
|
||||||
|
},
|
||||||
]}
|
]}
|
||||||
onPress={close}
|
onPress={close}
|
||||||
accessibilityLabel={_(msg`Tap to dismiss`)}
|
accessibilityLabel={_(msg`Tap to dismiss`)}
|
||||||
@@ -143,7 +163,6 @@ export const ProgressGuideToast = React.forwardRef<
|
|||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
</View>
|
|
||||||
</Portal>
|
</Portal>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
ProgressGuideToast,
|
||||||
|
ProgressGuideToastRef,
|
||||||
|
} from '#/components/ProgressGuide/Toast'
|
||||||
|
|
||||||
|
export enum ProgressGuideAction {
|
||||||
|
Like = 'like',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ProgressGuideName {
|
||||||
|
Like10Posts = 'like-10-posts',
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaseProgressGuide {
|
||||||
|
guide: ProgressGuideName
|
||||||
|
isComplete: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Like10PostsProgressGuide extends BaseProgressGuide {
|
||||||
|
guide: ProgressGuideName.Like10Posts
|
||||||
|
numLikes: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProgressGuide = Like10PostsProgressGuide | undefined
|
||||||
|
|
||||||
|
const ProgressGuideContext = React.createContext<ProgressGuide>(undefined)
|
||||||
|
|
||||||
|
const ProgressGuideControlContext = React.createContext<{
|
||||||
|
startProgressGuide(guide: ProgressGuideName): void
|
||||||
|
endProgressGuide(): void
|
||||||
|
captureAction(action: ProgressGuideAction): void
|
||||||
|
}>({
|
||||||
|
startProgressGuide: (_guide: ProgressGuideName) => {},
|
||||||
|
endProgressGuide: () => {},
|
||||||
|
captureAction: (_action: ProgressGuideAction) => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
export function useProgressGuide(guide: ProgressGuideName) {
|
||||||
|
const ctx = React.useContext(ProgressGuideContext)
|
||||||
|
if (ctx?.guide === guide) {
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useProgressGuideControls() {
|
||||||
|
return React.useContext(ProgressGuideControlContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
const [activeProgressGuide, setActiveProgressGuide] =
|
||||||
|
React.useState<ProgressGuide>({
|
||||||
|
guide: ProgressGuideName.Like10Posts,
|
||||||
|
numLikes: 0,
|
||||||
|
isComplete: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const firstLikeToastRef = React.useRef<ProgressGuideToastRef | null>(null)
|
||||||
|
const fifthLikeToastRef = React.useRef<ProgressGuideToastRef | null>(null)
|
||||||
|
const tenthLikeToastRef = React.useRef<ProgressGuideToastRef | null>(null)
|
||||||
|
|
||||||
|
const controls = React.useMemo(() => {
|
||||||
|
return {
|
||||||
|
startProgressGuide(guide: ProgressGuideName) {
|
||||||
|
if (guide === ProgressGuideName.Like10Posts) {
|
||||||
|
setActiveProgressGuide({
|
||||||
|
guide: ProgressGuideName.Like10Posts,
|
||||||
|
numLikes: 0,
|
||||||
|
isComplete: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
endProgressGuide() {
|
||||||
|
setActiveProgressGuide(undefined)
|
||||||
|
},
|
||||||
|
|
||||||
|
captureAction(action: ProgressGuideAction) {
|
||||||
|
let guide = activeProgressGuide
|
||||||
|
if (guide?.isComplete) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (guide?.guide === ProgressGuideName.Like10Posts) {
|
||||||
|
if (action === ProgressGuideAction.Like) {
|
||||||
|
guide = {
|
||||||
|
...guide,
|
||||||
|
numLikes: (guide.numLikes || 0) + 1,
|
||||||
|
}
|
||||||
|
if (guide.numLikes === 1) {
|
||||||
|
firstLikeToastRef.current?.open()
|
||||||
|
}
|
||||||
|
if (guide.numLikes === 5) {
|
||||||
|
fifthLikeToastRef.current?.open()
|
||||||
|
}
|
||||||
|
if (guide.numLikes === 10) {
|
||||||
|
tenthLikeToastRef.current?.open()
|
||||||
|
guide = {
|
||||||
|
...guide,
|
||||||
|
isComplete: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setActiveProgressGuide(guide)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}, [activeProgressGuide, setActiveProgressGuide])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProgressGuideContext.Provider value={activeProgressGuide}>
|
||||||
|
<ProgressGuideControlContext.Provider value={controls}>
|
||||||
|
{children}
|
||||||
|
<ProgressGuideToast
|
||||||
|
ref={firstLikeToastRef}
|
||||||
|
title={_(msg`Your first like!`)}
|
||||||
|
subtitle={_(msg`Like 10 posts to train the Discover feed`)}
|
||||||
|
/>
|
||||||
|
<ProgressGuideToast
|
||||||
|
ref={fifthLikeToastRef}
|
||||||
|
title={_(msg`Half way there!`)}
|
||||||
|
subtitle={_(msg`Like 10 posts to train the Discover feed`)}
|
||||||
|
/>
|
||||||
|
<ProgressGuideToast
|
||||||
|
ref={tenthLikeToastRef}
|
||||||
|
title={_(msg`Task complete - 10 likes!`)}
|
||||||
|
subtitle={_(msg`The Discover feed now knows what you like`)}
|
||||||
|
/>
|
||||||
|
</ProgressGuideControlContext.Provider>
|
||||||
|
</ProgressGuideContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,6 +6,10 @@ import {useLingui} from '@lingui/react'
|
|||||||
|
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {useSetDrawerOpen} from '#/state/shell/drawer-open'
|
import {useSetDrawerOpen} from '#/state/shell/drawer-open'
|
||||||
|
import {
|
||||||
|
ProgressGuideAction,
|
||||||
|
useProgressGuideControls,
|
||||||
|
} from '#/state/shell/progress-guide'
|
||||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||||
import {HITSLOP_10} from 'lib/constants'
|
import {HITSLOP_10} from 'lib/constants'
|
||||||
import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform'
|
import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform'
|
||||||
@@ -34,11 +38,19 @@ export function HomeHeaderLayoutMobile({
|
|||||||
const {headerHeight} = useShellLayout()
|
const {headerHeight} = useShellLayout()
|
||||||
const headerMinimalShellTransform = useMinimalShellHeaderTransform()
|
const headerMinimalShellTransform = useMinimalShellHeaderTransform()
|
||||||
const {hasSession} = useSession()
|
const {hasSession} = useSession()
|
||||||
|
const {captureAction} = useProgressGuideControls()
|
||||||
|
|
||||||
const onPressAvi = React.useCallback(() => {
|
const onPressAvi = React.useCallback(() => {
|
||||||
setDrawerOpen(true)
|
setDrawerOpen(true)
|
||||||
}, [setDrawerOpen])
|
}, [setDrawerOpen])
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const i = setInterval(() => captureAction(ProgressGuideAction.Like), 1000)
|
||||||
|
return () => {
|
||||||
|
clearInterval(i)
|
||||||
|
}
|
||||||
|
}, [captureAction])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
style={[pal.view, pal.border, styles.tabBar, headerMinimalShellTransform]}
|
style={[pal.view, pal.border, styles.tabBar, headerMinimalShellTransform]}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {Text} from 'view/com/util/text/Text'
|
|||||||
import {DesktopFeeds} from './Feeds'
|
import {DesktopFeeds} from './Feeds'
|
||||||
import {DesktopSearch} from './Search'
|
import {DesktopSearch} from './Search'
|
||||||
import hairlineWidth = StyleSheet.hairlineWidth
|
import hairlineWidth = StyleSheet.hairlineWidth
|
||||||
|
import {ProgressGuideList} from '#/components/ProgressGuide/List'
|
||||||
|
|
||||||
export function DesktopRightNav({routeName}: {routeName: string}) {
|
export function DesktopRightNav({routeName}: {routeName: string}) {
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
@@ -39,9 +40,12 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
|
|||||||
<DesktopSearch />
|
<DesktopSearch />
|
||||||
|
|
||||||
{hasSession && (
|
{hasSession && (
|
||||||
|
<>
|
||||||
|
<ProgressGuideList style={[{marginTop: 22, marginBottom: 8}]} />
|
||||||
<View style={[pal.border, styles.desktopFeedsContainer]}>
|
<View style={[pal.border, styles.desktopFeedsContainer]}>
|
||||||
<DesktopFeeds />
|
<DesktopFeeds />
|
||||||
</View>
|
</View>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user