diff --git a/src/components/hooks/useStarterPackEntry.android.ts b/src/components/hooks/useStarterPackEntry.android.ts index 939dfd91a5..09664db1b1 100644 --- a/src/components/hooks/useStarterPackEntry.android.ts +++ b/src/components/hooks/useStarterPackEntry.android.ts @@ -1,16 +1,14 @@ import React from 'react' import {makeStarterPackLink} from 'lib/routes/links' -import { - useSetUsedStarterPack, - useUsedStarterPack, -} from 'state/preferences/starter-pack' +import {useSetCurrentStarterPack} from 'state/preferences/starter-pack' +import {useUsedStarterPacks} from 'state/preferences/used-starter-packs' import GooglePlayReferrer from '../../../modules/expo-google-play-referrer' export function useStarterPackEntry() { const [ready, setReady] = React.useState(false) - const usedStarterPack = useUsedStarterPack() - const setUsedStarterPack = useSetUsedStarterPack() + const setCurrentStarterPack = useSetCurrentStarterPack() + const usedStarterPacks = useUsedStarterPacks() React.useEffect(() => { if (ready) return @@ -32,8 +30,8 @@ export function useStarterPackEntry() { // We won't actually set `lastUsedUri` until a _successful_ use of the starter pack, meaning a new account // has actually completed onboarding with the given starter pack. const uri = makeStarterPackLink(sourceParts[1], sourceParts[2]) - if (uri !== usedStarterPack?.lastUsedUri) { - setUsedStarterPack({ + if (!usedStarterPacks?.includes(uri)) { + setCurrentStarterPack({ uri: makeStarterPackLink(sourceParts[1], sourceParts[2]), }) } @@ -41,7 +39,7 @@ export function useStarterPackEntry() { } setReady(true) })() - }, [ready, setUsedStarterPack, usedStarterPack?.lastUsedUri]) + }, [ready, setCurrentStarterPack, usedStarterPacks]) return ready } diff --git a/src/components/hooks/useStarterPackEntry.ts b/src/components/hooks/useStarterPackEntry.ts index 51242a95ec..fd662df19a 100644 --- a/src/components/hooks/useStarterPackEntry.ts +++ b/src/components/hooks/useStarterPackEntry.ts @@ -1,9 +1,9 @@ import React from 'react' -import {useSetUsedStarterPack} from 'state/preferences/starter-pack' +import {useSetCurrentStarterPack} from 'state/preferences/starter-pack' export function useStarterPackEntry() { - const setUsedStarterPack = useSetUsedStarterPack() + const setCurrentStarterPack = useSetCurrentStarterPack() React.useEffect(() => { const url = new URL(window.location.href) @@ -12,13 +12,13 @@ export function useStarterPackEntry() { const isClip = url.searchParams.get('clip') === 'true' if (name && rkey) { - setUsedStarterPack({ + setCurrentStarterPack({ uri: window.location.href, isClip, }) } } - }, [setUsedStarterPack]) + }, [setCurrentStarterPack]) return true } diff --git a/src/screens/Onboarding/StepFinished.tsx b/src/screens/Onboarding/StepFinished.tsx index 925d6004c0..b925f97a69 100644 --- a/src/screens/Onboarding/StepFinished.tsx +++ b/src/screens/Onboarding/StepFinished.tsx @@ -15,10 +15,12 @@ import {useAgent} from '#/state/session' import {useOnboardingDispatch} from '#/state/shell' import {uploadBlob} from 'lib/api' import {useRequestNotificationsPermission} from 'lib/notifications/notifications' +import {makeStarterPackLink} from 'lib/routes/links' import { - useSetUsedStarterPack, - useUsedStarterPack, + useCurrentStarterPack, + useSetCurrentStarterPack, } from 'state/preferences/starter-pack' +import {useAddUsedStarterPack} from 'state/preferences/used-starter-packs' import { DescriptionText, OnboardingControls, @@ -46,17 +48,18 @@ export function StepFinished() { const queryClient = useQueryClient() const agent = useAgent() const requestNotificationsPermission = useRequestNotificationsPermission() - const usedStarterPack = useUsedStarterPack() - const setUsedStarterPack = useSetUsedStarterPack() + const currentStarterPack = useCurrentStarterPack() + const setCurrentStarterPack = useSetCurrentStarterPack() + const addUsedStarterPack = useAddUsedStarterPack() const finishOnboarding = React.useCallback(async () => { setSaving(true) try { let starterPack: AppBskyGraphDefs.StarterPackView | undefined let listItems: AppBskyGraphDefs.ListItemView[] | undefined - if (usedStarterPack) { + if (currentStarterPack) { const spRes = await agent.app.bsky.graph.getStarterPack({ - starterPack: usedStarterPack.uri, + starterPack: currentStarterPack.uri, }) starterPack = spRes.data.starterPack @@ -133,12 +136,12 @@ export function StepFinished() { requestNotificationsPermission('AfterOnboarding'), ]) - if (usedStarterPack) { - setUsedStarterPack({ - ...usedStarterPack, + if (currentStarterPack && starterPack) { + setCurrentStarterPack({ + ...currentStarterPack, initialFeed: starterPack?.feeds?.[0].uri ?? 'following', - lastUsedUri: usedStarterPack.uri, }) + addUsedStarterPack(makeStarterPackLink(starterPack)) } } catch (e: any) { logger.info(`onboarding: bulk save failed`) @@ -171,10 +174,11 @@ export function StepFinished() { dispatch, onboardDispatch, track, - usedStarterPack, + currentStarterPack, state, requestNotificationsPermission, - setUsedStarterPack, + setCurrentStarterPack, + addUsedStarterPack, ]) React.useEffect(() => { diff --git a/src/screens/StarterPack/StarterPackLandingScreen.tsx b/src/screens/StarterPack/StarterPackLandingScreen.tsx index 3ced61fa25..d3da521e27 100644 --- a/src/screens/StarterPack/StarterPackLandingScreen.tsx +++ b/src/screens/StarterPack/StarterPackLandingScreen.tsx @@ -1,15 +1,15 @@ import React from 'react' import {Pressable, ScrollView, View} from 'react-native' import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' -import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' +import {AppBskyGraphDefs, AppBskyGraphStarterpack, AtUri} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {isAndroidWeb} from 'lib/browser' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import { - useSetUsedStarterPack, - useUsedStarterPack, + useCurrentStarterPack, + useSetCurrentStarterPack, } from 'state/preferences/starter-pack' import {useResolveDidQuery} from 'state/queries/resolve-uri' import {useStarterPackQuery} from 'state/queries/useStarterPackQuery' @@ -40,11 +40,15 @@ function postAppClipMessage(message: AppClipMessage) { } function parseStarterPackHttpUri(uri: string): {name?: string; rkey?: string} { - const parsed = new URL(uri) - const [_, _path, name, rkey] = parsed.pathname.split('/') - return { - name, - rkey, + try { + const parsed = new URL(uri) + const [_, _path, name, rkey] = parsed.pathname.split('/') + return { + name, + rkey, + } + } catch (e) { + return {} } } @@ -57,8 +61,9 @@ export function LandingScreen({ }: { setScreenState: (state: LoggedOutScreenState) => void }) { - const usedStarterPack = useUsedStarterPack() - const {name, rkey} = parseStarterPackHttpUri(usedStarterPack?.uri || '') + const currentStarterPack = useCurrentStarterPack() + const {name, rkey} = + parseStarterPackHttpUri(currentStarterPack?.uri || '') ?? {} const { data: did, @@ -108,8 +113,8 @@ function LandingScreenInner({ const {record, creator, listItemsSample, feeds, joinedWeekCount} = starterPack const {_} = useLingui() const t = useTheme() - const setUsedStarterPack = useSetUsedStarterPack() - const usedStarterPack = useUsedStarterPack() + const currentStarterPack = useCurrentStarterPack() + const setCurrentStarterPack = useSetCurrentStarterPack() const {isTabletOrDesktop} = useWebMediaQueries() const androidDialogControl = useDialogControl() @@ -119,14 +124,14 @@ function LandingScreenInner({ const listItemsCount = starterPack.list?.listItemCount ?? 0 const onContinue = () => { - setUsedStarterPack({ + setCurrentStarterPack({ uri: starterPack.uri, }) setScreenState(LoggedOutScreenState.S_CreateAccount) } const onJoinPress = () => { - if (usedStarterPack?.isClip) { + if (currentStarterPack?.isClip) { setAppClipOverlayVisible(true) postAppClipMessage({ action: 'present', @@ -156,7 +161,7 @@ function LandingScreenInner({ borderBottomLeftRadius: 10, borderBottomRightRadius: 10, }, - usedStarterPack?.isClip && { + currentStarterPack?.isClip && { paddingTop: 100, }, ]}> @@ -210,7 +215,7 @@ function LandingScreenInner({ {starterPack.feeds?.length ? ( - Join Bluesky and subscribe to these feeds + These great feeds will be available after signing up! {listItemsCount <= 8 ? ( - Also follow these people right away! + + You'll also follow these people right away! + ) : ( - Also follow these people and {listItemsCount - 8}{' '} - others! + You'll also follow these people and{' '} + {listItemsCount - 8} others! )} ) : ( <> {listItemsCount <= 8 ? ( - - Get started by following these people right away! - + You'll follow these people right away! ) : ( - Get started by following these people and{' '} - {listItemsCount - 8} + You'll follow these people and {listItemsCount - 8} others! )} @@ -289,24 +293,31 @@ function LandingScreenInner({ setIsVisible={setAppClipOverlayVisible} /> - - + + Download Bluesky + + + + The experience is better in the app. Download Bluesky now and we'll + pick back up where you left off. + + - { - const rkey = parseStarterPackHttpUri(starterPack.uri).rkey + const rkey = new AtUri(starterPack.uri).rkey if (!rkey) return window.location.href = createGooglePlayLink(creator.handle, rkey) }} /> + diff --git a/src/screens/StarterPack/StarterPackScreen.tsx b/src/screens/StarterPack/StarterPackScreen.tsx index ecd35a4587..2398381ebd 100644 --- a/src/screens/StarterPack/StarterPackScreen.tsx +++ b/src/screens/StarterPack/StarterPackScreen.tsx @@ -12,7 +12,7 @@ import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types' import {shareUrl} from 'lib/sharing' import {logEvent} from 'lib/statsig/statsig' import {isWeb} from 'platform/detection' -import {useSetUsedStarterPack} from 'state/preferences/starter-pack' +import {useSetCurrentStarterPack} from 'state/preferences/starter-pack' import {RQKEY} from 'state/queries/list-members' import {useResolveDidQuery} from 'state/queries/resolve-uri' import {useStarterPackQuery} from 'state/queries/useStarterPackQuery' @@ -128,7 +128,7 @@ function Header({ const agent = useAgent() const queryClient = useQueryClient() const qrCodeDialogControl = useDialogControl() - const setUsedStarterPack = useSetUsedStarterPack() + const setCurrentStarterPack = useSetCurrentStarterPack() const {setShowLoggedOut} = useLoggedOutViewControls() const [isProcessing, setIsProcessing] = React.useState(false) @@ -190,7 +190,7 @@ function Header({ color="secondary" size="small" onPress={() => { - setUsedStarterPack({uri: starterPack.uri}) + setCurrentStarterPack({uri: starterPack.uri}) setShowLoggedOut(true) }}> diff --git a/src/state/persisted/schema.ts b/src/state/persisted/schema.ts index ba282f2748..c9eb4909e6 100644 --- a/src/state/persisted/schema.ts +++ b/src/state/persisted/schema.ts @@ -88,14 +88,14 @@ export const schema = z.object({ disableHaptics: z.boolean().optional(), disableAutoplay: z.boolean().optional(), kawaii: z.boolean().optional(), - usedStarterPack: z + currentStarterPack: z .object({ uri: z.string(), initialFeed: z.string().optional(), isClip: z.boolean().optional(), - lastUsedUri: z.string().optional(), }) .optional(), + usedStarterPacks: z.array(z.string()).optional(), }) export type Schema = z.infer @@ -134,5 +134,6 @@ export const defaults: Schema = { disableHaptics: false, disableAutoplay: prefersReducedMotion, kawaii: false, - usedStarterPack: undefined, + currentStarterPack: undefined, + usedStarterPacks: [], } diff --git a/src/state/preferences/starter-pack.tsx b/src/state/preferences/starter-pack.tsx index f237838570..7221765c3c 100644 --- a/src/state/preferences/starter-pack.tsx +++ b/src/state/preferences/starter-pack.tsx @@ -5,9 +5,9 @@ import * as persisted from '#/state/persisted' type StateContext = | { uri: string + cid?: string initialFeed?: string isClip?: boolean - lastUsedUri?: string } | undefined type SetContext = (v: StateContext) => void @@ -20,12 +20,12 @@ export function Provider({children}: {children: React.ReactNode}) { const setStateWrapped = (v: StateContext) => { setState(v) - persisted.write('usedStarterPack', v) + persisted.write('currentStarterPack', v) } React.useEffect(() => { return persisted.onUpdate(() => { - setState(persisted.get('usedStarterPack')) + setState(persisted.get('currentStarterPack')) }) }, []) @@ -38,5 +38,5 @@ export function Provider({children}: {children: React.ReactNode}) { ) } -export const useUsedStarterPack = () => React.useContext(stateContext) -export const useSetUsedStarterPack = () => React.useContext(setContext) +export const useCurrentStarterPack = () => React.useContext(stateContext) +export const useSetCurrentStarterPack = () => React.useContext(setContext) diff --git a/src/state/preferences/used-starter-packs.tsx b/src/state/preferences/used-starter-packs.tsx new file mode 100644 index 0000000000..8f3fd71822 --- /dev/null +++ b/src/state/preferences/used-starter-packs.tsx @@ -0,0 +1,35 @@ +import React from 'react' + +import * as persisted from '#/state/persisted' + +type StateContext = string[] | undefined +type SetContext = (v: string) => void + +const stateContext = React.createContext([]) +const setContext = React.createContext((_: string) => {}) + +export function Provider({children}: {children: React.ReactNode}) { + const [state, setState] = React.useState() + + const setStateWrapped = (v: StateContext) => { + persisted.write('usedStarterPacks', [...state, v]) + setState(prev => [...prev, v]) + } + + React.useEffect(() => { + return persisted.onUpdate(() => { + setState(persisted.get('usedStarterPack')) + }) + }, []) + + return ( + + + {children} + + + ) +} + +export const useUsedStarterPacks = () => React.useContext(stateContext) +export const useAddUsedStarterPack = () => React.useContext(setContext) diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index 0736d0ff2f..099151f07a 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -23,8 +23,8 @@ import {useOTAUpdates} from 'lib/hooks/useOTAUpdates' import {useRequestNotificationsPermission} from 'lib/notifications/notifications' import {HomeTabNavigatorParams, NativeStackScreenProps} from 'lib/routes/types' import { - useSetUsedStarterPack, - useUsedStarterPack, + useCurrentStarterPack, + useSetCurrentStarterPack, } from 'state/preferences/starter-pack' import {useLoggedOutViewControls} from 'state/shell/logged-out' import {FeedPage} from 'view/com/feeds/FeedPage' @@ -40,15 +40,19 @@ export function HomeScreen(props: Props) { const {data: preferences} = usePreferencesQuery() const {data: pinnedFeedInfos, isLoading: isPinnedFeedsLoading} = usePinnedFeedsInfos() - const usedStarterPack = useUsedStarterPack() + const currentStarterPack = useCurrentStarterPack() const {setShowLoggedOut, requestSwitchToAccount} = useLoggedOutViewControls() React.useEffect(() => { - if (usedStarterPack && !usedStarterPack.initialFeed) { + if (!currentStarterPack?.initialFeed) { setShowLoggedOut(true) requestSwitchToAccount({requestedAccount: 'starterpack'}) } - }, [usedStarterPack, setShowLoggedOut, requestSwitchToAccount]) + }, [ + setShowLoggedOut, + requestSwitchToAccount, + currentStarterPack?.initialFeed, + ]) if (preferences && pinnedFeedInfos && !isPinnedFeedsLoading) { return ( @@ -84,8 +88,8 @@ function HomeScreenReady({ const selectedIndex = Math.max(0, maybeFoundIndex) const selectedFeed = allFeeds[selectedIndex] const requestNotificationsPermission = useRequestNotificationsPermission() - const usedStarterPack = useUsedStarterPack() - const setUsedStarterPack = useSetUsedStarterPack() + const currentStarterPack = useCurrentStarterPack() + const setCurrentStarterPack = useSetCurrentStarterPack() useSetTitle(pinnedFeedInfos[selectedIndex]?.displayName) useOTAUpdates() @@ -98,21 +102,18 @@ function HomeScreenReady({ const lastPagerReportedIndexRef = React.useRef(selectedIndex) React.useLayoutEffect(() => { let initialIndex = selectedIndex - if (usedStarterPack?.initialFeed) { - if (usedStarterPack.initialFeed === 'following') { + if (currentStarterPack?.initialFeed) { + if (currentStarterPack.initialFeed === 'following') { initialIndex = allFeeds.findIndex(f => f === 'following') } else { initialIndex = allFeeds.findIndex( - f => f === `feedgen|${usedStarterPack.initialFeed}`, + f => f === `feedgen|${currentStarterPack.initialFeed}`, ) } if (initialIndex === -1) { initialIndex = 0 } - setUsedStarterPack({ - ...usedStarterPack, - initialFeed: undefined, - }) + setCurrentStarterPack(undefined) } // Since the pager is not a controlled component, adjust it imperatively @@ -125,7 +126,12 @@ function HomeScreenReady({ lastPagerReportedIndexRef.current = selectedIndex pagerRef.current?.setPage(selectedIndex, 'desktop-sidebar-click') } - }, [selectedIndex, usedStarterPack, setUsedStarterPack, allFeeds]) + }, [ + selectedIndex, + allFeeds, + currentStarterPack?.initialFeed, + setCurrentStarterPack, + ]) const {hasSession} = useSession() const setMinimalShellMode = useSetMinimalShellMode()