Implement progress-guide controller
This commit is contained in:
+8
-4
@@ -45,6 +45,7 @@ import {
|
||||
import {readLastActiveAccount} from '#/state/session/util'
|
||||
import {Provider as ShellStateProvider} from '#/state/shell'
|
||||
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 StarterPackProvider} from '#/state/shell/starter-pack'
|
||||
import {TestCtrls} from '#/view/com/testing/TestCtrls'
|
||||
@@ -119,10 +120,13 @@ function InnerApp() {
|
||||
<BackgroundNotificationPreferencesProvider>
|
||||
<MutedThreadsProvider>
|
||||
<TourProvider>
|
||||
<GestureHandlerRootView style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
</GestureHandlerRootView>
|
||||
<ProgressGuideProvider>
|
||||
<GestureHandlerRootView
|
||||
style={s.h100pct}>
|
||||
<TestCtrls />
|
||||
<Shell />
|
||||
</GestureHandlerRootView>
|
||||
</ProgressGuideProvider>
|
||||
</TourProvider>
|
||||
</MutedThreadsProvider>
|
||||
</BackgroundNotificationPreferencesProvider>
|
||||
|
||||
+4
-1
@@ -34,6 +34,7 @@ import {
|
||||
import {readLastActiveAccount} from '#/state/session/util'
|
||||
import {Provider as ShellStateProvider} from '#/state/shell'
|
||||
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 StarterPackProvider} from '#/state/shell/starter-pack'
|
||||
import * as Toast from '#/view/com/util/Toast'
|
||||
@@ -104,7 +105,9 @@ function InnerApp() {
|
||||
<MutedThreadsProvider>
|
||||
<SafeAreaProvider>
|
||||
<TourProvider>
|
||||
<Shell />
|
||||
<ProgressGuideProvider>
|
||||
<Shell />
|
||||
</ProgressGuideProvider>
|
||||
</TourProvider>
|
||||
</SafeAreaProvider>
|
||||
</MutedThreadsProvider>
|
||||
|
||||
@@ -1,58 +1,61 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||
import {msg, Trans} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {
|
||||
ProgressGuideAction,
|
||||
ProgressGuideName,
|
||||
useProgressGuide,
|
||||
useProgressGuideControls,
|
||||
} from '#/state/shell/progress-guide'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Button, ButtonIcon} from '../Button'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '../icons/Times'
|
||||
import {Text} from '../Typography'
|
||||
import {Button, ButtonIcon} from '#/components/Button'
|
||||
import {TimesLarge_Stroke2_Corner0_Rounded as Times} from '#/components/icons/Times'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {ProgressGuideTask} from './Task'
|
||||
|
||||
export function ProgressGuideList() {
|
||||
export function ProgressGuideList({style}: {style: StyleProp<ViewStyle>}) {
|
||||
const t = useTheme()
|
||||
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(() => {
|
||||
const i = setInterval(() => {
|
||||
setNumLikes(v => (v >= 10 ? 0 : v + 1))
|
||||
setNumFollows(v => (v >= 7 ? 0 : v + 1))
|
||||
}, 1000)
|
||||
return () => {
|
||||
clearInterval(i)
|
||||
}
|
||||
}, [setNumLikes])
|
||||
const i = setInterval(() => captureAction(ProgressGuideAction.Like), 2e3)
|
||||
return () => clearInterval(i)
|
||||
}, [captureAction])
|
||||
|
||||
return (
|
||||
<View style={[a.flex_col, a.gap_md]}>
|
||||
<View style={[a.flex_row, a.align_center]}>
|
||||
<Text
|
||||
style={[t.atoms.text_contrast_medium, a.font_semibold, a.text_xs]}>
|
||||
<Trans>GET STARTED</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="tiny"
|
||||
color="secondary"
|
||||
label={_(msg`Dismiss get started`)}>
|
||||
<ButtonIcon icon={Times} />
|
||||
</Button>
|
||||
if (guide?.guide === ProgressGuideName.Like10Posts) {
|
||||
return (
|
||||
<View style={[a.flex_col, a.gap_md, style]}>
|
||||
<View style={[a.flex_row, a.align_center]}>
|
||||
<Text
|
||||
style={[
|
||||
t.atoms.text_contrast_medium,
|
||||
a.font_semibold,
|
||||
a.text_xs,
|
||||
{textTransform: 'uppercase'},
|
||||
]}>
|
||||
<Trans>Get started</Trans>
|
||||
</Text>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="tiny"
|
||||
color="secondary"
|
||||
label={_(msg`Dismiss get started`)}
|
||||
onPress={endProgressGuide}>
|
||||
<ButtonIcon icon={Times} />
|
||||
</Button>
|
||||
</View>
|
||||
<ProgressGuideTask
|
||||
current={guide.numLikes}
|
||||
total={10}
|
||||
title={_(msg`Like 10 posts`)}
|
||||
subtitle={_(msg`Teach Discover what you like.`)}
|
||||
/>
|
||||
</View>
|
||||
<ProgressGuideTask
|
||||
current={numLikes}
|
||||
total={10}
|
||||
title={_(msg`Like 10 posts`)}
|
||||
subtitle={_(msg`Teach Discover what you like.`)}
|
||||
/>
|
||||
<ProgressGuideTask
|
||||
current={numFollows}
|
||||
total={7}
|
||||
title={_(msg`Follow 7 people`)}
|
||||
/>
|
||||
</View>
|
||||
)
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, {useImperativeHandle} from 'react'
|
||||
import {Keyboard, Pressable, View} from 'react-native'
|
||||
import {Pressable, useWindowDimensions, View} from 'react-native'
|
||||
import Animated, {
|
||||
Easing,
|
||||
runOnJS,
|
||||
@@ -11,6 +11,7 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
|
||||
import {isWeb} from '#/platform/detection'
|
||||
import {atoms as a, useTheme} from '#/alf'
|
||||
import {Portal} from '#/components/Portal'
|
||||
import {AnimatedCheck, AnimatedCheckRef} from '../anim/AnimatedCheck'
|
||||
@@ -39,6 +40,7 @@ export const ProgressGuideToast = React.forwardRef<
|
||||
const opacity = useSharedValue(0)
|
||||
const animatedCheckRef = React.useRef<AnimatedCheckRef | null>(null)
|
||||
const timeoutRef = React.useRef<NodeJS.Timeout | undefined>()
|
||||
const winDim = useWindowDimensions()
|
||||
|
||||
/**
|
||||
* Methods
|
||||
@@ -97,6 +99,20 @@ export const ProgressGuideToast = React.forwardRef<
|
||||
[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(() => ({
|
||||
transform: [{translateY: translateY.value}],
|
||||
opacity: opacity.value,
|
||||
@@ -105,45 +121,48 @@ export const ProgressGuideToast = React.forwardRef<
|
||||
return (
|
||||
isOpen && (
|
||||
<Portal>
|
||||
<View
|
||||
// iOS
|
||||
accessibilityViewIsModal
|
||||
// Android
|
||||
importantForAccessibility="yes"
|
||||
style={[a.absolute, a.inset_0]}
|
||||
onTouchMove={() => Keyboard.dismiss()}>
|
||||
<Animated.View
|
||||
style={[a.absolute, {top: 0, left: 10, right: 10}, animatedStyle]}>
|
||||
<Pressable
|
||||
style={[
|
||||
t.atoms.bg,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_md,
|
||||
a.border,
|
||||
t.atoms.border_contrast_high,
|
||||
a.rounded_md,
|
||||
a.px_lg,
|
||||
a.py_md,
|
||||
]}
|
||||
onPress={close}
|
||||
accessibilityLabel={_(msg`Tap to dismiss`)}
|
||||
accessibilityHint="">
|
||||
<AnimatedCheck
|
||||
fill={t.palette.primary_500}
|
||||
ref={animatedCheckRef}
|
||||
/>
|
||||
<View>
|
||||
<Text style={[a.text_md, a.font_semibold]}>{title}</Text>
|
||||
{subtitle && (
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
</View>
|
||||
<Animated.View
|
||||
style={[
|
||||
// @ts-ignore position: fixed is web only
|
||||
containerStyle,
|
||||
animatedStyle,
|
||||
]}>
|
||||
<Pressable
|
||||
style={[
|
||||
t.atoms.bg,
|
||||
a.flex_row,
|
||||
a.align_center,
|
||||
a.gap_md,
|
||||
a.border,
|
||||
t.atoms.border_contrast_high,
|
||||
a.rounded_md,
|
||||
a.px_lg,
|
||||
a.py_md,
|
||||
a.shadow_sm,
|
||||
{
|
||||
shadowRadius: 8,
|
||||
shadowOpacity: 0.1,
|
||||
shadowOffset: {width: 0, height: 2},
|
||||
elevation: 8,
|
||||
},
|
||||
]}
|
||||
onPress={close}
|
||||
accessibilityLabel={_(msg`Tap to dismiss`)}
|
||||
accessibilityHint="">
|
||||
<AnimatedCheck
|
||||
fill={t.palette.primary_500}
|
||||
ref={animatedCheckRef}
|
||||
/>
|
||||
<View>
|
||||
<Text style={[a.text_md, a.font_semibold]}>{title}</Text>
|
||||
{subtitle && (
|
||||
<Text style={[a.text_sm, t.atoms.text_contrast_medium]}>
|
||||
{subtitle}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</Pressable>
|
||||
</Animated.View>
|
||||
</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 {useSetDrawerOpen} from '#/state/shell/drawer-open'
|
||||
import {
|
||||
ProgressGuideAction,
|
||||
useProgressGuideControls,
|
||||
} from '#/state/shell/progress-guide'
|
||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||
import {HITSLOP_10} from 'lib/constants'
|
||||
import {useMinimalShellHeaderTransform} from 'lib/hooks/useMinimalShellTransform'
|
||||
@@ -34,11 +38,19 @@ export function HomeHeaderLayoutMobile({
|
||||
const {headerHeight} = useShellLayout()
|
||||
const headerMinimalShellTransform = useMinimalShellHeaderTransform()
|
||||
const {hasSession} = useSession()
|
||||
const {captureAction} = useProgressGuideControls()
|
||||
|
||||
const onPressAvi = React.useCallback(() => {
|
||||
setDrawerOpen(true)
|
||||
}, [setDrawerOpen])
|
||||
|
||||
React.useEffect(() => {
|
||||
const i = setInterval(() => captureAction(ProgressGuideAction.Like), 1000)
|
||||
return () => {
|
||||
clearInterval(i)
|
||||
}
|
||||
}, [captureAction])
|
||||
|
||||
return (
|
||||
<Animated.View
|
||||
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 {DesktopSearch} from './Search'
|
||||
import hairlineWidth = StyleSheet.hairlineWidth
|
||||
import {ProgressGuideList} from '#/components/ProgressGuide/List'
|
||||
|
||||
export function DesktopRightNav({routeName}: {routeName: string}) {
|
||||
const pal = usePalette('default')
|
||||
@@ -39,9 +40,12 @@ export function DesktopRightNav({routeName}: {routeName: string}) {
|
||||
<DesktopSearch />
|
||||
|
||||
{hasSession && (
|
||||
<View style={[pal.border, styles.desktopFeedsContainer]}>
|
||||
<DesktopFeeds />
|
||||
</View>
|
||||
<>
|
||||
<ProgressGuideList style={[{marginTop: 22, marginBottom: 8}]} />
|
||||
<View style={[pal.border, styles.desktopFeedsContainer]}>
|
||||
<DesktopFeeds />
|
||||
</View>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user