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>zio/stable
parent
a3d4fb652b
commit
aa7117edb6
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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()}
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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 (
|
||||
|
|
Loading…
Reference in New Issue