diff options
author | Hailey <me@haileyok.com> | 2024-07-03 18:15:08 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-07-04 02:15:08 +0100 |
commit | aa7117edb60711a67464f7559118334185f01680 (patch) | |
tree | b0ccd3d7ef0d792613542a1af48c3fbae1f36f21 | |
parent | a3d4fb652b888ba81aecbf0e81a954968ea65d39 (diff) | |
download | voidsky-aa7117edb60711a67464f7559118334185f01680.tar.zst |
Add starter pack embeds to posts (#4699)
* starter pack embeds * revert test code * Types * add `BaseLink` * precache on click * rm log * add a comment * loading state * top margin --------- Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
-rw-r--r-- | src/components/Link.tsx | 49 | ||||
-rw-r--r-- | src/components/StarterPack/Main/ProfilesList.tsx | 21 | ||||
-rw-r--r-- | src/components/StarterPack/StarterPackCard.tsx | 60 | ||||
-rw-r--r-- | src/lib/link-meta/bsky.ts | 51 | ||||
-rw-r--r-- | src/lib/strings/starter-pack.ts | 2 | ||||
-rw-r--r-- | src/lib/strings/url-helpers.ts | 24 | ||||
-rw-r--r-- | src/screens/StarterPack/StarterPackScreen.tsx | 22 | ||||
-rw-r--r-- | src/state/queries/starter-packs.ts | 33 | ||||
-rw-r--r-- | src/view/com/composer/useExternalLinkFetch.ts | 20 | ||||
-rw-r--r-- | src/view/com/util/post-embeds/index.tsx | 5 |
10 files changed, 246 insertions, 41 deletions
diff --git a/src/components/Link.tsx b/src/components/Link.tsx index d8ac829b6..a8b478be7 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -1,5 +1,10 @@ import React from 'react' -import {GestureResponderEvent} from 'react-native' +import { + GestureResponderEvent, + Pressable, + StyleProp, + ViewStyle, +} from 'react-native' import {sanitizeUrl} from '@braintree/sanitize-url' import {StackActions, useLinkProps} from '@react-navigation/native' @@ -323,3 +328,45 @@ export function InlineLinkText({ </Text> ) } + +/** + * A Pressable that uses useLink to handle navigation. It is unstyled, so can be used in cases where the Button styles + * in Link are not desired. + * @param displayText + * @param style + * @param children + * @param rest + * @constructor + */ +export function BaseLink({ + displayText, + onPress: onPressOuter, + style, + children, + ...rest +}: { + style?: StyleProp<ViewStyle> + children: React.ReactNode + to: string + action: 'push' | 'replace' | 'navigate' + onPress?: () => false | void + shareOnLongPress?: boolean + label: string + displayText?: string +}) { + const {onPress, ...btnProps} = useLink({ + displayText: displayText ?? rest.to, + ...rest, + }) + return ( + <Pressable + style={style} + onPress={e => { + onPressOuter?.() + onPress(e) + }} + {...btnProps}> + {children} + </Pressable> + ) +} diff --git a/src/components/StarterPack/Main/ProfilesList.tsx b/src/components/StarterPack/Main/ProfilesList.tsx index 0cc911d66..3249f1b32 100644 --- a/src/components/StarterPack/Main/ProfilesList.tsx +++ b/src/components/StarterPack/Main/ProfilesList.tsx @@ -11,10 +11,12 @@ import {InfiniteData, UseInfiniteQueryResult} from '@tanstack/react-query' import {useBottomBarOffset} from 'lib/hooks/useBottomBarOffset' import {isBlockedOrBlocking} from 'lib/moderation/blocked-and-muted' import {isNative, isWeb} from 'platform/detection' +import {useListMembersQuery} from 'state/queries/list-members' import {useSession} from 'state/session' import {List, ListRef} from 'view/com/util/List' import {SectionRef} from '#/screens/Profile/Sections/types' import {atoms as a, useTheme} from '#/alf' +import {ListMaybePlaceholder} from '#/components/Lists' import {Default as ProfileCard} from '#/components/ProfileCard' function keyExtractor(item: AppBskyActorDefs.ProfileViewBasic, index: number) { @@ -33,18 +35,17 @@ interface ProfilesListProps { export const ProfilesList = React.forwardRef<SectionRef, ProfilesListProps>( function ProfilesListImpl( - {listUri, listMembersQuery, moderationOpts, headerHeight, scrollElRef}, + {listUri, moderationOpts, headerHeight, scrollElRef}, ref, ) { const t = useTheme() const [initialHeaderHeight] = React.useState(headerHeight) const bottomBarOffset = useBottomBarOffset(20) const {currentAccount} = useSession() + const {data, refetch, isError} = useListMembersQuery(listUri, 50) const [isPTRing, setIsPTRing] = React.useState(false) - const {data, refetch} = listMembersQuery - // 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)) @@ -96,7 +97,19 @@ export const ProfilesList = React.forwardRef<SectionRef, ProfilesListProps>( ) } - if (listMembersQuery) + if (!data) { + return ( + <View style={{marginTop: headerHeight, marginBottom: bottomBarOffset}}> + <ListMaybePlaceholder + isLoading={true} + isError={isError} + onRetry={refetch} + /> + </View> + ) + } + + if (data) return ( <List data={getSortedProfiles()} diff --git a/src/components/StarterPack/StarterPackCard.tsx b/src/components/StarterPack/StarterPackCard.tsx index ab904d7ff..dc9e4b70d 100644 --- a/src/components/StarterPack/StarterPackCard.tsx +++ b/src/components/StarterPack/StarterPackCard.tsx @@ -1,15 +1,20 @@ import React from 'react' import {View} from 'react-native' +import {Image} from 'expo-image' import {AppBskyGraphStarterpack, AtUri} from '@atproto/api' import {StarterPackViewBasic} from '@atproto/api/dist/client/types/app/bsky/graph/defs' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useQueryClient} from '@tanstack/react-query' import {sanitizeHandle} from 'lib/strings/handles' +import {getStarterPackOgCard} from 'lib/strings/starter-pack' +import {precacheResolvedUri} from 'state/queries/resolve-uri' +import {precacheStarterPack} from 'state/queries/starter-packs' import {useSession} from 'state/session' import {atoms as a, useTheme} from '#/alf' import {StarterPack} from '#/components/icons/StarterPack' -import {Link as InternalLink, LinkProps} from '#/components/Link' +import {BaseLink} from '#/components/Link' import {Text} from '#/components/Typography' export function Default({starterPack}: {starterPack?: StarterPackViewBasic}) { @@ -88,10 +93,13 @@ export function Card({ export function Link({ starterPack, children, - ...rest }: { starterPack: StarterPackViewBasic -} & Omit<LinkProps, 'to'>) { + onPress?: () => void + children: React.ReactNode +}) { + const {_} = useLingui() + const queryClient = useQueryClient() const {record} = starterPack const {rkey, handleOrDid} = React.useMemo(() => { const rkey = new AtUri(starterPack.uri).rkey @@ -104,14 +112,46 @@ export function Link({ } return ( - <InternalLink - label={record.name} - {...rest} - to={{ - screen: 'StarterPack', - params: {name: handleOrDid, rkey}, + <BaseLink + action="push" + to={`/starter-pack/${handleOrDid}/${rkey}`} + label={_(msg`Navigate to ${record.name}`)} + onPress={() => { + precacheResolvedUri( + queryClient, + starterPack.creator.handle, + starterPack.creator.did, + ) + precacheStarterPack(queryClient, starterPack) }}> {children} - </InternalLink> + </BaseLink> + ) +} + +export function Embed({starterPack}: {starterPack: StarterPackViewBasic}) { + const t = useTheme() + const imageUri = getStarterPackOgCard(starterPack) + + return ( + <View + style={[ + a.mt_xs, + a.border, + a.rounded_sm, + a.overflow_hidden, + t.atoms.border_contrast_low, + ]}> + <Link starterPack={starterPack}> + <Image + source={imageUri} + style={[a.w_full, {aspectRatio: 1.91}]} + accessibilityIgnoresInvertColors={true} + /> + <View style={[a.px_sm, a.py_md]}> + <Card starterPack={starterPack} /> + </View> + </Link> + </View> ) } diff --git a/src/lib/link-meta/bsky.ts b/src/lib/link-meta/bsky.ts index c1fbb34b3..e3b4ea0c9 100644 --- a/src/lib/link-meta/bsky.ts +++ b/src/lib/link-meta/bsky.ts @@ -1,11 +1,16 @@ -import {AppBskyFeedPost, BskyAgent} from '@atproto/api' +import {AppBskyFeedPost, AppBskyGraphStarterpack, BskyAgent} from '@atproto/api' + +import {useFetchDid} from '#/state/queries/handle' +import {useGetPost} from '#/state/queries/post' import * as apilib from 'lib/api/index' -import {LikelyType, LinkMeta} from './link-meta' +import { + createStarterPackUri, + parseStarterPackUri, +} from 'lib/strings/starter-pack' +import {ComposerOptsQuote} from 'state/shell/composer' // import {match as matchRoute} from 'view/routes' import {convertBskyAppUrlIfNeeded, makeRecordUri} from '../strings/url-helpers' -import {ComposerOptsQuote} from 'state/shell/composer' -import {useGetPost} from '#/state/queries/post' -import {useFetchDid} from '#/state/queries/handle' +import {LikelyType, LinkMeta} from './link-meta' // TODO // import {Home} from 'view/screens/Home' @@ -174,3 +179,39 @@ export async function getListAsEmbed( }, } } + +export async function getStarterPackAsEmbed( + agent: BskyAgent, + fetchDid: ReturnType<typeof useFetchDid>, + url: string, +): Promise<apilib.ExternalEmbedDraft> { + const parsed = parseStarterPackUri(url) + if (!parsed) { + throw new Error( + 'Unexepectedly called getStarterPackAsEmbed with a non-starterpack url', + ) + } + const did = await fetchDid(parsed.name) + const starterPack = createStarterPackUri({did, rkey: parsed.rkey}) + const res = await agent.app.bsky.graph.getStarterPack({starterPack}) + const record = res.data.starterPack.record + return { + isLoading: false, + uri: starterPack, + meta: { + url: starterPack, + likelyType: LikelyType.AtpData, + // Validation here should never fail + title: AppBskyGraphStarterpack.isRecord(record) + ? record.name + : 'Starter Pack', + }, + embed: { + $type: 'app.bsky.embed.record', + record: { + uri: res.data.starterPack.uri, + cid: res.data.starterPack.cid, + }, + }, + } +} diff --git a/src/lib/strings/starter-pack.ts b/src/lib/strings/starter-pack.ts index 01b5a6587..ca3410015 100644 --- a/src/lib/strings/starter-pack.ts +++ b/src/lib/strings/starter-pack.ts @@ -96,7 +96,7 @@ export function createStarterPackUri({ }: { did: string rkey: string -}): string | null { +}): string { return new AtUri(`at://${did}/app.bsky.graph.starterpack/${rkey}`).toString() } diff --git a/src/lib/strings/url-helpers.ts b/src/lib/strings/url-helpers.ts index 948279fce..742c7ef79 100644 --- a/src/lib/strings/url-helpers.ts +++ b/src/lib/strings/url-helpers.ts @@ -152,6 +152,30 @@ export function isBskyListUrl(url: string): boolean { return false } +export function isBskyStartUrl(url: string): boolean { + if (isBskyAppUrl(url)) { + try { + const urlp = new URL(url) + return /start\/(?<name>[^/]+)\/(?<rkey>[^/]+)/i.test(urlp.pathname) + } catch { + console.error('Unexpected error in isBskyStartUrl()', url) + } + } + return false +} + +export function isBskyStarterPackUrl(url: string): boolean { + if (isBskyAppUrl(url)) { + try { + const urlp = new URL(url) + return /starter-pack\/(?<name>[^/]+)\/(?<rkey>[^/]+)/i.test(urlp.pathname) + } catch { + console.error('Unexpected error in isBskyStartUrl()', url) + } + } + return false +} + export function isBskyDownloadUrl(url: string): boolean { if (isExternalUrl(url)) { return false diff --git a/src/screens/StarterPack/StarterPackScreen.tsx b/src/screens/StarterPack/StarterPackScreen.tsx index 2b6f673b1..9b66e5157 100644 --- a/src/screens/StarterPack/StarterPackScreen.tsx +++ b/src/screens/StarterPack/StarterPackScreen.tsx @@ -3,7 +3,6 @@ import {View} from 'react-native' import {Image} from 'expo-image' import { AppBskyGraphDefs, - AppBskyGraphGetList, AppBskyGraphStarterpack, AtUri, ModerationOpts, @@ -14,11 +13,7 @@ import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' import {NativeStackScreenProps} from '@react-navigation/native-stack' -import { - InfiniteData, - UseInfiniteQueryResult, - useQueryClient, -} from '@tanstack/react-query' +import {useQueryClient} from '@tanstack/react-query' import {cleanError} from '#/lib/strings/errors' import {logger} from '#/logger' @@ -33,7 +28,6 @@ import {getStarterPackOgCard} from 'lib/strings/starter-pack' import {isWeb} from 'platform/detection' import {updateProfileShadow} from 'state/cache/profile-shadow' import {useModerationOpts} from 'state/preferences/moderation-opts' -import {useListMembersQuery} from 'state/queries/list-members' import {useResolvedStarterPackShortLink} from 'state/queries/resolve-short-link' import {useResolveDidQuery} from 'state/queries/resolve-uri' import {useShortenLink} from 'state/queries/shorten-link' @@ -123,7 +117,6 @@ export function StarterPackScreenInner({ isLoading: isLoadingStarterPack, isError: isErrorStarterPack, } = useStarterPackQuery({did, rkey}) - const listMembersQuery = useListMembersQuery(starterPack?.list?.uri, 50) const isValid = starterPack && @@ -134,12 +127,7 @@ export function StarterPackScreenInner({ if (!did || !starterPack || !isValid || !moderationOpts) { return ( <ListMaybePlaceholder - isLoading={ - isLoadingDid || - isLoadingStarterPack || - listMembersQuery.isLoading || - !moderationOpts - } + isLoading={isLoadingDid || isLoadingStarterPack || !moderationOpts} isError={isErrorDid || isErrorStarterPack || !isValid} errorMessage={_(msg`That starter pack could not be found.`)} emptyMessage={_(msg`That starter pack could not be found.`)} @@ -155,7 +143,6 @@ export function StarterPackScreenInner({ <StarterPackScreenLoaded starterPack={starterPack} routeParams={routeParams} - listMembersQuery={listMembersQuery} moderationOpts={moderationOpts} /> ) @@ -164,14 +151,10 @@ export function StarterPackScreenInner({ function StarterPackScreenLoaded({ starterPack, routeParams, - listMembersQuery, moderationOpts, }: { starterPack: AppBskyGraphDefs.StarterPackView routeParams: StarterPackScreeProps['route']['params'] - listMembersQuery: UseInfiniteQueryResult< - InfiniteData<AppBskyGraphGetList.OutputSchema> - > moderationOpts: ModerationOpts }) { const showPeopleTab = Boolean(starterPack.list) @@ -242,7 +225,6 @@ function StarterPackScreenLoaded({ headerHeight={headerHeight} // @ts-expect-error scrollElRef={scrollElRef} - listMembersQuery={listMembersQuery} moderationOpts={moderationOpts} /> ) diff --git a/src/state/queries/starter-packs.ts b/src/state/queries/starter-packs.ts index f441a8ed2..2cdb6b850 100644 --- a/src/state/queries/starter-packs.ts +++ b/src/state/queries/starter-packs.ts @@ -347,3 +347,36 @@ async function whenAppViewReady( () => agent.app.bsky.graph.getStarterPack({starterPack: uri}), ) } + +export async function precacheStarterPack( + queryClient: QueryClient, + starterPack: + | AppBskyGraphDefs.StarterPackViewBasic + | AppBskyGraphDefs.StarterPackView, +) { + if (!AppBskyGraphStarterpack.isRecord(starterPack.record)) { + return + } + + let starterPackView: AppBskyGraphDefs.StarterPackView | undefined + if (AppBskyGraphDefs.isStarterPackView(starterPack)) { + starterPackView = starterPack + } else if (AppBskyGraphDefs.isStarterPackViewBasic(starterPack)) { + const listView: AppBskyGraphDefs.ListViewBasic = { + uri: starterPack.record.list, + // This will be populated once the data from server is fetched + cid: '', + name: starterPack.record.name, + purpose: 'app.bsky.graph.defs#referencelist', + } + starterPackView = { + ...starterPack, + $type: 'app.bsky.graph.defs#starterPackView', + list: listView, + } + } + + if (starterPackView) { + queryClient.setQueryData(RQKEY({uri: starterPack.uri}), starterPackView) + } +} diff --git a/src/view/com/composer/useExternalLinkFetch.ts b/src/view/com/composer/useExternalLinkFetch.ts index 743535a5e..2938ea25a 100644 --- a/src/view/com/composer/useExternalLinkFetch.ts +++ b/src/view/com/composer/useExternalLinkFetch.ts @@ -10,6 +10,7 @@ import { getFeedAsEmbed, getListAsEmbed, getPostAsQuote, + getStarterPackAsEmbed, } from 'lib/link-meta/bsky' import {getLinkMeta} from 'lib/link-meta/link-meta' import {resolveShortLink} from 'lib/link-meta/resolve-short-link' @@ -18,6 +19,8 @@ import { isBskyCustomFeedUrl, isBskyListUrl, isBskyPostUrl, + isBskyStarterPackUrl, + isBskyStartUrl, isShortLink, } from 'lib/strings/url-helpers' import {ImageModel} from 'state/models/media/image' @@ -96,6 +99,23 @@ export function useExternalLinkFetch({ setExtLink(undefined) }, ) + } else if ( + isBskyStartUrl(extLink.uri) || + isBskyStarterPackUrl(extLink.uri) + ) { + getStarterPackAsEmbed(agent, fetchDid, extLink.uri).then( + ({embed, meta}) => { + if (aborted) { + return + } + setExtLink({ + uri: extLink.uri, + isLoading: false, + meta, + embed, + }) + }, + ) } else if (isShortLink(extLink.uri)) { if (isShortLink(extLink.uri)) { resolveShortLink(extLink.uri).then(res => { diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index be34a2869..942ad57b8 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -30,6 +30,7 @@ import {ListEmbed} from './ListEmbed' import {MaybeQuoteEmbed} from './QuoteEmbed' import hairlineWidth = StyleSheet.hairlineWidth import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' +import {Embed as StarterPackCard} from '#/components/StarterPack/StarterPackCard' type Embed = | AppBskyEmbedRecord.View @@ -90,6 +91,10 @@ export function PostEmbeds({ return <ListEmbed item={embed.record} /> } + if (AppBskyGraphDefs.isStarterPackViewBasic(embed.record)) { + return <StarterPackCard starterPack={embed.record} /> + } + // quote post // = return ( |