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 {
createStarterPackGooglePlayUri,
createStarterPackLinkFromAndroidReferrer,
parseStarterPackHttpUri,
parseStarterPackUri,
} from 'lib/strings/starter-pack'
import {cleanError} from '../../src/lib/strings/errors'
import {createFullHandle, makeValidHandle} from '../../src/lib/strings/handles'
@@ -803,62 +803,129 @@ describe('parseEmbedPlayerFromUrl', () => {
})
describe('createStarterPackLinkFromAndroidReferrer', () => {
const inputs = [
'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,
]
const validOutput = 'https://bsky.app/start/haileyok.com/rkey'
it('returns a starter pack link when input is valid', () => {
for (let i = 0; i < inputs.length; i++) {
const result = createStarterPackLinkFromAndroidReferrer(inputs[i])
expect(result).toEqual(outputs[i])
}
it('returns a link when input contains utm_source and utm_content', () => {
expect(
createStarterPackLinkFromAndroidReferrer(
'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', () => {
const inputs = [
'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,
]
const baseUri = 'https://bsky.app/start'
it('returns the correct name and rkey when input is valid', () => {
for (let i = 0; i < inputs.length; i++) {
const result = parseStarterPackHttpUri(inputs[i])
expect(result).toEqual(outputs[i])
}
it('returns a valid at uri when http uri is valid', () => {
const validHttpUri = `${baseUri}/haileyok.com/rkey`
expect(parseStarterPackUri(validHttpUri)).toEqual({
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 =
'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', () => {
for (let i = 0; i < inputs.length; i++) {
const result = createStarterPackGooglePlayUri(inputs[i][0], inputs[i][1])
expect(result).toEqual(outputs[i])
}
expect(createStarterPackGooglePlayUri('name', 'rkey')).toEqual(
`${base}name-rkey`,
)
})
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 {isAndroid} from 'platform/detection'
import {useSetCurrentStarterPack} from 'state/preferences/starter-pack'
import {useUsedStarterPacks} from 'state/preferences/used-starter-packs'
import {useHasCheckedForStarterPack} from 'state/preferences/used-starter-packs'
import {useSetActiveStarterPack} from 'state/shell/starter-pack'
import SwissArmyKnife from '../../../modules/expo-bluesky-swiss-army'
import GooglePlayReferrer from '../../../modules/expo-google-play-referrer'
export function useStarterPackEntry() {
const [ready, setReady] = React.useState(false)
const setCurrentStarterPack = useSetCurrentStarterPack()
const usedStarterPacks = useUsedStarterPacks()
const hasRan = React.useRef(false)
const setActiveStarterPack = useSetActiveStarterPack()
const hasCheckedForStarterPack = useHasCheckedForStarterPack()
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 () => {
let uri: string | null | undefined
@@ -28,17 +38,22 @@ export function useStarterPackEntry() {
}
} else {
uri = await SwissArmyKnife.getStringValueAsync('starterPackUri', true)
SwissArmyKnife.setStringValueAsync('starterPackUri', null, true)
}
if (uri && !usedStarterPacks?.includes(uri)) {
setCurrentStarterPack({
if (uri) {
setActiveStarterPack({
uri,
})
}
setReady(true)
})()
}, [ready, setCurrentStarterPack, usedStarterPacks])
return () => {
clearTimeout(timeout)
}
}, [ready, setActiveStarterPack, hasCheckedForStarterPack])
return ready
}
+9 -9
View File
@@ -1,25 +1,25 @@
import React from 'react'
import {parseStarterPackHttpUri} from 'lib/strings/starter-pack'
import {useSetCurrentStarterPack} from 'state/preferences/starter-pack'
import {httpStarterPackUriToAtUri} from 'lib/strings/starter-pack'
import {useSetActiveStarterPack} from 'state/shell/starter-pack'
export function useStarterPackEntry() {
const setCurrentStarterPack = useSetCurrentStarterPack()
const setActiveStarterPack = useSetActiveStarterPack()
React.useEffect(() => {
const href = window.location.href
const parsed = parseStarterPackHttpUri(href)
const atUri = httpStarterPackUriToAtUri(href)
if (parsed) {
if (atUri) {
const url = new URL(href)
// Determines if an App Clip is loading this landing page
const isClip = url.searchParams.get('clip') === 'true'
setCurrentStarterPack({
uri: href,
setActiveStarterPack({
uri: atUri,
isClip,
})
}
}, [setCurrentStarterPack])
}, [setActiveStarterPack])
return true
}
+40 -13
View File
@@ -1,3 +1,5 @@
import {AtUri} from '@atproto/api'
import {makeStarterPackLink} from 'lib/routes/links'
export function createStarterPackLinkFromAndroidReferrer(
@@ -24,21 +26,35 @@ export function createStarterPackLinkFromAndroidReferrer(
}
}
export function parseStarterPackHttpUri(uri: string): {
name?: string
rkey?: string
export function parseStarterPackUri(uri?: string): {
name: string
rkey: string
} | null {
try {
const url = new URL(uri)
const parts = url.pathname.split('/')
const name = parts[2]
const rkey = parts[3]
if (!uri) return null
if (parts.length !== 4) return null
if (!name || !rkey) return null
return {
name,
rkey,
try {
if (uri.startsWith('at://')) {
const atUri = new AtUri(uri)
if (atUri.collection !== 'app.bsky.graph.starterpack') return null
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) {
return null
@@ -52,3 +68,14 @@ export function createStarterPackGooglePlayUri(
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}`
}
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 {useLoggedOutViewControls} from '#/state/shell/logged-out'
import {useRequestNotificationsPermission} from 'lib/notifications/notifications'
import {useSetHasCheckedForStarterPack} from 'state/preferences/used-starter-packs'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {FormError} from '#/components/forms/FormError'
@@ -69,6 +70,7 @@ export const LoginForm = ({
const {login} = useSessionApi()
const requestNotificationsPermission = useRequestNotificationsPermission()
const {setShowLoggedOut} = useLoggedOutViewControls()
const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack()
const onPressSelectService = React.useCallback(() => {
Keyboard.dismiss()
@@ -116,6 +118,7 @@ export const LoginForm = ({
'LoginForm',
)
setShowLoggedOut(false)
setHasCheckedForStarterPack(true)
requestNotificationsPermission('Login')
} catch (e: any) {
const errMsg = e.toString()
+21 -23
View File
@@ -15,12 +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 {useSetHasCheckedForStarterPack} from 'state/preferences/used-starter-packs'
import {useSetSelectedFeed} from 'state/shell/selected-feed'
import {
useCurrentStarterPack,
useSetCurrentStarterPack,
} from 'state/preferences/starter-pack'
import {useAddUsedStarterPack} from 'state/preferences/used-starter-packs'
useActiveStarterPack,
useSetActiveStarterPack,
} from 'state/shell/starter-pack'
import {
DescriptionText,
OnboardingControls,
@@ -48,18 +48,20 @@ export function StepFinished() {
const queryClient = useQueryClient()
const agent = useAgent()
const requestNotificationsPermission = useRequestNotificationsPermission()
const currentStarterPack = useCurrentStarterPack()
const setCurrentStarterPack = useSetCurrentStarterPack()
const addUsedStarterPack = useAddUsedStarterPack()
const activeStarterPack = useActiveStarterPack()
const setActiveStarterPack = useSetActiveStarterPack()
const setHasCheckedForStarterPack = useSetHasCheckedForStarterPack()
const setSelectedFeed = useSetSelectedFeed()
const finishOnboarding = React.useCallback(async () => {
setSaving(true)
try {
let starterPack: AppBskyGraphDefs.StarterPackView | undefined
let listItems: AppBskyGraphDefs.ListItemView[] | undefined
if (currentStarterPack) {
if (activeStarterPack?.uri) {
const spRes = await agent.app.bsky.graph.getStarterPack({
starterPack: currentStarterPack.uri,
starterPack: activeStarterPack.uri,
})
starterPack = spRes.data.starterPack
@@ -92,17 +94,11 @@ export function StepFinished() {
pinned: true,
})),
)
setCurrentStarterPack({
uri: '',
initialFeed: starterPack.feeds?.[0].uri,
})
setSelectedFeed(`feedgen|${starterPack.feeds[0].uri}`)
} else {
setCurrentStarterPack({
uri: '',
initialFeed: 'following',
})
setSelectedFeed('following')
}
addUsedStarterPack(makeStarterPackLink(starterPack))
setActiveStarterPack(undefined)
}
})(),
(async () => {
@@ -150,7 +146,7 @@ export function StepFinished() {
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
// app restarts
setCurrentStarterPack(undefined)
setActiveStarterPack(undefined)
// don't alert the user, just let them into their account
}
@@ -168,6 +164,7 @@ export function StepFinished() {
})
setSaving(false)
setHasCheckedForStarterPack(true)
dispatch({type: 'finish'})
onboardDispatch({type: 'finish'})
track('OnboardingV2:StepFinished:End')
@@ -179,11 +176,12 @@ export function StepFinished() {
dispatch,
onboardDispatch,
track,
currentStarterPack,
activeStarterPack,
state,
requestNotificationsPermission,
addUsedStarterPack,
setCurrentStarterPack,
setActiveStarterPack,
setHasCheckedForStarterPack,
setSelectedFeed,
])
React.useEffect(() => {
@@ -12,18 +12,14 @@ import {useLingui} from '@lingui/react'
import {isAndroidWeb} from 'lib/browser'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {
createStarterPackGooglePlayUri,
parseStarterPackHttpUri,
} from 'lib/strings/starter-pack'
import {createStarterPackGooglePlayUri} from 'lib/strings/starter-pack'
import {isWeb} from 'platform/detection'
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 {
useActiveStarterPack,
useSetActiveStarterPack,
} from 'state/shell/starter-pack'
import {LoggedOutScreenState} from 'view/com/auth/LoggedOut'
import {CenteredView} from 'view/com/util/Views'
import {Logo} from 'view/icons/Logo'
@@ -54,43 +50,15 @@ export function LandingScreen({
setScreenState,
}: {
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 currentStarterPack = useCurrentStarterPack()
const {name, rkey} =
parseStarterPackHttpUri(currentStarterPack?.uri || '') ?? {}
const activeStarterPack = useActiveStarterPack()
const {
data: did,
isLoading: isLoadingDid,
isError: isErrorDid,
} = useResolveDidQuery(name)
const {
data: starterPack,
isLoading: isLoadingStarterPack,
isError: isErrorStarterPack,
} = useStarterPackQuery({did, rkey})
} = useStarterPackQuery({uri: activeStarterPack?.uri})
const isValid =
starterPack &&
@@ -98,17 +66,15 @@ export function LandingScreenInner({
AppBskyGraphStarterpack.validateRecord(starterPack.record)
React.useEffect(() => {
if (isErrorDid || isErrorStarterPack || (starterPack && !isValid)) {
if (isErrorStarterPack || (starterPack && !isValid)) {
setScreenState(LoggedOutScreenState.S_LoginOrCreateAccount)
}
}, [isErrorDid, isErrorStarterPack, setScreenState, isValid, starterPack])
}, [isErrorStarterPack, setScreenState, isValid, starterPack])
if (!did || !starterPack || !isValid || !moderationOpts) {
if (!starterPack || !isValid || !moderationOpts) {
return (
<ListMaybePlaceholder
isLoading={
isLoadingDid || isLoadingStarterPack || !isValid || !moderationOpts
}
isLoading={isLoadingStarterPack || !isValid || !moderationOpts}
/>
)
}
@@ -136,8 +102,8 @@ function LandingScreenLoaded({
const {record, creator, listItemsSample, feeds, joinedWeekCount} = starterPack
const {_} = useLingui()
const t = useTheme()
const currentStarterPack = useCurrentStarterPack()
const setCurrentStarterPack = useSetCurrentStarterPack()
const activeStarterPack = useActiveStarterPack()
const setActiveStarterPack = useSetActiveStarterPack()
const {isTabletOrDesktop} = useWebMediaQueries()
const androidDialogControl = useDialogControl()
@@ -147,14 +113,14 @@ function LandingScreenLoaded({
const listItemsCount = starterPack.list?.listItemCount ?? 0
const onContinue = () => {
setCurrentStarterPack({
setActiveStarterPack({
uri: starterPack.uri,
})
setScreenState(LoggedOutScreenState.S_CreateAccount)
}
const onJoinPress = () => {
if (currentStarterPack?.isClip) {
if (activeStarterPack?.isClip) {
setAppClipOverlayVisible(true)
postAppClipMessage({
action: 'present',
@@ -185,7 +151,7 @@ function LandingScreenLoaded({
borderBottomLeftRadius: 10,
borderBottomRightRadius: 10,
},
currentStarterPack?.isClip && {
activeStarterPack?.isClip && {
paddingTop: 100,
},
]}>
@@ -312,7 +278,7 @@ function LandingScreenLoaded({
size="medium"
style={[a.mt_2xl]}
onPress={() => {
setCurrentStarterPack(undefined)
setActiveStarterPack(undefined)
setScreenState(LoggedOutScreenState.S_CreateAccount)
}}>
<ButtonText>
+1 -3
View File
@@ -113,12 +113,10 @@ function reducer(state: State, action: Action): State {
export function Provider({
starterPack,
listItems,
profile,
children,
}: {
starterPack?: AppBskyGraphDefs.StarterPackView
listItems?: AppBskyGraphDefs.ListItemView[]
profile: AppBskyActorDefs.ProfileView
children: React.ReactNode
}) {
const createInitialState = (): State => {
@@ -138,7 +136,7 @@ export function Provider({
return {
canNext: true,
currentStep: 'Details',
profiles: [profile],
profiles: [],
feeds: [],
processing: false,
transitionDirection: 'Forward',
+11 -2
View File
@@ -127,7 +127,7 @@ export function Wizard({
}
return (
<Provider starterPack={starterPack} listItems={listItems} profile={profile}>
<Provider starterPack={starterPack} listItems={listItems}>
<WizardInner
did={did}
rkey={rkey}
@@ -138,6 +138,7 @@ export function Wizard({
}
listItems={listItems}
listUri={listUri}
profile={profile}
moderationOpts={moderationOpts}
/>
</Provider>
@@ -150,6 +151,7 @@ function WizardInner({
createdAt: initialCreatedAt,
listUri: initialListUri,
listItems: initialListItems,
profile,
moderationOpts,
}: {
did?: string
@@ -157,6 +159,7 @@ function WizardInner({
createdAt?: string
listUri?: string
listItems?: AppBskyGraphDefs.ListItemView[]
profile: AppBskyActorDefs.ProfileViewBasic
moderationOpts: ModerationOpts
}) {
const navigation = useNavigation<NavigationProp>()
@@ -470,6 +473,7 @@ function WizardInner({
onNext={onNext}
nextBtnText={currUiStrings.nextBtn}
moderationOpts={moderationOpts}
profile={profile}
/>
)}
</CenteredView>
@@ -552,10 +556,12 @@ function Footer({
onNext,
nextBtnText,
moderationOpts,
profile,
}: {
onNext: () => void
nextBtnText: string
moderationOpts: ModerationOpts
profile: AppBskyActorDefs.ProfileViewBasic
}) {
const {_} = useLingui()
const t = useTheme()
@@ -563,7 +569,10 @@ function Footer({
const editDialogControl = useDialogControl()
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 isEditEnabled =
+2 -10
View File
@@ -87,14 +87,7 @@ export const schema = z.object({
disableHaptics: z.boolean().optional(),
disableAutoplay: z.boolean().optional(),
kawaii: z.boolean().optional(),
currentStarterPack: z
.object({
uri: z.string(),
initialFeed: z.string().optional(),
isClip: z.boolean().optional(),
})
.optional(),
usedStarterPacks: z.array(z.string()).optional(),
hasCheckedForStarterPack: z.boolean().optional(),
/** @deprecated */
mutedThreads: z.array(z.string()),
})
@@ -135,6 +128,5 @@ export const defaults: Schema = {
disableHaptics: false,
disableAutoplay: prefersReducedMotion,
kawaii: false,
currentStarterPack: undefined,
usedStarterPacks: [],
hasCheckedForStarterPack: false,
}
+1 -1
View File
@@ -1,5 +1,6 @@
import React from 'react'
import {Provider as StarterPackProvider} from '../shell/starter-pack'
import {Provider as AltTextRequiredProvider} from './alt-text-required'
import {Provider as AutoplayProvider} from './autoplay'
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 KawaiiProvider} from './kawaii'
import {Provider as LanguagesProvider} from './languages'
import {Provider as StarterPackProvider} from './starter-pack'
import {Provider as UsedStarterPacksProvider} from './used-starter-packs'
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'
type StateContext = string[] | undefined
type SetContext = (v: string) => void
type StateContext = boolean | undefined
type SetContext = (v: boolean) => void
const stateContext = React.createContext<StateContext>([])
const setContext = React.createContext<SetContext>((_: string) => {})
const stateContext = React.createContext<StateContext>(false)
const setContext = React.createContext<SetContext>((_: boolean) => {})
export function Provider({children}: {children: React.ReactNode}) {
const [state, setState] = React.useState<StateContext>(() =>
persisted.get('usedStarterPacks'),
persisted.get('hasCheckedForStarterPack'),
)
const setStateWrapped = (v: string) => {
persisted.write('usedStarterPacks', [...(state ? state : []), v])
setState(prev => [...(prev ? prev : []), v])
const setStateWrapped = (v: boolean) => {
setState(v)
persisted.write('hasCheckedForStarterPack', v)
}
React.useEffect(() => {
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 useAddUsedStarterPack = () => React.useContext(setContext)
export const useHasCheckedForStarterPack = () => React.useContext(stateContext)
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 {QueryClient, useQuery} from '@tanstack/react-query'
import {httpStarterPackUriToAtUri} from 'lib/strings/starter-pack'
import {useAgent} from 'state/session'
const RQKEY_ROOT = 'starter-pack'
const RQKEY = (did?: string, rkey?: string) => [RQKEY_ROOT, did, rkey]
export function useStarterPackQuery({
uri,
did,
rkey,
}: {
uri?: string
did?: string
rkey?: string
}) {
const agent = useAgent()
const uri = `at://${did}/app.bsky.graph.starterpack/${rkey}`
return useQuery<StarterPackView>({
queryKey: RQKEY(did, rkey),
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({
starterPack: uri,
})
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 {useRequestNotificationsPermission} from 'lib/notifications/notifications'
import {HomeTabNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
import {
useCurrentStarterPack,
useSetCurrentStarterPack,
} from 'state/preferences/starter-pack'
import {useUsedStarterPacks} from 'state/preferences/used-starter-packs'
import {parseStarterPackUri} from 'lib/strings/starter-pack'
import {isWeb} from 'platform/detection'
import {useLoggedOutViewControls} from 'state/shell/logged-out'
import {
useActiveStarterPack,
useSetActiveStarterPack,
} from 'state/shell/starter-pack'
import {FeedPage} from 'view/com/feeds/FeedPage'
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState'
@@ -38,33 +39,37 @@ import {HomeHeader} from '../com/home/HomeHeader'
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
export function HomeScreen(props: Props) {
const {navigation} = props
const {hasSession} = useSession()
const {data: preferences} = usePreferencesQuery()
const {data: pinnedFeedInfos, isLoading: isPinnedFeedsLoading} =
usePinnedFeedsInfos()
const currentStarterPack = useCurrentStarterPack()
const usedStarterPacks = useUsedStarterPacks()
const activeStarterPack = useActiveStarterPack()
const setActiveStarterPack = useSetActiveStarterPack()
const {setShowLoggedOut, requestSwitchToAccount} = useLoggedOutViewControls()
React.useEffect(() => {
if (currentStarterPack && !currentStarterPack?.initialFeed) {
// In test environments, the URL won't start with `https://bsky.app`, so we want to find it by the route
try {
const route = new URL(currentStarterPack.uri).pathname
const foundIndex = usedStarterPacks?.findIndex(p => p.includes(route))
if (foundIndex === -1) {
setShowLoggedOut(true)
requestSwitchToAccount({requestedAccount: 'starterpack'})
}
} catch {
// Don't need to handle anything here, just put the user on the home screen
// This will be true if the app was launched with a starter pack referral.
if (activeStarterPack?.uri) {
if (hasSession) {
const parsed = parseStarterPackUri(activeStarterPack.uri)
if (!parsed) return
setActiveStarterPack(undefined)
navigation.navigate('StarterPack', parsed)
} else {
setShowLoggedOut(true)
requestSwitchToAccount({
requestedAccount: isWeb ? 'starterpack' : 'new',
})
}
}
}, [
hasSession,
setShowLoggedOut,
requestSwitchToAccount,
currentStarterPack?.initialFeed,
currentStarterPack,
usedStarterPacks,
activeStarterPack,
setActiveStarterPack,
navigation,
])
if (preferences && pinnedFeedInfos && !isPinnedFeedsLoading) {
@@ -96,23 +101,9 @@ function HomeScreenReady({
[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 setSelectedFeed = useSetSelectedFeed()
const maybeFoundIndex = allFeeds.indexOf(
starterPackInitialFeed ?? rawSelectedFeed,
)
const maybeFoundIndex = allFeeds.indexOf(rawSelectedFeed)
const selectedIndex = Math.max(0, maybeFoundIndex)
const selectedFeed = allFeeds[selectedIndex]
const requestNotificationsPermission = useRequestNotificationsPermission()
@@ -138,12 +129,7 @@ function HomeScreenReady({
lastPagerReportedIndexRef.current = selectedIndex
pagerRef.current?.setPage(selectedIndex, 'desktop-sidebar-click')
}
}, [
selectedIndex,
allFeeds,
setCurrentStarterPack,
currentStarterPack?.initialFeed,
])
}, [selectedIndex, allFeeds])
const {hasSession} = useSession()
const setMinimalShellMode = useSetMinimalShellMode()