SP state cleanup (#4569)
This commit is contained in:
+136
-58
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user