diff --git a/src/components/Link.tsx b/src/components/Link.tsx index d8ac829b..a8b478be 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({ ) } + +/** + * 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 + 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 ( + { + onPressOuter?.() + onPress(e) + }} + {...btnProps}> + {children} + + ) +} diff --git a/src/components/StarterPack/Main/ProfilesList.tsx b/src/components/StarterPack/Main/ProfilesList.tsx index 0cc911d6..3249f1b3 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( 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( ) } - if (listMembersQuery) + if (!data) { + return ( + + + + ) + } + + if (data) return ( ) { + 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 ( - { + precacheResolvedUri( + queryClient, + starterPack.creator.handle, + starterPack.creator.did, + ) + precacheStarterPack(queryClient, starterPack) }}> {children} - + + ) +} + +export function Embed({starterPack}: {starterPack: StarterPackViewBasic}) { + const t = useTheme() + const imageUri = getStarterPackOgCard(starterPack) + + return ( + + + + + + + + ) } diff --git a/src/lib/link-meta/bsky.ts b/src/lib/link-meta/bsky.ts index c1fbb34b..e3b4ea0c 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, + url: string, +): Promise { + 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 01b5a658..ca341001 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 948279fc..742c7ef7 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\/(?[^/]+)\/(?[^/]+)/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\/(?[^/]+)\/(?[^/]+)/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 2b6f673b..9b66e515 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 ( ) @@ -164,14 +151,10 @@ export function StarterPackScreenInner({ function StarterPackScreenLoaded({ starterPack, routeParams, - listMembersQuery, moderationOpts, }: { starterPack: AppBskyGraphDefs.StarterPackView routeParams: StarterPackScreeProps['route']['params'] - listMembersQuery: UseInfiniteQueryResult< - InfiniteData - > 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 f441a8ed..2cdb6b85 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 743535a5..2938ea25 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 be34a286..942ad57b 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 } + if (AppBskyGraphDefs.isStarterPackViewBasic(embed.record)) { + return + } + // quote post // = return (