diff --git a/src/lib/link-meta/link-meta.ts b/src/lib/link-meta/link-meta.ts index fa951432..6416df2b 100644 --- a/src/lib/link-meta/link-meta.ts +++ b/src/lib/link-meta/link-meta.ts @@ -1,8 +1,10 @@ import {BskyAgent} from '@atproto/api' -import {isBskyAppUrl} from '../strings/url-helpers' -import {extractBskyMeta} from './bsky' + import {LINK_META_PROXY} from 'lib/constants' import {getGiphyMetaUri} from 'lib/strings/embed-player' +import {parseStarterPackUri} from 'lib/strings/starter-pack' +import {isBskyAppUrl} from '../strings/url-helpers' +import {extractBskyMeta} from './bsky' export enum LikelyType { HTML, @@ -28,7 +30,7 @@ export async function getLinkMeta( url: string, timeout = 15e3, ): Promise { - if (isBskyAppUrl(url)) { + if (isBskyAppUrl(url) && !parseStarterPackUri(url)) { return extractBskyMeta(agent, url) } diff --git a/src/lib/link-meta/resolve-short-link.ts b/src/lib/link-meta/resolve-short-link.ts new file mode 100644 index 00000000..3a3e2ab4 --- /dev/null +++ b/src/lib/link-meta/resolve-short-link.ts @@ -0,0 +1,23 @@ +import {logger} from '#/logger' +import {startUriToStarterPackUri} from 'lib/strings/starter-pack' + +export async function resolveShortLink(shortLink: string) { + const controller = new AbortController() + const to = setTimeout(() => controller.abort(), 2e3) + + try { + const res = await fetch(shortLink, { + method: 'GET', + signal: controller.signal, + }) + if (res.status !== 200) { + return shortLink + } + return startUriToStarterPackUri(res.url) + } catch (e: unknown) { + logger.error('Failed to resolve short link', {safeMessage: e}) + return null + } finally { + clearTimeout(to) + } +} diff --git a/src/lib/strings/starter-pack.ts b/src/lib/strings/starter-pack.ts index 489d0b92..01b5a658 100644 --- a/src/lib/strings/starter-pack.ts +++ b/src/lib/strings/starter-pack.ts @@ -99,3 +99,7 @@ export function createStarterPackUri({ }): string | null { return new AtUri(`at://${did}/app.bsky.graph.starterpack/${rkey}`).toString() } + +export function startUriToStarterPackUri(uri: string) { + return uri.replace('/start/', '/starter-pack/') +} diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts index 4c75f47a..b88b77f7 100644 --- a/src/lib/strings/url-helpers.ts +++ b/src/lib/strings/url-helpers.ts @@ -2,6 +2,7 @@ import {AtUri} from '@atproto/api' import psl from 'psl' import TLDs from 'tlds' +import {logger} from '#/logger' import {BSKY_SERVICE} from 'lib/constants' import {isInvalidHandle} from 'lib/strings/handles' @@ -285,3 +286,13 @@ export function createBskyAppAbsoluteUrl(path: string): string { const sanitizedPath = path.replace(BSKY_APP_HOST, '').replace(/^\/+/, '') return `${BSKY_APP_HOST.replace(/\/$/, '')}/${sanitizedPath}` } + +export function isShortLink(url: string): boolean { + try { + const urlp = new URL(url) + return urlp.host === 'go.bsky.app' + } catch (e) { + logger.error('Failed to parse possible short link', {safeMessage: e}) + return false + } +} diff --git a/src/view/com/composer/useExternalLinkFetch.ts b/src/view/com/composer/useExternalLinkFetch.ts index 2e0297a4..743535a5 100644 --- a/src/view/com/composer/useExternalLinkFetch.ts +++ b/src/view/com/composer/useExternalLinkFetch.ts @@ -12,11 +12,13 @@ import { getPostAsQuote, } from 'lib/link-meta/bsky' import {getLinkMeta} from 'lib/link-meta/link-meta' +import {resolveShortLink} from 'lib/link-meta/resolve-short-link' import {downloadAndResize} from 'lib/media/manip' import { isBskyCustomFeedUrl, isBskyListUrl, isBskyPostUrl, + isShortLink, } from 'lib/strings/url-helpers' import {ImageModel} from 'state/models/media/image' import {ComposerOpts} from 'state/shell/composer' @@ -94,6 +96,17 @@ export function useExternalLinkFetch({ setExtLink(undefined) }, ) + } else if (isShortLink(extLink.uri)) { + if (isShortLink(extLink.uri)) { + resolveShortLink(extLink.uri).then(res => { + if (res && res !== extLink.uri) { + setExtLink({ + uri: res, + isLoading: true, + }) + } + }) + } } else { getLinkMeta(agent, extLink.uri).then(meta => { if (aborted) { diff --git a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx index 3b2a12c2..f5f220c6 100644 --- a/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx +++ b/src/view/com/util/post-embeds/ExternalLinkEmbed.tsx @@ -2,11 +2,17 @@ import React, {useCallback} from 'react' import {StyleProp, View, ViewStyle} from 'react-native' import {Image} from 'expo-image' import {AppBskyEmbedExternal} from '@atproto/api' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {shareUrl} from 'lib/sharing' import {parseEmbedPlayerFromUrl} from 'lib/strings/embed-player' +import { + getStarterPackOgCard, + parseStarterPackUri, +} from 'lib/strings/starter-pack' import {toNiceDomain} from 'lib/strings/url-helpers' import {isNative} from 'platform/detection' import {useExternalEmbedsPrefs} from 'state/preferences' @@ -28,10 +34,16 @@ export const ExternalLinkEmbed = ({ style?: StyleProp hideAlt?: boolean }) => { + const {_} = useLingui() const pal = usePalette('default') const {isMobile} = useWebMediaQueries() const externalEmbedPrefs = useExternalEmbedsPrefs() + const starterPackParsed = parseStarterPackUri(link.uri) + const imageUri = starterPackParsed + ? getStarterPackOgCard(starterPackParsed.name, starterPackParsed.rkey) + : link.thumb + const embedPlayerParams = React.useMemo(() => { const params = parseEmbedPlayerFromUrl(link.uri) @@ -47,15 +59,19 @@ export const ExternalLinkEmbed = ({ return ( - {link.thumb && !embedPlayerParams ? ( + {imageUri && !embedPlayerParams ? ( ) : undefined} {embedPlayerParams?.isGif ? ( @@ -63,35 +79,37 @@ export const ExternalLinkEmbed = ({ ) : embedPlayerParams ? ( ) : undefined} - - - {toNiceDomain(link.uri)} - - - {!embedPlayerParams?.isGif && !embedPlayerParams?.dimensions && ( - - {link.title || link.uri} - - )} - {link.description ? ( + {!starterPackParsed ? ( + - {link.description} + type="sm" + numberOfLines={1} + style={[pal.textLight, {marginVertical: 2}]}> + {toNiceDomain(link.uri)} - ) : undefined} - + + {!embedPlayerParams?.isGif && !embedPlayerParams?.dimensions && ( + + {link.title || link.uri} + + )} + {link.description ? ( + + {link.description} + + ) : undefined} + + ) : null} )