SP state cleanup (#4569)

This commit is contained in:
Hailey
2024-06-18 18:42:22 -07:00
committed by GitHub
parent 156de402b8
commit 8b85546f2e
16 changed files with 340 additions and 278 deletions
+136 -58
View File
@@ -4,7 +4,7 @@ import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player'
import { import {
createStarterPackGooglePlayUri, createStarterPackGooglePlayUri,
createStarterPackLinkFromAndroidReferrer, createStarterPackLinkFromAndroidReferrer,
parseStarterPackHttpUri, parseStarterPackUri,
} from 'lib/strings/starter-pack' } from 'lib/strings/starter-pack'
import {cleanError} from '../../src/lib/strings/errors' import {cleanError} from '../../src/lib/strings/errors'
import {createFullHandle, makeValidHandle} from '../../src/lib/strings/handles' import {createFullHandle, makeValidHandle} from '../../src/lib/strings/handles'
@@ -803,62 +803,129 @@ describe('parseEmbedPlayerFromUrl', () => {
}) })
describe('createStarterPackLinkFromAndroidReferrer', () => { describe('createStarterPackLinkFromAndroidReferrer', () => {
const inputs = [ const validOutput = 'https://bsky.app/start/haileyok.com/rkey'
'utm_source=bluesky&utm_medium=starterpack&utm_content=starterpack-haileyok.com-rkey',
'utm_source=bluesky&utm_content=starterpack-haileyok.com-rkey&utm_medium=starterpack',
'utm_source=bluesky&utm_content=starterpack-haileyok.com-rkey',
'utm_content=starterpack-haileyok.com-rkey',
'utm_source=redsea&utm_content=starterpack-haileyok.com-rkey',
'utm_source=bluesky&utm_content=starterpack-haileyok.com',
'utm_source=bluesky&utm_content=starterpack',
'utm_source=bluesky&utm_content=nope',
'utm_source=bluesky',
'utm_content=starterpack-haileyok.com-rkey',
]
const outputs = [
'https://bsky.app/start/haileyok.com/rkey',
'https://bsky.app/start/haileyok.com/rkey',
'https://bsky.app/start/haileyok.com/rkey',
null,
null,
null,
null,
null,
null,
null,
]
it('returns a starter pack link when input is valid', () => { it('returns a link when input contains utm_source and utm_content', () => {
for (let i = 0; i < inputs.length; i++) { expect(
const result = createStarterPackLinkFromAndroidReferrer(inputs[i]) createStarterPackLinkFromAndroidReferrer(
expect(result).toEqual(outputs[i]) 'utm_source=bluesky&utm_content=starterpack-haileyok.com-rkey',
} ),
).toEqual(validOutput)
})
it('returns a link when input contains utm_source and utm_content in different order', () => {
expect(
createStarterPackLinkFromAndroidReferrer(
'utm_content=starterpack-haileyok.com-rkey&utm_source=bluesky',
),
).toEqual(validOutput)
})
it('returns a link when input contains other parameters as well', () => {
expect(
createStarterPackLinkFromAndroidReferrer(
'utm_source=bluesky&utm_medium=starterpack&utm_content=starterpack-haileyok.com-rkey',
),
).toEqual(validOutput)
})
it('returns null when utm_source is not present', () => {
expect(
createStarterPackLinkFromAndroidReferrer(
'utm_content=starterpack-haileyok.com-rkey',
),
).toEqual(null)
})
it('returns null when utm_content is not present', () => {
expect(
createStarterPackLinkFromAndroidReferrer('utm_source=bluesky'),
).toEqual(null)
})
it('returns null when utm_content is malformed', () => {
expect(
createStarterPackLinkFromAndroidReferrer(
'utm_content=starterpack-haileyok.com',
),
).toEqual(null)
expect(
createStarterPackLinkFromAndroidReferrer('utm_content=starterpack'),
).toEqual(null)
expect(
createStarterPackLinkFromAndroidReferrer(
'utm_content=starterpack-haileyok.com-rkey-more',
),
).toEqual(null)
expect(
createStarterPackLinkFromAndroidReferrer(
'utm_content=notastarterpack-haileyok.com-rkey',
),
).toEqual(null)
}) })
}) })
describe('parseStarterPackHttpUri', () => { describe('parseStarterPackHttpUri', () => {
const inputs = [ const baseUri = 'https://bsky.app/start'
'https://bsky.app/start/haileyok.com/rkey',
'https://bsky.app/start/haileyok.com/ilovetesting',
'https://bsky.app/start/testlover9000.com/rkey',
'https://bsky.app/start/testlover9000.com',
'https://bsky.app/start/testlover9000.com/rkey/other',
'https://bsky.app/start',
]
const outputs = [
{name: 'haileyok.com', rkey: 'rkey'},
{name: 'haileyok.com', rkey: 'ilovetesting'},
{name: 'testlover9000.com', rkey: 'rkey'},
null,
null,
null,
]
it('returns the correct name and rkey when input is valid', () => { it('returns a valid at uri when http uri is valid', () => {
for (let i = 0; i < inputs.length; i++) { const validHttpUri = `${baseUri}/haileyok.com/rkey`
const result = parseStarterPackHttpUri(inputs[i]) expect(parseStarterPackUri(validHttpUri)).toEqual({
expect(result).toEqual(outputs[i]) name: 'haileyok.com',
} rkey: 'rkey',
})
const validHttpUri2 = `${baseUri}/haileyok.com/ilovetesting`
expect(parseStarterPackUri(validHttpUri2)).toEqual({
name: 'haileyok.com',
rkey: 'ilovetesting',
})
const validHttpUri3 = `${baseUri}/testlover9000.com/rkey`
expect(parseStarterPackUri(validHttpUri3)).toEqual({
name: 'testlover9000.com',
rkey: 'rkey',
})
})
it('returns null when there is no rkey', () => {
const validHttpUri = `${baseUri}/haileyok.com`
expect(parseStarterPackUri(validHttpUri)).toEqual(null)
})
it('returns null when there is an extra path', () => {
const validHttpUri = `${baseUri}/haileyok.com/rkey/other`
expect(parseStarterPackUri(validHttpUri)).toEqual(null)
})
it('returns null when there is no handle or rkey', () => {
const validHttpUri = `${baseUri}`
expect(parseStarterPackUri(validHttpUri)).toEqual(null)
})
it('returns the at uri when the input is a valid starterpack at uri', () => {
const validAtUri = 'at://did:123/app.bsky.graph.starterpack/rkey'
expect(parseStarterPackUri(validAtUri)).toEqual({
name: 'did:123',
rkey: 'rkey',
})
})
it('returns null when the at uri has no rkey', () => {
const validAtUri = 'at://did:123/app.bsky.graph.starterpack'
expect(parseStarterPackUri(validAtUri)).toEqual(null)
})
it('returns null when the collection is not app.bsky.graph.starterpack', () => {
const validAtUri = 'at://did:123/app.bsky.graph.list/rkey'
expect(parseStarterPackUri(validAtUri)).toEqual(null)
})
it('returns null when the input is undefined', () => {
expect(parseStarterPackUri(undefined)).toEqual(null)
}) })
}) })
@@ -866,13 +933,24 @@ describe('createStarterPackGooglePlayUri', () => {
const base = const base =
'https://play.google.com/store/apps/details?id=xyz.blueskyweb.app&referrer=utm_source%3Dbluesky%26utm_medium%3Dstarterpack%26utm_content%3Dstarterpack-' 'https://play.google.com/store/apps/details?id=xyz.blueskyweb.app&referrer=utm_source%3Dbluesky%26utm_medium%3Dstarterpack%26utm_content%3Dstarterpack-'
const inputs = [['name', 'rkey'], ['name'], []]
const outputs = [base + 'name-rkey', null, null]
it('returns valid google play uri when input is valid', () => { it('returns valid google play uri when input is valid', () => {
for (let i = 0; i < inputs.length; i++) { expect(createStarterPackGooglePlayUri('name', 'rkey')).toEqual(
const result = createStarterPackGooglePlayUri(inputs[i][0], inputs[i][1]) `${base}name-rkey`,
expect(result).toEqual(outputs[i]) )
} })
it('returns null when no rkey is supplied', () => {
// @ts-expect-error test
expect(createStarterPackGooglePlayUri('name', undefined)).toEqual(null)
})
it('returns null when no name or rkey are supplied', () => {
// @ts-expect-error test
expect(createStarterPackGooglePlayUri(undefined, undefined)).toEqual(null)
})
it('returns null when rkey is supplied but no name', () => {
// @ts-expect-error test
expect(createStarterPackGooglePlayUri(undefined, 'rkey')).toEqual(null)
}) })
}) })
@@ -2,21 +2,31 @@ import React from 'react'
import {createStarterPackLinkFromAndroidReferrer} from 'lib/strings/starter-pack' import {createStarterPackLinkFromAndroidReferrer} from 'lib/strings/starter-pack'
import {isAndroid} from 'platform/detection' import {isAndroid} from 'platform/detection'
import {useSetCurrentStarterPack} from 'state/preferences/starter-pack' import {useHasCheckedForStarterPack} from 'state/preferences/used-starter-packs'
import {useUsedStarterPacks} from 'state/preferences/used-starter-packs' import {useSetActiveStarterPack} from 'state/shell/starter-pack'
import SwissArmyKnife from '../../../modules/expo-bluesky-swiss-army' import SwissArmyKnife from '../../../modules/expo-bluesky-swiss-army'
import GooglePlayReferrer from '../../../modules/expo-google-play-referrer' import GooglePlayReferrer from '../../../modules/expo-google-play-referrer'
export function useStarterPackEntry() { export function useStarterPackEntry() {
const [ready, setReady] = React.useState(false) const [ready, setReady] = React.useState(false)
const setCurrentStarterPack = useSetCurrentStarterPack() const setActiveStarterPack = useSetActiveStarterPack()
const usedStarterPacks = useUsedStarterPacks() const hasCheckedForStarterPack = useHasCheckedForStarterPack()
const hasRan = React.useRef(false)
React.useEffect(() => { React.useEffect(() => {
if (ready || hasRan.current) return if (ready) return
// On Android, we cannot clear the referral link. It gets stored for 90 days and all we can do is query for it. So,
// let's just ensure we never check again after the first time.
if (hasCheckedForStarterPack) {
setReady(true)
return
}
// Safety for Android. Very unlike this could happen, but just in case. The response should be nearly immediate
const timeout = setTimeout(() => {
setReady(true)
}, 500)
hasRan.current = true
;(async () => { ;(async () => {
let uri: string | null | undefined let uri: string | null | undefined
@@ -28,17 +38,22 @@ export function useStarterPackEntry() {
} }
} else { } else {
uri = await SwissArmyKnife.getStringValueAsync('starterPackUri', true) uri = await SwissArmyKnife.getStringValueAsync('starterPackUri', true)
SwissArmyKnife.setStringValueAsync('starterPackUri', null, true)
} }
if (uri && !usedStarterPacks?.includes(uri)) { if (uri) {
setCurrentStarterPack({ setActiveStarterPack({
uri, uri,
}) })
} }
setReady(true) setReady(true)
})() })()
}, [ready, setCurrentStarterPack, usedStarterPacks])
return () => {
clearTimeout(timeout)
}
}, [ready, setActiveStarterPack, hasCheckedForStarterPack])
return ready return ready
} }
+9 -9
View File
@@ -1,25 +1,25 @@
import React from 'react' import React from 'react'
import {parseStarterPackHttpUri} from 'lib/strings/starter-pack' import {httpStarterPackUriToAtUri} from 'lib/strings/starter-pack'
import {useSetCurrentStarterPack} from 'state/preferences/starter-pack' import {useSetActiveStarterPack} from 'state/shell/starter-pack'
export function useStarterPackEntry() { export function useStarterPackEntry() {
const setCurrentStarterPack = useSetCurrentStarterPack() const setActiveStarterPack = useSetActiveStarterPack()
React.useEffect(() => { React.useEffect(() => {
const href = window.location.href const href = window.location.href
const parsed = parseStarterPackHttpUri(href) const atUri = httpStarterPackUriToAtUri(href)
if (parsed) { if (atUri) {
const url = new URL(href) const url = new URL(href)
// Determines if an App Clip is loading this landing page
const isClip = url.searchParams.get('clip') === 'true' const isClip = url.searchParams.get('clip') === 'true'
setActiveStarterPack({
setCurrentStarterPack({ uri: atUri,
uri: href,
isClip, isClip,
}) })
} }
}, [setCurrentStarterPack]) }, [setActiveStarterPack])
return true return true
} }
+40 -13
View File
@@ -1,3 +1,5 @@
import {AtUri} from '@atproto/api'
import {makeStarterPackLink} from 'lib/routes/links' import {makeStarterPackLink} from 'lib/routes/links'
export function createStarterPackLinkFromAndroidReferrer( export function createStarterPackLinkFromAndroidReferrer(
@@ -24,21 +26,35 @@ export function createStarterPackLinkFromAndroidReferrer(
} }
} }
export function parseStarterPackHttpUri(uri: string): { export function parseStarterPackUri(uri?: string): {
name?: string name: string
rkey?: string rkey: string
} | null { } | null {
try { if (!uri) return null
const url = new URL(uri)
const parts = url.pathname.split('/')
const name = parts[2]
const rkey = parts[3]
if (parts.length !== 4) return null try {
if (!name || !rkey) return null if (uri.startsWith('at://')) {
return { const atUri = new AtUri(uri)
name, if (atUri.collection !== 'app.bsky.graph.starterpack') return null
rkey, if (atUri.rkey) {
return {
name: atUri.hostname,
rkey: atUri.rkey,
}
}
return null
} else {
const url = new URL(uri)
const parts = url.pathname.split('/')
const name = parts[2]
const rkey = parts[3]
if (parts.length !== 4) return null
if (!name || !rkey) return null
return {
name,
rkey,
}
} }
} catch (e) { } catch (e) {
return null return null
@@ -52,3 +68,14 @@ export function createStarterPackGooglePlayUri(
if (!name || !rkey) return null if (!name || !rkey) return null
return `https://play.google.com/store/apps/details?id=xyz.blueskyweb.app&referrer=utm_source%3Dbluesky%26utm_medium%3Dstarterpack%26utm_content%3Dstarterpack-${name}-${rkey}` return `https://play.google.com/store/apps/details?id=xyz.blueskyweb.app&referrer=utm_source%3Dbluesky%26utm_medium%3Dstarterpack%26utm_content%3Dstarterpack-${name}-${rkey}`
} }
export function httpStarterPackUriToAtUri(httpUri?: string): string | null {
if (!httpUri) return null
const parsed = parseStarterPackUri(httpUri)
if (!parsed) return null
if (httpUri.startsWith('at://')) return httpUri
return `at://${parsed.name}/app.bsky.graph.starterpack/${parsed.rkey}`
}
+3
View File
@@ -21,6 +21,7 @@ import {logger} from '#/logger'
import {useSessionApi} from '#/state/session' import {useSessionApi} from '#/state/session'
import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useRequestNotificationsPermission} from 'lib/notifications/notifications' import {useRequestNotificationsPermission} from 'lib/notifications/notifications'
import {useSetHasCheckedForStarterPack} from 'state/preferences/used-starter-packs'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {FormError} from '#/components/forms/FormError' import {FormError} from '#/components/forms/FormError'
@@ -69,6 +70,7 @@ export const LoginForm = ({
const {login} = useSessionApi() const {login} = useSessionApi()
const requestNotificationsPermission = useRequestNotificationsPermission() const requestNotificationsPermission = useRequestNotificationsPermission()
const {setShowLoggedOut} = useLoggedOutViewControls() const {setShowLoggedOut} = useLoggedOutViewControls()
const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack()
const onPressSelectService = React.useCallback(() => { const onPressSelectService = React.useCallback(() => {
Keyboard.dismiss() Keyboard.dismiss()
@@ -116,6 +118,7 @@ export const LoginForm = ({
'LoginForm', 'LoginForm',
) )
setShowLoggedOut(false) setShowLoggedOut(false)
setHasCheckedForStarterPack(true)
requestNotificationsPermission('Login') requestNotificationsPermission('Login')
} catch (e: any) { } catch (e: any) {
const errMsg = e.toString() const errMsg = e.toString()
+21 -23
View File
@@ -15,12 +15,12 @@ import {useAgent} from '#/state/session'
import {useOnboardingDispatch} from '#/state/shell' import {useOnboardingDispatch} from '#/state/shell'
import {uploadBlob} from 'lib/api' import {uploadBlob} from 'lib/api'
import {useRequestNotificationsPermission} from 'lib/notifications/notifications' import {useRequestNotificationsPermission} from 'lib/notifications/notifications'
import {makeStarterPackLink} from 'lib/routes/links' import {useSetHasCheckedForStarterPack} from 'state/preferences/used-starter-packs'
import {useSetSelectedFeed} from 'state/shell/selected-feed'
import { import {
useCurrentStarterPack, useActiveStarterPack,
useSetCurrentStarterPack, useSetActiveStarterPack,
} from 'state/preferences/starter-pack' } from 'state/shell/starter-pack'
import {useAddUsedStarterPack} from 'state/preferences/used-starter-packs'
import { import {
DescriptionText, DescriptionText,
OnboardingControls, OnboardingControls,
@@ -48,18 +48,20 @@ export function StepFinished() {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const agent = useAgent() const agent = useAgent()
const requestNotificationsPermission = useRequestNotificationsPermission() const requestNotificationsPermission = useRequestNotificationsPermission()
const currentStarterPack = useCurrentStarterPack() const activeStarterPack = useActiveStarterPack()
const setCurrentStarterPack = useSetCurrentStarterPack() const setActiveStarterPack = useSetActiveStarterPack()
const addUsedStarterPack = useAddUsedStarterPack() const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack()
const setSelectedFeed = useSetSelectedFeed()
const finishOnboarding = React.useCallback(async () => { const finishOnboarding = React.useCallback(async () => {
setSaving(true) setSaving(true)
try { try {
let starterPack: AppBskyGraphDefs.StarterPackView | undefined let starterPack: AppBskyGraphDefs.StarterPackView | undefined
let listItems: AppBskyGraphDefs.ListItemView[] | undefined let listItems: AppBskyGraphDefs.ListItemView[] | undefined
if (currentStarterPack) {
if (activeStarterPack?.uri) {
const spRes = await agent.app.bsky.graph.getStarterPack({ const spRes = await agent.app.bsky.graph.getStarterPack({
starterPack: currentStarterPack.uri, starterPack: activeStarterPack.uri,
}) })
starterPack = spRes.data.starterPack starterPack = spRes.data.starterPack
@@ -92,17 +94,11 @@ export function StepFinished() {
pinned: true, pinned: true,
})), })),
) )
setCurrentStarterPack({ setSelectedFeed(`feedgen|${starterPack.feeds[0].uri}`)
uri: '',
initialFeed: starterPack.feeds?.[0].uri,
})
} else { } else {
setCurrentStarterPack({ setSelectedFeed('following')
uri: '',
initialFeed: 'following',
})
} }
addUsedStarterPack(makeStarterPackLink(starterPack)) setActiveStarterPack(undefined)
} }
})(), })(),
(async () => { (async () => {
@@ -150,7 +146,7 @@ export function StepFinished() {
logger.error(e) logger.error(e)
// If there was an error encountered, we need to just clear the starter pack so we don't break things for subsequent // If there was an error encountered, we need to just clear the starter pack so we don't break things for subsequent
// app restarts // app restarts
setCurrentStarterPack(undefined) setActiveStarterPack(undefined)
// don't alert the user, just let them into their account // don't alert the user, just let them into their account
} }
@@ -168,6 +164,7 @@ export function StepFinished() {
}) })
setSaving(false) setSaving(false)
setHasCheckedForStarterPack(true)
dispatch({type: 'finish'}) dispatch({type: 'finish'})
onboardDispatch({type: 'finish'}) onboardDispatch({type: 'finish'})
track('OnboardingV2:StepFinished:End') track('OnboardingV2:StepFinished:End')
@@ -179,11 +176,12 @@ export function StepFinished() {
dispatch, dispatch,
onboardDispatch, onboardDispatch,
track, track,
currentStarterPack, activeStarterPack,
state, state,
requestNotificationsPermission, requestNotificationsPermission,
addUsedStarterPack, setActiveStarterPack,
setCurrentStarterPack, setHasCheckedForStarterPack,
setSelectedFeed,
]) ])
React.useEffect(() => { React.useEffect(() => {
@@ -12,18 +12,14 @@ import {useLingui} from '@lingui/react'
import {isAndroidWeb} from 'lib/browser' import {isAndroidWeb} from 'lib/browser'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import { import {createStarterPackGooglePlayUri} from 'lib/strings/starter-pack'
createStarterPackGooglePlayUri,
parseStarterPackHttpUri,
} from 'lib/strings/starter-pack'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {useModerationOpts} from 'state/preferences/moderation-opts' import {useModerationOpts} from 'state/preferences/moderation-opts'
import {
useCurrentStarterPack,
useSetCurrentStarterPack,
} from 'state/preferences/starter-pack'
import {useResolveDidQuery} from 'state/queries/resolve-uri'
import {useStarterPackQuery} from 'state/queries/useStarterPackQuery' import {useStarterPackQuery} from 'state/queries/useStarterPackQuery'
import {
useActiveStarterPack,
useSetActiveStarterPack,
} from 'state/shell/starter-pack'
import {LoggedOutScreenState} from 'view/com/auth/LoggedOut' import {LoggedOutScreenState} from 'view/com/auth/LoggedOut'
import {CenteredView} from 'view/com/util/Views' import {CenteredView} from 'view/com/util/Views'
import {Logo} from 'view/icons/Logo' import {Logo} from 'view/icons/Logo'
@@ -54,43 +50,15 @@ export function LandingScreen({
setScreenState, setScreenState,
}: { }: {
setScreenState: (state: LoggedOutScreenState) => void setScreenState: (state: LoggedOutScreenState) => void
}) {
const currentStarterPack = useCurrentStarterPack()
const parsed = parseStarterPackHttpUri(currentStarterPack?.uri || '')
React.useEffect(() => {
if (!parsed) {
setScreenState(LoggedOutScreenState.S_LoginOrCreateAccount)
}
}, [parsed, setScreenState])
if (!parsed) {
return null
}
return <LandingScreenInner setScreenState={setScreenState} />
}
export function LandingScreenInner({
setScreenState,
}: {
setScreenState: (state: LoggedOutScreenState) => void
}) { }) {
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const currentStarterPack = useCurrentStarterPack() const activeStarterPack = useActiveStarterPack()
const {name, rkey} =
parseStarterPackHttpUri(currentStarterPack?.uri || '') ?? {}
const {
data: did,
isLoading: isLoadingDid,
isError: isErrorDid,
} = useResolveDidQuery(name)
const { const {
data: starterPack, data: starterPack,
isLoading: isLoadingStarterPack, isLoading: isLoadingStarterPack,
isError: isErrorStarterPack, isError: isErrorStarterPack,
} = useStarterPackQuery({did, rkey}) } = useStarterPackQuery({uri: activeStarterPack?.uri})
const isValid = const isValid =
starterPack && starterPack &&
@@ -98,17 +66,15 @@ export function LandingScreenInner({
AppBskyGraphStarterpack.validateRecord(starterPack.record) AppBskyGraphStarterpack.validateRecord(starterPack.record)
React.useEffect(() => { React.useEffect(() => {
if (isErrorDid || isErrorStarterPack || (starterPack && !isValid)) { if (isErrorStarterPack || (starterPack && !isValid)) {
setScreenState(LoggedOutScreenState.S_LoginOrCreateAccount) setScreenState(LoggedOutScreenState.S_LoginOrCreateAccount)
} }
}, [isErrorDid, isErrorStarterPack, setScreenState, isValid, starterPack]) }, [isErrorStarterPack, setScreenState, isValid, starterPack])
if (!did || !starterPack || !isValid || !moderationOpts) { if (!starterPack || !isValid || !moderationOpts) {
return ( return (
<ListMaybePlaceholder <ListMaybePlaceholder
isLoading={ isLoading={isLoadingStarterPack || !isValid || !moderationOpts}
isLoadingDid || isLoadingStarterPack || !isValid || !moderationOpts
}
/> />
) )
} }
@@ -136,8 +102,8 @@ function LandingScreenLoaded({
const {record, creator, listItemsSample, feeds, joinedWeekCount} = starterPack const {record, creator, listItemsSample, feeds, joinedWeekCount} = starterPack
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
const currentStarterPack = useCurrentStarterPack() const activeStarterPack = useActiveStarterPack()
const setCurrentStarterPack = useSetCurrentStarterPack() const setActiveStarterPack = useSetActiveStarterPack()
const {isTabletOrDesktop} = useWebMediaQueries() const {isTabletOrDesktop} = useWebMediaQueries()
const androidDialogControl = useDialogControl() const androidDialogControl = useDialogControl()
@@ -147,14 +113,14 @@ function LandingScreenLoaded({
const listItemsCount = starterPack.list?.listItemCount ?? 0 const listItemsCount = starterPack.list?.listItemCount ?? 0
const onContinue = () => { const onContinue = () => {
setCurrentStarterPack({ setActiveStarterPack({
uri: starterPack.uri, uri: starterPack.uri,
}) })
setScreenState(LoggedOutScreenState.S_CreateAccount) setScreenState(LoggedOutScreenState.S_CreateAccount)
} }
const onJoinPress = () => { const onJoinPress = () => {
if (currentStarterPack?.isClip) { if (activeStarterPack?.isClip) {
setAppClipOverlayVisible(true) setAppClipOverlayVisible(true)
postAppClipMessage({ postAppClipMessage({
action: 'present', action: 'present',
@@ -185,7 +151,7 @@ function LandingScreenLoaded({
borderBottomLeftRadius: 10, borderBottomLeftRadius: 10,
borderBottomRightRadius: 10, borderBottomRightRadius: 10,
}, },
currentStarterPack?.isClip && { activeStarterPack?.isClip && {
paddingTop: 100, paddingTop: 100,
}, },
]}> ]}>
@@ -312,7 +278,7 @@ function LandingScreenLoaded({
size="medium" size="medium"
style={[a.mt_2xl]} style={[a.mt_2xl]}
onPress={() => { onPress={() => {
setCurrentStarterPack(undefined) setActiveStarterPack(undefined)
setScreenState(LoggedOutScreenState.S_CreateAccount) setScreenState(LoggedOutScreenState.S_CreateAccount)
}}> }}>
<ButtonText> <ButtonText>
+1 -3
View File
@@ -113,12 +113,10 @@ function reducer(state: State, action: Action): State {
export function Provider({ export function Provider({
starterPack, starterPack,
listItems, listItems,
profile,
children, children,
}: { }: {
starterPack?: AppBskyGraphDefs.StarterPackView starterPack?: AppBskyGraphDefs.StarterPackView
listItems?: AppBskyGraphDefs.ListItemView[] listItems?: AppBskyGraphDefs.ListItemView[]
profile: AppBskyActorDefs.ProfileView
children: React.ReactNode children: React.ReactNode
}) { }) {
const createInitialState = (): State => { const createInitialState = (): State => {
@@ -138,7 +136,7 @@ export function Provider({
return { return {
canNext: true, canNext: true,
currentStep: 'Details', currentStep: 'Details',
profiles: [profile], profiles: [],
feeds: [], feeds: [],
processing: false, processing: false,
transitionDirection: 'Forward', transitionDirection: 'Forward',
+11 -2
View File
@@ -127,7 +127,7 @@ export function Wizard({
} }
return ( return (
<Provider starterPack={starterPack} listItems={listItems} profile={profile}> <Provider starterPack={starterPack} listItems={listItems}>
<WizardInner <WizardInner
did={did} did={did}
rkey={rkey} rkey={rkey}
@@ -138,6 +138,7 @@ export function Wizard({
} }
listItems={listItems} listItems={listItems}
listUri={listUri} listUri={listUri}
profile={profile}
moderationOpts={moderationOpts} moderationOpts={moderationOpts}
/> />
</Provider> </Provider>
@@ -150,6 +151,7 @@ function WizardInner({
createdAt: initialCreatedAt, createdAt: initialCreatedAt,
listUri: initialListUri, listUri: initialListUri,
listItems: initialListItems, listItems: initialListItems,
profile,
moderationOpts, moderationOpts,
}: { }: {
did?: string did?: string
@@ -157,6 +159,7 @@ function WizardInner({
createdAt?: string createdAt?: string
listUri?: string listUri?: string
listItems?: AppBskyGraphDefs.ListItemView[] listItems?: AppBskyGraphDefs.ListItemView[]
profile: AppBskyActorDefs.ProfileViewBasic
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
}) { }) {
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
@@ -470,6 +473,7 @@ function WizardInner({
onNext={onNext} onNext={onNext}
nextBtnText={currUiStrings.nextBtn} nextBtnText={currUiStrings.nextBtn}
moderationOpts={moderationOpts} moderationOpts={moderationOpts}
profile={profile}
/> />
)} )}
</CenteredView> </CenteredView>
@@ -552,10 +556,12 @@ function Footer({
onNext, onNext,
nextBtnText, nextBtnText,
moderationOpts, moderationOpts,
profile,
}: { }: {
onNext: () => void onNext: () => void
nextBtnText: string nextBtnText: string
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
profile: AppBskyActorDefs.ProfileViewBasic
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
@@ -563,7 +569,10 @@ function Footer({
const editDialogControl = useDialogControl() const editDialogControl = useDialogControl()
const {bottom: bottomInset} = useSafeAreaInsets() const {bottom: bottomInset} = useSafeAreaInsets()
const items = state.currentStep === 'Profiles' ? state.profiles : state.feeds const items =
state.currentStep === 'Profiles'
? [profile, ...state.profiles]
: state.feeds
const initialNamesIndex = state.currentStep === 'Profiles' ? 1 : 0 const initialNamesIndex = state.currentStep === 'Profiles' ? 1 : 0
const isEditEnabled = const isEditEnabled =
+2 -10
View File
@@ -87,14 +87,7 @@ export const schema = z.object({
disableHaptics: z.boolean().optional(), disableHaptics: z.boolean().optional(),
disableAutoplay: z.boolean().optional(), disableAutoplay: z.boolean().optional(),
kawaii: z.boolean().optional(), kawaii: z.boolean().optional(),
currentStarterPack: z hasCheckedForStarterPack: z.boolean().optional(),
.object({
uri: z.string(),
initialFeed: z.string().optional(),
isClip: z.boolean().optional(),
})
.optional(),
usedStarterPacks: z.array(z.string()).optional(),
/** @deprecated */ /** @deprecated */
mutedThreads: z.array(z.string()), mutedThreads: z.array(z.string()),
}) })
@@ -135,6 +128,5 @@ export const defaults: Schema = {
disableHaptics: false, disableHaptics: false,
disableAutoplay: prefersReducedMotion, disableAutoplay: prefersReducedMotion,
kawaii: false, kawaii: false,
currentStarterPack: undefined, hasCheckedForStarterPack: false,
usedStarterPacks: [],
} }
+1 -1
View File
@@ -1,5 +1,6 @@
import React from 'react' import React from 'react'
import {Provider as StarterPackProvider} from '../shell/starter-pack'
import {Provider as AltTextRequiredProvider} from './alt-text-required' import {Provider as AltTextRequiredProvider} from './alt-text-required'
import {Provider as AutoplayProvider} from './autoplay' import {Provider as AutoplayProvider} from './autoplay'
import {Provider as DisableHapticsProvider} from './disable-haptics' import {Provider as DisableHapticsProvider} from './disable-haptics'
@@ -8,7 +9,6 @@ import {Provider as HiddenPostsProvider} from './hidden-posts'
import {Provider as InAppBrowserProvider} from './in-app-browser' import {Provider as InAppBrowserProvider} from './in-app-browser'
import {Provider as KawaiiProvider} from './kawaii' import {Provider as KawaiiProvider} from './kawaii'
import {Provider as LanguagesProvider} from './languages' import {Provider as LanguagesProvider} from './languages'
import {Provider as StarterPackProvider} from './starter-pack'
import {Provider as UsedStarterPacksProvider} from './used-starter-packs' import {Provider as UsedStarterPacksProvider} from './used-starter-packs'
export { export {
-43
View File
@@ -1,43 +0,0 @@
import React from 'react'
import * as persisted from '#/state/persisted'
type StateContext =
| {
uri: string
initialFeed?: string
isClip?: boolean
}
| undefined
type SetContext = (v: StateContext) => void
const stateContext = React.createContext<StateContext>(undefined)
const setContext = React.createContext<SetContext>((_: StateContext) => {})
export function Provider({children}: {children: React.ReactNode}) {
const [state, setState] = React.useState<StateContext>(() =>
persisted.get('currentStarterPack'),
)
const setStateWrapped = (v: StateContext) => {
setState(v)
persisted.write('currentStarterPack', v)
}
React.useEffect(() => {
return persisted.onUpdate(() => {
setState(persisted.get('currentStarterPack'))
})
}, [])
return (
<stateContext.Provider value={state}>
<setContext.Provider value={setStateWrapped}>
{children}
</setContext.Provider>
</stateContext.Provider>
)
}
export const useCurrentStarterPack = () => React.useContext(stateContext)
export const useSetCurrentStarterPack = () => React.useContext(setContext)
+11 -11
View File
@@ -2,25 +2,25 @@ import React from 'react'
import * as persisted from '#/state/persisted' import * as persisted from '#/state/persisted'
type StateContext = string[] | undefined type StateContext = boolean | undefined
type SetContext = (v: string) => void type SetContext = (v: boolean) => void
const stateContext = React.createContext<StateContext>([]) const stateContext = React.createContext<StateContext>(false)
const setContext = React.createContext<SetContext>((_: string) => {}) const setContext = React.createContext<SetContext>((_: boolean) => {})
export function Provider({children}: {children: React.ReactNode}) { export function Provider({children}: {children: React.ReactNode}) {
const [state, setState] = React.useState<StateContext>(() => const [state, setState] = React.useState<StateContext>(() =>
persisted.get('usedStarterPacks'), persisted.get('hasCheckedForStarterPack'),
) )
const setStateWrapped = (v: string) => { const setStateWrapped = (v: boolean) => {
persisted.write('usedStarterPacks', [...(state ? state : []), v]) setState(v)
setState(prev => [...(prev ? prev : []), v]) persisted.write('hasCheckedForStarterPack', v)
} }
React.useEffect(() => { React.useEffect(() => {
return persisted.onUpdate(() => { return persisted.onUpdate(() => {
setState(persisted.get('usedStarterPacks')) setState(persisted.get('hasCheckedForStarterPack'))
}) })
}, []) }, [])
@@ -33,5 +33,5 @@ export function Provider({children}: {children: React.ReactNode}) {
) )
} }
export const useUsedStarterPacks = () => React.useContext(stateContext) export const useHasCheckedForStarterPack = () => React.useContext(stateContext)
export const useAddUsedStarterPack = () => React.useContext(setContext) export const useSetHasCheckedForStarterPack = () => React.useContext(setContext)
+10 -2
View File
@@ -1,30 +1,38 @@
import {StarterPackView} from '@atproto/api/dist/client/types/app/bsky/graph/defs' import {StarterPackView} from '@atproto/api/dist/client/types/app/bsky/graph/defs'
import {QueryClient, useQuery} from '@tanstack/react-query' import {QueryClient, useQuery} from '@tanstack/react-query'
import {httpStarterPackUriToAtUri} from 'lib/strings/starter-pack'
import {useAgent} from 'state/session' import {useAgent} from 'state/session'
const RQKEY_ROOT = 'starter-pack' const RQKEY_ROOT = 'starter-pack'
const RQKEY = (did?: string, rkey?: string) => [RQKEY_ROOT, did, rkey] const RQKEY = (did?: string, rkey?: string) => [RQKEY_ROOT, did, rkey]
export function useStarterPackQuery({ export function useStarterPackQuery({
uri,
did, did,
rkey, rkey,
}: { }: {
uri?: string
did?: string did?: string
rkey?: string rkey?: string
}) { }) {
const agent = useAgent() const agent = useAgent()
const uri = `at://${did}/app.bsky.graph.starterpack/${rkey}`
return useQuery<StarterPackView>({ return useQuery<StarterPackView>({
queryKey: RQKEY(did, rkey), queryKey: RQKEY(did, rkey),
queryFn: async () => { queryFn: async () => {
if (!uri) {
uri = `at://${did}/app.bsky.graph.starterpack/${rkey}`
} else if (uri && !uri.startsWith('at://')) {
// TODO remove this assertion
uri = httpStarterPackUriToAtUri(uri) as string
}
const res = await agent.app.bsky.graph.getStarterPack({ const res = await agent.app.bsky.graph.getStarterPack({
starterPack: uri, starterPack: uri,
}) })
return res.data.starterPack return res.data.starterPack
}, },
enabled: Boolean(did) && Boolean(rkey), enabled: Boolean(uri) || Boolean(did && rkey),
}) })
} }
+25
View File
@@ -0,0 +1,25 @@
import React from 'react'
type StateContext =
| {
uri: string
isClip?: boolean
}
| undefined
type SetContext = (v: StateContext) => void
const stateContext = React.createContext<StateContext>(undefined)
const setContext = React.createContext<SetContext>((_: StateContext) => {})
export function Provider({children}: {children: React.ReactNode}) {
const [state, setState] = React.useState<StateContext>()
return (
<stateContext.Provider value={state}>
<setContext.Provider value={setState}>{children}</setContext.Provider>
</stateContext.Provider>
)
}
export const useActiveStarterPack = () => React.useContext(stateContext)
export const useSetActiveStarterPack = () => React.useContext(setContext)
+28 -42
View File
@@ -22,12 +22,13 @@ import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
import {useOTAUpdates} from 'lib/hooks/useOTAUpdates' import {useOTAUpdates} from 'lib/hooks/useOTAUpdates'
import {useRequestNotificationsPermission} from 'lib/notifications/notifications' import {useRequestNotificationsPermission} from 'lib/notifications/notifications'
import {HomeTabNavigatorParams, NativeStackScreenProps} from 'lib/routes/types' import {HomeTabNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
import { import {parseStarterPackUri} from 'lib/strings/starter-pack'
useCurrentStarterPack, import {isWeb} from 'platform/detection'
useSetCurrentStarterPack,
} from 'state/preferences/starter-pack'
import {useUsedStarterPacks} from 'state/preferences/used-starter-packs'
import {useLoggedOutViewControls} from 'state/shell/logged-out' import {useLoggedOutViewControls} from 'state/shell/logged-out'
import {
useActiveStarterPack,
useSetActiveStarterPack,
} from 'state/shell/starter-pack'
import {FeedPage} from 'view/com/feeds/FeedPage' import {FeedPage} from 'view/com/feeds/FeedPage'
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager' import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState' import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState'
@@ -38,33 +39,37 @@ import {HomeHeader} from '../com/home/HomeHeader'
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'> type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
export function HomeScreen(props: Props) { export function HomeScreen(props: Props) {
const {navigation} = props
const {hasSession} = useSession()
const {data: preferences} = usePreferencesQuery() const {data: preferences} = usePreferencesQuery()
const {data: pinnedFeedInfos, isLoading: isPinnedFeedsLoading} = const {data: pinnedFeedInfos, isLoading: isPinnedFeedsLoading} =
usePinnedFeedsInfos() usePinnedFeedsInfos()
const currentStarterPack = useCurrentStarterPack() const activeStarterPack = useActiveStarterPack()
const usedStarterPacks = useUsedStarterPacks() const setActiveStarterPack = useSetActiveStarterPack()
const {setShowLoggedOut, requestSwitchToAccount} = useLoggedOutViewControls() const {setShowLoggedOut, requestSwitchToAccount} = useLoggedOutViewControls()
React.useEffect(() => { React.useEffect(() => {
if (currentStarterPack && !currentStarterPack?.initialFeed) { // This will be true if the app was launched with a starter pack referral.
// In test environments, the URL won't start with `https://bsky.app`, so we want to find it by the route if (activeStarterPack?.uri) {
try { if (hasSession) {
const route = new URL(currentStarterPack.uri).pathname const parsed = parseStarterPackUri(activeStarterPack.uri)
const foundIndex = usedStarterPacks?.findIndex(p => p.includes(route)) if (!parsed) return
if (foundIndex === -1) { setActiveStarterPack(undefined)
setShowLoggedOut(true) navigation.navigate('StarterPack', parsed)
requestSwitchToAccount({requestedAccount: 'starterpack'}) } else {
} setShowLoggedOut(true)
} catch { requestSwitchToAccount({
// Don't need to handle anything here, just put the user on the home screen requestedAccount: isWeb ? 'starterpack' : 'new',
})
} }
} }
}, [ }, [
hasSession,
setShowLoggedOut, setShowLoggedOut,
requestSwitchToAccount, requestSwitchToAccount,
currentStarterPack?.initialFeed, activeStarterPack,
currentStarterPack, setActiveStarterPack,
usedStarterPacks, navigation,
]) ])
if (preferences && pinnedFeedInfos && !isPinnedFeedsLoading) { if (preferences && pinnedFeedInfos && !isPinnedFeedsLoading) {
@@ -96,23 +101,9 @@ function HomeScreenReady({
[pinnedFeedInfos], [pinnedFeedInfos],
) )
const currentStarterPack = useCurrentStarterPack()
const setCurrentStarterPack = useSetCurrentStarterPack()
const starterPackInitialFeed = currentStarterPack?.initialFeed
? allFeeds.find(f => {
if (currentStarterPack.initialFeed === 'following') {
return f === 'following'
} else {
return f === `feedgen|${currentStarterPack.initialFeed}`
}
})
: undefined
const rawSelectedFeed = useSelectedFeed() ?? allFeeds[0] const rawSelectedFeed = useSelectedFeed() ?? allFeeds[0]
const setSelectedFeed = useSetSelectedFeed() const setSelectedFeed = useSetSelectedFeed()
const maybeFoundIndex = allFeeds.indexOf( const maybeFoundIndex = allFeeds.indexOf(rawSelectedFeed)
starterPackInitialFeed ?? rawSelectedFeed,
)
const selectedIndex = Math.max(0, maybeFoundIndex) const selectedIndex = Math.max(0, maybeFoundIndex)
const selectedFeed = allFeeds[selectedIndex] const selectedFeed = allFeeds[selectedIndex]
const requestNotificationsPermission = useRequestNotificationsPermission() const requestNotificationsPermission = useRequestNotificationsPermission()
@@ -138,12 +129,7 @@ function HomeScreenReady({
lastPagerReportedIndexRef.current = selectedIndex lastPagerReportedIndexRef.current = selectedIndex
pagerRef.current?.setPage(selectedIndex, 'desktop-sidebar-click') pagerRef.current?.setPage(selectedIndex, 'desktop-sidebar-click')
} }
}, [ }, [selectedIndex, allFeeds])
selectedIndex,
allFeeds,
setCurrentStarterPack,
currentStarterPack?.initialFeed,
])
const {hasSession} = useSession() const {hasSession} = useSession()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()