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"