diff --git a/__tests__/lib/string.test.ts b/__tests__/lib/string.test.ts
index c5ae06591e..9cab8b6415 100644
--- a/__tests__/lib/string.test.ts
+++ b/__tests__/lib/string.test.ts
@@ -808,15 +808,21 @@ describe('createStarterPackLinkFromAndroidReferrer', () => {
it('returns a link when input contains utm_source and utm_content', () => {
expect(
createStarterPackLinkFromAndroidReferrer(
- 'utm_source=bluesky&utm_content=starterpack-haileyok.com-rkey',
+ 'utm_source=bluesky&utm_content=starterpack_haileyok.com_rkey',
),
).toEqual(validOutput)
+
+ expect(
+ createStarterPackLinkFromAndroidReferrer(
+ 'utm_source=bluesky&utm_content=starterpack_test-lover-9000.com_rkey',
+ ),
+ ).toEqual('https://bsky.app/start/test-lover-9000.com/rkey')
})
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',
+ 'utm_content=starterpack_haileyok.com_rkey&utm_source=bluesky',
),
).toEqual(validOutput)
})
@@ -824,7 +830,7 @@ describe('createStarterPackLinkFromAndroidReferrer', () => {
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',
+ 'utm_source=bluesky&utm_medium=starterpack&utm_content=starterpack_haileyok.com_rkey',
),
).toEqual(validOutput)
})
@@ -832,7 +838,7 @@ describe('createStarterPackLinkFromAndroidReferrer', () => {
it('returns null when utm_source is not present', () => {
expect(
createStarterPackLinkFromAndroidReferrer(
- 'utm_content=starterpack-haileyok.com-rkey',
+ 'utm_content=starterpack_haileyok.com_rkey',
),
).toEqual(null)
})
@@ -846,7 +852,7 @@ describe('createStarterPackLinkFromAndroidReferrer', () => {
it('returns null when utm_content is malformed', () => {
expect(
createStarterPackLinkFromAndroidReferrer(
- 'utm_content=starterpack-haileyok.com',
+ 'utm_content=starterpack_haileyok.com',
),
).toEqual(null)
@@ -856,13 +862,13 @@ describe('createStarterPackLinkFromAndroidReferrer', () => {
expect(
createStarterPackLinkFromAndroidReferrer(
- 'utm_content=starterpack-haileyok.com-rkey-more',
+ 'utm_content=starterpack_haileyok.com_rkey_more',
),
).toEqual(null)
expect(
createStarterPackLinkFromAndroidReferrer(
- 'utm_content=notastarterpack-haileyok.com-rkey',
+ 'utm_content=notastarterpack_haileyok.com_rkey',
),
).toEqual(null)
})
@@ -906,6 +912,23 @@ describe('parseStarterPackHttpUri', () => {
expect(parseStarterPackUri(validHttpUri)).toEqual(null)
})
+ it('returns null when the route is not /start or /starter-pack', () => {
+ const validHttpUri = 'https://bsky.app/start/haileyok.com/rkey'
+ expect(parseStarterPackUri(validHttpUri)).toEqual({
+ name: 'haileyok.com',
+ rkey: 'rkey',
+ })
+
+ const validHttpUri2 = 'https://bsky.app/starter-pack/haileyok.com/rkey'
+ expect(parseStarterPackUri(validHttpUri2)).toEqual({
+ name: 'haileyok.com',
+ rkey: 'rkey',
+ })
+
+ const invalidHttpUri = 'https://bsky.app/profile/haileyok.com/rkey'
+ expect(parseStarterPackUri(invalidHttpUri)).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({
@@ -931,11 +954,11 @@ describe('parseStarterPackHttpUri', () => {
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-'
+ 'https://play.google.com/store/apps/details?id=xyz.blueskyweb.app&referrer=utm_source%3Dbluesky%26utm_medium%3Dstarterpack%26utm_content%3Dstarterpack_'
it('returns valid google play uri when input is valid', () => {
expect(createStarterPackGooglePlayUri('name', 'rkey')).toEqual(
- `${base}name-rkey`,
+ `${base}name_rkey`,
)
})
diff --git a/assets/icons/starterPack.svg b/assets/icons/starterPack.svg
new file mode 100644
index 0000000000..7f0df55952
--- /dev/null
+++ b/assets/icons/starterPack.svg
@@ -0,0 +1 @@
+
diff --git a/src/Navigation.tsx b/src/Navigation.tsx
index ec2dc6a69b..a7b9dbe462 100644
--- a/src/Navigation.tsx
+++ b/src/Navigation.tsx
@@ -384,6 +384,7 @@ function HomeTabNavigator() {
contentStyle: pal.view,
}}>
HomeScreen} />
+ HomeScreen} />
{commonScreens(HomeTab)}
)
@@ -520,6 +521,11 @@ const FlatNavigator = () => {
getComponent={() => MessagesScreen}
options={{title: title(msg`Messages`), requireAuth: true}}
/>
+ HomeScreen}
+ options={{title: title(msg`Home`)}}
+ />
{commonScreens(Flat as typeof HomeTab, numUnread)}
)
diff --git a/src/components/FeedCard.tsx b/src/components/FeedCard.tsx
index 94d97cb620..fae599bc3b 100644
--- a/src/components/FeedCard.tsx
+++ b/src/components/FeedCard.tsx
@@ -54,7 +54,11 @@ export function Link({
const handleOrDid = feed.creator.handle || feed.creator.did
return `/profile/${handleOrDid}/feed/${urip.rkey}`
}, [feed])
- return {children}
+ return (
+
+ {children}
+
+ )
}
export function Outer({children}: {children: React.ReactNode}) {
diff --git a/src/components/NewskieDialog.tsx b/src/components/NewskieDialog.tsx
index 0354bfc432..da57603fa6 100644
--- a/src/components/NewskieDialog.tsx
+++ b/src/components/NewskieDialog.tsx
@@ -9,11 +9,12 @@ import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {HITSLOP_10} from 'lib/constants'
import {sanitizeDisplayName} from 'lib/strings/display-names'
-import {atoms as a} from '#/alf'
+import {atoms as a, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {useDialogControl} from '#/components/Dialog'
import {Newskie} from '#/components/icons/Newskie'
+import {Default as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
import {Text} from '#/components/Typography'
export function NewskieDialog({
@@ -24,6 +25,7 @@ export function NewskieDialog({
disabled?: boolean
}) {
const {_} = useLingui()
+ const t = useTheme()
const moderationOpts = useModerationOpts()
const control = useDialogControl()
const profileName = React.useMemo(() => {
@@ -72,11 +74,30 @@ export function NewskieDialog({
Say hello!
-
- {profileName} joined Bluesky{' '}
- {timeAgo(createdAt, now, {format: 'long'})} ago
-
+ {profile.joinedViaStarterPack ? (
+
+ {profileName} joined Bluesky using a starter pack{' '}
+ {timeAgo(createdAt, now, {format: 'long'})} ago
+
+ ) : (
+
+ {profileName} joined Bluesky{' '}
+ {timeAgo(createdAt, now, {format: 'long'})} ago
+
+ )}
+ {profile.joinedViaStarterPack ? (
+
+
+
+ ) : null}
diff --git a/src/components/StarterPack/Main/FeedsList.tsx b/src/components/StarterPack/Main/FeedsList.tsx
index b2e395ebf6..84110998d8 100644
--- a/src/components/StarterPack/Main/FeedsList.tsx
+++ b/src/components/StarterPack/Main/FeedsList.tsx
@@ -4,8 +4,7 @@ import {AppBskyFeedDefs} from '@atproto/api'
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
import {useBottomBarOffset} from 'lib/hooks/useBottomBarOffset'
-import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
-import {isNative} from 'platform/detection'
+import {isNative, isWeb} from 'platform/detection'
import {List, ListRef} from 'view/com/util/List'
import {SectionRef} from '#/screens/Profile/Sections/types'
import {atoms as a, useTheme} from '#/alf'
@@ -25,7 +24,6 @@ export const FeedsList = React.forwardRef(
function FeedsListImpl({feeds, headerHeight, scrollElRef}, ref) {
const [initialHeaderHeight] = React.useState(headerHeight)
const bottomBarOffset = useBottomBarOffset(20)
- const {isTabletOrDesktop} = useWebMediaQueries()
const t = useTheme()
const onScrollToTop = useCallback(() => {
@@ -44,7 +42,7 @@ export const FeedsList = React.forwardRef(
diff --git a/src/components/StarterPack/Main/ProfilesList.tsx b/src/components/StarterPack/Main/ProfilesList.tsx
index cce14f59ab..cc3a80ec04 100644
--- a/src/components/StarterPack/Main/ProfilesList.tsx
+++ b/src/components/StarterPack/Main/ProfilesList.tsx
@@ -40,19 +40,26 @@ export const ProfilesList = React.forwardRef(
const {currentAccount} = useSession()
const [isPTRing, setIsPTRing] = React.useState(false)
- const {data, refetch} = useListMembersQuery(listUri)
- const profiles = data?.pages.flatMap(p => p.items.map(i => i.subject))
+ const {data, refetch} = useListMembersQuery(listUri, 50)
+
+ // The server returns these sorted by descending creation date, so we want to invert
+ const profiles = data?.pages
+ .flatMap(p => p.items.map(i => i.subject))
+ .reverse()
const isOwn = new AtUri(listUri).host === currentAccount?.did
const getSortedProfiles = () => {
if (!profiles) return
if (!isOwn) return profiles
+
const myIndex = profiles.findIndex(p => p.did === currentAccount?.did)
- return [
- profiles[myIndex],
- ...profiles.slice(0, myIndex),
- ...profiles.slice(myIndex + 1),
- ]
+ return myIndex !== -1
+ ? [
+ profiles[myIndex],
+ ...profiles.slice(0, myIndex),
+ ...profiles.slice(myIndex + 1),
+ ]
+ : profiles
}
const onScrollToTop = useCallback(() => {
scrollElRef.current?.scrollToOffset({
diff --git a/src/components/StarterPack/NewskieDialog.tsx b/src/components/StarterPack/NewskieDialog.tsx
deleted file mode 100644
index 0354bfc432..0000000000
--- a/src/components/StarterPack/NewskieDialog.tsx
+++ /dev/null
@@ -1,85 +0,0 @@
-import React from 'react'
-import {View} from 'react-native'
-import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
-import {msg, Trans} from '@lingui/macro'
-import {useLingui} from '@lingui/react'
-import {differenceInSeconds} from 'date-fns'
-
-import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
-import {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {HITSLOP_10} from 'lib/constants'
-import {sanitizeDisplayName} from 'lib/strings/display-names'
-import {atoms as a} from '#/alf'
-import {Button} from '#/components/Button'
-import * as Dialog from '#/components/Dialog'
-import {useDialogControl} from '#/components/Dialog'
-import {Newskie} from '#/components/icons/Newskie'
-import {Text} from '#/components/Typography'
-
-export function NewskieDialog({
- profile,
- disabled,
-}: {
- profile: AppBskyActorDefs.ProfileViewDetailed
- disabled?: boolean
-}) {
- const {_} = useLingui()
- const moderationOpts = useModerationOpts()
- const control = useDialogControl()
- const profileName = React.useMemo(() => {
- const name = profile.displayName || profile.handle
- if (!moderationOpts) return name
- const moderation = moderateProfile(profile, moderationOpts)
- return sanitizeDisplayName(name, moderation.ui('displayName'))
- }, [moderationOpts, profile])
- const [now] = React.useState(() => Date.now())
- const timeAgo = useGetTimeAgo()
- const createdAt = profile.createdAt as string | undefined
- const daysOld = React.useMemo(() => {
- if (!createdAt) return Infinity
- return differenceInSeconds(now, new Date(createdAt)) / 86400
- }, [createdAt, now])
-
- if (!createdAt || daysOld > 7) return null
-
- return (
-
-
-
-
-
-
-
-
- Say hello!
-
-
-
- {profileName} joined Bluesky{' '}
- {timeAgo(createdAt, now, {format: 'long'})} ago
-
-
-
-
-
-
- )
-}
diff --git a/src/components/StarterPack/ProfileStarterPacks.tsx b/src/components/StarterPack/ProfileStarterPacks.tsx
index 5ad86a82be..c2c2fa7839 100644
--- a/src/components/StarterPack/ProfileStarterPacks.tsx
+++ b/src/components/StarterPack/ProfileStarterPacks.tsx
@@ -25,12 +25,13 @@ import {useAgent} from 'state/session'
import {List, ListRef} from 'view/com/util/List'
import {Text} from 'view/com/util/text/Text'
import {atoms as a, useTheme} from '#/alf'
-import {Button, ButtonText} from '#/components/Button'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {LinearGradientBackground} from '#/components/LinearGradientBackground'
import {Loader} from '#/components/Loader'
import * as Prompt from '#/components/Prompt'
import {Default as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
+import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '../icons/Plus'
interface SectionRef {
scrollToTop: () => void
@@ -47,6 +48,7 @@ interface ProfileFeedgensProps {
style?: StyleProp
testID?: string
setScrollViewTag: (tag: number | null) => void
+ isMe: boolean
}
function keyExtractor(item: AppBskyGraphDefs.StarterPackView) {
@@ -65,6 +67,7 @@ export const ProfileStarterPacks = React.forwardRef<
style,
testID,
setScrollViewTag,
+ isMe,
},
ref,
) {
@@ -140,7 +143,9 @@ export const ProfileStarterPacks = React.forwardRef<
onEndReached={onEndReached}
onRefresh={onRefresh}
ListEmptyComponent={Empty}
- ListFooterComponent={items?.length !== 0 ? CreateAnother : undefined}
+ ListFooterComponent={
+ items?.length !== 0 && isMe ? CreateAnother : undefined
+ }
/>
)
@@ -148,19 +153,29 @@ export const ProfileStarterPacks = React.forwardRef<
function CreateAnother() {
const {_} = useLingui()
+ const t = useTheme()
const navigation = useNavigation()
return (
-
+
)
@@ -203,8 +218,8 @@ function Empty() {
return (
- Generate a starter pack?
+ Generate a starter pack
- You can customize your starter pack with feeds and your favorite
- people if you create your own.
+ Bluesky will choose a set of recommended accounts from people in
+ your network.
{
- navigation.navigate('StarterPackWizard')
- }}
+ cta={_(msg`Choose for me`)}
+ onPress={generate}
/>
{
+ navigation.navigate('StarterPackWizard')
+ }}
/>
diff --git a/src/components/StarterPack/QrCode.tsx b/src/components/StarterPack/QrCode.tsx
index deb55222a8..6d348194d6 100644
--- a/src/components/StarterPack/QrCode.tsx
+++ b/src/components/StarterPack/QrCode.tsx
@@ -69,7 +69,7 @@ export const QrCode = React.forwardRef(function QrCode(
on
-
+
diff --git a/src/components/StarterPack/QrCodeDialog.tsx b/src/components/StarterPack/QrCodeDialog.tsx
index 3c2068166f..2eb31ce8d5 100644
--- a/src/components/StarterPack/QrCodeDialog.tsx
+++ b/src/components/StarterPack/QrCodeDialog.tsx
@@ -1,9 +1,9 @@
import React from 'react'
import {View} from 'react-native'
import ViewShot from 'react-native-view-shot'
-import {setImageAsync} from 'expo-clipboard'
import * as FS from 'expo-file-system'
import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
+import * as Sharing from 'expo-sharing'
import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
@@ -12,14 +12,14 @@ import {nanoid} from 'nanoid/non-secure'
import {logger} from '#/logger'
import {saveImageToMediaLibrary} from 'lib/media/manip'
import {logEvent} from 'lib/statsig/statsig'
-import {isNative} from 'platform/detection'
+import {isNative, isWeb} from 'platform/detection'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {DialogControlProps} from '#/components/Dialog'
+import {Loader} from '#/components/Loader'
import {QrCode} from '#/components/StarterPack/QrCode'
-import {Text} from '#/components/Typography'
export function QrCodeDialog({
control,
@@ -29,6 +29,7 @@ export function QrCodeDialog({
starterPack: AppBskyGraphDefs.StarterPackView
}) {
const {_} = useLingui()
+ const [isProcessing, setIsProcessing] = React.useState(false)
const ref = React.useRef(null)
@@ -75,6 +76,8 @@ export function QrCodeDialog({
return
}
} else {
+ setIsProcessing(true)
+
if (!AppBskyGraphStarterpack.isRecord(starterPack.record)) {
return
}
@@ -98,24 +101,25 @@ export function QrCodeDialog({
shareType: 'qrcode',
qrShareType: 'save',
})
- Toast.show(_(msg`QR code saved to your camera roll!`))
+ setIsProcessing(false)
+ Toast.show(
+ isWeb
+ ? _(msg`QR code has been downloaded!`)
+ : _(msg`QR code saved to your camera roll!`),
+ )
control.close()
})
}
const onCopyPress = async () => {
+ setIsProcessing(true)
ref.current?.capture?.().then(async (uri: string) => {
- if (isNative) {
- const base64 = await FS.readAsStringAsync(uri, {encoding: 'base64'})
- await setImageAsync(base64)
- } else {
- const canvas = await getCanvas(uri)
- // @ts-expect-error web only
- canvas.toBlob((blob: Blob) => {
- const item = new ClipboardItem({'image/png': blob})
- navigator.clipboard.write([item])
- })
- }
+ const canvas = await getCanvas(uri)
+ // @ts-expect-error web only
+ canvas.toBlob((blob: Blob) => {
+ const item = new ClipboardItem({'image/png': blob})
+ navigator.clipboard.write([item])
+ })
logEvent('starterPack:share', {
starterPack: starterPack.uri,
@@ -123,42 +127,62 @@ export function QrCodeDialog({
qrShareType: 'copy',
})
Toast.show(_(msg`QR code copied to your clipboard!`))
+ setIsProcessing(false)
control.close()
})
}
+ const onSharePress = async () => {
+ ref.current?.capture?.().then(async (uri: string) => {
+ control.close(() => {
+ Sharing.shareAsync(uri, {mimeType: 'image/png', UTI: 'image/png'}).then(
+ () => {
+ logEvent('starterPack:share', {
+ starterPack: starterPack.uri,
+ shareType: 'qrcode',
+ qrShareType: 'share',
+ })
+ },
+ )
+ })
+ })
+ }
+
return (
-
- Share this starter pack with friends!
-
-
-
-
-
+ {isProcessing ? (
+
+
+
+ ) : (
+
+
+
+
+ )}
diff --git a/src/components/StarterPack/StarterPackCard.tsx b/src/components/StarterPack/StarterPackCard.tsx
index c80e794cc0..b5532fc437 100644
--- a/src/components/StarterPack/StarterPackCard.tsx
+++ b/src/components/StarterPack/StarterPackCard.tsx
@@ -8,7 +8,7 @@ import {useLingui} from '@lingui/react'
import {sanitizeHandle} from 'lib/strings/handles'
import {useSession} from 'state/session'
import {atoms as a, useTheme} from '#/alf'
-import {StarterPackIcon} from '#/components/icons/StarterPackIcon'
+import {StarterPack} from '#/components/icons/StarterPack'
import {Link} from '#/components/Link'
import {Text} from '#/components/Typography'
@@ -25,9 +25,9 @@ export function Default({starterPack}: {starterPack: StarterPackViewBasic}) {
}
return (
-
-
-
+
+
+
{record.name}
@@ -43,7 +43,9 @@ export function Default({starterPack}: {starterPack: StarterPackViewBasic}) {
{record.description && (
- {record.description}
+
+ {record.description}
+
)}
{!!joinedAllTimeCount && joinedAllTimeCount >= 50 && (
@@ -55,10 +57,12 @@ export function Default({starterPack}: {starterPack: StarterPackViewBasic}) {
}
function Wrapper({
+ name,
creator,
children,
rkey,
}: {
+ name: string
creator: AppBskyActorDefs.ProfileViewBasic
rkey: string
children: React.ReactNode
@@ -68,7 +72,8 @@ function Wrapper({
to={{
screen: 'StarterPack',
params: {name: creator.handle || creator.did, rkey},
- }}>
+ }}
+ label={name}>
{children}
)
diff --git a/src/components/StarterPack/Wizard/WizardEditListDialog.tsx b/src/components/StarterPack/Wizard/WizardEditListDialog.tsx
index 1aaa7d076d..bf250ac354 100644
--- a/src/components/StarterPack/Wizard/WizardEditListDialog.tsx
+++ b/src/components/StarterPack/Wizard/WizardEditListDialog.tsx
@@ -31,11 +31,13 @@ export function WizardEditListDialog({
state,
dispatch,
moderationOpts,
+ profile,
}: {
control: Dialog.DialogControlProps
state: WizardState
dispatch: (action: WizardAction) => void
moderationOpts: ModerationOpts
+ profile: AppBskyActorDefs.ProfileViewBasic
}) {
const {_} = useLingui()
const t = useTheme()
@@ -46,12 +48,9 @@ export function WizardEditListDialog({
const getData = () => {
if (state.currentStep === 'Feeds') return state.feeds
- const myIndex = state.profiles.findIndex(p => p.did === currentAccount?.did)
-
return [
- state.profiles[myIndex],
- ...state.profiles.slice(0, myIndex),
- ...state.profiles.slice(myIndex + 1),
+ profile,
+ ...state.profiles.filter(p => p.did !== currentAccount?.did),
]
}
diff --git a/src/components/StarterPack/Wizard/WizardListCard.tsx b/src/components/StarterPack/Wizard/WizardListCard.tsx
index ec6c3549c2..da217ecbf6 100644
--- a/src/components/StarterPack/Wizard/WizardListCard.tsx
+++ b/src/components/StarterPack/Wizard/WizardListCard.tsx
@@ -51,6 +51,10 @@ function WizardListCard({
return (
-
-
-
+
+
+
+
+
)
}
@@ -110,9 +116,9 @@ export function WizardProfileCard({
}) {
const {currentAccount} = useSession()
- const included = state.profiles.some(p => p.did === profile.did)
const isMe = profile.did === currentAccount?.did
- const disabled = isMe || state.profiles.length >= 50
+ const included = isMe || state.profiles.some(p => p.did === profile.did)
+ const disabled = isMe || (!included && state.profiles.length >= 49)
const moderationUi = moderateProfile(profile, moderationOpts).ui('avatar')
const displayName = profile.displayName
? sanitizeDisplayName(profile.displayName)
diff --git a/src/components/hooks/useStarterPackEntry.ts b/src/components/hooks/useStarterPackEntry.ts
index 3518fbced2..dba801e093 100644
--- a/src/components/hooks/useStarterPackEntry.ts
+++ b/src/components/hooks/useStarterPackEntry.ts
@@ -4,6 +4,8 @@ import {httpStarterPackUriToAtUri} from 'lib/strings/starter-pack'
import {useSetActiveStarterPack} from 'state/shell/starter-pack'
export function useStarterPackEntry() {
+ const [ready, setReady] = React.useState(false)
+
const setActiveStarterPack = useSetActiveStarterPack()
React.useEffect(() => {
@@ -19,7 +21,9 @@ export function useStarterPackEntry() {
isClip,
})
}
+
+ setReady(true)
}, [setActiveStarterPack])
- return true
+ return ready
}
diff --git a/src/components/icons/StarterPack.tsx b/src/components/icons/StarterPack.tsx
new file mode 100644
index 0000000000..8c678bca47
--- /dev/null
+++ b/src/components/icons/StarterPack.tsx
@@ -0,0 +1,8 @@
+import {createMultiPathSVG} from './TEMPLATE'
+
+export const StarterPack = createMultiPathSVG({
+ paths: [
+ 'M11.26 5.227 5.02 6.899c-.734.197-1.17.95-.973 1.685l1.672 6.24c.197.734.951 1.17 1.685.973l6.24-1.672c.734-.197 1.17-.951.973-1.685L12.945 6.2a1.375 1.375 0 0 0-1.685-.973Zm-6.566.459a2.632 2.632 0 0 0-1.86 3.223l1.672 6.24a2.632 2.632 0 0 0 3.223 1.861l6.24-1.672a2.631 2.631 0 0 0 1.861-3.223l-1.672-6.24a2.632 2.632 0 0 0-3.223-1.861l-6.24 1.672Z',
+ 'M15.138 18.411a4.606 4.606 0 1 0 0-9.211 4.606 4.606 0 0 0 0 9.211Zm0 1.257a5.862 5.862 0 1 0 0-11.724 5.862 5.862 0 0 0 0 11.724Z',
+ ],
+})
diff --git a/src/components/icons/StarterPackIcon.tsx b/src/components/icons/StarterPackIcon.tsx
deleted file mode 100644
index dea27ea85d..0000000000
--- a/src/components/icons/StarterPackIcon.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import * as React from 'react'
-import Svg, {Defs, LinearGradient, Path, Stop} from 'react-native-svg'
-export function StarterPackIcon({...props}: React.ComponentProps) {
- return (
-
- )
-}
diff --git a/src/components/icons/TEMPLATE.tsx b/src/components/icons/TEMPLATE.tsx
index f49c4280bb..47a5c36b2a 100644
--- a/src/components/icons/TEMPLATE.tsx
+++ b/src/components/icons/TEMPLATE.tsx
@@ -30,7 +30,7 @@ export const IconTemplate_Stroke2_Corner0_Rounded = React.forwardRef(
export function createSinglePathSVG({path}: {path: string}) {
return React.forwardRef
-
+
{record.description ? (
{record.description}
) : null}
-
- {joinedWeekCount && joinedWeekCount >= 25 ? (
-
- {joinedWeekCount} joined this week!
-
- ) : null}
-
+
+
+ {joinedWeekCount && joinedWeekCount >= 25 ? (
+
+
+
+ 123,659 joined this week
+
+
+ ) : null}
+
- {starterPack.feeds?.length ? (
+ {Boolean(listItemsSample?.length) && (
-
- These great feeds will be available after signing up!
+
+ {listItemsCount <= 8 ? (
+ You'll follow these people right away
+ ) : (
+
+ You'll follow these people and {listItemsCount - 8} others
+
+ )}
+
+
+ {starterPack.listItemsSample?.slice(0, 8).map(item => (
+
+
+
+ ))}
+
+
+ )}
+ {feeds?.length ? (
+
+
+ You'll stay updated with these feeds
-
- {starterPack.feeds?.map((feed, index) => (
+
+ {feeds?.map(feed => (
@@ -222,69 +265,22 @@ function LandingScreenLoaded({
) : null}
-
- {Boolean(listItemsSample?.length) && (
-
-
- {feeds?.length ? (
- <>
- {listItemsCount <= 8 ? (
-
- You'll also follow these people right away!
-
- ) : (
-
- You'll also follow these people and{' '}
- {listItemsCount - 8} others!
-
- )}
- >
- ) : (
- <>
- {listItemsCount <= 8 ? (
- You'll follow these people right away!
- ) : (
-
- You'll follow these people and {listItemsCount - 8}{' '}
- others!
-
- )}
- >
- )}
-
-
- {starterPack.listItemsSample
- ?.slice(0, 8)
- .map((item, index) => (
-
-
-
- ))}
-
-
- )}
+
-
{
if (!starterPack.list) return
@@ -182,7 +184,7 @@ function Header({
avatar={undefined}
creator={creator}
avatarType="starter-pack">
-
+
{isOwn ? (
-
- {record.description}
- {starterPack.joinedWeekCount && starterPack.joinedWeekCount >= 25 ? (
-
-
- {starterPack.joinedAllTimeCount || 0} people have used this
- starter pack!
-
-
- ) : null}
-
+ {record.description || joinedAllTimeCount >= 25 ? (
+
+ {record.description ? (
+ {record.description}
+ ) : null}
+ {joinedAllTimeCount >= 25 ? (
+
+
+
+
+ {starterPack.joinedAllTimeCount || 0} people have used this
+ starter pack!
+
+
+
+ ) : null}
+
+ ) : null}
{
if (starterPack && AppBskyGraphStarterpack.isRecord(starterPack.record)) {
return {
@@ -126,7 +129,10 @@ export function Provider({
currentStep: 'Details',
name: starterPack.record.name,
description: starterPack.record.description,
- profiles: listItems?.map(i => i.subject) ?? [],
+ profiles:
+ listItems
+ ?.map(i => i.subject)
+ .filter(p => p.did !== currentAccount?.did) ?? [],
feeds: starterPack.feeds ?? [],
processing: false,
transitionDirection: 'Forward',
diff --git a/src/screens/StarterPack/Wizard/StepDetails.tsx b/src/screens/StarterPack/Wizard/StepDetails.tsx
index 42ef0c20a2..24c992c60b 100644
--- a/src/screens/StarterPack/Wizard/StepDetails.tsx
+++ b/src/screens/StarterPack/Wizard/StepDetails.tsx
@@ -8,7 +8,7 @@ import {useSession} from 'state/session'
import {useWizardState} from '#/screens/StarterPack/Wizard/State'
import {atoms as a, useTheme} from '#/alf'
import * as TextField from '#/components/forms/TextField'
-import {StarterPackIcon} from '#/components/icons/StarterPackIcon'
+import {StarterPack} from '#/components/icons/StarterPack'
import {ScreenTransition} from '#/components/StarterPack/Wizard/ScreenTransition'
import {Text} from '#/components/Typography'
@@ -26,10 +26,8 @@ export function StepDetails() {
return (
-
-
-
+
Invites, but personal
diff --git a/src/screens/StarterPack/Wizard/StepProfiles.tsx b/src/screens/StarterPack/Wizard/StepProfiles.tsx
index c33bcb09c4..b24c01e523 100644
--- a/src/screens/StarterPack/Wizard/StepProfiles.tsx
+++ b/src/screens/StarterPack/Wizard/StepProfiles.tsx
@@ -70,8 +70,9 @@ export function StepProfiles({
sideBorders={false}
style={[a.flex_1]}
onEndReached={() => {
- console.log('test')
- fetchNextPage()
+ if (!query) {
+ fetchNextPage()
+ }
}}
onEndReachedThreshold={isNative ? 2 : 0.25}
ListEmptyComponent={
diff --git a/src/screens/StarterPack/Wizard/index.tsx b/src/screens/StarterPack/Wizard/index.tsx
index 534460d0cf..e8c01fd5c9 100644
--- a/src/screens/StarterPack/Wizard/index.tsx
+++ b/src/screens/StarterPack/Wizard/index.tsx
@@ -104,7 +104,7 @@ export function Wizard({
isLoadingStarterPack || isLoadingProfiles || isLoadingProfile
}
isError={isErrorStarterPack || isErrorProfiles || isErrorProfile}
- errorMessage={_(msg`Could not find that starter pack`)}
+ errorMessage={_(msg`That starter pack could not be found.`)}
/>
)
} else if (isEdit && starterPack?.creator.did !== currentAccount?.did) {
@@ -112,7 +112,7 @@ export function Wizard({
)
}
@@ -182,11 +182,18 @@ function WizardInner({
}, [setMinimalShellMode, setEnabled]),
)
- const defaultName = _(
- msg`${
- currentProfile?.displayName || `@${currentProfile?.handle}`
- }'s Starter Pack`,
- ).slice(0, 50)
+ const getDefaultName = () => {
+ let displayName
+ if (
+ currentProfile?.displayName != null &&
+ currentProfile?.displayName !== ''
+ ) {
+ displayName = sanitizeDisplayName(currentProfile.displayName)
+ } else {
+ displayName = sanitizeHandle(currentProfile!.handle)
+ }
+ return _(msg`${displayName}'s Starter Pack`).slice(0, 50)
+ }
const wizardUiStrings: Record<
WizardStep,
@@ -273,7 +280,7 @@ function WizardInner({
}
} else {
list = await createStarterPackList({
- name: state.name ?? defaultName,
+ name: state.name ?? getDefaultName(),
description: state.description,
descriptionFacets: [],
profiles: state.profiles,
@@ -286,7 +293,7 @@ function WizardInner({
collection: 'app.bsky.graph.starterpack',
rkey,
record: {
- name: state.name ?? defaultName,
+ name: state.name ?? getDefaultName(),
description: state.description,
descriptionFacets: [],
list: list?.uri,
@@ -299,9 +306,8 @@ function WizardInner({
validate: false, // TODO remove!
})
- await invalidateQueries()
-
- setTimeout(() => {
+ setTimeout(async () => {
+ await invalidateQueries()
if (navigation.canGoBack()) {
navigation.goBack()
} else {
@@ -311,11 +317,11 @@ function WizardInner({
})
}
dispatch({type: 'SetProcessing', processing: false})
- }, 1000)
+ }, 2000)
} else {
// Creating a new starter pack
const list = await createStarterPackList({
- name: state.name ?? defaultName,
+ name: state.name ?? getDefaultName(),
description: state.description,
descriptionFacets: [],
profiles: state.profiles,
@@ -327,7 +333,7 @@ function WizardInner({
validate: false,
},
{
- name: state.name ?? defaultName,
+ name: state.name ?? getDefaultName(),
description: state.description,
descriptionFacets: [],
list: list.uri,
@@ -346,16 +352,18 @@ function WizardInner({
})
const newRkey = new AtUri(res.uri).rkey
- setTimeout(() => {
+
+ setTimeout(async () => {
+ await invalidateQueries()
navigation.replace('StarterPack', {
name: currentAccount!.handle,
rkey: newRkey,
})
dispatch({type: 'SetProcessing', processing: false})
- }, 1000)
+ }, 2000)
}
} catch (e: unknown) {
- logger.error('Failed to create starter pack', {error: e})
+ logger.error('Failed to create starter pack', {safeMessage: e})
Toast.show(_(msg`Failed to create starter pack`))
dispatch({type: 'SetProcessing', processing: false})
return
@@ -376,9 +384,12 @@ function WizardInner({
repo: currentAccount!.did,
rkey,
})
- await invalidateQueries()
- logEvent('starterPack:delete', {})
- navigation.popToTop()
+
+ setTimeout(async () => {
+ await invalidateQueries()
+ logEvent('starterPack:delete', {})
+ navigation.popToTop()
+ }, 2000)
} catch (e) {
Toast.show(_(msg`Failed to delete starter pack`))
} finally {
@@ -426,7 +437,7 @@ function WizardInner({
accessibilityHint={_(msg`Go back to the previous step`)}
onPress={() => {
if (state.currentStep === 'Details') {
- navigation.goBack()
+ navigation.pop()
} else {
dispatch({type: 'Back'})
}
@@ -715,6 +726,7 @@ function Footer({
state={state}
dispatch={dispatch}
moderationOpts={moderationOpts}
+ profile={profile}
/>
)
diff --git a/src/state/queries/useStarterPackQuery.ts b/src/state/queries/useStarterPackQuery.ts
index 0348c2e0ea..0c3416d640 100644
--- a/src/state/queries/useStarterPackQuery.ts
+++ b/src/state/queries/useStarterPackQuery.ts
@@ -1,11 +1,21 @@
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 {
+ httpStarterPackUriToAtUri,
+ parseStarterPackUri,
+} 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]
+const RQKEY = (did?: string, rkey?: string) => {
+ if (did?.startsWith('https://') || did?.startsWith('at://')) {
+ const parsed = parseStarterPackUri(did)
+ return [RQKEY_ROOT, parsed?.name, parsed?.rkey]
+ } else {
+ return [RQKEY_ROOT, did, rkey]
+ }
+}
export function useStarterPackQuery({
uri,
diff --git a/src/state/shell/logged-out.tsx b/src/state/shell/logged-out.tsx
index 8fe2a9c01f..555a399ae7 100644
--- a/src/state/shell/logged-out.tsx
+++ b/src/state/shell/logged-out.tsx
@@ -1,5 +1,8 @@
import React from 'react'
+import {useSession} from 'state/session'
+import {useActiveStarterPack} from 'state/shell/starter-pack'
+
type State = {
showLoggedOut: boolean
/**
@@ -22,7 +25,7 @@ type Controls = {
/**
* The did of the account to populate the login form with.
*/
- requestedAccount?: string | 'none' | 'new'
+ requestedAccount?: string | 'none' | 'new' | 'starterpack'
}) => void
/**
* Clears the requested account so that next time the logged out view is
@@ -43,9 +46,12 @@ const ControlsContext = React.createContext({
})
export function Provider({children}: React.PropsWithChildren<{}>) {
+ const activeStarterPack = useActiveStarterPack()
+ const {hasSession} = useSession()
+ const shouldShowStarterPack = Boolean(activeStarterPack?.uri) && !hasSession
const [state, setState] = React.useState({
- showLoggedOut: false,
- requestedAccountSwitchTo: undefined,
+ showLoggedOut: shouldShowStarterPack,
+ requestedAccountSwitchTo: shouldShowStarterPack ? 'starterpack' : undefined,
})
const controls = React.useMemo(
diff --git a/src/view/com/auth/LoggedOut.tsx b/src/view/com/auth/LoggedOut.tsx
index bce86d3f65..29127ec45c 100644
--- a/src/view/com/auth/LoggedOut.tsx
+++ b/src/view/com/auth/LoggedOut.tsx
@@ -7,7 +7,6 @@ import {useNavigation} from '@react-navigation/native'
import {useAnalytics} from '#/lib/analytics/analytics'
import {usePalette} from '#/lib/hooks/usePalette'
-import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {logEvent} from '#/lib/statsig/statsig'
import {s} from '#/lib/styles'
import {isIOS, isNative} from '#/platform/detection'
@@ -51,7 +50,6 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
return ScreenState.S_LoginOrCreateAccount
}
})
- const {isMobile} = useWebMediaQueries()
const {clearRequestedAccount} = useLoggedOutViewControls()
const navigation = useNavigation()
@@ -73,21 +71,9 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
}, [navigation])
return (
-
+
- {onDismiss && screenState !== ScreenState.S_StarterPack ? (
+ {onDismiss && screenState === ScreenState.S_LoginOrCreateAccount ? (
-
+
)
action = _(msg`signed up with your starter pack`)
diff --git a/src/view/com/profile/ProfileSubpageHeader.tsx b/src/view/com/profile/ProfileSubpageHeader.tsx
index fe296f8659..ac5febcda1 100644
--- a/src/view/com/profile/ProfileSubpageHeader.tsx
+++ b/src/view/com/profile/ProfileSubpageHeader.tsx
@@ -23,7 +23,7 @@ import {CenteredView} from '../util/Views'
import hairlineWidth = StyleSheet.hairlineWidth
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
-import {StarterPackIcon} from '#/components/icons/StarterPackIcon'
+import {StarterPack} from '#/components/icons/StarterPack'
export function ProfileSubpageHeader({
isLoading,
@@ -130,7 +130,7 @@ export function ProfileSubpageHeader({
accessibilityHint=""
style={{width: 58}}>
{avatarType === 'starter-pack' ? (
-
+
) : (
)}
diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx
index 11f6ded67e..04423e6d1f 100644
--- a/src/view/screens/Home.tsx
+++ b/src/view/screens/Home.tsx
@@ -22,13 +22,6 @@ 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 {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'
@@ -37,40 +30,11 @@ import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed'
import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned'
import {HomeHeader} from '../com/home/HomeHeader'
-type Props = NativeStackScreenProps
+type Props = NativeStackScreenProps
export function HomeScreen(props: Props) {
- const {navigation} = props
- const {hasSession} = useSession()
const {data: preferences} = usePreferencesQuery()
const {data: pinnedFeedInfos, isLoading: isPinnedFeedsLoading} =
usePinnedFeedsInfos()
- const activeStarterPack = useActiveStarterPack()
- const setActiveStarterPack = useSetActiveStarterPack()
- const {setShowLoggedOut, requestSwitchToAccount} = useLoggedOutViewControls()
-
- React.useEffect(() => {
- // 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,
- activeStarterPack,
- setActiveStarterPack,
- navigation,
- ])
if (preferences && pinnedFeedInfos && !isPinnedFeedsLoading) {
return (
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 9d47642632..37111c02e5 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -447,6 +447,7 @@ function ProfileScreenLoaded({
? ({headerHeight, isFocused, scrollElRef}) => (
+
+
+
+
+
+
+
+
)
}
diff --git a/src/view/shell/desktop/LeftNav.tsx b/src/view/shell/desktop/LeftNav.tsx
index 9b2b4922a8..ca8073f573 100644
--- a/src/view/shell/desktop/LeftNav.tsx
+++ b/src/view/shell/desktop/LeftNav.tsx
@@ -100,12 +100,18 @@ function ProfileCard() {
)
}
+const HIDDEN_BACK_BNT_ROUTES = ['StarterPackWizard', 'StarterPackEdit']
+
function BackBtn() {
const {isTablet} = useWebMediaQueries()
const pal = usePalette('default')
const navigation = useNavigation()
const {_} = useLingui()
- const shouldShow = useNavigationState(state => !isStateAtTabRoot(state))
+ const shouldShow = useNavigationState(
+ state =>
+ !isStateAtTabRoot(state) &&
+ !HIDDEN_BACK_BNT_ROUTES.includes(getCurrentRoute(state).name),
+ )
const onPressBack = React.useCallback(() => {
if (navigation.canGoBack()) {