Add link shortening to starter pack link sharing (#4581)

This commit is contained in:
Hailey
2024-06-20 11:31:59 -07:00
committed by GitHub
parent 1ab8c8bc1f
commit 8eaa538673
4 changed files with 150 additions and 57 deletions
+5 -5
View File
@@ -5,7 +5,6 @@ import ViewShot from 'react-native-view-shot'
import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api'
import {Trans} from '@lingui/macro' import {Trans} from '@lingui/macro'
import {makeStarterPackLink} from 'lib/routes/links'
import {isWeb} from 'platform/detection' import {isWeb} from 'platform/detection'
import {Logo} from 'view/icons/Logo' import {Logo} from 'view/icons/Logo'
import {Logotype} from 'view/icons/Logotype' import {Logotype} from 'view/icons/Logotype'
@@ -16,10 +15,11 @@ import {Text} from '#/components/Typography'
interface Props { interface Props {
starterPack: AppBskyGraphDefs.StarterPackView starterPack: AppBskyGraphDefs.StarterPackView
link: string
} }
export const QrCode = React.forwardRef<ViewShot, Props>(function QrCode( export const QrCode = React.forwardRef<ViewShot, Props>(function QrCode(
{starterPack}, {starterPack, link},
ref, ref,
) { ) {
const {record} = starterPack const {record} = starterPack
@@ -56,7 +56,7 @@ export const QrCode = React.forwardRef<ViewShot, Props>(function QrCode(
<Trans>Join the conversation</Trans> <Trans>Join the conversation</Trans>
</Text> </Text>
<View style={[a.rounded_sm, a.overflow_hidden]}> <View style={[a.rounded_sm, a.overflow_hidden]}>
<QrCodeInner url={makeStarterPackLink(starterPack)} /> <QrCodeInner link={link} />
</View> </View>
<View style={[a.flex_row, a.align_center, {gap: 5}]}> <View style={[a.flex_row, a.align_center, {gap: 5}]}>
@@ -79,12 +79,12 @@ export const QrCode = React.forwardRef<ViewShot, Props>(function QrCode(
) )
}) })
export function QrCodeInner({url}: {url: string}) { export function QrCodeInner({link}: {link: string}) {
const t = useTheme() const t = useTheme()
return ( return (
<QRCode <QRCode
data={url} data={link}
style={[ style={[
a.rounded_sm, a.rounded_sm,
{height: 225, width: 225, backgroundColor: '#f3f3f3'}, {height: 225, width: 225, backgroundColor: '#f3f3f3'},
+65 -28
View File
@@ -4,15 +4,17 @@ import ViewShot from 'react-native-view-shot'
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 * as Sharing from 'expo-sharing'
import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' import {AppBskyGraphDefs, AppBskyGraphStarterpack, AtUri} 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'
import {nanoid} from 'nanoid/non-secure' 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 {makeStarterPackLink} from 'lib/routes/links'
import {logEvent} from 'lib/statsig/statsig' import {logEvent} from 'lib/statsig/statsig'
import {isNative, isWeb} from 'platform/detection' import {isNative, isWeb} from 'platform/detection'
import {useShortenLink} from 'state/queries/shorten-link'
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'
@@ -27,12 +29,39 @@ export function QrCodeDialog({
}: { }: {
control: DialogControlProps control: DialogControlProps
starterPack: AppBskyGraphDefs.StarterPackView starterPack: AppBskyGraphDefs.StarterPackView
}) {
return (
<Dialog.Outer control={control}>
<Inner starterPack={starterPack} control={control} />
</Dialog.Outer>
)
}
function Inner({
starterPack,
control,
}: {
starterPack: AppBskyGraphDefs.StarterPackView
control: DialogControlProps
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const [isProcessing, setIsProcessing] = React.useState(false) const [isProcessing, setIsProcessing] = React.useState(false)
const [link, setLink] = React.useState<string>()
const shortenLink = useShortenLink()
const ref = React.useRef<ViewShot>(null) const ref = React.useRef<ViewShot>(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<HTMLCanvasElement> => { const getCanvas = (base64: string): Promise<HTMLCanvasElement> => {
return new Promise(resolve => { return new Promise(resolve => {
const image = new Image() const image = new Image()
@@ -149,42 +178,50 @@ export function QrCodeDialog({
} }
return ( return (
<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]}>
<QrCode starterPack={starterPack} ref={ref} /> {!link ? (
{isProcessing ? ( <View style={[a.align_center, a.p_xl]}>
<View>
<Loader size="xl" /> <Loader size="xl" />
</View> </View>
) : ( ) : (
<View style={[a.w_full, a.gap_md]}> <>
<Button <QrCode starterPack={starterPack} link={link} ref={ref} />
label={_(msg`Copy QR code`)} {isProcessing ? (
variant="solid" <View>
color="primary" <Loader size="xl" />
size="medium" </View>
onPress={isWeb ? onCopyPress : onSharePress}> ) : (
<ButtonText> <View style={[a.w_full, a.gap_md]}>
{isWeb ? <Trans>Copy</Trans> : <Trans>Share</Trans>} <Button
</ButtonText> label={_(msg`Copy QR code`)}
</Button> variant="solid"
<Button color="primary"
label={_(msg`Save QR code`)} size="medium"
variant="solid" onPress={isWeb ? onCopyPress : onSharePress}>
color="secondary" <ButtonText>
size="medium" {isWeb ? <Trans>Copy</Trans> : <Trans>Share</Trans>}
onPress={onSavePress}> </ButtonText>
<ButtonText> </Button>
<Trans>Save</Trans> <Button
</ButtonText> label={_(msg`Save QR code`)}
</Button> variant="solid"
</View> color="secondary"
size="medium"
onPress={onSavePress}>
<ButtonText>
<Trans>Save</Trans>
</ButtonText>
</Button>
</View>
)}
</>
)} )}
</View> </View>
</Dialog.ScrollableInner> </Dialog.ScrollableInner>
</Dialog.Outer> </>
) )
} }
+57 -24
View File
@@ -1,6 +1,10 @@
import React from 'react' import React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {AppBskyGraphDefs, AppBskyGraphStarterpack} from '@atproto/api' import {
AppBskyGraphDefs,
AppBskyGraphStarterpack,
ModerationOpts,
} from '@atproto/api'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 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'
@@ -20,6 +24,7 @@ import {isWeb} from 'platform/detection'
import {useModerationOpts} from 'state/preferences/moderation-opts' import {useModerationOpts} from 'state/preferences/moderation-opts'
import {RQKEY} from 'state/queries/list-members' import {RQKEY} from 'state/queries/list-members'
import {useResolveDidQuery} from 'state/queries/resolve-uri' import {useResolveDidQuery} from 'state/queries/resolve-uri'
import {useShortenLink} from 'state/queries/shorten-link'
import {useStarterPackQuery} from 'state/queries/starter-packs' import {useStarterPackQuery} from 'state/queries/starter-packs'
import {useAgent, useSession} from 'state/session' import {useAgent, useSession} from 'state/session'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
@@ -83,11 +88,42 @@ export function StarterPackScreen({route}: StarterPackScreeProps) {
) )
} }
return (
<StarterPackScreenInner
starterPack={starterPack}
routeParams={route.params}
moderationOpts={moderationOpts}
/>
)
}
function StarterPackScreenInner({
starterPack,
routeParams,
moderationOpts,
}: {
starterPack: AppBskyGraphDefs.StarterPackView
routeParams: StarterPackScreeProps['route']['params']
moderationOpts: ModerationOpts
}) {
const shortenLink = useShortenLink()
const tabs = [ const tabs = [
...(starterPack.list ? ['People'] : []), ...(starterPack.list ? ['People'] : []),
...(starterPack.feeds?.length ? ['Feeds'] : []), ...(starterPack.feeds?.length ? ['Feeds'] : []),
] ]
const onShareLink = async () => {
const res = await shortenLink(
makeStarterPackLink(starterPack.creator.did, routeParams.rkey),
)
shareUrl(res.url)
logEvent('starterPack:share', {
starterPack: starterPack.uri,
shareType: 'link',
})
}
return ( return (
<CenteredView style={[a.h_full_vh]}> <CenteredView style={[a.h_full_vh]}>
<View style={isWeb ? {minHeight: '100%'} : {height: '100%'}}> <View style={isWeb ? {minHeight: '100%'} : {height: '100%'}}>
@@ -95,7 +131,11 @@ export function StarterPackScreen({route}: StarterPackScreeProps) {
items={tabs} items={tabs}
isHeaderReady={true} isHeaderReady={true}
renderHeader={() => ( renderHeader={() => (
<Header starterPack={starterPack} routeParams={route.params} /> <Header
starterPack={starterPack}
routeParams={routeParams}
onShareLink={onShareLink}
/>
)}> )}>
{starterPack.list != null {starterPack.list != null
? ({headerHeight, scrollElRef}) => ( ? ({headerHeight, scrollElRef}) => (
@@ -131,9 +171,11 @@ export function StarterPackScreen({route}: StarterPackScreeProps) {
function Header({ function Header({
starterPack, starterPack,
routeParams, routeParams,
onShareLink,
}: { }: {
starterPack: AppBskyGraphDefs.StarterPackView starterPack: AppBskyGraphDefs.StarterPackView
routeParams: StarterPackScreeProps['route']['params'] routeParams: StarterPackScreeProps['route']['params']
onShareLink: () => void
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const t = useTheme() const t = useTheme()
@@ -195,8 +237,8 @@ function Header({
<View style={[a.flex_row, a.gap_sm, a.align_center]}> <View style={[a.flex_row, a.gap_sm, a.align_center]}>
{isOwn ? ( {isOwn ? (
<OwnerShareMenu <OwnerShareMenu
routeParams={routeParams}
starterPack={starterPack} starterPack={starterPack}
onShareLink={onShareLink}
/> />
) : ( ) : (
<Button <Button
@@ -212,7 +254,11 @@ function Header({
</ButtonText> </ButtonText>
</Button> </Button>
)} )}
<OverflowMenu routeParams={routeParams} starterPack={starterPack} /> <OverflowMenu
routeParams={routeParams}
starterPack={starterPack}
onShareLink={onShareLink}
/>
</View> </View>
</ProfileSubpageHeader> </ProfileSubpageHeader>
{record.description || joinedAllTimeCount >= 25 ? ( {record.description || joinedAllTimeCount >= 25 ? (
@@ -247,9 +293,11 @@ function Header({
function OverflowMenu({ function OverflowMenu({
starterPack, starterPack,
routeParams, routeParams,
onShareLink,
}: { }: {
starterPack: AppBskyGraphDefs.StarterPackView starterPack: AppBskyGraphDefs.StarterPackView
routeParams: StarterPackScreeProps['route']['params'] routeParams: StarterPackScreeProps['route']['params']
onShareLink: () => void
}) { }) {
const t = useTheme() const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
@@ -259,6 +307,7 @@ function OverflowMenu({
const reportDialogControl = useReportDialogControl() const reportDialogControl = useReportDialogControl()
const deleteDialogControl = useDialogControl() const deleteDialogControl = useDialogControl()
const navigation = useNavigation<NavigationProp>() const navigation = useNavigation<NavigationProp>()
const { const {
mutate: deleteStarterPack, mutate: deleteStarterPack,
isPending: isDeletePending, isPending: isDeletePending,
@@ -342,15 +391,7 @@ function OverflowMenu({
<Menu.Item <Menu.Item
label={_(msg`Share link`)} label={_(msg`Share link`)}
testID="shareStarterPackLinkBtn" testID="shareStarterPackLinkBtn"
onPress={() => { onPress={onShareLink}>
logEvent('starterPack:share', {
starterPack: starterPack.uri,
shareType: 'link',
})
shareUrl(
makeStarterPackLink(routeParams.name, routeParams.rkey),
)
}}>
<Menu.ItemText> <Menu.ItemText>
<Trans>Share link</Trans> <Trans>Share link</Trans>
</Menu.ItemText> </Menu.ItemText>
@@ -439,10 +480,10 @@ function OverflowMenu({
function OwnerShareMenu({ function OwnerShareMenu({
starterPack, starterPack,
routeParams, onShareLink,
}: { }: {
starterPack: AppBskyGraphDefs.StarterPackView starterPack: AppBskyGraphDefs.StarterPackView
routeParams: StarterPackScreeProps['route']['params'] onShareLink: () => void
}) { }) {
const {_} = useLingui() const {_} = useLingui()
const qrCodeDialogControl = useDialogControl() const qrCodeDialogControl = useDialogControl()
@@ -471,15 +512,7 @@ function OwnerShareMenu({
<Menu.Item <Menu.Item
label={_(msg`Share link`)} label={_(msg`Share link`)}
testID="shareStarterPackLinkBtn" testID="shareStarterPackLinkBtn"
onPress={() => { onPress={onShareLink}>
logEvent('starterPack:share', {
starterPack: starterPack.uri,
shareType: 'link',
})
shareUrl(
makeStarterPackLink(routeParams.name, routeParams.rkey),
)
}}>
<Menu.ItemText> <Menu.ItemText>
<Trans>Share link</Trans> <Trans>Share link</Trans>
</Menu.ItemText> </Menu.ItemText>
+23
View File
@@ -0,0 +1,23 @@
import {logger} from '#/logger'
export function useShortenLink() {
return async (inputUrl: string): Promise<{url: string}> => {
const url = new URL(inputUrl)
const res = await fetch('https://go.bsky.app/link', {
method: 'POST',
body: JSON.stringify({
path: url.pathname,
}),
headers: {
'Content-Type': 'application/json',
},
})
if (!res.ok) {
logger.error('Failed to shorten link', {safeMessage: res.status})
return {url: inputUrl}
}
return res.json()
}
}