diff --git a/src/components/StarterPack/QrCodeDialog.tsx b/src/components/StarterPack/QrCodeDialog.tsx
index 5ad47b7aaf..580c6cc7c8 100644
--- a/src/components/StarterPack/QrCodeDialog.tsx
+++ b/src/components/StarterPack/QrCodeDialog.tsx
@@ -4,17 +4,15 @@ import ViewShot from 'react-native-view-shot'
import * as FS from 'expo-file-system'
import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
import * as Sharing from 'expo-sharing'
-import {AppBskyGraphDefs, AppBskyGraphStarterpack, AtUri} from '@atproto/api'
+import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {nanoid} from 'nanoid/non-secure'
import {logger} from '#/logger'
import {saveImageToMediaLibrary} from 'lib/media/manip'
-import {makeStarterPackLink} from 'lib/routes/links'
import {logEvent} from 'lib/statsig/statsig'
import {isNative, isWeb} from 'platform/detection'
-import {useShortenLink} from 'state/queries/shorten-link'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
@@ -24,44 +22,19 @@ import {Loader} from '#/components/Loader'
import {QrCode} from '#/components/StarterPack/QrCode'
export function QrCodeDialog({
- control,
- starterPack,
-}: {
- control: DialogControlProps
- starterPack: AppBskyGraphDefs.StarterPackView
-}) {
- return (
-
-
-
- )
-}
-
-function Inner({
starterPack,
+ link,
control,
}: {
starterPack: AppBskyGraphDefs.StarterPackView
+ link?: string
control: DialogControlProps
}) {
const {_} = useLingui()
const [isProcessing, setIsProcessing] = React.useState(false)
- const [link, setLink] = React.useState()
- const shortenLink = useShortenLink()
const ref = React.useRef(null)
- React.useEffect(() => {
- if (link) return
- ;(async () => {
- const rkey = new AtUri(starterPack.uri).rkey
- const res = await shortenLink(
- makeStarterPackLink(starterPack.creator.did, rkey),
- )
- setLink(res.url)
- })()
- }, [link, shortenLink, starterPack.creator.did, starterPack.uri])
-
const getCanvas = (base64: string): Promise => {
return new Promise(resolve => {
const image = new Image()
@@ -178,7 +151,7 @@ function Inner({
}
return (
- <>
+
@@ -195,12 +168,13 @@ function Inner({
) : (
-
+
- >
+
)
}
diff --git a/src/components/StarterPack/ShareDialog.tsx b/src/components/StarterPack/ShareDialog.tsx
new file mode 100644
index 0000000000..23fa10fb39
--- /dev/null
+++ b/src/components/StarterPack/ShareDialog.tsx
@@ -0,0 +1,180 @@
+import React from 'react'
+import {View} from 'react-native'
+import * as FS from 'expo-file-system'
+import {Image} from 'expo-image'
+import {requestMediaLibraryPermissionsAsync} from 'expo-image-picker'
+import {AppBskyGraphDefs} from '@atproto/api'
+import {msg, Trans} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+import {nanoid} from 'nanoid/non-secure'
+
+import {logger} from '#/logger'
+import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
+import {saveImageToMediaLibrary} from 'lib/media/manip'
+import {shareUrl} from 'lib/sharing'
+import {logEvent} from 'lib/statsig/statsig'
+import {getStarterPackOgCard} from 'lib/strings/starter-pack'
+import {isNative, isWeb} from 'platform/detection'
+import * as Toast from 'view/com/util/Toast'
+import {atoms as a, useTheme} from '#/alf'
+import {Button, ButtonText} from '#/components/Button'
+import {DialogControlProps} from '#/components/Dialog'
+import * as Dialog from '#/components/Dialog'
+import {Loader} from '#/components/Loader'
+import {Text} from '#/components/Typography'
+
+interface Props {
+ starterPack: AppBskyGraphDefs.StarterPackView
+ link?: string
+ imageLoaded?: boolean
+ qrDialogControl: DialogControlProps
+ control: DialogControlProps
+}
+
+export function ShareDialog(props: Props) {
+ return (
+
+
+
+ )
+}
+
+function ShareDialogInner({
+ starterPack,
+ link,
+ imageLoaded,
+ qrDialogControl,
+ control,
+}: Props) {
+ const {_} = useLingui()
+ const t = useTheme()
+ const {isTabletOrDesktop} = useWebMediaQueries()
+
+ const imageUrl = getStarterPackOgCard(starterPack)
+
+ const onShareLink = async () => {
+ if (!link) return
+ shareUrl(link)
+ logEvent('starterPack:share', {
+ starterPack: starterPack.uri,
+ shareType: 'link',
+ })
+ control.close()
+ }
+
+ const onSave = async () => {
+ const res = await requestMediaLibraryPermissionsAsync()
+
+ if (!res) {
+ Toast.show(
+ _(msg`You must grant access to your photo library to save the image.`),
+ )
+ return
+ }
+
+ const cachePath = await Image.getCachePathAsync(imageUrl)
+ const filename = `${FS.documentDirectory}/${nanoid(12)}.png`
+
+ if (!cachePath) {
+ Toast.show(_(msg`An error occurred while saving the image.`))
+ return
+ }
+
+ try {
+ await FS.copyAsync({from: cachePath, to: filename})
+ await saveImageToMediaLibrary({uri: filename})
+ await FS.deleteAsync(filename)
+
+ Toast.show(_(msg`Image saved to your camera roll!`))
+ control.close()
+ } catch (e: unknown) {
+ Toast.show(_(msg`An error occurred while saving the QR code!`))
+ logger.error('Failed to save QR code', {error: e})
+ return
+ }
+ }
+
+ return (
+ <>
+
+
+ {!imageLoaded || !link ? (
+
+
+
+ ) : (
+
+
+
+ Invite people to this starter pack!
+
+
+
+ Share this starter pack and help people join your community on
+ Bluesky.
+
+
+
+
+
+
+
+ {isNative && (
+
+ )}
+
+
+ )}
+
+ >
+ )
+}
diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts
index 14139e40a5..8a173b6756 100644
--- a/src/lib/routes/types.ts
+++ b/src/lib/routes/types.ts
@@ -43,7 +43,7 @@ export type CommonNavigatorParams = {
MessagesSettings: undefined
Feeds: undefined
Start: {name: string; rkey: string}
- StarterPack: {name: string; rkey: string}
+ StarterPack: {name: string; rkey: string; new?: boolean}
StarterPackWizard: undefined
StarterPackEdit: {
rkey?: string
@@ -100,7 +100,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
MessagesTab: undefined
Messages: {animation?: 'push' | 'pop'}
Start: {name: string; rkey: string}
- StarterPack: {name: string; rkey: string}
+ StarterPack: {name: string; rkey: string; new?: boolean}
StarterPackWizard: undefined
StarterPackEdit: {
rkey?: string
diff --git a/src/lib/strings/starter-pack.ts b/src/lib/strings/starter-pack.ts
index eec7301220..3794651836 100644
--- a/src/lib/strings/starter-pack.ts
+++ b/src/lib/strings/starter-pack.ts
@@ -1,4 +1,4 @@
-import {AtUri} from '@atproto/api'
+import {AppBskyGraphDefs, AtUri} from '@atproto/api'
export function createStarterPackLinkFromAndroidReferrer(
referrerQueryString: string,
@@ -77,3 +77,15 @@ export function httpStarterPackUriToAtUri(httpUri?: string): string | null {
return `at://${parsed.name}/app.bsky.graph.starterpack/${parsed.rkey}`
}
+
+export function getStarterPackOgCard(
+ didOrStarterPack: AppBskyGraphDefs.StarterPackView | string,
+ rkey?: string,
+) {
+ if (typeof didOrStarterPack === 'string') {
+ return `https://ogcard.cdn.bsky.app/start/${didOrStarterPack}/${rkey}`
+ } else {
+ const rkey = new AtUri(didOrStarterPack.uri).rkey
+ return `https://ogcard.cdn.bsky.app/start/${didOrStarterPack.creator.did}/${rkey}`
+ }
+}
diff --git a/src/screens/StarterPack/StarterPackScreen.tsx b/src/screens/StarterPack/StarterPackScreen.tsx
index d645d9db47..26879aab32 100644
--- a/src/screens/StarterPack/StarterPackScreen.tsx
+++ b/src/screens/StarterPack/StarterPackScreen.tsx
@@ -1,8 +1,10 @@
import React from 'react'
import {View} from 'react-native'
+import {Image} from 'expo-image'
import {
AppBskyGraphDefs,
AppBskyGraphStarterpack,
+ AtUri,
ModerationOpts,
} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
@@ -18,8 +20,8 @@ import {useDeleteStarterPackMutation} from '#/state/queries/starter-packs'
import {HITSLOP_20} from 'lib/constants'
import {makeProfileLink, makeStarterPackLink} from 'lib/routes/links'
import {CommonNavigatorParams, NavigationProp} from 'lib/routes/types'
-import {shareUrl} from 'lib/sharing'
import {logEvent} from 'lib/statsig/statsig'
+import {getStarterPackOgCard} from 'lib/strings/starter-pack'
import {isWeb} from 'platform/detection'
import {useModerationOpts} from 'state/preferences/moderation-opts'
import {RQKEY} from 'state/queries/list-members'
@@ -36,11 +38,9 @@ import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded as ArrowOutOfBox} from '#/components/icons/ArrowOutOfBox'
-import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo'
import {DotGrid_Stroke2_Corner0_Rounded as Ellipsis} from '#/components/icons/DotGrid'
import {Pencil_Stroke2_Corner0_Rounded as Pencil} from '#/components/icons/Pencil'
-import {QrCode_Stroke2_Corner0_Rounded as QrCode} from '#/components/icons/QrCode'
import {Trash_Stroke2_Corner0_Rounded as Trash} from '#/components/icons/Trash'
import {ListMaybePlaceholder} from '#/components/Lists'
import {Loader} from '#/components/Loader'
@@ -50,6 +50,7 @@ import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
import {FeedsList} from '#/components/StarterPack/Main/FeedsList'
import {ProfilesList} from '#/components/StarterPack/Main/ProfilesList'
import {QrCodeDialog} from '#/components/StarterPack/QrCodeDialog'
+import {ShareDialog} from '#/components/StarterPack/ShareDialog'
import {Text} from '#/components/Typography'
type StarterPackScreeProps = NativeStackScreenProps<
@@ -106,23 +107,40 @@ function StarterPackScreenInner({
routeParams: StarterPackScreeProps['route']['params']
moderationOpts: ModerationOpts
}) {
- const shortenLink = useShortenLink()
-
const tabs = [
...(starterPack.list ? ['People'] : []),
...(starterPack.feeds?.length ? ['Feeds'] : []),
]
- const onShareLink = async () => {
- const res = await shortenLink(
- makeStarterPackLink(starterPack.creator.did, routeParams.rkey),
+ const qrCodeDialogControl = useDialogControl()
+ const shareDialogControl = useDialogControl()
+
+ const shortenLink = useShortenLink()
+ const [link, setLink] = React.useState()
+ const [imageLoaded, setImageLoaded] = React.useState(false)
+
+ const onOpenShareDialog = React.useCallback(() => {
+ const rkey = new AtUri(starterPack.uri).rkey
+ shortenLink(makeStarterPackLink(starterPack.creator.did, rkey)).then(
+ res => {
+ setLink(res.url)
+ },
)
- shareUrl(res.url)
- logEvent('starterPack:share', {
- starterPack: starterPack.uri,
- shareType: 'link',
- })
- }
+ Image.prefetch(getStarterPackOgCard(starterPack))
+ .then(() => {
+ setImageLoaded(true)
+ })
+ .catch(() => {
+ setImageLoaded(true)
+ })
+ shareDialogControl.open()
+ }, [shareDialogControl, shortenLink, starterPack])
+
+ React.useEffect(() => {
+ if (routeParams.new) {
+ onOpenShareDialog()
+ }
+ }, [onOpenShareDialog, routeParams.new, shareDialogControl])
return (
@@ -134,7 +152,7 @@ function StarterPackScreenInner({
)}>
{starterPack.list != null
@@ -164,6 +182,19 @@ function StarterPackScreenInner({
: null}
+
+
+
)
}
@@ -171,11 +202,11 @@ function StarterPackScreenInner({
function Header({
starterPack,
routeParams,
- onShareLink,
+ onOpenShareDialog,
}: {
starterPack: AppBskyGraphDefs.StarterPackView
routeParams: StarterPackScreeProps['route']['params']
- onShareLink: () => void
+ onOpenShareDialog: () => void
}) {
const {_} = useLingui()
const t = useTheme()
@@ -236,10 +267,17 @@ function Header({
avatarType="starter-pack">
{isOwn ? (
-
+
) : (
@@ -293,17 +331,16 @@ function Header({
function OverflowMenu({
starterPack,
routeParams,
- onShareLink,
+ onOpenShareDialog,
}: {
starterPack: AppBskyGraphDefs.StarterPackView
routeParams: StarterPackScreeProps['route']['params']
- onShareLink: () => void
+ onOpenShareDialog: () => void
}) {
const t = useTheme()
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
const {currentAccount} = useSession()
- const qrCodeDialogControl = useDialogControl()
const reportDialogControl = useReportDialogControl()
const deleteDialogControl = useDialogControl()
const navigation = useNavigation()
@@ -389,23 +426,14 @@ function OverflowMenu({
<>
+ onPress={onOpenShareDialog}>
Share link
-
-
- Create QR code
-
-
-
-
@@ -477,61 +504,3 @@ function OverflowMenu({
>
)
}
-
-function OwnerShareMenu({
- starterPack,
- onShareLink,
-}: {
- starterPack: AppBskyGraphDefs.StarterPackView
- onShareLink: () => void
-}) {
- const {_} = useLingui()
- const qrCodeDialogControl = useDialogControl()
-
- return (
- <>
-
-
- {({props}) => (
-
- )}
-
-
-
-
-
- Share link
-
-
-
-
-
- Create QR code
-
-
-
-
-
-
-
-
- >
- )
-}
diff --git a/src/screens/StarterPack/Wizard/index.tsx b/src/screens/StarterPack/Wizard/index.tsx
index 727955a575..ab43c898f8 100644
--- a/src/screens/StarterPack/Wizard/index.tsx
+++ b/src/screens/StarterPack/Wizard/index.tsx
@@ -5,6 +5,7 @@ import {
useKeyboardController,
} from 'react-native-keyboard-controller'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
+import {Image} from 'expo-image'
import {
AppBskyActorDefs,
AppBskyGraphDefs,
@@ -25,7 +26,10 @@ import {logEvent} from 'lib/statsig/statsig'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {sanitizeHandle} from 'lib/strings/handles'
import {enforceLen} from 'lib/strings/helpers'
-import {parseStarterPackUri} from 'lib/strings/starter-pack'
+import {
+ getStarterPackOgCard,
+ parseStarterPackUri,
+} from 'lib/strings/starter-pack'
import {isAndroid, isNative, isWeb} from 'platform/detection'
import {useModerationOpts} from 'state/preferences/moderation-opts'
import {useListMembersQuery} from 'state/queries/list-members'
@@ -209,10 +213,12 @@ function WizardInner({
profilesCount: state.profiles.length,
feedsCount: state.feeds.length,
})
+ Image.prefetch([getStarterPackOgCard(currentProfile!.did, rkey)])
dispatch({type: 'SetProcessing', processing: false})
navigation.replace('StarterPack', {
name: currentAccount!.handle,
rkey,
+ new: true,
})
}