diff --git a/package.json b/package.json
index 85256551..5f9f0345 100644
--- a/package.json
+++ b/package.json
@@ -49,7 +49,7 @@
"open-analyzer": "EXPO_PUBLIC_OPEN_ANALYZER=1 yarn build-web"
},
"dependencies": {
- "@atproto/api": "^0.12.22",
+ "@atproto/api": "^0.12.23",
"@bam.tech/react-native-image-resizer": "^3.0.4",
"@braintree/sanitize-url": "^6.0.2",
"@discord/bottom-sheet": "bluesky-social/react-native-bottom-sheet",
diff --git a/src/App.native.tsx b/src/App.native.tsx
index 18af7440..f0dde6ee 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -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() {
-
-
-
-
+
+
+
+
+
+
diff --git a/src/App.web.tsx b/src/App.web.tsx
index f45806e4..eb4a925d 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -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() {
-
+
+
+
diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx
index 00342b39..ca3b085b 100644
--- a/src/components/FeedInterstitials.tsx
+++ b/src/components/FeedInterstitials.tsx
@@ -6,11 +6,13 @@ import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/native'
+import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {NavigationProp} from '#/lib/routes/types'
import {logEvent} from '#/lib/statsig/statsig'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useGetPopularFeedsQuery} from '#/state/queries/feed'
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 {Button} from '#/components/Button'
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 * as ProfileCard from '#/components/ProfileCard'
import {Text} from '#/components/Typography'
+import {ProgressGuideList} from './ProgressGuide/List'
function CardOuter({
children,
@@ -352,3 +355,26 @@ export function SuggestedFeeds() {
)
}
+
+export function ProgressGuide() {
+ const t = useTheme()
+ const {isDesktop} = useWebMediaQueries()
+ const guide = useProgressGuide('like-10-and-follow-7')
+
+ if (isDesktop) {
+ return null
+ }
+
+ return guide ? (
+
+
+
+ ) : null
+}
diff --git a/src/components/ProgressGuide/List.tsx b/src/components/ProgressGuide/List.tsx
new file mode 100644
index 00000000..f68445d2
--- /dev/null
+++ b/src/components/ProgressGuide/List.tsx
@@ -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}) {
+ const t = useTheme()
+ const {_} = useLingui()
+ const guide = useProgressGuide('like-10-and-follow-7')
+ const {endProgressGuide} = useProgressGuideControls()
+
+ if (guide) {
+ return (
+
+
+
+ Getting started
+
+
+
+
+
+
+ )
+ }
+ return null
+}
diff --git a/src/components/ProgressGuide/Task.tsx b/src/components/ProgressGuide/Task.tsx
new file mode 100644
index 00000000..d286b884
--- /dev/null
+++ b/src/components/ProgressGuide/Task.tsx
@@ -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 (
+
+ {current === total ? (
+
+ ) : (
+
+ )}
+
+
+
+ {title}
+
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+
+ )
+}
diff --git a/src/components/ProgressGuide/Toast.tsx b/src/components/ProgressGuide/Toast.tsx
new file mode 100644
index 00000000..346312af
--- /dev/null
+++ b/src/components/ProgressGuide/Toast.tsx
@@ -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(null)
+ const timeoutRef = React.useRef()
+ 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 && (
+
+
+
+
+
+ {title}
+ {subtitle && (
+
+ {subtitle}
+
+ )}
+
+
+
+
+ )
+ )
+})
diff --git a/src/components/anim/AnimatedCheck.tsx b/src/components/anim/AnimatedCheck.tsx
new file mode 100644
index 00000000..7fdfc14c
--- /dev/null
+++ b/src/components/anim/AnimatedCheck.tsx
@@ -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 (
+
+ )
+})
diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts
index e4991ad3..c8a55b92 100644
--- a/src/lib/statsig/gates.ts
+++ b/src/lib/statsig/gates.ts
@@ -7,5 +7,6 @@ export type Gate =
| 'show_avi_follow_button'
| 'show_follow_back_label_v2'
| 'new_user_guided_tour'
+ | 'new_user_progress_guide'
| 'suggested_feeds_interstitial'
| 'suggested_follows_interstitial'
diff --git a/src/screens/Onboarding/StepFinished.tsx b/src/screens/Onboarding/StepFinished.tsx
index 1cb925c1..825a0e72 100644
--- a/src/screens/Onboarding/StepFinished.tsx
+++ b/src/screens/Onboarding/StepFinished.tsx
@@ -19,6 +19,7 @@ import {preferencesQueryKey} from '#/state/queries/preferences'
import {RQKEY as profileRQKey} from '#/state/queries/profile'
import {useAgent} from '#/state/session'
import {useOnboardingDispatch} from '#/state/shell'
+import {useProgressGuideControls} from '#/state/shell/progress-guide'
import {uploadBlob} from 'lib/api'
import {useRequestNotificationsPermission} from 'lib/notifications/notifications'
import {useSetHasCheckedForStarterPack} from 'state/preferences/used-starter-packs'
@@ -58,6 +59,7 @@ export function StepFinished() {
const setActiveStarterPack = useSetActiveStarterPack()
const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack()
const setQueuedTour = useSetQueuedTour()
+ const {startProgressGuide} = useProgressGuideControls()
const finishOnboarding = React.useCallback(async () => {
setSaving(true)
@@ -185,6 +187,7 @@ export function StepFinished() {
setActiveStarterPack(undefined)
setHasCheckedForStarterPack(true)
setQueuedTour(TOURS.HOME)
+ startProgressGuide('like-10-and-follow-7')
dispatch({type: 'finish'})
onboardDispatch({type: 'finish'})
track('OnboardingV2:StepFinished:End')
@@ -218,6 +221,7 @@ export function StepFinished() {
setActiveStarterPack,
setHasCheckedForStarterPack,
setQueuedTour,
+ startProgressGuide,
])
React.useEffect(() => {
diff --git a/src/screens/StarterPack/StarterPackScreen.tsx b/src/screens/StarterPack/StarterPackScreen.tsx
index 9b66e515..518318f7 100644
--- a/src/screens/StarterPack/StarterPackScreen.tsx
+++ b/src/screens/StarterPack/StarterPackScreen.tsx
@@ -18,6 +18,10 @@ import {useQueryClient} from '@tanstack/react-query'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {useDeleteStarterPackMutation} from '#/state/queries/starter-packs'
+import {
+ ProgressGuideAction,
+ useProgressGuideControls,
+} from '#/state/shell/progress-guide'
import {batchedUpdates} from 'lib/batchedUpdates'
import {HITSLOP_20} from 'lib/constants'
import {isBlockedOrBlocking, isMuted} from 'lib/moderation/blocked-and-muted'
@@ -287,6 +291,7 @@ function Header({
const queryClient = useQueryClient()
const setActiveStarterPack = useSetActiveStarterPack()
const {requestSwitchToAccount} = useLoggedOutViewControls()
+ const {captureAction} = useProgressGuideControls()
const [isProcessing, setIsProcessing] = React.useState(false)
@@ -351,6 +356,7 @@ function Header({
starterPack: starterPack.uri,
count: dids.length,
})
+ captureAction(ProgressGuideAction.Follow, dids.length)
Toast.show(_(msg`All accounts have been followed!`))
} catch (e) {
Toast.show(_(msg`An error occurred while trying to follow all`))
diff --git a/src/state/queries/preferences/const.ts b/src/state/queries/preferences/const.ts
index d94edb47..2a8c5116 100644
--- a/src/state/queries/preferences/const.ts
+++ b/src/state/queries/preferences/const.ts
@@ -34,4 +34,8 @@ export const DEFAULT_LOGGED_OUT_PREFERENCES: UsePreferencesQueryResponse = {
userAge: 13, // TODO(pwi)
interests: {tags: []},
savedFeeds: [],
+ bskyAppState: {
+ queuedNudges: [],
+ activeProgressGuide: undefined,
+ },
}
diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts
index 672abfca..9bb57fca 100644
--- a/src/state/queries/preferences/index.ts
+++ b/src/state/queries/preferences/index.ts
@@ -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,
+ })
+ },
+ })
+}
diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts
index 6f7f2de7..af00faf2 100644
--- a/src/state/queries/profile.ts
+++ b/src/state/queries/profile.ts
@@ -25,6 +25,10 @@ import {STALE} from '#/state/queries'
import {resetProfilePostsQueries} from '#/state/queries/post-feed'
import {updateProfileShadow} from '../cache/profile-shadow'
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_MY_BLOCKED} from './my-blocked-accounts'
import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
@@ -274,12 +278,15 @@ function useProfileFollowMutation(
const {currentAccount} = useSession()
const agent = useAgent()
const queryClient = useQueryClient()
+ const {captureAction} = useProgressGuideControls()
+
return useMutation<{uri: string; cid: string}, Error, {did: string}>({
mutationFn: async ({did}) => {
let ownProfile: AppBskyActorDefs.ProfileViewDetailed | undefined
if (currentAccount) {
ownProfile = findProfileQueryData(queryClient, currentAccount.did)
}
+ captureAction(ProgressGuideAction.Follow)
logEvent('profile:follow', {
logContext,
didBecomeMutual: profile.viewer
diff --git a/src/state/shell/progress-guide.tsx b/src/state/shell/progress-guide.tsx
new file mode 100644
index 00000000..d10d5829
--- /dev/null
+++ b/src/state/shell/progress-guide.tsx
@@ -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(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(undefined)
+
+ if (activeProgressGuide && !localGuideState) {
+ // hydrate from the server if needed
+ setLocalGuideState(activeProgressGuide)
+ }
+
+ const firstLikeToastRef = React.useRef(null)
+ const fifthLikeToastRef = React.useRef(null)
+ const tenthLikeToastRef = React.useRef(null)
+ const guideCompleteToastRef = React.useRef(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 (
+
+
+ {children}
+ {localGuideState?.guide === 'like-10-and-follow-7' && (
+ <>
+
+
+
+
+ >
+ )}
+
+
+ )
+}
diff --git a/src/view/com/posts/Feed.tsx b/src/view/com/posts/Feed.tsx
index 3d90b889..e6ad3561 100644
--- a/src/view/com/posts/Feed.tsx
+++ b/src/view/com/posts/Feed.tsx
@@ -34,7 +34,11 @@ import {useSession} from '#/state/session'
import {useAnalytics} from 'lib/analytics/analytics'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
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 {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
@@ -85,12 +89,26 @@ type FeedItem =
}
slot: number
}
+ | {
+ type: 'interstitialProgressGuide'
+ key: string
+ params: {
+ variant: 'default' | string
+ }
+ slot: number
+ }
const feedInterstitialType = 'interstitialFeeds'
const followInterstitialType = 'interstitialFollows'
+const progressGuideInterstitialType = 'interstitialProgressGuide'
const interstials: Record<
'following' | 'discover',
- (FeedItem & {type: 'interstitialFeeds' | 'interstitialFollows'})[]
+ (FeedItem & {
+ type:
+ | 'interstitialFeeds'
+ | 'interstitialFollows'
+ | 'interstitialProgressGuide'
+ })[]
> = {
following: [
{
@@ -111,6 +129,14 @@ const interstials: Record<
},
],
discover: [
+ {
+ type: progressGuideInterstitialType,
+ params: {
+ variant: 'default',
+ },
+ key: progressGuideInterstitialType,
+ slot: 0,
+ },
{
type: feedInterstitialType,
params: {
@@ -336,14 +362,14 @@ let Feed = ({
if (feedType) {
for (const interstitial of interstials[feedType]) {
- const feedInterstitialEnabled =
- interstitial.type === feedInterstitialType &&
- gate('suggested_feeds_interstitial')
- const followInterstitialEnabled =
- interstitial.type === followInterstitialType &&
- gate('suggested_follows_interstitial')
+ const shouldShow =
+ (interstitial.type === feedInterstitialType &&
+ gate('suggested_feeds_interstitial')) ||
+ (interstitial.type === followInterstitialType &&
+ gate('suggested_follows_interstitial')) ||
+ interstitial.type === progressGuideInterstitialType
- if (feedInterstitialEnabled || followInterstitialEnabled) {
+ if (shouldShow) {
const variant = 'default' // replace with experiment variant
const int = {
...interstitial,
@@ -460,6 +486,8 @@ let Feed = ({
return
} else if (item.type === followInterstitialType) {
return
+ } else if (item.type === progressGuideInterstitialType) {
+ return
} else if (item.type === 'slice') {
if (item.slice.rootUri === FALLBACK_MARKER_POST.post.uri) {
// HACK
diff --git a/src/view/com/util/post-ctrls/PostCtrls.tsx b/src/view/com/util/post-ctrls/PostCtrls.tsx
index 231808bf..c3af3a61 100644
--- a/src/view/com/util/post-ctrls/PostCtrls.tsx
+++ b/src/view/com/util/post-ctrls/PostCtrls.tsx
@@ -31,6 +31,10 @@ import {
} from '#/state/queries/post'
import {useRequireAuth, useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
+import {
+ ProgressGuideAction,
+ useProgressGuideControls,
+} from '#/state/shell/progress-guide'
import {atoms as a, useTheme} from '#/alf'
import {useDialogControl} from '#/components/Dialog'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ArrowOutOfBox} from '#/components/icons/ArrowOutOfBox'
@@ -77,6 +81,7 @@ let PostCtrls = ({
const requireAuth = useRequireAuth()
const loggedOutWarningPromptControl = useDialogControl()
const {sendInteraction} = useFeedFeedbackContext()
+ const {captureAction} = useProgressGuideControls()
const playHaptic = useHaptics()
const gate = useGate()
@@ -103,6 +108,7 @@ let PostCtrls = ({
event: 'app.bsky.feed.defs#interactionLike',
feedContext,
})
+ captureAction(ProgressGuideAction.Like)
await queueLike()
} else {
await queueUnlike()
@@ -119,6 +125,7 @@ let PostCtrls = ({
queueLike,
queueUnlike,
sendInteraction,
+ captureAction,
feedContext,
])
diff --git a/src/view/shell/desktop/RightNav.tsx b/src/view/shell/desktop/RightNav.tsx
index 633f0493..8dfa671c 100644
--- a/src/view/shell/desktop/RightNav.tsx
+++ b/src/view/shell/desktop/RightNav.tsx
@@ -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}) {
{hasSession && (
-
-
-
+ <>
+
+
+
+
+ >
)}
>
)}
diff --git a/yarn.lock b/yarn.lock
index d31fa2f6..84318d1a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -34,15 +34,16 @@
jsonpointer "^5.0.0"
leven "^3.1.0"
-"@atproto/api@^0.12.22":
- version "0.12.22"
- resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.22.tgz#1880a93a0caa4485cd8463bd1e10bf2424b9826c"
- integrity sha512-TIXSnf3qqyX40Ei/FkK4H24w+7s5rOc63TPwrGakRBOqIgSNBKOggei8I600fJ/AXB7HO6Vp9tBmDVOt2+021A==
+"@atproto/api@^0.12.23":
+ version "0.12.23"
+ resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.12.23.tgz#b3409817d0b981a64f30d16e8257f0fe261338af"
+ integrity sha512-fgQ30u+q9smX5g41eep7fISSkSAhRkX0inc81PZ82QwcHbFkC8ePaha/KP0CoTaPWKi7EsC89Z/8BEBCJo0oBA==
dependencies:
"@atproto/common-web" "^0.3.0"
"@atproto/lexicon" "^0.4.0"
"@atproto/syntax" "^0.3.0"
"@atproto/xrpc" "^0.5.0"
+ await-lock "^2.2.2"
multiformats "^9.9.0"
tlds "^1.234.0"