New user progress guides (#4716)
* Add the animated checkmark svg * Add progress guide list and task components * Add ProgressGuide Toast component * Implement progress-guide controller * Add 7 follows to the progress guide * Wire up action captures * Wire up progress-guide persistence * Trigger progress guide on account creation * Clear the progress guide from storage on complete * Add progress guide interstitial, put behind gate * Fix: read progress guide state from prefs * Some defensive type checks * Create separate toast for completion * List tweaks * Only show on Discover * Spacing and progress tweaks * Completely hide when complete * Capture the progress guide in local state, and only render toasts while guide is active * Fix: ensure persisted hydrates into local state * Gate --------- Co-authored-by: Eric Bailey <git@esb.lol> Co-authored-by: Dan Abramov <dan.abramov@gmail.com>zio/stable
parent
aa7117edb6
commit
0ed99b840d
|
@ -49,7 +49,7 @@
|
||||||
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
|
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@atproto/api": "^0.12.22",
|
"@atproto/api": "^0.12.23",
|
||||||
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
"@bam.tech/react-native-image-resizer": "^3.0.4",
|
||||||
"@braintree/sanitize-url": "^6.0.2",
|
"@braintree/sanitize-url": "^6.0.2",
|
||||||
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -6,11 +6,13 @@ import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
|
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
||||||
import {NavigationProp} from '#/lib/routes/types'
|
import {NavigationProp} from '#/lib/routes/types'
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
|
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
|
||||||
import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows'
|
import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows'
|
||||||
|
import {useProgressGuide} from '#/state/shell/progress-guide'
|
||||||
import {atoms as a, useBreakpoints, useTheme, ViewStyleProp, web} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme, ViewStyleProp, web} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import * as FeedCard from '#/components/FeedCard'
|
import * as FeedCard from '#/components/FeedCard'
|
||||||
|
@ -20,6 +22,7 @@ import {PersonPlus_Stroke2_Corner0_Rounded as Person} from '#/components/icons/P
|
||||||
import {InlineLinkText} from '#/components/Link'
|
import {InlineLinkText} from '#/components/Link'
|
||||||
import * as ProfileCard from '#/components/ProfileCard'
|
import * as ProfileCard from '#/components/ProfileCard'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
import {ProgressGuideList} from './ProgressGuide/List'
|
||||||
|
|
||||||
function CardOuter({
|
function CardOuter({
|
||||||
children,
|
children,
|
||||||
|
@ -352,3 +355,26 @@ export function SuggestedFeeds() {
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ProgressGuide() {
|
||||||
|
const t = useTheme()
|
||||||
|
const {isDesktop} = useWebMediaQueries()
|
||||||
|
const guide = useProgressGuide('like-10-and-follow-7')
|
||||||
|
|
||||||
|
if (isDesktop) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return guide ? (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.border_t,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
a.px_lg,
|
||||||
|
a.py_lg,
|
||||||
|
a.pb_lg,
|
||||||
|
]}>
|
||||||
|
<ProgressGuideList />
|
||||||
|
</View>
|
||||||
|
) : null
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,61 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {StyleProp, View, ViewStyle} from 'react-native'
|
||||||
|
import {msg, Trans} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {
|
||||||
|
useProgressGuide,
|
||||||
|
useProgressGuideControls,
|
||||||
|
} from '#/state/shell/progress-guide'
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
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({style}: {style?: StyleProp<ViewStyle>}) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const guide = useProgressGuide('like-10-and-follow-7')
|
||||||
|
const {endProgressGuide} = useProgressGuideControls()
|
||||||
|
|
||||||
|
if (guide) {
|
||||||
|
return (
|
||||||
|
<View style={[a.flex_col, a.gap_md, style]}>
|
||||||
|
<View style={[a.flex_row, a.align_center, a.justify_between]}>
|
||||||
|
<Text
|
||||||
|
style={[
|
||||||
|
t.atoms.text_contrast_medium,
|
||||||
|
a.font_semibold,
|
||||||
|
a.text_sm,
|
||||||
|
{textTransform: 'uppercase'},
|
||||||
|
]}>
|
||||||
|
<Trans>Getting started</Trans>
|
||||||
|
</Text>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="tiny"
|
||||||
|
color="secondary"
|
||||||
|
shape="round"
|
||||||
|
label={_(msg`Dismiss getting started guide`)}
|
||||||
|
onPress={endProgressGuide}>
|
||||||
|
<ButtonIcon icon={Times} size="sm" />
|
||||||
|
</Button>
|
||||||
|
</View>
|
||||||
|
<ProgressGuideTask
|
||||||
|
current={guide.numLikes + 1}
|
||||||
|
total={10 + 1}
|
||||||
|
title={_(msg`Like 10 posts`)}
|
||||||
|
subtitle={_(msg`Teach our algorithm what you like`)}
|
||||||
|
/>
|
||||||
|
<ProgressGuideTask
|
||||||
|
current={guide.numFollows + 1}
|
||||||
|
total={7 + 1}
|
||||||
|
title={_(msg`Follow 7 accounts`)}
|
||||||
|
subtitle={_(msg`Bluesky is better with friends!`)}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
|
@ -0,0 +1,50 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {View} from 'react-native'
|
||||||
|
import * as Progress from 'react-native-progress'
|
||||||
|
|
||||||
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
|
import {AnimatedCheck} from '../anim/AnimatedCheck'
|
||||||
|
import {Text} from '../Typography'
|
||||||
|
|
||||||
|
export function ProgressGuideTask({
|
||||||
|
current,
|
||||||
|
total,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
}: {
|
||||||
|
current: number
|
||||||
|
total: number
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
}) {
|
||||||
|
const t = useTheme()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View style={[a.flex_row, a.gap_sm, !subtitle && a.align_center]}>
|
||||||
|
{current === total ? (
|
||||||
|
<AnimatedCheck playOnMount fill={t.palette.primary_500} width={24} />
|
||||||
|
) : (
|
||||||
|
<Progress.Circle
|
||||||
|
progress={current / total}
|
||||||
|
color={t.palette.primary_400}
|
||||||
|
size={20}
|
||||||
|
thickness={3}
|
||||||
|
borderWidth={0}
|
||||||
|
unfilledColor={t.palette.contrast_50}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<View style={[a.flex_col, a.gap_2xs, {marginTop: -2}]}>
|
||||||
|
<Text style={[a.text_sm, a.font_semibold, a.leading_tight]}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
{subtitle && (
|
||||||
|
<Text
|
||||||
|
style={[a.text_sm, t.atoms.text_contrast_medium, a.leading_tight]}>
|
||||||
|
{subtitle}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,169 @@
|
||||||
|
import React, {useImperativeHandle} from 'react'
|
||||||
|
import {Pressable, useWindowDimensions, View} from 'react-native'
|
||||||
|
import Animated, {
|
||||||
|
Easing,
|
||||||
|
runOnJS,
|
||||||
|
useAnimatedStyle,
|
||||||
|
useSharedValue,
|
||||||
|
withTiming,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
|
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'
|
||||||
|
import {Text} from '../Typography'
|
||||||
|
|
||||||
|
export interface ProgressGuideToastRef {
|
||||||
|
open(): void
|
||||||
|
close(): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProgressGuideToastProps {
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
visibleDuration?: number // default 5s
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProgressGuideToast = React.forwardRef<
|
||||||
|
ProgressGuideToastRef,
|
||||||
|
ProgressGuideToastProps
|
||||||
|
>(function ProgressGuideToast({title, subtitle, visibleDuration}, ref) {
|
||||||
|
const t = useTheme()
|
||||||
|
const {_} = useLingui()
|
||||||
|
const insets = useSafeAreaInsets()
|
||||||
|
const [isOpen, setIsOpen] = React.useState(false)
|
||||||
|
const translateY = useSharedValue(0)
|
||||||
|
const opacity = useSharedValue(0)
|
||||||
|
const animatedCheckRef = React.useRef<AnimatedCheckRef | null>(null)
|
||||||
|
const timeoutRef = React.useRef<NodeJS.Timeout | undefined>()
|
||||||
|
const winDim = useWindowDimensions()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Methods
|
||||||
|
*/
|
||||||
|
|
||||||
|
const close = React.useCallback(() => {
|
||||||
|
// clear the timeout, in case this was called imperatively
|
||||||
|
if (timeoutRef.current) {
|
||||||
|
clearTimeout(timeoutRef.current)
|
||||||
|
timeoutRef.current = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// animate the opacity then set isOpen to false when done
|
||||||
|
const setIsntOpen = () => setIsOpen(false)
|
||||||
|
opacity.value = withTiming(
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
duration: 400,
|
||||||
|
easing: Easing.out(Easing.cubic),
|
||||||
|
},
|
||||||
|
() => runOnJS(setIsntOpen)(),
|
||||||
|
)
|
||||||
|
}, [setIsOpen, opacity])
|
||||||
|
|
||||||
|
const open = React.useCallback(() => {
|
||||||
|
// set isOpen=true to render
|
||||||
|
setIsOpen(true)
|
||||||
|
|
||||||
|
// animate the vertical translation, the opacity, and the checkmark
|
||||||
|
const playCheckmark = () => animatedCheckRef.current?.play()
|
||||||
|
opacity.value = 0
|
||||||
|
opacity.value = withTiming(
|
||||||
|
1,
|
||||||
|
{
|
||||||
|
duration: 100,
|
||||||
|
easing: Easing.out(Easing.cubic),
|
||||||
|
},
|
||||||
|
() => runOnJS(playCheckmark)(),
|
||||||
|
)
|
||||||
|
translateY.value = 0
|
||||||
|
translateY.value = withTiming(insets.top + 10, {
|
||||||
|
duration: 500,
|
||||||
|
easing: Easing.out(Easing.cubic),
|
||||||
|
})
|
||||||
|
|
||||||
|
// start the countdown timer to autoclose
|
||||||
|
timeoutRef.current = setTimeout(close, visibleDuration || 5e3)
|
||||||
|
}, [setIsOpen, translateY, opacity, insets, close, visibleDuration])
|
||||||
|
|
||||||
|
useImperativeHandle(
|
||||||
|
ref,
|
||||||
|
() => ({
|
||||||
|
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(() => ({
|
||||||
|
transform: [{translateY: translateY.value}],
|
||||||
|
opacity: opacity.value,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return (
|
||||||
|
isOpen && (
|
||||||
|
<Portal>
|
||||||
|
<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,92 @@
|
||||||
|
import React from 'react'
|
||||||
|
import Animated, {
|
||||||
|
Easing,
|
||||||
|
useAnimatedProps,
|
||||||
|
useSharedValue,
|
||||||
|
withDelay,
|
||||||
|
withTiming,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
|
import Svg, {Circle, Path} from 'react-native-svg'
|
||||||
|
|
||||||
|
import {Props, useCommonSVGProps} from '#/components/icons/common'
|
||||||
|
|
||||||
|
const AnimatedPath = Animated.createAnimatedComponent(Path)
|
||||||
|
const AnimatedCircle = Animated.createAnimatedComponent(Circle)
|
||||||
|
|
||||||
|
const PATH = 'M14.1 27.2l7.1 7.2 16.7-16.8'
|
||||||
|
|
||||||
|
export interface AnimatedCheckRef {
|
||||||
|
play(cb?: () => void): void
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AnimatedCheckProps extends Props {
|
||||||
|
playOnMount?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AnimatedCheck = React.forwardRef<
|
||||||
|
AnimatedCheckRef,
|
||||||
|
AnimatedCheckProps
|
||||||
|
>(function AnimatedCheck({playOnMount, ...props}, ref) {
|
||||||
|
const {fill, size, style, ...rest} = useCommonSVGProps(props)
|
||||||
|
const circleAnim = useSharedValue(0)
|
||||||
|
const checkAnim = useSharedValue(0)
|
||||||
|
|
||||||
|
const circleAnimatedProps = useAnimatedProps(() => ({
|
||||||
|
strokeDashoffset: 166 - circleAnim.value * 166,
|
||||||
|
}))
|
||||||
|
const checkAnimatedProps = useAnimatedProps(() => ({
|
||||||
|
strokeDashoffset: 48 - 48 * checkAnim.value,
|
||||||
|
}))
|
||||||
|
|
||||||
|
const play = React.useCallback(
|
||||||
|
(cb?: () => void) => {
|
||||||
|
circleAnim.value = 0
|
||||||
|
checkAnim.value = 0
|
||||||
|
|
||||||
|
circleAnim.value = withTiming(1, {duration: 500, easing: Easing.linear})
|
||||||
|
checkAnim.value = withDelay(
|
||||||
|
500,
|
||||||
|
withTiming(1, {duration: 300, easing: Easing.linear}, cb),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
[circleAnim, checkAnim],
|
||||||
|
)
|
||||||
|
|
||||||
|
React.useImperativeHandle(ref, () => ({
|
||||||
|
play,
|
||||||
|
}))
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (playOnMount) {
|
||||||
|
play()
|
||||||
|
}
|
||||||
|
}, [play, playOnMount])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Svg
|
||||||
|
fill="none"
|
||||||
|
{...rest}
|
||||||
|
viewBox="0 0 52 52"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
style={style}>
|
||||||
|
<AnimatedCircle
|
||||||
|
animatedProps={circleAnimatedProps}
|
||||||
|
cx="26"
|
||||||
|
cy="26"
|
||||||
|
r="24"
|
||||||
|
fill="none"
|
||||||
|
stroke={fill}
|
||||||
|
strokeWidth={4}
|
||||||
|
strokeDasharray={166}
|
||||||
|
/>
|
||||||
|
<AnimatedPath
|
||||||
|
animatedProps={checkAnimatedProps}
|
||||||
|
stroke={fill}
|
||||||
|
d={PATH}
|
||||||
|
strokeWidth={4}
|
||||||
|
strokeDasharray={48}
|
||||||
|
/>
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
})
|
|
@ -7,5 +7,6 @@ export type Gate =
|
||||||
| 'show_avi_follow_button'
|
| 'show_avi_follow_button'
|
||||||
| 'show_follow_back_label_v2'
|
| 'show_follow_back_label_v2'
|
||||||
| 'new_user_guided_tour'
|
| 'new_user_guided_tour'
|
||||||
|
| 'new_user_progress_guide'
|
||||||
| 'suggested_feeds_interstitial'
|
| 'suggested_feeds_interstitial'
|
||||||
| 'suggested_follows_interstitial'
|
| 'suggested_follows_interstitial'
|
||||||
|
|
|
@ -19,6 +19,7 @@ import {preferencesQueryKey} from '#/state/queries/preferences'
|
||||||
import {RQKEY as profileRQKey} from '#/state/queries/profile'
|
import {RQKEY as profileRQKey} from '#/state/queries/profile'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
import {useOnboardingDispatch} from '#/state/shell'
|
import {useOnboardingDispatch} from '#/state/shell'
|
||||||
|
import {useProgressGuideControls} from '#/state/shell/progress-guide'
|
||||||
import {uploadBlob} from 'lib/api'
|
import {uploadBlob} from 'lib/api'
|
||||||
import {useRequestNotificationsPermission} from 'lib/notifications/notifications'
|
import {useRequestNotificationsPermission} from 'lib/notifications/notifications'
|
||||||
import {useSetHasCheckedForStarterPack} from 'state/preferences/used-starter-packs'
|
import {useSetHasCheckedForStarterPack} from 'state/preferences/used-starter-packs'
|
||||||
|
@ -58,6 +59,7 @@ export function StepFinished() {
|
||||||
const setActiveStarterPack = useSetActiveStarterPack()
|
const setActiveStarterPack = useSetActiveStarterPack()
|
||||||
const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack()
|
const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack()
|
||||||
const setQueuedTour = useSetQueuedTour()
|
const setQueuedTour = useSetQueuedTour()
|
||||||
|
const {startProgressGuide} = useProgressGuideControls()
|
||||||
|
|
||||||
const finishOnboarding = React.useCallback(async () => {
|
const finishOnboarding = React.useCallback(async () => {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
|
@ -185,6 +187,7 @@ export function StepFinished() {
|
||||||
setActiveStarterPack(undefined)
|
setActiveStarterPack(undefined)
|
||||||
setHasCheckedForStarterPack(true)
|
setHasCheckedForStarterPack(true)
|
||||||
setQueuedTour(TOURS.HOME)
|
setQueuedTour(TOURS.HOME)
|
||||||
|
startProgressGuide('like-10-and-follow-7')
|
||||||
dispatch({type: 'finish'})
|
dispatch({type: 'finish'})
|
||||||
onboardDispatch({type: 'finish'})
|
onboardDispatch({type: 'finish'})
|
||||||
track('OnboardingV2:StepFinished:End')
|
track('OnboardingV2:StepFinished:End')
|
||||||
|
@ -218,6 +221,7 @@ export function StepFinished() {
|
||||||
setActiveStarterPack,
|
setActiveStarterPack,
|
||||||
setHasCheckedForStarterPack,
|
setHasCheckedForStarterPack,
|
||||||
setQueuedTour,
|
setQueuedTour,
|
||||||
|
startProgressGuide,
|
||||||
])
|
])
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
|
|
|
@ -18,6 +18,10 @@ import {useQueryClient} from '@tanstack/react-query'
|
||||||
import {cleanError} from '#/lib/strings/errors'
|
import {cleanError} from '#/lib/strings/errors'
|
||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useDeleteStarterPackMutation} from '#/state/queries/starter-packs'
|
import {useDeleteStarterPackMutation} from '#/state/queries/starter-packs'
|
||||||
|
import {
|
||||||
|
ProgressGuideAction,
|
||||||
|
useProgressGuideControls,
|
||||||
|
} from '#/state/shell/progress-guide'
|
||||||
import {batchedUpdates} from 'lib/batchedUpdates'
|
import {batchedUpdates} from 'lib/batchedUpdates'
|
||||||
import {HITSLOP_20} from 'lib/constants'
|
import {HITSLOP_20} from 'lib/constants'
|
||||||
import {isBlockedOrBlocking, isMuted} from 'lib/moderation/blocked-and-muted'
|
import {isBlockedOrBlocking, isMuted} from 'lib/moderation/blocked-and-muted'
|
||||||
|
@ -287,6 +291,7 @@ function Header({
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const setActiveStarterPack = useSetActiveStarterPack()
|
const setActiveStarterPack = useSetActiveStarterPack()
|
||||||
const {requestSwitchToAccount} = useLoggedOutViewControls()
|
const {requestSwitchToAccount} = useLoggedOutViewControls()
|
||||||
|
const {captureAction} = useProgressGuideControls()
|
||||||
|
|
||||||
const [isProcessing, setIsProcessing] = React.useState(false)
|
const [isProcessing, setIsProcessing] = React.useState(false)
|
||||||
|
|
||||||
|
@ -351,6 +356,7 @@ function Header({
|
||||||
starterPack: starterPack.uri,
|
starterPack: starterPack.uri,
|
||||||
count: dids.length,
|
count: dids.length,
|
||||||
})
|
})
|
||||||
|
captureAction(ProgressGuideAction.Follow, dids.length)
|
||||||
Toast.show(_(msg`All accounts have been followed!`))
|
Toast.show(_(msg`All accounts have been followed!`))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.show(_(msg`An error occurred while trying to follow all`))
|
Toast.show(_(msg`An error occurred while trying to follow all`))
|
||||||
|
|
|
@ -34,4 +34,8 @@ export const DEFAULT_LOGGED_OUT_PREFERENCES: UsePreferencesQueryResponse = {
|
||||||
userAge: 13, // TODO(pwi)
|
userAge: 13, // TODO(pwi)
|
||||||
interests: {tags: []},
|
interests: {tags: []},
|
||||||
savedFeeds: [],
|
savedFeeds: [],
|
||||||
|
bskyAppState: {
|
||||||
|
queuedNudges: [],
|
||||||
|
activeProgressGuide: undefined,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -342,3 +342,50 @@ export function useRemoveMutedWordMutation() {
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useQueueNudgesMutation() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const agent = useAgent()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (nudges: string | string[]) => {
|
||||||
|
await agent.bskyAppQueueNudges(nudges)
|
||||||
|
// triggers a refetch
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: preferencesQueryKey,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDismissNudgesMutation() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const agent = useAgent()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (nudges: string | string[]) => {
|
||||||
|
await agent.bskyAppDismissNudges(nudges)
|
||||||
|
// triggers a refetch
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: preferencesQueryKey,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSetActiveProgressGuideMutation() {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const agent = useAgent()
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async (
|
||||||
|
guide: AppBskyActorDefs.BskyAppProgressGuide | undefined,
|
||||||
|
) => {
|
||||||
|
await agent.bskyAppSetActiveProgressGuide(guide)
|
||||||
|
// triggers a refetch
|
||||||
|
await queryClient.invalidateQueries({
|
||||||
|
queryKey: preferencesQueryKey,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
@ -25,6 +25,10 @@ import {STALE} from '#/state/queries'
|
||||||
import {resetProfilePostsQueries} from '#/state/queries/post-feed'
|
import {resetProfilePostsQueries} from '#/state/queries/post-feed'
|
||||||
import {updateProfileShadow} from '../cache/profile-shadow'
|
import {updateProfileShadow} from '../cache/profile-shadow'
|
||||||
import {useAgent, useSession} from '../session'
|
import {useAgent, useSession} from '../session'
|
||||||
|
import {
|
||||||
|
ProgressGuideAction,
|
||||||
|
useProgressGuideControls,
|
||||||
|
} from '../shell/progress-guide'
|
||||||
import {RQKEY as RQKEY_LIST_CONVOS} from './messages/list-converations'
|
import {RQKEY as RQKEY_LIST_CONVOS} from './messages/list-converations'
|
||||||
import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts'
|
import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts'
|
||||||
import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
|
import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
|
||||||
|
@ -274,12 +278,15 @@ function useProfileFollowMutation(
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
|
const {captureAction} = useProgressGuideControls()
|
||||||
|
|
||||||
return useMutation<{uri: string; cid: string}, Error, {did: string}>({
|
return useMutation<{uri: string; cid: string}, Error, {did: string}>({
|
||||||
mutationFn: async ({did}) => {
|
mutationFn: async ({did}) => {
|
||||||
let ownProfile: AppBskyActorDefs.ProfileViewDetailed | undefined
|
let ownProfile: AppBskyActorDefs.ProfileViewDetailed | undefined
|
||||||
if (currentAccount) {
|
if (currentAccount) {
|
||||||
ownProfile = findProfileQueryData(queryClient, currentAccount.did)
|
ownProfile = findProfileQueryData(queryClient, currentAccount.did)
|
||||||
}
|
}
|
||||||
|
captureAction(ProgressGuideAction.Follow)
|
||||||
logEvent('profile:follow', {
|
logEvent('profile:follow', {
|
||||||
logContext,
|
logContext,
|
||||||
didBecomeMutual: profile.viewer
|
didBecomeMutual: profile.viewer
|
||||||
|
|
|
@ -0,0 +1,185 @@
|
||||||
|
import React from 'react'
|
||||||
|
import {msg} from '@lingui/macro'
|
||||||
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
|
import {useGate} from '#/lib/statsig/statsig'
|
||||||
|
import {
|
||||||
|
ProgressGuideToast,
|
||||||
|
ProgressGuideToastRef,
|
||||||
|
} from '#/components/ProgressGuide/Toast'
|
||||||
|
import {
|
||||||
|
usePreferencesQuery,
|
||||||
|
useSetActiveProgressGuideMutation,
|
||||||
|
} from '../queries/preferences'
|
||||||
|
|
||||||
|
export enum ProgressGuideAction {
|
||||||
|
Like = 'like',
|
||||||
|
Follow = 'follow',
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProgressGuideName = 'like-10-and-follow-7'
|
||||||
|
|
||||||
|
interface BaseProgressGuide {
|
||||||
|
guide: string
|
||||||
|
isComplete: boolean
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Like10AndFollow7ProgressGuide extends BaseProgressGuide {
|
||||||
|
numLikes: number
|
||||||
|
numFollows: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProgressGuide = Like10AndFollow7ProgressGuide | undefined
|
||||||
|
|
||||||
|
const ProgressGuideContext = React.createContext<ProgressGuide>(undefined)
|
||||||
|
|
||||||
|
const ProgressGuideControlContext = React.createContext<{
|
||||||
|
startProgressGuide(guide: ProgressGuideName): void
|
||||||
|
endProgressGuide(): void
|
||||||
|
captureAction(action: ProgressGuideAction, count?: number): void
|
||||||
|
}>({
|
||||||
|
startProgressGuide: (_guide: ProgressGuideName) => {},
|
||||||
|
endProgressGuide: () => {},
|
||||||
|
captureAction: (_action: ProgressGuideAction, _count = 1) => {},
|
||||||
|
})
|
||||||
|
|
||||||
|
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 {data: preferences} = usePreferencesQuery()
|
||||||
|
const {mutateAsync, variables} = useSetActiveProgressGuideMutation()
|
||||||
|
const gate = useGate()
|
||||||
|
|
||||||
|
const activeProgressGuide = (variables ||
|
||||||
|
preferences?.bskyAppState?.activeProgressGuide) as ProgressGuide
|
||||||
|
|
||||||
|
// ensure the unspecced attributes have the correct types
|
||||||
|
if (activeProgressGuide?.guide === 'like-10-and-follow-7') {
|
||||||
|
activeProgressGuide.numLikes = Number(activeProgressGuide.numLikes) || 0
|
||||||
|
activeProgressGuide.numFollows = Number(activeProgressGuide.numFollows) || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const [localGuideState, setLocalGuideState] =
|
||||||
|
React.useState<ProgressGuide>(undefined)
|
||||||
|
|
||||||
|
if (activeProgressGuide && !localGuideState) {
|
||||||
|
// hydrate from the server if needed
|
||||||
|
setLocalGuideState(activeProgressGuide)
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstLikeToastRef = React.useRef<ProgressGuideToastRef | null>(null)
|
||||||
|
const fifthLikeToastRef = React.useRef<ProgressGuideToastRef | null>(null)
|
||||||
|
const tenthLikeToastRef = React.useRef<ProgressGuideToastRef | null>(null)
|
||||||
|
const guideCompleteToastRef = React.useRef<ProgressGuideToastRef | null>(null)
|
||||||
|
|
||||||
|
const controls = React.useMemo(() => {
|
||||||
|
return {
|
||||||
|
startProgressGuide(guide: ProgressGuideName) {
|
||||||
|
if (!gate('new_user_progress_guide')) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (guide === 'like-10-and-follow-7') {
|
||||||
|
const guideObj = {
|
||||||
|
guide: 'like-10-and-follow-7',
|
||||||
|
numLikes: 0,
|
||||||
|
numFollows: 0,
|
||||||
|
isComplete: false,
|
||||||
|
}
|
||||||
|
setLocalGuideState(guideObj)
|
||||||
|
mutateAsync(guideObj)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
endProgressGuide() {
|
||||||
|
// update the persisted first
|
||||||
|
mutateAsync(undefined).then(() => {
|
||||||
|
// now clear local state, to avoid rehydrating from the server
|
||||||
|
setLocalGuideState(undefined)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
captureAction(action: ProgressGuideAction, count = 1) {
|
||||||
|
let guide = activeProgressGuide
|
||||||
|
if (!guide || guide?.isComplete) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (guide?.guide === 'like-10-and-follow-7') {
|
||||||
|
if (action === ProgressGuideAction.Like) {
|
||||||
|
guide = {
|
||||||
|
...guide,
|
||||||
|
numLikes: (Number(guide.numLikes) || 0) + count,
|
||||||
|
}
|
||||||
|
if (guide.numLikes === 1) {
|
||||||
|
firstLikeToastRef.current?.open()
|
||||||
|
}
|
||||||
|
if (guide.numLikes === 5) {
|
||||||
|
fifthLikeToastRef.current?.open()
|
||||||
|
}
|
||||||
|
if (guide.numLikes === 10) {
|
||||||
|
tenthLikeToastRef.current?.open()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (action === ProgressGuideAction.Follow) {
|
||||||
|
guide = {
|
||||||
|
...guide,
|
||||||
|
numFollows: (Number(guide.numFollows) || 0) + count,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Number(guide.numLikes) >= 10 && Number(guide.numFollows) >= 7) {
|
||||||
|
guide = {
|
||||||
|
...guide,
|
||||||
|
isComplete: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setLocalGuideState(guide)
|
||||||
|
mutateAsync(guide?.isComplete ? undefined : guide)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}, [activeProgressGuide, mutateAsync, gate, setLocalGuideState])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProgressGuideContext.Provider value={localGuideState}>
|
||||||
|
<ProgressGuideControlContext.Provider value={controls}>
|
||||||
|
{children}
|
||||||
|
{localGuideState?.guide === 'like-10-and-follow-7' && (
|
||||||
|
<>
|
||||||
|
<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`)}
|
||||||
|
/>
|
||||||
|
<ProgressGuideToast
|
||||||
|
ref={guideCompleteToastRef}
|
||||||
|
title={_(msg`Algorithm training complete!`)}
|
||||||
|
subtitle={_(msg`The Discover feed now knows what you like`)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ProgressGuideControlContext.Provider>
|
||||||
|
</ProgressGuideContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
|
@ -34,7 +34,11 @@ import {useSession} from '#/state/session'
|
||||||
import {useAnalytics} from 'lib/analytics/analytics'
|
import {useAnalytics} from 'lib/analytics/analytics'
|
||||||
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
|
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
|
||||||
import {useTheme} from 'lib/ThemeContext'
|
import {useTheme} from 'lib/ThemeContext'
|
||||||
import {SuggestedFeeds, SuggestedFollows} from '#/components/FeedInterstitials'
|
import {
|
||||||
|
ProgressGuide,
|
||||||
|
SuggestedFeeds,
|
||||||
|
SuggestedFollows,
|
||||||
|
} from '#/components/FeedInterstitials'
|
||||||
import {List, ListRef} from '../util/List'
|
import {List, ListRef} from '../util/List'
|
||||||
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
|
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
|
||||||
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
|
||||||
|
@ -85,12 +89,26 @@ type FeedItem =
|
||||||
}
|
}
|
||||||
slot: number
|
slot: number
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'interstitialProgressGuide'
|
||||||
|
key: string
|
||||||
|
params: {
|
||||||
|
variant: 'default' | string
|
||||||
|
}
|
||||||
|
slot: number
|
||||||
|
}
|
||||||
|
|
||||||
const feedInterstitialType = 'interstitialFeeds'
|
const feedInterstitialType = 'interstitialFeeds'
|
||||||
const followInterstitialType = 'interstitialFollows'
|
const followInterstitialType = 'interstitialFollows'
|
||||||
|
const progressGuideInterstitialType = 'interstitialProgressGuide'
|
||||||
const interstials: Record<
|
const interstials: Record<
|
||||||
'following' | 'discover',
|
'following' | 'discover',
|
||||||
(FeedItem & {type: 'interstitialFeeds' | 'interstitialFollows'})[]
|
(FeedItem & {
|
||||||
|
type:
|
||||||
|
| 'interstitialFeeds'
|
||||||
|
| 'interstitialFollows'
|
||||||
|
| 'interstitialProgressGuide'
|
||||||
|
})[]
|
||||||
> = {
|
> = {
|
||||||
following: [
|
following: [
|
||||||
{
|
{
|
||||||
|
@ -111,6 +129,14 @@ const interstials: Record<
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
discover: [
|
discover: [
|
||||||
|
{
|
||||||
|
type: progressGuideInterstitialType,
|
||||||
|
params: {
|
||||||
|
variant: 'default',
|
||||||
|
},
|
||||||
|
key: progressGuideInterstitialType,
|
||||||
|
slot: 0,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: feedInterstitialType,
|
type: feedInterstitialType,
|
||||||
params: {
|
params: {
|
||||||
|
@ -336,14 +362,14 @@ let Feed = ({
|
||||||
|
|
||||||
if (feedType) {
|
if (feedType) {
|
||||||
for (const interstitial of interstials[feedType]) {
|
for (const interstitial of interstials[feedType]) {
|
||||||
const feedInterstitialEnabled =
|
const shouldShow =
|
||||||
interstitial.type === feedInterstitialType &&
|
(interstitial.type === feedInterstitialType &&
|
||||||
gate('suggested_feeds_interstitial')
|
gate('suggested_feeds_interstitial')) ||
|
||||||
const followInterstitialEnabled =
|
(interstitial.type === followInterstitialType &&
|
||||||
interstitial.type === followInterstitialType &&
|
gate('suggested_follows_interstitial')) ||
|
||||||
gate('suggested_follows_interstitial')
|
interstitial.type === progressGuideInterstitialType
|
||||||
|
|
||||||
if (feedInterstitialEnabled || followInterstitialEnabled) {
|
if (shouldShow) {
|
||||||
const variant = 'default' // replace with experiment variant
|
const variant = 'default' // replace with experiment variant
|
||||||
const int = {
|
const int = {
|
||||||
...interstitial,
|
...interstitial,
|
||||||
|
@ -460,6 +486,8 @@ let Feed = ({
|
||||||
return <SuggestedFeeds />
|
return <SuggestedFeeds />
|
||||||
} else if (item.type === followInterstitialType) {
|
} else if (item.type === followInterstitialType) {
|
||||||
return <SuggestedFollows />
|
return <SuggestedFollows />
|
||||||
|
} else if (item.type === progressGuideInterstitialType) {
|
||||||
|
return <ProgressGuide />
|
||||||
} else if (item.type === 'slice') {
|
} else if (item.type === 'slice') {
|
||||||
if (item.slice.rootUri === FALLBACK_MARKER_POST.post.uri) {
|
if (item.slice.rootUri === FALLBACK_MARKER_POST.post.uri) {
|
||||||
// HACK
|
// HACK
|
||||||
|
|
|
@ -31,6 +31,10 @@ import {
|
||||||
} from '#/state/queries/post'
|
} from '#/state/queries/post'
|
||||||
import {useRequireAuth, useSession} from '#/state/session'
|
import {useRequireAuth, useSession} from '#/state/session'
|
||||||
import {useComposerControls} from '#/state/shell/composer'
|
import {useComposerControls} from '#/state/shell/composer'
|
||||||
|
import {
|
||||||
|
ProgressGuideAction,
|
||||||
|
useProgressGuideControls,
|
||||||
|
} from '#/state/shell/progress-guide'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import {useDialogControl} from '#/components/Dialog'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ArrowOutOfBox} from '#/components/icons/ArrowOutOfBox'
|
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ArrowOutOfBox} from '#/components/icons/ArrowOutOfBox'
|
||||||
|
@ -77,6 +81,7 @@ let PostCtrls = ({
|
||||||
const requireAuth = useRequireAuth()
|
const requireAuth = useRequireAuth()
|
||||||
const loggedOutWarningPromptControl = useDialogControl()
|
const loggedOutWarningPromptControl = useDialogControl()
|
||||||
const {sendInteraction} = useFeedFeedbackContext()
|
const {sendInteraction} = useFeedFeedbackContext()
|
||||||
|
const {captureAction} = useProgressGuideControls()
|
||||||
const playHaptic = useHaptics()
|
const playHaptic = useHaptics()
|
||||||
const gate = useGate()
|
const gate = useGate()
|
||||||
|
|
||||||
|
@ -103,6 +108,7 @@ let PostCtrls = ({
|
||||||
event: 'app.bsky.feed.defs#interactionLike',
|
event: 'app.bsky.feed.defs#interactionLike',
|
||||||
feedContext,
|
feedContext,
|
||||||
})
|
})
|
||||||
|
captureAction(ProgressGuideAction.Like)
|
||||||
await queueLike()
|
await queueLike()
|
||||||
} else {
|
} else {
|
||||||
await queueUnlike()
|
await queueUnlike()
|
||||||
|
@ -119,6 +125,7 @@ let PostCtrls = ({
|
||||||
queueLike,
|
queueLike,
|
||||||
queueUnlike,
|
queueUnlike,
|
||||||
sendInteraction,
|
sendInteraction,
|
||||||
|
captureAction,
|
||||||
feedContext,
|
feedContext,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -34,15 +34,16 @@
|
||||||
jsonpointer "^5.0.0"
|
jsonpointer "^5.0.0"
|
||||||
leven "^3.1.0"
|
leven "^3.1.0"
|
||||||
|
|
||||||
"@atproto/api@^0.12.22":
|
"@atproto/api@^0.12.23":
|
||||||
version "0.12.22"
|
version "0.12.23"
|
||||||
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.22.tgz#1880a93a0caa4485cd8463bd1e10bf2424b9826c"
|
resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.23.tgz#b3409817d0b981a64f30d16e8257f0fe261338af"
|
||||||
integrity sha512-TIXSnf3qqyX40Ei/FkK4H24w+7s5rOc63TPwrGakRBOqIgSNBKOggei8I600fJ/AXB7HO6Vp9tBmDVOt2+021A==
|
integrity sha512-fgQ30u+q9smX5g41eep7fISSkSAhRkX0inc81PZ82QwcHbFkC8ePaha/KP0CoTaPWKi7EsC89Z/8BEBCJo0oBA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@atproto/common-web" "^0.3.0"
|
"@atproto/common-web" "^0.3.0"
|
||||||
"@atproto/lexicon" "^0.4.0"
|
"@atproto/lexicon" "^0.4.0"
|
||||||
"@atproto/syntax" "^0.3.0"
|
"@atproto/syntax" "^0.3.0"
|
||||||
"@atproto/xrpc" "^0.5.0"
|
"@atproto/xrpc" "^0.5.0"
|
||||||
|
await-lock "^2.2.2"
|
||||||
multiformats "^9.9.0"
|
multiformats "^9.9.0"
|
||||||
tlds "^1.234.0"
|
tlds "^1.234.0"
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue