UI tweaks and nits for starter packs (#4548)
Co-authored-by: Dan Abramov <dan.abramov@gmail.com> Co-authored-by: Paul Frazee <pfrazee@gmail.com> Co-authored-by: Eric Bailey <git@esb.lol> Co-authored-by: Samuel Newman <mozzius@protonmail.com> fix truncation on native (#4575)
This commit is contained in:
@@ -808,15 +808,21 @@ describe('createStarterPackLinkFromAndroidReferrer', () => {
|
|||||||
it('returns a link when input contains utm_source and utm_content', () => {
|
it('returns a link when input contains utm_source and utm_content', () => {
|
||||||
expect(
|
expect(
|
||||||
createStarterPackLinkFromAndroidReferrer(
|
createStarterPackLinkFromAndroidReferrer(
|
||||||
'utm_source=bluesky&utm_content=starterpack-haileyok.com-rkey',
|
'utm_source=bluesky&utm_content=starterpack_haileyok.com_rkey',
|
||||||
),
|
),
|
||||||
).toEqual(validOutput)
|
).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', () => {
|
it('returns a link when input contains utm_source and utm_content in different order', () => {
|
||||||
expect(
|
expect(
|
||||||
createStarterPackLinkFromAndroidReferrer(
|
createStarterPackLinkFromAndroidReferrer(
|
||||||
'utm_content=starterpack-haileyok.com-rkey&utm_source=bluesky',
|
'utm_content=starterpack_haileyok.com_rkey&utm_source=bluesky',
|
||||||
),
|
),
|
||||||
).toEqual(validOutput)
|
).toEqual(validOutput)
|
||||||
})
|
})
|
||||||
@@ -824,7 +830,7 @@ describe('createStarterPackLinkFromAndroidReferrer', () => {
|
|||||||
it('returns a link when input contains other parameters as well', () => {
|
it('returns a link when input contains other parameters as well', () => {
|
||||||
expect(
|
expect(
|
||||||
createStarterPackLinkFromAndroidReferrer(
|
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)
|
).toEqual(validOutput)
|
||||||
})
|
})
|
||||||
@@ -832,7 +838,7 @@ describe('createStarterPackLinkFromAndroidReferrer', () => {
|
|||||||
it('returns null when utm_source is not present', () => {
|
it('returns null when utm_source is not present', () => {
|
||||||
expect(
|
expect(
|
||||||
createStarterPackLinkFromAndroidReferrer(
|
createStarterPackLinkFromAndroidReferrer(
|
||||||
'utm_content=starterpack-haileyok.com-rkey',
|
'utm_content=starterpack_haileyok.com_rkey',
|
||||||
),
|
),
|
||||||
).toEqual(null)
|
).toEqual(null)
|
||||||
})
|
})
|
||||||
@@ -846,7 +852,7 @@ describe('createStarterPackLinkFromAndroidReferrer', () => {
|
|||||||
it('returns null when utm_content is malformed', () => {
|
it('returns null when utm_content is malformed', () => {
|
||||||
expect(
|
expect(
|
||||||
createStarterPackLinkFromAndroidReferrer(
|
createStarterPackLinkFromAndroidReferrer(
|
||||||
'utm_content=starterpack-haileyok.com',
|
'utm_content=starterpack_haileyok.com',
|
||||||
),
|
),
|
||||||
).toEqual(null)
|
).toEqual(null)
|
||||||
|
|
||||||
@@ -856,13 +862,13 @@ describe('createStarterPackLinkFromAndroidReferrer', () => {
|
|||||||
|
|
||||||
expect(
|
expect(
|
||||||
createStarterPackLinkFromAndroidReferrer(
|
createStarterPackLinkFromAndroidReferrer(
|
||||||
'utm_content=starterpack-haileyok.com-rkey-more',
|
'utm_content=starterpack_haileyok.com_rkey_more',
|
||||||
),
|
),
|
||||||
).toEqual(null)
|
).toEqual(null)
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
createStarterPackLinkFromAndroidReferrer(
|
createStarterPackLinkFromAndroidReferrer(
|
||||||
'utm_content=notastarterpack-haileyok.com-rkey',
|
'utm_content=notastarterpack_haileyok.com_rkey',
|
||||||
),
|
),
|
||||||
).toEqual(null)
|
).toEqual(null)
|
||||||
})
|
})
|
||||||
@@ -906,6 +912,23 @@ describe('parseStarterPackHttpUri', () => {
|
|||||||
expect(parseStarterPackUri(validHttpUri)).toEqual(null)
|
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', () => {
|
it('returns the at uri when the input is a valid starterpack at uri', () => {
|
||||||
const validAtUri = 'at://did:123/app.bsky.graph.starterpack/rkey'
|
const validAtUri = 'at://did:123/app.bsky.graph.starterpack/rkey'
|
||||||
expect(parseStarterPackUri(validAtUri)).toEqual({
|
expect(parseStarterPackUri(validAtUri)).toEqual({
|
||||||
@@ -931,11 +954,11 @@ describe('parseStarterPackHttpUri', () => {
|
|||||||
|
|
||||||
describe('createStarterPackGooglePlayUri', () => {
|
describe('createStarterPackGooglePlayUri', () => {
|
||||||
const base =
|
const base =
|
||||||
'https://play.google.com/store/apps/details?id=xyz.blueskyweb.app&referrer=utm_source%3Dbluesky%26utm_medium%3Dstarterpack%26utm_content%3Dstarterpack-'
|
'https://play.google.com/store/apps/details?id=xyz.blueskyweb.app&referrer=utm_source%3Dbluesky%26utm_medium%3Dstarterpack%26utm_content%3Dstarterpack_'
|
||||||
|
|
||||||
it('returns valid google play uri when input is valid', () => {
|
it('returns valid google play uri when input is valid', () => {
|
||||||
expect(createStarterPackGooglePlayUri('name', 'rkey')).toEqual(
|
expect(createStarterPackGooglePlayUri('name', 'rkey')).toEqual(
|
||||||
`${base}name-rkey`,
|
`${base}name_rkey`,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"><path fill="#000" fill-rule="evenodd" d="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" clip-rule="evenodd"/><path fill="#000" fill-rule="evenodd" d="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" clip-rule="evenodd"/></svg>
|
||||||
|
After Width: | Height: | Size: 687 B |
@@ -384,6 +384,7 @@ function HomeTabNavigator() {
|
|||||||
contentStyle: pal.view,
|
contentStyle: pal.view,
|
||||||
}}>
|
}}>
|
||||||
<HomeTab.Screen name="Home" getComponent={() => HomeScreen} />
|
<HomeTab.Screen name="Home" getComponent={() => HomeScreen} />
|
||||||
|
<HomeTab.Screen name="Start" getComponent={() => HomeScreen} />
|
||||||
{commonScreens(HomeTab)}
|
{commonScreens(HomeTab)}
|
||||||
</HomeTab.Navigator>
|
</HomeTab.Navigator>
|
||||||
)
|
)
|
||||||
@@ -520,6 +521,11 @@ const FlatNavigator = () => {
|
|||||||
getComponent={() => MessagesScreen}
|
getComponent={() => MessagesScreen}
|
||||||
options={{title: title(msg`Messages`), requireAuth: true}}
|
options={{title: title(msg`Messages`), requireAuth: true}}
|
||||||
/>
|
/>
|
||||||
|
<Flat.Screen
|
||||||
|
name="Start"
|
||||||
|
getComponent={() => HomeScreen}
|
||||||
|
options={{title: title(msg`Home`)}}
|
||||||
|
/>
|
||||||
{commonScreens(Flat as typeof HomeTab, numUnread)}
|
{commonScreens(Flat as typeof HomeTab, numUnread)}
|
||||||
</Flat.Navigator>
|
</Flat.Navigator>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -54,7 +54,11 @@ export function Link({
|
|||||||
const handleOrDid = feed.creator.handle || feed.creator.did
|
const handleOrDid = feed.creator.handle || feed.creator.did
|
||||||
return `/profile/${handleOrDid}/feed/${urip.rkey}`
|
return `/profile/${handleOrDid}/feed/${urip.rkey}`
|
||||||
}, [feed])
|
}, [feed])
|
||||||
return <InternalLink to={href}>{children}</InternalLink>
|
return (
|
||||||
|
<InternalLink to={href} label={feed.displayName}>
|
||||||
|
{children}
|
||||||
|
</InternalLink>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Outer({children}: {children: React.ReactNode}) {
|
export function Outer({children}: {children: React.ReactNode}) {
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ import {useGetTimeAgo} from '#/lib/hooks/useTimeAgo'
|
|||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {HITSLOP_10} from 'lib/constants'
|
import {HITSLOP_10} from 'lib/constants'
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
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 {Button} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {useDialogControl} from '#/components/Dialog'
|
import {useDialogControl} from '#/components/Dialog'
|
||||||
import {Newskie} from '#/components/icons/Newskie'
|
import {Newskie} from '#/components/icons/Newskie'
|
||||||
|
import {Default as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
export function NewskieDialog({
|
export function NewskieDialog({
|
||||||
@@ -24,6 +25,7 @@ export function NewskieDialog({
|
|||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
const control = useDialogControl()
|
const control = useDialogControl()
|
||||||
const profileName = React.useMemo(() => {
|
const profileName = React.useMemo(() => {
|
||||||
@@ -72,11 +74,30 @@ export function NewskieDialog({
|
|||||||
<Trans>Say hello!</Trans>
|
<Trans>Say hello!</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Text style={[a.text_md]}>
|
<Text style={[a.text_md]}>
|
||||||
|
{profile.joinedViaStarterPack ? (
|
||||||
|
<Trans>
|
||||||
|
{profileName} joined Bluesky using a starter pack{' '}
|
||||||
|
{timeAgo(createdAt, now, {format: 'long'})} ago
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
<Trans>
|
<Trans>
|
||||||
{profileName} joined Bluesky{' '}
|
{profileName} joined Bluesky{' '}
|
||||||
{timeAgo(createdAt, now, {format: 'long'})} ago
|
{timeAgo(createdAt, now, {format: 'long'})} ago
|
||||||
</Trans>
|
</Trans>
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
|
{profile.joinedViaStarterPack ? (
|
||||||
|
<View
|
||||||
|
style={[
|
||||||
|
a.mt_lg,
|
||||||
|
a.p_lg,
|
||||||
|
a.border,
|
||||||
|
a.rounded_sm,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
]}>
|
||||||
|
<StarterPackCard starterPack={profile.joinedViaStarterPack} />
|
||||||
|
</View>
|
||||||
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
</Dialog.ScrollableInner>
|
</Dialog.ScrollableInner>
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import {AppBskyFeedDefs} from '@atproto/api'
|
|||||||
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
|
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
|
||||||
|
|
||||||
import {useBottomBarOffset} from 'lib/hooks/useBottomBarOffset'
|
import {useBottomBarOffset} from 'lib/hooks/useBottomBarOffset'
|
||||||
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
|
import {isNative, isWeb} from 'platform/detection'
|
||||||
import {isNative} from 'platform/detection'
|
|
||||||
import {List, ListRef} from 'view/com/util/List'
|
import {List, ListRef} from 'view/com/util/List'
|
||||||
import {SectionRef} from '#/screens/Profile/Sections/types'
|
import {SectionRef} from '#/screens/Profile/Sections/types'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
@@ -25,7 +24,6 @@ export const FeedsList = React.forwardRef<SectionRef, ProfilesListProps>(
|
|||||||
function FeedsListImpl({feeds, headerHeight, scrollElRef}, ref) {
|
function FeedsListImpl({feeds, headerHeight, scrollElRef}, ref) {
|
||||||
const [initialHeaderHeight] = React.useState(headerHeight)
|
const [initialHeaderHeight] = React.useState(headerHeight)
|
||||||
const bottomBarOffset = useBottomBarOffset(20)
|
const bottomBarOffset = useBottomBarOffset(20)
|
||||||
const {isTabletOrDesktop} = useWebMediaQueries()
|
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|
||||||
const onScrollToTop = useCallback(() => {
|
const onScrollToTop = useCallback(() => {
|
||||||
@@ -44,7 +42,7 @@ export const FeedsList = React.forwardRef<SectionRef, ProfilesListProps>(
|
|||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.p_lg,
|
a.p_lg,
|
||||||
(isTabletOrDesktop || index !== 0) && a.border_t,
|
(isWeb || index !== 0) && a.border_t,
|
||||||
t.atoms.border_contrast_low,
|
t.atoms.border_contrast_low,
|
||||||
]}>
|
]}>
|
||||||
<FeedCard.Default feed={item} />
|
<FeedCard.Default feed={item} />
|
||||||
|
|||||||
@@ -40,19 +40,26 @@ export const ProfilesList = React.forwardRef<SectionRef, ProfilesListProps>(
|
|||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
const [isPTRing, setIsPTRing] = React.useState(false)
|
const [isPTRing, setIsPTRing] = React.useState(false)
|
||||||
const {data, refetch} = useListMembersQuery(listUri)
|
const {data, refetch} = useListMembersQuery(listUri, 50)
|
||||||
const profiles = data?.pages.flatMap(p => p.items.map(i => i.subject))
|
|
||||||
|
// 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 isOwn = new AtUri(listUri).host === currentAccount?.did
|
||||||
|
|
||||||
const getSortedProfiles = () => {
|
const getSortedProfiles = () => {
|
||||||
if (!profiles) return
|
if (!profiles) return
|
||||||
if (!isOwn) return profiles
|
if (!isOwn) return profiles
|
||||||
|
|
||||||
const myIndex = profiles.findIndex(p => p.did === currentAccount?.did)
|
const myIndex = profiles.findIndex(p => p.did === currentAccount?.did)
|
||||||
return [
|
return myIndex !== -1
|
||||||
|
? [
|
||||||
profiles[myIndex],
|
profiles[myIndex],
|
||||||
...profiles.slice(0, myIndex),
|
...profiles.slice(0, myIndex),
|
||||||
...profiles.slice(myIndex + 1),
|
...profiles.slice(myIndex + 1),
|
||||||
]
|
]
|
||||||
|
: profiles
|
||||||
}
|
}
|
||||||
const onScrollToTop = useCallback(() => {
|
const onScrollToTop = useCallback(() => {
|
||||||
scrollElRef.current?.scrollToOffset({
|
scrollElRef.current?.scrollToOffset({
|
||||||
|
|||||||
@@ -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 (
|
|
||||||
<View style={[a.pr_2xs]}>
|
|
||||||
<Button
|
|
||||||
disabled={disabled}
|
|
||||||
label={_(
|
|
||||||
msg`This user is new here. Press for more info about when they joined.`,
|
|
||||||
)}
|
|
||||||
hitSlop={HITSLOP_10}
|
|
||||||
onPress={control.open}>
|
|
||||||
{({hovered, pressed}) => (
|
|
||||||
<Newskie
|
|
||||||
size="lg"
|
|
||||||
fill="#FFC404"
|
|
||||||
style={{
|
|
||||||
opacity: hovered || pressed ? 0.5 : 1,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Dialog.Outer control={control}>
|
|
||||||
<Dialog.Handle />
|
|
||||||
<Dialog.ScrollableInner
|
|
||||||
label={_(msg`New user info dialog`)}
|
|
||||||
style={[{width: 'auto', maxWidth: 400, minWidth: 200}]}>
|
|
||||||
<View style={[a.gap_sm]}>
|
|
||||||
<Text style={[a.font_bold, a.text_xl]}>
|
|
||||||
<Trans>Say hello!</Trans>
|
|
||||||
</Text>
|
|
||||||
<Text style={[a.text_md]}>
|
|
||||||
<Trans>
|
|
||||||
{profileName} joined Bluesky{' '}
|
|
||||||
{timeAgo(createdAt, now, {format: 'long'})} ago
|
|
||||||
</Trans>
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</Dialog.ScrollableInner>
|
|
||||||
</Dialog.Outer>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -25,12 +25,13 @@ import {useAgent} from 'state/session'
|
|||||||
import {List, ListRef} from 'view/com/util/List'
|
import {List, ListRef} from 'view/com/util/List'
|
||||||
import {Text} from 'view/com/util/text/Text'
|
import {Text} from 'view/com/util/text/Text'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
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 {useDialogControl} from '#/components/Dialog'
|
||||||
import {LinearGradientBackground} from '#/components/LinearGradientBackground'
|
import {LinearGradientBackground} from '#/components/LinearGradientBackground'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {Default as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
|
import {Default as StarterPackCard} from '#/components/StarterPack/StarterPackCard'
|
||||||
|
import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '../icons/Plus'
|
||||||
|
|
||||||
interface SectionRef {
|
interface SectionRef {
|
||||||
scrollToTop: () => void
|
scrollToTop: () => void
|
||||||
@@ -47,6 +48,7 @@ interface ProfileFeedgensProps {
|
|||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
testID?: string
|
testID?: string
|
||||||
setScrollViewTag: (tag: number | null) => void
|
setScrollViewTag: (tag: number | null) => void
|
||||||
|
isMe: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function keyExtractor(item: AppBskyGraphDefs.StarterPackView) {
|
function keyExtractor(item: AppBskyGraphDefs.StarterPackView) {
|
||||||
@@ -65,6 +67,7 @@ export const ProfileStarterPacks = React.forwardRef<
|
|||||||
style,
|
style,
|
||||||
testID,
|
testID,
|
||||||
setScrollViewTag,
|
setScrollViewTag,
|
||||||
|
isMe,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) {
|
) {
|
||||||
@@ -140,7 +143,9 @@ export const ProfileStarterPacks = React.forwardRef<
|
|||||||
onEndReached={onEndReached}
|
onEndReached={onEndReached}
|
||||||
onRefresh={onRefresh}
|
onRefresh={onRefresh}
|
||||||
ListEmptyComponent={Empty}
|
ListEmptyComponent={Empty}
|
||||||
ListFooterComponent={items?.length !== 0 ? CreateAnother : undefined}
|
ListFooterComponent={
|
||||||
|
items?.length !== 0 && isMe ? CreateAnother : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
@@ -148,19 +153,29 @@ export const ProfileStarterPacks = React.forwardRef<
|
|||||||
|
|
||||||
function CreateAnother() {
|
function CreateAnother() {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const t = useTheme()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={[a.px_md, a.py_lg, a.justify_between, a.gap_lg]}>
|
<View
|
||||||
|
style={[
|
||||||
|
a.pr_md,
|
||||||
|
a.pt_lg,
|
||||||
|
a.gap_lg,
|
||||||
|
a.border_t,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
]}>
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`Create a starter pack`)}
|
label={_(msg`Create a starter pack`)}
|
||||||
variant="ghost"
|
variant="solid"
|
||||||
color="primary"
|
color="secondary"
|
||||||
size="small"
|
size="small"
|
||||||
|
style={[a.self_center]}
|
||||||
onPress={() => navigation.navigate('StarterPackWizard')}>
|
onPress={() => navigation.navigate('StarterPackWizard')}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Create another</Trans>
|
<Trans>Create another</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
|
<ButtonIcon icon={Plus} position="right" />
|
||||||
</Button>
|
</Button>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
@@ -203,8 +218,8 @@ function Empty() {
|
|||||||
return (
|
return (
|
||||||
<LinearGradientBackground
|
<LinearGradientBackground
|
||||||
style={[
|
style={[
|
||||||
a.px_md,
|
a.px_lg,
|
||||||
a.py_xl,
|
a.py_lg,
|
||||||
a.justify_between,
|
a.justify_between,
|
||||||
a.gap_lg,
|
a.gap_lg,
|
||||||
a.shadow_lg,
|
a.shadow_lg,
|
||||||
@@ -260,26 +275,26 @@ function Empty() {
|
|||||||
|
|
||||||
<Prompt.Outer control={confirmDialogControl}>
|
<Prompt.Outer control={confirmDialogControl}>
|
||||||
<Prompt.TitleText>
|
<Prompt.TitleText>
|
||||||
<Trans>Generate a starter pack?</Trans>
|
<Trans>Generate a starter pack</Trans>
|
||||||
</Prompt.TitleText>
|
</Prompt.TitleText>
|
||||||
<Prompt.DescriptionText>
|
<Prompt.DescriptionText>
|
||||||
<Trans>
|
<Trans>
|
||||||
You can customize your starter pack with feeds and your favorite
|
Bluesky will choose a set of recommended accounts from people in
|
||||||
people if you create your own.
|
your network.
|
||||||
</Trans>
|
</Trans>
|
||||||
</Prompt.DescriptionText>
|
</Prompt.DescriptionText>
|
||||||
<Prompt.Actions>
|
<Prompt.Actions>
|
||||||
<Prompt.Action
|
<Prompt.Action
|
||||||
color="primary"
|
color="primary"
|
||||||
cta={_(msg`I'll create one`)}
|
cta={_(msg`Choose for me`)}
|
||||||
onPress={() => {
|
onPress={generate}
|
||||||
navigation.navigate('StarterPackWizard')
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Prompt.Action
|
<Prompt.Action
|
||||||
color="secondary"
|
color="secondary"
|
||||||
cta={_(msg`Generate anyway`)}
|
cta={_(msg`Let me choose`)}
|
||||||
onPress={generate}
|
onPress={() => {
|
||||||
|
navigation.navigate('StarterPackWizard')
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Prompt.Actions>
|
</Prompt.Actions>
|
||||||
</Prompt.Outer>
|
</Prompt.Outer>
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export const QrCode = React.forwardRef<ViewShot, Props>(function QrCode(
|
|||||||
<Trans>on</Trans>
|
<Trans>on</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
<Logo width={26} fill="white" />
|
<Logo width={26} fill="white" />
|
||||||
<View style={[{marginTop: 7, marginLeft: 3}]}>
|
<View style={[{marginTop: 5, marginLeft: 2.5}]}>
|
||||||
<Logotype width={68} fill="white" />
|
<Logotype width={68} fill="white" />
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import ViewShot from 'react-native-view-shot'
|
import ViewShot from 'react-native-view-shot'
|
||||||
import {setImageAsync} from 'expo-clipboard'
|
|
||||||
import * as FS from 'expo-file-system'
|
import * as FS from 'expo-file-system'
|
||||||
import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
|
import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
|
||||||
|
import * as Sharing from 'expo-sharing'
|
||||||
import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
|
import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
@@ -12,14 +12,14 @@ import {nanoid} from 'nanoid/non-secure'
|
|||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {saveImageToMediaLibrary} from 'lib/media/manip'
|
import {saveImageToMediaLibrary} from 'lib/media/manip'
|
||||||
import {logEvent} from 'lib/statsig/statsig'
|
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 * as Toast from '#/view/com/util/Toast'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import * as Dialog from '#/components/Dialog'
|
import * as Dialog from '#/components/Dialog'
|
||||||
import {DialogControlProps} from '#/components/Dialog'
|
import {DialogControlProps} from '#/components/Dialog'
|
||||||
|
import {Loader} from '#/components/Loader'
|
||||||
import {QrCode} from '#/components/StarterPack/QrCode'
|
import {QrCode} from '#/components/StarterPack/QrCode'
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
|
|
||||||
export function QrCodeDialog({
|
export function QrCodeDialog({
|
||||||
control,
|
control,
|
||||||
@@ -29,6 +29,7 @@ export function QrCodeDialog({
|
|||||||
starterPack: AppBskyGraphDefs.StarterPackView
|
starterPack: AppBskyGraphDefs.StarterPackView
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const [isProcessing, setIsProcessing] = React.useState(false)
|
||||||
|
|
||||||
const ref = React.useRef<ViewShot>(null)
|
const ref = React.useRef<ViewShot>(null)
|
||||||
|
|
||||||
@@ -75,6 +76,8 @@ export function QrCodeDialog({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
setIsProcessing(true)
|
||||||
|
|
||||||
if (!AppBskyGraphStarterpack.isRecord(starterPack.record)) {
|
if (!AppBskyGraphStarterpack.isRecord(starterPack.record)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -98,24 +101,25 @@ export function QrCodeDialog({
|
|||||||
shareType: 'qrcode',
|
shareType: 'qrcode',
|
||||||
qrShareType: 'save',
|
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()
|
control.close()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const onCopyPress = async () => {
|
const onCopyPress = async () => {
|
||||||
|
setIsProcessing(true)
|
||||||
ref.current?.capture?.().then(async (uri: string) => {
|
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)
|
const canvas = await getCanvas(uri)
|
||||||
// @ts-expect-error web only
|
// @ts-expect-error web only
|
||||||
canvas.toBlob((blob: Blob) => {
|
canvas.toBlob((blob: Blob) => {
|
||||||
const item = new ClipboardItem({'image/png': blob})
|
const item = new ClipboardItem({'image/png': blob})
|
||||||
navigator.clipboard.write([item])
|
navigator.clipboard.write([item])
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
logEvent('starterPack:share', {
|
logEvent('starterPack:share', {
|
||||||
starterPack: starterPack.uri,
|
starterPack: starterPack.uri,
|
||||||
@@ -123,42 +127,62 @@ export function QrCodeDialog({
|
|||||||
qrShareType: 'copy',
|
qrShareType: 'copy',
|
||||||
})
|
})
|
||||||
Toast.show(_(msg`QR code copied to your clipboard!`))
|
Toast.show(_(msg`QR code copied to your clipboard!`))
|
||||||
|
setIsProcessing(false)
|
||||||
control.close()
|
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 (
|
return (
|
||||||
<Dialog.Outer control={control}>
|
<Dialog.Outer control={control}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<Dialog.ScrollableInner
|
<Dialog.ScrollableInner
|
||||||
label={_(msg`Create a QR code for a starter pack`)}>
|
label={_(msg`Create a QR code for a starter pack`)}>
|
||||||
<View style={[a.flex_1, a.align_center, a.gap_5xl]}>
|
<View style={[a.flex_1, a.align_center, a.gap_5xl]}>
|
||||||
<Text style={[a.font_bold, a.text_xl, a.text_center]}>
|
|
||||||
Share this starter pack with friends!
|
|
||||||
</Text>
|
|
||||||
<QrCode starterPack={starterPack} ref={ref} />
|
<QrCode starterPack={starterPack} ref={ref} />
|
||||||
<View style={[a.flex_row, a.gap_md]}>
|
{isProcessing ? (
|
||||||
|
<View>
|
||||||
|
<Loader size="xl" />
|
||||||
|
</View>
|
||||||
|
) : (
|
||||||
|
<View style={[a.w_full, a.gap_md]}>
|
||||||
|
<Button
|
||||||
|
label={_(msg`Copy QR code`)}
|
||||||
|
variant="solid"
|
||||||
|
color="primary"
|
||||||
|
size="medium"
|
||||||
|
onPress={isWeb ? onCopyPress : onSharePress}>
|
||||||
|
<ButtonText>
|
||||||
|
{isWeb ? <Trans>Copy</Trans> : <Trans>Share</Trans>}
|
||||||
|
</ButtonText>
|
||||||
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`Save QR code`)}
|
label={_(msg`Save QR code`)}
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="primary"
|
color="secondary"
|
||||||
size="medium"
|
size="medium"
|
||||||
onPress={onSavePress}>
|
onPress={onSavePress}>
|
||||||
<ButtonText>
|
<ButtonText>
|
||||||
<Trans>Save</Trans>
|
<Trans>Save</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
|
||||||
label={_(msg`Copy QR code`)}
|
|
||||||
variant="solid"
|
|
||||||
color="primary"
|
|
||||||
size="medium"
|
|
||||||
onPress={onCopyPress}>
|
|
||||||
<ButtonText>
|
|
||||||
<Trans>Copy</Trans>
|
|
||||||
</ButtonText>
|
|
||||||
</Button>
|
|
||||||
</View>
|
</View>
|
||||||
|
)}
|
||||||
</View>
|
</View>
|
||||||
</Dialog.ScrollableInner>
|
</Dialog.ScrollableInner>
|
||||||
</Dialog.Outer>
|
</Dialog.Outer>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {useLingui} from '@lingui/react'
|
|||||||
import {sanitizeHandle} from 'lib/strings/handles'
|
import {sanitizeHandle} from 'lib/strings/handles'
|
||||||
import {useSession} from 'state/session'
|
import {useSession} from 'state/session'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
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 {Link} from '#/components/Link'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
@@ -25,9 +25,9 @@ export function Default({starterPack}: {starterPack: StarterPackViewBasic}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper creator={starterPack.creator} rkey={rkey}>
|
<Wrapper creator={starterPack.creator} name={record.name} rkey={rkey}>
|
||||||
<View style={[a.flex_row, a.gap_md]}>
|
<View style={[a.flex_row, a.gap_sm]}>
|
||||||
<StarterPackIcon width={36} height={36} />
|
<StarterPack width={40} gradient="sky" />
|
||||||
<View>
|
<View>
|
||||||
<Text style={[a.text_md, a.font_bold, a.leading_snug]}>
|
<Text style={[a.text_md, a.font_bold, a.leading_snug]}>
|
||||||
{record.name}
|
{record.name}
|
||||||
@@ -43,7 +43,9 @@ export function Default({starterPack}: {starterPack: StarterPackViewBasic}) {
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{record.description && (
|
{record.description && (
|
||||||
<Text numberOfLines={3}>{record.description}</Text>
|
<Text numberOfLines={3} style={[a.leading_snug]}>
|
||||||
|
{record.description}
|
||||||
|
</Text>
|
||||||
)}
|
)}
|
||||||
{!!joinedAllTimeCount && joinedAllTimeCount >= 50 && (
|
{!!joinedAllTimeCount && joinedAllTimeCount >= 50 && (
|
||||||
<Text style={[a.font_bold, t.atoms.text_contrast_medium]}>
|
<Text style={[a.font_bold, t.atoms.text_contrast_medium]}>
|
||||||
@@ -55,10 +57,12 @@ export function Default({starterPack}: {starterPack: StarterPackViewBasic}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Wrapper({
|
function Wrapper({
|
||||||
|
name,
|
||||||
creator,
|
creator,
|
||||||
children,
|
children,
|
||||||
rkey,
|
rkey,
|
||||||
}: {
|
}: {
|
||||||
|
name: string
|
||||||
creator: AppBskyActorDefs.ProfileViewBasic
|
creator: AppBskyActorDefs.ProfileViewBasic
|
||||||
rkey: string
|
rkey: string
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
@@ -68,7 +72,8 @@ function Wrapper({
|
|||||||
to={{
|
to={{
|
||||||
screen: 'StarterPack',
|
screen: 'StarterPack',
|
||||||
params: {name: creator.handle || creator.did, rkey},
|
params: {name: creator.handle || creator.did, rkey},
|
||||||
}}>
|
}}
|
||||||
|
label={name}>
|
||||||
<View style={[a.flex_1, a.gap_md]}>{children}</View>
|
<View style={[a.flex_1, a.gap_md]}>{children}</View>
|
||||||
</Link>
|
</Link>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,11 +31,13 @@ export function WizardEditListDialog({
|
|||||||
state,
|
state,
|
||||||
dispatch,
|
dispatch,
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
|
profile,
|
||||||
}: {
|
}: {
|
||||||
control: Dialog.DialogControlProps
|
control: Dialog.DialogControlProps
|
||||||
state: WizardState
|
state: WizardState
|
||||||
dispatch: (action: WizardAction) => void
|
dispatch: (action: WizardAction) => void
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
|
profile: AppBskyActorDefs.ProfileViewBasic
|
||||||
}) {
|
}) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
@@ -46,12 +48,9 @@ export function WizardEditListDialog({
|
|||||||
const getData = () => {
|
const getData = () => {
|
||||||
if (state.currentStep === 'Feeds') return state.feeds
|
if (state.currentStep === 'Feeds') return state.feeds
|
||||||
|
|
||||||
const myIndex = state.profiles.findIndex(p => p.did === currentAccount?.did)
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
state.profiles[myIndex],
|
profile,
|
||||||
...state.profiles.slice(0, myIndex),
|
...state.profiles.filter(p => p.did !== currentAccount?.did),
|
||||||
...state.profiles.slice(myIndex + 1),
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ function WizardListCard({
|
|||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={displayName}
|
||||||
|
accessibilityHint={
|
||||||
|
included ? _(msg`Remove from list`) : _(msg`Add to list`)
|
||||||
|
}
|
||||||
style={[
|
style={[
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
a.align_center,
|
a.align_center,
|
||||||
@@ -81,6 +85,7 @@ function WizardListCard({
|
|||||||
{subtitle}
|
{subtitle}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
<View accessible={false}>
|
||||||
<Toggle.Item
|
<Toggle.Item
|
||||||
name={type === 'user' ? _(msg`Person toggle`) : _(msg`Feed toggle`)}
|
name={type === 'user' ? _(msg`Person toggle`) : _(msg`Feed toggle`)}
|
||||||
label={
|
label={
|
||||||
@@ -93,6 +98,7 @@ function WizardListCard({
|
|||||||
onChange={onPress}>
|
onChange={onPress}>
|
||||||
<Checkbox />
|
<Checkbox />
|
||||||
</Toggle.Item>
|
</Toggle.Item>
|
||||||
|
</View>
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -110,9 +116,9 @@ export function WizardProfileCard({
|
|||||||
}) {
|
}) {
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
const included = state.profiles.some(p => p.did === profile.did)
|
|
||||||
const isMe = profile.did === currentAccount?.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 moderationUi = moderateProfile(profile, moderationOpts).ui('avatar')
|
||||||
const displayName = profile.displayName
|
const displayName = profile.displayName
|
||||||
? sanitizeDisplayName(profile.displayName)
|
? sanitizeDisplayName(profile.displayName)
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import {httpStarterPackUriToAtUri} from 'lib/strings/starter-pack'
|
|||||||
import {useSetActiveStarterPack} from 'state/shell/starter-pack'
|
import {useSetActiveStarterPack} from 'state/shell/starter-pack'
|
||||||
|
|
||||||
export function useStarterPackEntry() {
|
export function useStarterPackEntry() {
|
||||||
|
const [ready, setReady] = React.useState(false)
|
||||||
|
|
||||||
const setActiveStarterPack = useSetActiveStarterPack()
|
const setActiveStarterPack = useSetActiveStarterPack()
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
@@ -19,7 +21,9 @@ export function useStarterPackEntry() {
|
|||||||
isClip,
|
isClip,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setReady(true)
|
||||||
}, [setActiveStarterPack])
|
}, [setActiveStarterPack])
|
||||||
|
|
||||||
return true
|
return ready
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
],
|
||||||
|
})
|
||||||
@@ -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<typeof Svg>) {
|
|
||||||
return (
|
|
||||||
<Svg fill="none" viewBox="0 0 153 133" {...props}>
|
|
||||||
<Path
|
|
||||||
fill="url(#a)"
|
|
||||||
fillRule="evenodd"
|
|
||||||
d="m60.196 105.445-18.1 4.85c-11.73 3.143-23.788-3.819-26.931-15.55L1.19 42.597c-3.143-11.731 3.819-23.79 15.55-26.932L68.889 1.69C80.62-1.452 92.68 5.51 95.821 17.241l4.667 17.416a49.7 49.7 0 0 1 3.522-.125c27.053 0 48.984 21.931 48.984 48.984S131.063 132.5 104.01 132.5c-19.17 0-35.769-11.012-43.814-27.055ZM19.457 25.804 71.606 11.83c6.131-1.643 12.434 1.996 14.076 8.127l4.44 16.571c-20.289 5.987-35.096 24.758-35.096 46.988 0 4.157.517 8.193 1.492 12.047l-17.138 4.593c-6.131 1.642-12.434-1.996-14.077-8.128L11.33 39.88c-1.643-6.131 1.996-12.434 8.127-14.077Zm83.812 19.232c.246-.005.493-.007.741-.007 21.256 0 38.487 17.231 38.487 38.487s-17.231 38.488-38.487 38.488c-14.29 0-26.76-7.788-33.4-19.35l23.635-6.333c11.731-3.143 18.693-15.2 15.55-26.932l-6.526-24.353Zm-10.428 1.638 6.815 25.432c1.642 6.131-1.996 12.434-8.128 14.076l-24.867 6.664a38.57 38.57 0 0 1-1.139-9.33c0-17.372 11.51-32.056 27.32-36.842Z"
|
|
||||||
clipRule="evenodd"
|
|
||||||
/>
|
|
||||||
<Defs>
|
|
||||||
<LinearGradient
|
|
||||||
id="a"
|
|
||||||
x1={76.715}
|
|
||||||
x2={76.715}
|
|
||||||
y1={0.937}
|
|
||||||
y2={132.5}
|
|
||||||
gradientUnits="userSpaceOnUse">
|
|
||||||
<Stop stopColor="#0A7AFF" />
|
|
||||||
<Stop offset={1} stopColor="#59B9FF" />
|
|
||||||
</LinearGradient>
|
|
||||||
</Defs>
|
|
||||||
</Svg>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -30,7 +30,7 @@ export const IconTemplate_Stroke2_Corner0_Rounded = React.forwardRef(
|
|||||||
|
|
||||||
export function createSinglePathSVG({path}: {path: string}) {
|
export function createSinglePathSVG({path}: {path: string}) {
|
||||||
return React.forwardRef<Svg, Props>(function LogoImpl(props, ref) {
|
return React.forwardRef<Svg, Props>(function LogoImpl(props, ref) {
|
||||||
const {fill, size, style, ...rest} = useCommonSVGProps(props)
|
const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Svg
|
<Svg
|
||||||
@@ -41,8 +41,37 @@ export function createSinglePathSVG({path}: {path: string}) {
|
|||||||
width={size}
|
width={size}
|
||||||
height={size}
|
height={size}
|
||||||
style={[style]}>
|
style={[style]}>
|
||||||
|
{gradient}
|
||||||
<Path fill={fill} fillRule="evenodd" clipRule="evenodd" d={path} />
|
<Path fill={fill} fillRule="evenodd" clipRule="evenodd" d={path} />
|
||||||
</Svg>
|
</Svg>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createMultiPathSVG({paths}: {paths: string[]}) {
|
||||||
|
return React.forwardRef<Svg, Props>(function LogoImpl(props, ref) {
|
||||||
|
const {fill, size, style, gradient, ...rest} = useCommonSVGProps(props)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Svg
|
||||||
|
fill="none"
|
||||||
|
{...rest}
|
||||||
|
ref={ref}
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
style={[style]}>
|
||||||
|
{gradient}
|
||||||
|
{paths.map((path, i) => (
|
||||||
|
<Path
|
||||||
|
key={i}
|
||||||
|
fill={fill}
|
||||||
|
fillRule="evenodd"
|
||||||
|
clipRule="evenodd"
|
||||||
|
d={path}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Svg>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
import {StyleSheet, TextProps} from 'react-native'
|
|
||||||
import type {PathProps, SvgProps} from 'react-native-svg'
|
|
||||||
|
|
||||||
import {tokens} from '#/alf'
|
|
||||||
|
|
||||||
export type Props = {
|
|
||||||
fill?: PathProps['fill']
|
|
||||||
style?: TextProps['style']
|
|
||||||
size?: keyof typeof sizes
|
|
||||||
} & Omit<SvgProps, 'style' | 'size'>
|
|
||||||
|
|
||||||
export const sizes = {
|
|
||||||
xs: 12,
|
|
||||||
sm: 16,
|
|
||||||
md: 20,
|
|
||||||
lg: 24,
|
|
||||||
xl: 28,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useCommonSVGProps(props: Props) {
|
|
||||||
const {fill, size, ...rest} = props
|
|
||||||
const style = StyleSheet.flatten(rest.style)
|
|
||||||
const _fill = fill || style?.color || tokens.color.blue_500
|
|
||||||
const _size = Number(size ? sizes[size] : rest.width || sizes.md)
|
|
||||||
|
|
||||||
return {
|
|
||||||
fill: _fill,
|
|
||||||
size: _size,
|
|
||||||
style,
|
|
||||||
...rest,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
import React from 'react'
|
||||||
|
import {StyleSheet, TextProps} from 'react-native'
|
||||||
|
import type {PathProps, SvgProps} from 'react-native-svg'
|
||||||
|
import {Defs, LinearGradient, Stop} from 'react-native-svg'
|
||||||
|
import {nanoid} from 'nanoid/non-secure'
|
||||||
|
|
||||||
|
import {tokens} from '#/alf'
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
fill?: PathProps['fill']
|
||||||
|
style?: TextProps['style']
|
||||||
|
size?: keyof typeof sizes
|
||||||
|
gradient?: keyof typeof tokens.gradients
|
||||||
|
} & Omit<SvgProps, 'style' | 'size'>
|
||||||
|
|
||||||
|
export const sizes = {
|
||||||
|
xs: 12,
|
||||||
|
sm: 16,
|
||||||
|
md: 20,
|
||||||
|
lg: 24,
|
||||||
|
xl: 28,
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCommonSVGProps(props: Props) {
|
||||||
|
const {fill, size, gradient, ...rest} = props
|
||||||
|
const style = StyleSheet.flatten(rest.style)
|
||||||
|
const _size = Number(size ? sizes[size] : rest.width || sizes.md)
|
||||||
|
let _fill = fill || style?.color || tokens.color.blue_500
|
||||||
|
let gradientDef = null
|
||||||
|
|
||||||
|
if (gradient && tokens.gradients[gradient]) {
|
||||||
|
const id = gradient + '_' + nanoid()
|
||||||
|
const config = tokens.gradients[gradient]
|
||||||
|
_fill = `url(#${id})`
|
||||||
|
gradientDef = (
|
||||||
|
<Defs>
|
||||||
|
<LinearGradient
|
||||||
|
id={id}
|
||||||
|
x1="0"
|
||||||
|
y1="0"
|
||||||
|
x2="100%"
|
||||||
|
y2="0"
|
||||||
|
gradientTransform="rotate(45)">
|
||||||
|
{config.values.map(([stop, fill]) => (
|
||||||
|
<Stop key={stop} offset={stop} stopColor={fill} />
|
||||||
|
))}
|
||||||
|
</LinearGradient>
|
||||||
|
</Defs>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
fill: _fill,
|
||||||
|
size: _size,
|
||||||
|
style,
|
||||||
|
gradient: gradientDef,
|
||||||
|
...rest,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -34,16 +34,14 @@ export const createStarterPackList = async ({
|
|||||||
if (!list) throw new Error('List creation failed')
|
if (!list) throw new Error('List creation failed')
|
||||||
await agent.com.atproto.repo.applyWrites({
|
await agent.com.atproto.repo.applyWrites({
|
||||||
repo: agent.session!.did,
|
repo: agent.session!.did,
|
||||||
writes: profiles.map(p => ({
|
writes: [
|
||||||
$type: 'com.atproto.repo.applyWrites#create',
|
createListItem({did: agent.session!.did, listUri: list.uri}),
|
||||||
collection: 'app.bsky.graph.listitem',
|
].concat(
|
||||||
value: {
|
profiles
|
||||||
$type: 'app.bsky.graph.listitem',
|
// Ensure we don't have ourselves in this list twice
|
||||||
subject: p.did,
|
.filter(p => p.did !== agent.session!.did)
|
||||||
list: list?.uri,
|
.map(p => createListItem({did: p.did, listUri: list.uri})),
|
||||||
createdAt: new Date().toISOString(),
|
),
|
||||||
},
|
|
||||||
})),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
return list
|
return list
|
||||||
@@ -80,8 +78,8 @@ export async function generateStarterpack({
|
|||||||
return 'ERROR'
|
return 'ERROR'
|
||||||
}
|
}
|
||||||
|
|
||||||
profiles = [profile, ...profiles]
|
// We include ourselves when we make the list
|
||||||
if (profiles.length < 8) {
|
if (profiles.length < 7) {
|
||||||
return 'NOT_ENOUGH_FOLLOWERS'
|
return 'NOT_ENOUGH_FOLLOWERS'
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +109,20 @@ export async function generateStarterpack({
|
|||||||
)
|
)
|
||||||
).uri
|
).uri
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
logger.error('Failed to generate starter pack', {error: e})
|
logger.error('Failed to generate starter pack', {safeMessage: e})
|
||||||
return 'ERROR'
|
return 'ERROR'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createListItem({did, listUri}: {did: string; listUri: string}) {
|
||||||
|
return {
|
||||||
|
$type: 'com.atproto.repo.applyWrites#create',
|
||||||
|
collection: 'app.bsky.graph.listitem',
|
||||||
|
value: {
|
||||||
|
$type: 'app.bsky.graph.listitem',
|
||||||
|
subject: did,
|
||||||
|
list: listUri,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export type CommonNavigatorParams = {
|
|||||||
MessagesConversation: {conversation: string; embed?: string}
|
MessagesConversation: {conversation: string; embed?: string}
|
||||||
MessagesSettings: undefined
|
MessagesSettings: undefined
|
||||||
Feeds: undefined
|
Feeds: undefined
|
||||||
|
Start: {name: string; rkey: string}
|
||||||
StarterPack: {name: string; rkey: string}
|
StarterPack: {name: string; rkey: string}
|
||||||
StarterPackWizard: undefined
|
StarterPackWizard: undefined
|
||||||
StarterPackEdit: {
|
StarterPackEdit: {
|
||||||
@@ -98,6 +99,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
|
|||||||
Hashtag: {tag: string; author?: string}
|
Hashtag: {tag: string; author?: string}
|
||||||
MessagesTab: undefined
|
MessagesTab: undefined
|
||||||
Messages: {animation?: 'push' | 'pop'}
|
Messages: {animation?: 'push' | 'pop'}
|
||||||
|
Start: {name: string; rkey: string}
|
||||||
StarterPack: {name: string; rkey: string}
|
StarterPack: {name: string; rkey: string}
|
||||||
StarterPackWizard: undefined
|
StarterPackWizard: undefined
|
||||||
StarterPackEdit: {
|
StarterPackEdit: {
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ export type LogEvents = {
|
|||||||
'starterPack:share': {
|
'starterPack:share': {
|
||||||
starterPack: string
|
starterPack: string
|
||||||
shareType: 'link' | 'qrcode'
|
shareType: 'link' | 'qrcode'
|
||||||
qrShareType?: 'save' | 'copy'
|
qrShareType?: 'save' | 'copy' | 'share'
|
||||||
}
|
}
|
||||||
'starterPack:followAll': {
|
'starterPack:followAll': {
|
||||||
starterPack: string
|
starterPack: string
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ export function createStarterPackLinkFromAndroidReferrer(
|
|||||||
if (!utmContent) return null
|
if (!utmContent) return null
|
||||||
if (utmSource !== 'bluesky') return null
|
if (utmSource !== 'bluesky') return null
|
||||||
|
|
||||||
// This should be a string like `starterpack-haileyok.com-rkey`
|
// This should be a string like `starterpack_haileyok.com_rkey`
|
||||||
const contentParts = utmContent.split('-')
|
const contentParts = utmContent.split('_')
|
||||||
|
|
||||||
if (contentParts[0] !== 'starterpack') return null
|
if (contentParts[0] !== 'starterpack') return null
|
||||||
if (contentParts.length !== 3) return null
|
if (contentParts.length !== 3) return null
|
||||||
@@ -46,10 +46,10 @@ export function parseStarterPackUri(uri?: string): {
|
|||||||
} else {
|
} else {
|
||||||
const url = new URL(uri)
|
const url = new URL(uri)
|
||||||
const parts = url.pathname.split('/')
|
const parts = url.pathname.split('/')
|
||||||
const name = parts[2]
|
const [_, path, name, rkey] = parts
|
||||||
const rkey = parts[3]
|
|
||||||
|
|
||||||
if (parts.length !== 4) return null
|
if (parts.length !== 4) return null
|
||||||
|
if (path !== 'starter-pack' && path !== 'start') return null
|
||||||
if (!name || !rkey) return null
|
if (!name || !rkey) return null
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
@@ -66,7 +66,7 @@ export function createStarterPackGooglePlayUri(
|
|||||||
rkey: string,
|
rkey: string,
|
||||||
): string | null {
|
): string | null {
|
||||||
if (!name || !rkey) return null
|
if (!name || !rkey) return null
|
||||||
return `https://play.google.com/store/apps/details?id=xyz.blueskyweb.app&referrer=utm_source%3Dbluesky%26utm_medium%3Dstarterpack%26utm_content%3Dstarterpack-${name}-${rkey}`
|
return `https://play.google.com/store/apps/details?id=xyz.blueskyweb.app&referrer=utm_source%3Dbluesky%26utm_medium%3Dstarterpack%26utm_content%3Dstarterpack_${name}_${rkey}`
|
||||||
}
|
}
|
||||||
|
|
||||||
export function httpStarterPackUriToAtUri(httpUri?: string): string | null {
|
export function httpStarterPackUriToAtUri(httpUri?: string): string | null {
|
||||||
|
|||||||
+3
-2
@@ -1,7 +1,7 @@
|
|||||||
import {Router} from 'lib/routes/router'
|
import {Router} from 'lib/routes/router'
|
||||||
|
|
||||||
export const router = new Router({
|
export const router = new Router({
|
||||||
Home: ['/', '/start/:name/:rkey'],
|
Home: '/',
|
||||||
Search: '/search',
|
Search: '/search',
|
||||||
Feeds: '/feeds',
|
Feeds: '/feeds',
|
||||||
Notifications: '/notifications',
|
Notifications: '/notifications',
|
||||||
@@ -41,7 +41,8 @@ export const router = new Router({
|
|||||||
Messages: '/messages',
|
Messages: '/messages',
|
||||||
MessagesSettings: '/messages/settings',
|
MessagesSettings: '/messages/settings',
|
||||||
MessagesConversation: '/messages/:conversation',
|
MessagesConversation: '/messages/:conversation',
|
||||||
|
Start: '/start/:name/:rkey',
|
||||||
|
StarterPackEdit: '/starter-pack/edit/:rkey',
|
||||||
StarterPack: '/starter-pack/:name/:rkey',
|
StarterPack: '/starter-pack/:name/:rkey',
|
||||||
StarterPackWizard: '/starter-pack/create',
|
StarterPackWizard: '/starter-pack/create',
|
||||||
StarterPackEdit: '/starter-pack/edit/:rkey',
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -55,11 +55,12 @@ export function StepFinished() {
|
|||||||
|
|
||||||
const finishOnboarding = React.useCallback(async () => {
|
const finishOnboarding = React.useCallback(async () => {
|
||||||
setSaving(true)
|
setSaving(true)
|
||||||
try {
|
|
||||||
let starterPack: AppBskyGraphDefs.StarterPackView | undefined
|
let starterPack: AppBskyGraphDefs.StarterPackView | undefined
|
||||||
let listItems: AppBskyGraphDefs.ListItemView[] | undefined
|
let listItems: AppBskyGraphDefs.ListItemView[] | undefined
|
||||||
|
|
||||||
if (activeStarterPack?.uri) {
|
if (activeStarterPack?.uri) {
|
||||||
|
try {
|
||||||
const spRes = await agent.app.bsky.graph.getStarterPack({
|
const spRes = await agent.app.bsky.graph.getStarterPack({
|
||||||
starterPack: activeStarterPack.uri,
|
starterPack: activeStarterPack.uri,
|
||||||
})
|
})
|
||||||
@@ -72,8 +73,13 @@ export function StepFinished() {
|
|||||||
})
|
})
|
||||||
listItems = listRes.data.items
|
listItems = listRes.data.items
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('Failed to fetch starter pack', {safeMessage: e})
|
||||||
|
// don't tell the user, just get them through onboarding.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const {interestsStepResults, profileStepResults} = state
|
const {interestsStepResults, profileStepResults} = state
|
||||||
const {selectedInterests} = interestsStepResults
|
const {selectedInterests} = interestsStepResults
|
||||||
|
|
||||||
@@ -144,9 +150,6 @@ export function StepFinished() {
|
|||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
logger.info(`onboarding: bulk save failed`)
|
logger.info(`onboarding: bulk save failed`)
|
||||||
logger.error(e)
|
logger.error(e)
|
||||||
// If there was an error encountered, we need to just clear the starter pack so we don't break things for subsequent
|
|
||||||
// app restarts
|
|
||||||
setActiveStarterPack(undefined)
|
|
||||||
// don't alert the user, just let them into their account
|
// don't alert the user, just let them into their account
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {LayoutAnimationConfig} from 'react-native-reanimated'
|
import Animated, {
|
||||||
|
FadeIn,
|
||||||
|
FadeOut,
|
||||||
|
LayoutAnimationConfig,
|
||||||
|
} from 'react-native-reanimated'
|
||||||
|
import {AppBskyGraphStarterpack} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
@@ -11,6 +16,8 @@ import {createFullHandle} from '#/lib/strings/handles'
|
|||||||
import {logger} from '#/logger'
|
import {logger} from '#/logger'
|
||||||
import {useServiceQuery} from '#/state/queries/service'
|
import {useServiceQuery} from '#/state/queries/service'
|
||||||
import {useAgent} from '#/state/session'
|
import {useAgent} from '#/state/session'
|
||||||
|
import {useStarterPackQuery} from 'state/queries/useStarterPackQuery'
|
||||||
|
import {useActiveStarterPack} from 'state/shell/starter-pack'
|
||||||
import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout'
|
import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout'
|
||||||
import {
|
import {
|
||||||
initialState,
|
initialState,
|
||||||
@@ -26,6 +33,7 @@ import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
|||||||
import {AppLanguageDropdown} from '#/components/AppLanguageDropdown'
|
import {AppLanguageDropdown} from '#/components/AppLanguageDropdown'
|
||||||
import {Button, ButtonText} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import {Divider} from '#/components/Divider'
|
import {Divider} from '#/components/Divider'
|
||||||
|
import {LinearGradientBackground} from '#/components/LinearGradientBackground'
|
||||||
import {InlineLinkText} from '#/components/Link'
|
import {InlineLinkText} from '#/components/Link'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
@@ -38,6 +46,11 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
|
|||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const agent = useAgent()
|
const agent = useAgent()
|
||||||
|
|
||||||
|
const activeStarterPack = useActiveStarterPack()
|
||||||
|
const {data: starterPack} = useStarterPackQuery({
|
||||||
|
uri: activeStarterPack?.uri,
|
||||||
|
})
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data: serviceInfo,
|
data: serviceInfo,
|
||||||
isFetching,
|
isFetching,
|
||||||
@@ -142,6 +155,31 @@ export function Signup({onPressBack}: {onPressBack: () => void}) {
|
|||||||
description={_(msg`We're so excited to have you join us!`)}
|
description={_(msg`We're so excited to have you join us!`)}
|
||||||
scrollable>
|
scrollable>
|
||||||
<View testID="createAccount" style={a.flex_1}>
|
<View testID="createAccount" style={a.flex_1}>
|
||||||
|
{state.activeStep === SignupStep.INFO &&
|
||||||
|
starterPack &&
|
||||||
|
AppBskyGraphStarterpack.isRecord(starterPack.record) ? (
|
||||||
|
<Animated.View entering={FadeIn} exiting={FadeOut}>
|
||||||
|
<LinearGradientBackground
|
||||||
|
style={[a.mx_lg, a.p_lg, a.gap_sm, a.rounded_sm]}>
|
||||||
|
<Text style={[a.font_bold, a.text_xl, {color: 'white'}]}>
|
||||||
|
{starterPack.record.name}
|
||||||
|
</Text>
|
||||||
|
<Text style={[{color: 'white'}]}>
|
||||||
|
{starterPack.feeds?.length ? (
|
||||||
|
<Trans>
|
||||||
|
You'll follow the suggested users and feeds once you
|
||||||
|
finish creating your account!
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>
|
||||||
|
You'll follow the suggested users once you finish creating
|
||||||
|
your account!
|
||||||
|
</Trans>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</LinearGradientBackground>
|
||||||
|
</Animated.View>
|
||||||
|
) : null}
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.flex_1,
|
a.flex_1,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
AtUri,
|
AtUri,
|
||||||
ModerationOpts,
|
ModerationOpts,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
|
|
||||||
@@ -139,12 +140,12 @@ function LandingScreenLoaded({
|
|||||||
return (
|
return (
|
||||||
<CenteredView style={a.flex_1}>
|
<CenteredView style={a.flex_1}>
|
||||||
<ScrollView
|
<ScrollView
|
||||||
style={[a.flex_1]}
|
style={[a.flex_1, t.atoms.bg]}
|
||||||
contentContainerStyle={{paddingBottom: 100}}>
|
contentContainerStyle={{paddingBottom: 100}}>
|
||||||
<LinearGradientBackground
|
<LinearGradientBackground
|
||||||
style={[
|
style={[
|
||||||
a.align_center,
|
a.align_center,
|
||||||
a.gap_lg,
|
a.gap_sm,
|
||||||
a.px_lg,
|
a.px_lg,
|
||||||
a.py_2xl,
|
a.py_2xl,
|
||||||
isTabletOrDesktop && {
|
isTabletOrDesktop && {
|
||||||
@@ -159,60 +160,102 @@ function LandingScreenLoaded({
|
|||||||
<Logo width={76} fill="white" />
|
<Logo width={76} fill="white" />
|
||||||
</View>
|
</View>
|
||||||
<Text
|
<Text
|
||||||
style={[a.font_bold, a.text_5xl, a.text_center, {color: 'white'}]}>
|
style={[
|
||||||
|
a.font_bold,
|
||||||
|
a.text_4xl,
|
||||||
|
a.text_center,
|
||||||
|
a.leading_tight,
|
||||||
|
{color: 'white'},
|
||||||
|
]}>
|
||||||
{record.name}
|
{record.name}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<Text
|
||||||
style={[a.text_center, a.font_bold, a.text_md, {color: 'white'}]}>
|
style={[
|
||||||
|
a.text_center,
|
||||||
|
a.font_semibold,
|
||||||
|
a.text_md,
|
||||||
|
{color: 'white'},
|
||||||
|
]}>
|
||||||
Starter pack by {creator.displayName || `@${creator.handle}`}
|
Starter pack by {creator.displayName || `@${creator.handle}`}
|
||||||
</Text>
|
</Text>
|
||||||
</LinearGradientBackground>
|
</LinearGradientBackground>
|
||||||
<View style={[a.gap_2xl, a.mx_lg, {marginTop: 50}]}>
|
<View style={[a.gap_2xl, a.mx_lg, a.my_2xl]}>
|
||||||
{record.description ? (
|
{record.description ? (
|
||||||
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
|
<Text style={[a.text_md, t.atoms.text_contrast_medium]}>
|
||||||
{record.description}
|
{record.description}
|
||||||
</Text>
|
</Text>
|
||||||
) : null}
|
) : null}
|
||||||
|
<View style={[a.gap_sm]}>
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`Join the conversation now!`)}
|
label={_(msg`Join Bluesky`)}
|
||||||
onPress={onJoinPress}
|
onPress={onJoinPress}
|
||||||
variant="solid"
|
variant="solid"
|
||||||
color="primary"
|
color="primary"
|
||||||
size="large">
|
size="large">
|
||||||
<ButtonText style={[a.text_lg]}>
|
<ButtonText style={[a.text_lg]}>
|
||||||
<Trans>Join the conversation now!</Trans>
|
<Trans>Join Bluesky</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
{joinedWeekCount && joinedWeekCount >= 25 ? (
|
{joinedWeekCount && joinedWeekCount >= 25 ? (
|
||||||
|
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="arrow-trend-up"
|
||||||
|
size={12}
|
||||||
|
color={t.atoms.text_contrast_medium.color}
|
||||||
|
/>
|
||||||
<Text
|
<Text
|
||||||
style={[
|
style={[
|
||||||
a.font_bold,
|
a.font_semibold,
|
||||||
a.text_md,
|
a.text_sm,
|
||||||
a.text_center,
|
|
||||||
t.atoms.text_contrast_medium,
|
t.atoms.text_contrast_medium,
|
||||||
]}>
|
]}
|
||||||
{joinedWeekCount} joined this week!
|
numberOfLines={1}>
|
||||||
|
123,659 joined this week
|
||||||
</Text>
|
</Text>
|
||||||
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
<View style={{height: 4}} />
|
</View>
|
||||||
<View style={[a.gap_3xl]}>
|
<View style={[a.gap_3xl]}>
|
||||||
{starterPack.feeds?.length ? (
|
{Boolean(listItemsSample?.length) && (
|
||||||
<View style={[a.gap_md]}>
|
<View style={[a.gap_md]}>
|
||||||
<Text style={[a.font_bold, a.text_lg]}>
|
<Text style={[a.font_heavy, a.text_lg]}>
|
||||||
These great feeds will be available after signing up!
|
{listItemsCount <= 8 ? (
|
||||||
|
<Trans>You'll follow these people right away</Trans>
|
||||||
|
) : (
|
||||||
|
<Trans>
|
||||||
|
You'll follow these people and {listItemsCount - 8} others
|
||||||
|
</Trans>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
<View>
|
||||||
|
{starterPack.listItemsSample?.slice(0, 8).map(item => (
|
||||||
|
<View
|
||||||
|
key={item.subject.did}
|
||||||
|
style={[
|
||||||
|
a.py_lg,
|
||||||
|
a.px_md,
|
||||||
|
a.border_t,
|
||||||
|
t.atoms.border_contrast_low,
|
||||||
|
]}>
|
||||||
|
<ProfileCard profile={item.subject} />
|
||||||
|
</View>
|
||||||
|
))}
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
{feeds?.length ? (
|
||||||
|
<View style={[a.gap_md]}>
|
||||||
|
<Text style={[a.font_heavy, a.text_lg]}>
|
||||||
|
<Trans>You'll stay updated with these feeds</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
|
<View style={[{pointerEvents: 'none'}]}>
|
||||||
|
{feeds?.map(feed => (
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
t.atoms.bg_contrast_25,
|
a.py_lg,
|
||||||
a.rounded_sm,
|
a.px_md,
|
||||||
{pointerEvents: 'none'},
|
a.border_t,
|
||||||
]}>
|
|
||||||
{starterPack.feeds?.map((feed, index) => (
|
|
||||||
<View
|
|
||||||
style={[
|
|
||||||
a.p_lg,
|
|
||||||
index !== 0 && a.border_t,
|
|
||||||
t.atoms.border_contrast_low,
|
t.atoms.border_contrast_low,
|
||||||
]}
|
]}
|
||||||
key={feed.uri}>
|
key={feed.uri}>
|
||||||
@@ -222,61 +265,13 @@ function LandingScreenLoaded({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{Boolean(listItemsSample?.length) && (
|
|
||||||
<View style={[a.gap_md]}>
|
|
||||||
<Text style={[a.font_bold, a.text_lg]}>
|
|
||||||
{feeds?.length ? (
|
|
||||||
<>
|
|
||||||
{listItemsCount <= 8 ? (
|
|
||||||
<Trans>
|
|
||||||
You'll also follow these people right away!
|
|
||||||
</Trans>
|
|
||||||
) : (
|
|
||||||
<Trans>
|
|
||||||
You'll also follow these people and{' '}
|
|
||||||
{listItemsCount - 8} others!
|
|
||||||
</Trans>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{listItemsCount <= 8 ? (
|
|
||||||
<Trans>You'll follow these people right away!</Trans>
|
|
||||||
) : (
|
|
||||||
<Trans>
|
|
||||||
You'll follow these people and {listItemsCount - 8}{' '}
|
|
||||||
others!
|
|
||||||
</Trans>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Text>
|
|
||||||
<View style={[t.atoms.bg_contrast_25, a.rounded_sm]}>
|
|
||||||
{starterPack.listItemsSample
|
|
||||||
?.slice(0, 8)
|
|
||||||
.map((item, index) => (
|
|
||||||
<View
|
|
||||||
key={item.subject.did}
|
|
||||||
style={[
|
|
||||||
a.p_lg,
|
|
||||||
index !== 0 && a.border_t,
|
|
||||||
t.atoms.border_contrast_low,
|
|
||||||
]}>
|
|
||||||
<ProfileCard profile={item.subject} />
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`Signup without a starter pack`)}
|
label={_(msg`Signup without a starter pack`)}
|
||||||
variant="ghost"
|
variant="solid"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
size="medium"
|
size="medium"
|
||||||
style={[a.mt_2xl]}
|
style={[a.py_lg]}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
setActiveStarterPack(undefined)
|
setActiveStarterPack(undefined)
|
||||||
setScreenState(LoggedOutScreenState.S_CreateAccount)
|
setScreenState(LoggedOutScreenState.S_CreateAccount)
|
||||||
@@ -285,6 +280,7 @@ function LandingScreenLoaded({
|
|||||||
<Trans>Signup without a starter pack</Trans>
|
<Trans>Signup without a starter pack</Trans>
|
||||||
</ButtonText>
|
</ButtonText>
|
||||||
</Button>
|
</Button>
|
||||||
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
<AppClipOverlay
|
<AppClipOverlay
|
||||||
visible={appClipOverlayVisible}
|
visible={appClipOverlayVisible}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
import {Pressable, View} from 'react-native'
|
import {Pressable, View} from 'react-native'
|
||||||
import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
|
import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
|
||||||
|
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
@@ -136,6 +137,7 @@ function Header({
|
|||||||
|
|
||||||
const {record, creator} = starterPack
|
const {record, creator} = starterPack
|
||||||
const isOwn = creator?.did === currentAccount?.did
|
const isOwn = creator?.did === currentAccount?.did
|
||||||
|
const joinedAllTimeCount = starterPack.joinedAllTimeCount ?? 0
|
||||||
|
|
||||||
const onFollowAll = async () => {
|
const onFollowAll = async () => {
|
||||||
if (!starterPack.list) return
|
if (!starterPack.list) return
|
||||||
@@ -182,7 +184,7 @@ function Header({
|
|||||||
avatar={undefined}
|
avatar={undefined}
|
||||||
creator={creator}
|
creator={creator}
|
||||||
avatarType="starter-pack">
|
avatarType="starter-pack">
|
||||||
<View style={[a.gap_sm, isOwn ? a.flex_row_reverse : a.flex_row]}>
|
<View style={[a.flex_row, a.gap_sm]}>
|
||||||
{isOwn ? (
|
{isOwn ? (
|
||||||
<Button
|
<Button
|
||||||
label={_(msg`Edit`)}
|
label={_(msg`Edit`)}
|
||||||
@@ -217,17 +219,29 @@ function Header({
|
|||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</ProfileSubpageHeader>
|
</ProfileSubpageHeader>
|
||||||
<View style={[a.px_md, a.py_lg, a.gap_md]}>
|
{record.description || joinedAllTimeCount >= 25 ? (
|
||||||
|
<View style={[a.px_lg, a.pt_md, a.pb_sm, a.gap_md]}>
|
||||||
|
{record.description ? (
|
||||||
<Text style={[a.text_md]}>{record.description}</Text>
|
<Text style={[a.text_md]}>{record.description}</Text>
|
||||||
{starterPack.joinedWeekCount && starterPack.joinedWeekCount >= 25 ? (
|
) : null}
|
||||||
<Text style={[a.font_bold, a.text_md, t.atoms.text_contrast_medium]}>
|
{joinedAllTimeCount >= 25 ? (
|
||||||
|
<View style={[a.flex_row, a.align_center, a.gap_sm]}>
|
||||||
|
<FontAwesomeIcon
|
||||||
|
icon="arrow-trend-up"
|
||||||
|
size={12}
|
||||||
|
color={t.atoms.text_contrast_medium.color}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={[a.font_bold, a.text_sm, t.atoms.text_contrast_medium]}>
|
||||||
<Trans>
|
<Trans>
|
||||||
{starterPack.joinedAllTimeCount || 0} people have used this
|
{starterPack.joinedAllTimeCount || 0} people have used this
|
||||||
starter pack!
|
starter pack!
|
||||||
</Trans>
|
</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
) : null}
|
||||||
<QrCodeDialog control={qrCodeDialogControl} starterPack={starterPack} />
|
<QrCodeDialog control={qrCodeDialogControl} starterPack={starterPack} />
|
||||||
<ReportDialog
|
<ReportDialog
|
||||||
control={reportDialogControl}
|
control={reportDialogControl}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
|
import {GeneratorView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
|
|
||||||
|
import {useSession} from 'state/session'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
|
|
||||||
const steps = ['Details', 'Profiles', 'Feeds'] as const
|
const steps = ['Details', 'Profiles', 'Feeds'] as const
|
||||||
@@ -119,6 +120,8 @@ export function Provider({
|
|||||||
listItems?: AppBskyGraphDefs.ListItemView[]
|
listItems?: AppBskyGraphDefs.ListItemView[]
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
|
||||||
const createInitialState = (): State => {
|
const createInitialState = (): State => {
|
||||||
if (starterPack && AppBskyGraphStarterpack.isRecord(starterPack.record)) {
|
if (starterPack && AppBskyGraphStarterpack.isRecord(starterPack.record)) {
|
||||||
return {
|
return {
|
||||||
@@ -126,7 +129,10 @@ export function Provider({
|
|||||||
currentStep: 'Details',
|
currentStep: 'Details',
|
||||||
name: starterPack.record.name,
|
name: starterPack.record.name,
|
||||||
description: starterPack.record.description,
|
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 ?? [],
|
feeds: starterPack.feeds ?? [],
|
||||||
processing: false,
|
processing: false,
|
||||||
transitionDirection: 'Forward',
|
transitionDirection: 'Forward',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {useSession} from 'state/session'
|
|||||||
import {useWizardState} from '#/screens/StarterPack/Wizard/State'
|
import {useWizardState} from '#/screens/StarterPack/Wizard/State'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
import {atoms as a, useTheme} from '#/alf'
|
||||||
import * as TextField from '#/components/forms/TextField'
|
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 {ScreenTransition} from '#/components/StarterPack/Wizard/ScreenTransition'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
|
|
||||||
@@ -26,10 +26,8 @@ export function StepDetails() {
|
|||||||
return (
|
return (
|
||||||
<ScreenTransition direction={state.transitionDirection}>
|
<ScreenTransition direction={state.transitionDirection}>
|
||||||
<View style={[a.px_xl, a.gap_xl, a.mt_4xl]}>
|
<View style={[a.px_xl, a.gap_xl, a.mt_4xl]}>
|
||||||
<View style={[{height: 90}]}>
|
|
||||||
<StarterPackIcon />
|
|
||||||
</View>
|
|
||||||
<View style={[a.gap_md, a.align_center, a.px_md, a.mb_md]}>
|
<View style={[a.gap_md, a.align_center, a.px_md, a.mb_md]}>
|
||||||
|
<StarterPack width={90} gradient="sky" />
|
||||||
<Text style={[a.font_bold, a.text_3xl]}>
|
<Text style={[a.font_bold, a.text_3xl]}>
|
||||||
<Trans>Invites, but personal</Trans>
|
<Trans>Invites, but personal</Trans>
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -70,8 +70,9 @@ export function StepProfiles({
|
|||||||
sideBorders={false}
|
sideBorders={false}
|
||||||
style={[a.flex_1]}
|
style={[a.flex_1]}
|
||||||
onEndReached={() => {
|
onEndReached={() => {
|
||||||
console.log('test')
|
if (!query) {
|
||||||
fetchNextPage()
|
fetchNextPage()
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
onEndReachedThreshold={isNative ? 2 : 0.25}
|
onEndReachedThreshold={isNative ? 2 : 0.25}
|
||||||
ListEmptyComponent={
|
ListEmptyComponent={
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ export function Wizard({
|
|||||||
isLoadingStarterPack || isLoadingProfiles || isLoadingProfile
|
isLoadingStarterPack || isLoadingProfiles || isLoadingProfile
|
||||||
}
|
}
|
||||||
isError={isErrorStarterPack || isErrorProfiles || isErrorProfile}
|
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) {
|
} else if (isEdit && starterPack?.creator.did !== currentAccount?.did) {
|
||||||
@@ -112,7 +112,7 @@ export function Wizard({
|
|||||||
<ListMaybePlaceholder
|
<ListMaybePlaceholder
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
isError={true}
|
isError={true}
|
||||||
errorMessage={_(msg`Could not find that starter pack`)}
|
errorMessage={_(msg`That starter pack could not be found.`)}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -182,11 +182,18 @@ function WizardInner({
|
|||||||
}, [setMinimalShellMode, setEnabled]),
|
}, [setMinimalShellMode, setEnabled]),
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultName = _(
|
const getDefaultName = () => {
|
||||||
msg`${
|
let displayName
|
||||||
currentProfile?.displayName || `@${currentProfile?.handle}`
|
if (
|
||||||
}'s Starter Pack`,
|
currentProfile?.displayName != null &&
|
||||||
).slice(0, 50)
|
currentProfile?.displayName !== ''
|
||||||
|
) {
|
||||||
|
displayName = sanitizeDisplayName(currentProfile.displayName)
|
||||||
|
} else {
|
||||||
|
displayName = sanitizeHandle(currentProfile!.handle)
|
||||||
|
}
|
||||||
|
return _(msg`${displayName}'s Starter Pack`).slice(0, 50)
|
||||||
|
}
|
||||||
|
|
||||||
const wizardUiStrings: Record<
|
const wizardUiStrings: Record<
|
||||||
WizardStep,
|
WizardStep,
|
||||||
@@ -273,7 +280,7 @@ function WizardInner({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
list = await createStarterPackList({
|
list = await createStarterPackList({
|
||||||
name: state.name ?? defaultName,
|
name: state.name ?? getDefaultName(),
|
||||||
description: state.description,
|
description: state.description,
|
||||||
descriptionFacets: [],
|
descriptionFacets: [],
|
||||||
profiles: state.profiles,
|
profiles: state.profiles,
|
||||||
@@ -286,7 +293,7 @@ function WizardInner({
|
|||||||
collection: 'app.bsky.graph.starterpack',
|
collection: 'app.bsky.graph.starterpack',
|
||||||
rkey,
|
rkey,
|
||||||
record: {
|
record: {
|
||||||
name: state.name ?? defaultName,
|
name: state.name ?? getDefaultName(),
|
||||||
description: state.description,
|
description: state.description,
|
||||||
descriptionFacets: [],
|
descriptionFacets: [],
|
||||||
list: list?.uri,
|
list: list?.uri,
|
||||||
@@ -299,9 +306,8 @@ function WizardInner({
|
|||||||
validate: false, // TODO remove!
|
validate: false, // TODO remove!
|
||||||
})
|
})
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
await invalidateQueries()
|
await invalidateQueries()
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
if (navigation.canGoBack()) {
|
if (navigation.canGoBack()) {
|
||||||
navigation.goBack()
|
navigation.goBack()
|
||||||
} else {
|
} else {
|
||||||
@@ -311,11 +317,11 @@ function WizardInner({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
dispatch({type: 'SetProcessing', processing: false})
|
dispatch({type: 'SetProcessing', processing: false})
|
||||||
}, 1000)
|
}, 2000)
|
||||||
} else {
|
} else {
|
||||||
// Creating a new starter pack
|
// Creating a new starter pack
|
||||||
const list = await createStarterPackList({
|
const list = await createStarterPackList({
|
||||||
name: state.name ?? defaultName,
|
name: state.name ?? getDefaultName(),
|
||||||
description: state.description,
|
description: state.description,
|
||||||
descriptionFacets: [],
|
descriptionFacets: [],
|
||||||
profiles: state.profiles,
|
profiles: state.profiles,
|
||||||
@@ -327,7 +333,7 @@ function WizardInner({
|
|||||||
validate: false,
|
validate: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: state.name ?? defaultName,
|
name: state.name ?? getDefaultName(),
|
||||||
description: state.description,
|
description: state.description,
|
||||||
descriptionFacets: [],
|
descriptionFacets: [],
|
||||||
list: list.uri,
|
list: list.uri,
|
||||||
@@ -346,16 +352,18 @@ function WizardInner({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const newRkey = new AtUri(res.uri).rkey
|
const newRkey = new AtUri(res.uri).rkey
|
||||||
setTimeout(() => {
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
await invalidateQueries()
|
||||||
navigation.replace('StarterPack', {
|
navigation.replace('StarterPack', {
|
||||||
name: currentAccount!.handle,
|
name: currentAccount!.handle,
|
||||||
rkey: newRkey,
|
rkey: newRkey,
|
||||||
})
|
})
|
||||||
dispatch({type: 'SetProcessing', processing: false})
|
dispatch({type: 'SetProcessing', processing: false})
|
||||||
}, 1000)
|
}, 2000)
|
||||||
}
|
}
|
||||||
} catch (e: unknown) {
|
} 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`))
|
Toast.show(_(msg`Failed to create starter pack`))
|
||||||
dispatch({type: 'SetProcessing', processing: false})
|
dispatch({type: 'SetProcessing', processing: false})
|
||||||
return
|
return
|
||||||
@@ -376,9 +384,12 @@ function WizardInner({
|
|||||||
repo: currentAccount!.did,
|
repo: currentAccount!.did,
|
||||||
rkey,
|
rkey,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
await invalidateQueries()
|
await invalidateQueries()
|
||||||
logEvent('starterPack:delete', {})
|
logEvent('starterPack:delete', {})
|
||||||
navigation.popToTop()
|
navigation.popToTop()
|
||||||
|
}, 2000)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.show(_(msg`Failed to delete starter pack`))
|
Toast.show(_(msg`Failed to delete starter pack`))
|
||||||
} finally {
|
} finally {
|
||||||
@@ -426,7 +437,7 @@ function WizardInner({
|
|||||||
accessibilityHint={_(msg`Go back to the previous step`)}
|
accessibilityHint={_(msg`Go back to the previous step`)}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (state.currentStep === 'Details') {
|
if (state.currentStep === 'Details') {
|
||||||
navigation.goBack()
|
navigation.pop()
|
||||||
} else {
|
} else {
|
||||||
dispatch({type: 'Back'})
|
dispatch({type: 'Back'})
|
||||||
}
|
}
|
||||||
@@ -715,6 +726,7 @@ function Footer({
|
|||||||
state={state}
|
state={state}
|
||||||
dispatch={dispatch}
|
dispatch={dispatch}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
|
profile={profile}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,21 @@
|
|||||||
import {StarterPackView} from '@atproto/api/dist/client/types/app/bsky/graph/defs'
|
import {StarterPackView} from '@atproto/api/dist/client/types/app/bsky/graph/defs'
|
||||||
import {QueryClient, useQuery} from '@tanstack/react-query'
|
import {QueryClient, useQuery} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {httpStarterPackUriToAtUri} from 'lib/strings/starter-pack'
|
import {
|
||||||
|
httpStarterPackUriToAtUri,
|
||||||
|
parseStarterPackUri,
|
||||||
|
} from 'lib/strings/starter-pack'
|
||||||
import {useAgent} from 'state/session'
|
import {useAgent} from 'state/session'
|
||||||
|
|
||||||
const RQKEY_ROOT = 'starter-pack'
|
const RQKEY_ROOT = 'starter-pack'
|
||||||
const RQKEY = (did?: string, rkey?: string) => [RQKEY_ROOT, did, rkey]
|
const RQKEY = (did?: string, rkey?: string) => {
|
||||||
|
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({
|
export function useStarterPackQuery({
|
||||||
uri,
|
uri,
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
|
import {useSession} from 'state/session'
|
||||||
|
import {useActiveStarterPack} from 'state/shell/starter-pack'
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
showLoggedOut: boolean
|
showLoggedOut: boolean
|
||||||
/**
|
/**
|
||||||
@@ -22,7 +25,7 @@ type Controls = {
|
|||||||
/**
|
/**
|
||||||
* The did of the account to populate the login form with.
|
* The did of the account to populate the login form with.
|
||||||
*/
|
*/
|
||||||
requestedAccount?: string | 'none' | 'new'
|
requestedAccount?: string | 'none' | 'new' | 'starterpack'
|
||||||
}) => void
|
}) => void
|
||||||
/**
|
/**
|
||||||
* Clears the requested account so that next time the logged out view is
|
* Clears the requested account so that next time the logged out view is
|
||||||
@@ -43,9 +46,12 @@ const ControlsContext = React.createContext<Controls>({
|
|||||||
})
|
})
|
||||||
|
|
||||||
export function Provider({children}: React.PropsWithChildren<{}>) {
|
export function Provider({children}: React.PropsWithChildren<{}>) {
|
||||||
|
const activeStarterPack = useActiveStarterPack()
|
||||||
|
const {hasSession} = useSession()
|
||||||
|
const shouldShowStarterPack = Boolean(activeStarterPack?.uri) && !hasSession
|
||||||
const [state, setState] = React.useState<State>({
|
const [state, setState] = React.useState<State>({
|
||||||
showLoggedOut: false,
|
showLoggedOut: shouldShowStarterPack,
|
||||||
requestedAccountSwitchTo: undefined,
|
requestedAccountSwitchTo: shouldShowStarterPack ? 'starterpack' : undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
const controls = React.useMemo<Controls>(
|
const controls = React.useMemo<Controls>(
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {useNavigation} from '@react-navigation/native'
|
|||||||
|
|
||||||
import {useAnalytics} from '#/lib/analytics/analytics'
|
import {useAnalytics} from '#/lib/analytics/analytics'
|
||||||
import {usePalette} from '#/lib/hooks/usePalette'
|
import {usePalette} from '#/lib/hooks/usePalette'
|
||||||
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
|
|
||||||
import {logEvent} from '#/lib/statsig/statsig'
|
import {logEvent} from '#/lib/statsig/statsig'
|
||||||
import {s} from '#/lib/styles'
|
import {s} from '#/lib/styles'
|
||||||
import {isIOS, isNative} from '#/platform/detection'
|
import {isIOS, isNative} from '#/platform/detection'
|
||||||
@@ -51,7 +50,6 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
|
|||||||
return ScreenState.S_LoginOrCreateAccount
|
return ScreenState.S_LoginOrCreateAccount
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
const {isMobile} = useWebMediaQueries()
|
|
||||||
const {clearRequestedAccount} = useLoggedOutViewControls()
|
const {clearRequestedAccount} = useLoggedOutViewControls()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
|
|
||||||
@@ -73,21 +71,9 @@ export function LoggedOut({onDismiss}: {onDismiss?: () => void}) {
|
|||||||
}, [navigation])
|
}, [navigation])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View
|
<View testID="noSessionView" style={[s.hContentRegion, pal.view]}>
|
||||||
testID="noSessionView"
|
|
||||||
style={[
|
|
||||||
s.hContentRegion,
|
|
||||||
pal.view,
|
|
||||||
{
|
|
||||||
// only needed if dismiss button is present
|
|
||||||
paddingTop:
|
|
||||||
onDismiss && isMobile && screenState !== ScreenState.S_StarterPack
|
|
||||||
? 40
|
|
||||||
: 0,
|
|
||||||
},
|
|
||||||
]}>
|
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
{onDismiss && screenState !== ScreenState.S_StarterPack ? (
|
{onDismiss && screenState === ScreenState.S_LoginOrCreateAccount ? (
|
||||||
<Pressable
|
<Pressable
|
||||||
accessibilityHint={_(msg`Go back`)}
|
accessibilityHint={_(msg`Go back`)}
|
||||||
accessibilityLabel={_(msg`Go back`)}
|
accessibilityLabel={_(msg`Go back`)}
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ import {PreviewableUserAvatar, UserAvatar} from '../util/UserAvatar'
|
|||||||
|
|
||||||
import hairlineWidth = StyleSheet.hairlineWidth
|
import hairlineWidth = StyleSheet.hairlineWidth
|
||||||
import {parseTenorGif} from '#/lib/strings/embed-player'
|
import {parseTenorGif} from '#/lib/strings/embed-player'
|
||||||
import {StarterPackIcon} from '#/components/icons/StarterPackIcon'
|
import {StarterPack} from '#/components/icons/StarterPack'
|
||||||
|
|
||||||
const MAX_AUTHORS = 5
|
const MAX_AUTHORS = 5
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ let FeedItem = ({
|
|||||||
} else if (item.type === 'starter-pack-signup') {
|
} else if (item.type === 'starter-pack-signup') {
|
||||||
icon = (
|
icon = (
|
||||||
<View style={{height: 30, width: 30}}>
|
<View style={{height: 30, width: 30}}>
|
||||||
<StarterPackIcon />
|
<StarterPack width={30} gradient="sky" />
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
action = _(msg`signed up with your starter pack`)
|
action = _(msg`signed up with your starter pack`)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import {CenteredView} from '../util/Views'
|
|||||||
import hairlineWidth = StyleSheet.hairlineWidth
|
import hairlineWidth = StyleSheet.hairlineWidth
|
||||||
|
|
||||||
import {Menu_Stroke2_Corner0_Rounded as Menu} from '#/components/icons/Menu'
|
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({
|
export function ProfileSubpageHeader({
|
||||||
isLoading,
|
isLoading,
|
||||||
@@ -130,7 +130,7 @@ export function ProfileSubpageHeader({
|
|||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
style={{width: 58}}>
|
style={{width: 58}}>
|
||||||
{avatarType === 'starter-pack' ? (
|
{avatarType === 'starter-pack' ? (
|
||||||
<StarterPackIcon width={58} height={58} />
|
<StarterPack width={58} gradient="sky" />
|
||||||
) : (
|
) : (
|
||||||
<UserAvatar type={avatarType} size={58} avatar={avatar} />
|
<UserAvatar type={avatarType} size={58} avatar={avatar} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -22,13 +22,6 @@ import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
|
|||||||
import {useOTAUpdates} from 'lib/hooks/useOTAUpdates'
|
import {useOTAUpdates} from 'lib/hooks/useOTAUpdates'
|
||||||
import {useRequestNotificationsPermission} from 'lib/notifications/notifications'
|
import {useRequestNotificationsPermission} from 'lib/notifications/notifications'
|
||||||
import {HomeTabNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
|
import {HomeTabNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
|
||||||
import {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 {FeedPage} from 'view/com/feeds/FeedPage'
|
||||||
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
|
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
|
||||||
import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState'
|
import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState'
|
||||||
@@ -37,40 +30,11 @@ import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed'
|
|||||||
import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned'
|
import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned'
|
||||||
import {HomeHeader} from '../com/home/HomeHeader'
|
import {HomeHeader} from '../com/home/HomeHeader'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
|
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home' | 'Start'>
|
||||||
export function HomeScreen(props: Props) {
|
export function HomeScreen(props: Props) {
|
||||||
const {navigation} = props
|
|
||||||
const {hasSession} = useSession()
|
|
||||||
const {data: preferences} = usePreferencesQuery()
|
const {data: preferences} = usePreferencesQuery()
|
||||||
const {data: pinnedFeedInfos, isLoading: isPinnedFeedsLoading} =
|
const {data: pinnedFeedInfos, isLoading: isPinnedFeedsLoading} =
|
||||||
usePinnedFeedsInfos()
|
usePinnedFeedsInfos()
|
||||||
const 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) {
|
if (preferences && pinnedFeedInfos && !isPinnedFeedsLoading) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -447,6 +447,7 @@ function ProfileScreenLoaded({
|
|||||||
? ({headerHeight, isFocused, scrollElRef}) => (
|
? ({headerHeight, isFocused, scrollElRef}) => (
|
||||||
<ProfileStarterPacks
|
<ProfileStarterPacks
|
||||||
ref={starterPacksSectionRef}
|
ref={starterPacksSectionRef}
|
||||||
|
isMe={isMe}
|
||||||
starterPacksQuery={starterPacksQuery}
|
starterPacksQuery={starterPacksQuery}
|
||||||
scrollElRef={scrollElRef as ListRef}
|
scrollElRef={scrollElRef as ListRef}
|
||||||
headerOffset={headerHeight}
|
headerOffset={headerHeight}
|
||||||
|
|||||||
@@ -45,6 +45,14 @@ export function Icons() {
|
|||||||
<Loader size="lg" fill={t.atoms.text.color} />
|
<Loader size="lg" fill={t.atoms.text.color} />
|
||||||
<Loader size="xl" fill={t.atoms.text.color} />
|
<Loader size="xl" fill={t.atoms.text.color} />
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
<View style={[a.flex_row, a.gap_xl]}>
|
||||||
|
<Globe size="xs" gradient="sky" />
|
||||||
|
<Globe size="sm" gradient="sky" />
|
||||||
|
<Globe size="md" gradient="sky" />
|
||||||
|
<Globe size="lg" gradient="sky" />
|
||||||
|
<Globe size="xl" gradient="sky" />
|
||||||
|
</View>
|
||||||
</View>
|
</View>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,12 +100,18 @@ function ProfileCard() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const HIDDEN_BACK_BNT_ROUTES = ['StarterPackWizard', 'StarterPackEdit']
|
||||||
|
|
||||||
function BackBtn() {
|
function BackBtn() {
|
||||||
const {isTablet} = useWebMediaQueries()
|
const {isTablet} = useWebMediaQueries()
|
||||||
const pal = usePalette('default')
|
const pal = usePalette('default')
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const {_} = useLingui()
|
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(() => {
|
const onPressBack = React.useCallback(() => {
|
||||||
if (navigation.canGoBack()) {
|
if (navigation.canGoBack()) {
|
||||||
|
|||||||
Reference in New Issue
Block a user