show quote posts
This commit is contained in:
@@ -241,6 +241,7 @@ func serve(cctx *cli.Context) error {
|
||||
e.GET("/profile/:handleOrDID/post/:rkey", server.WebPost)
|
||||
e.GET("/profile/:handleOrDID/post/:rkey/liked-by", server.WebGeneric)
|
||||
e.GET("/profile/:handleOrDID/post/:rkey/reposted-by", server.WebGeneric)
|
||||
e.GET("/profile/:handleOrDID/post/:rkey/quotes", server.WebGeneric)
|
||||
|
||||
// starter packs
|
||||
e.GET("/starter-pack/:handleOrDID/:rkey", server.WebStarterPack)
|
||||
|
||||
@@ -58,6 +58,7 @@ import {router} from './routes'
|
||||
import {MessagesConversationScreen} from './screens/Messages/Conversation'
|
||||
import {MessagesScreen} from './screens/Messages/List'
|
||||
import {MessagesSettingsScreen} from './screens/Messages/Settings'
|
||||
import {PostQuotesScreen} from './screens/Post/PostQuotes'
|
||||
import {useModalControls} from './state/modals'
|
||||
import {useUnreadNotifications} from './state/queries/notifications/unread'
|
||||
import {useSession} from './state/session'
|
||||
@@ -212,6 +213,13 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) {
|
||||
title: title(msg`Post by @${route.params.name}`),
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="PostQuotes"
|
||||
getComponent={() => PostQuotesScreen}
|
||||
options={({route}) => ({
|
||||
title: title(msg`Post by @${route.params.name}`),
|
||||
})}
|
||||
/>
|
||||
<Stack.Screen
|
||||
name="ProfileFeed"
|
||||
getComponent={() => ProfileFeedScreen}
|
||||
|
||||
@@ -20,6 +20,7 @@ export type CommonNavigatorParams = {
|
||||
PostThread: {name: string; rkey: string}
|
||||
PostLikedBy: {name: string; rkey: string}
|
||||
PostRepostedBy: {name: string; rkey: string}
|
||||
PostQuotes: {name: string; rkey: string}
|
||||
ProfileFeed: {name: string; rkey: string}
|
||||
ProfileFeedLikedBy: {name: string; rkey: string}
|
||||
ProfileLabelerLikedBy: {name: string}
|
||||
|
||||
@@ -21,6 +21,7 @@ export const router = new Router({
|
||||
PostThread: '/profile/:name/post/:rkey',
|
||||
PostLikedBy: '/profile/:name/post/:rkey/liked-by',
|
||||
PostRepostedBy: '/profile/:name/post/:rkey/reposted-by',
|
||||
PostQuotes: '/profile/:name/post/:rkey/quotes',
|
||||
ProfileFeed: '/profile/:name/feed/:rkey',
|
||||
ProfileFeedLikedBy: '/profile/:name/feed/:rkey/liked-by',
|
||||
ProfileLabelerLikedBy: '/profile/:name/labeler/liked-by',
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import React from 'react'
|
||||
import {View} from 'react-native'
|
||||
import {msg} from '@lingui/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
import {useFocusEffect} from '@react-navigation/native'
|
||||
|
||||
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
|
||||
import {makeRecordUri} from '#/lib/strings/url-helpers'
|
||||
import {useSetMinimalShellMode} from '#/state/shell'
|
||||
import {PostQuotes as PostQuotesComponent} from '#/view/com/post-thread/PostQuotes'
|
||||
import {ViewHeader} from '#/view/com/util/ViewHeader'
|
||||
|
||||
type Props = NativeStackScreenProps<CommonNavigatorParams, 'PostQuotes'>
|
||||
export const PostQuotesScreen = ({route}: Props) => {
|
||||
const setMinimalShellMode = useSetMinimalShellMode()
|
||||
const {name, rkey} = route.params
|
||||
const uri = makeRecordUri(name, 'app.bsky.feed.post', rkey)
|
||||
const {_} = useLingui()
|
||||
|
||||
useFocusEffect(
|
||||
React.useCallback(() => {
|
||||
setMinimalShellMode(false)
|
||||
}, [setMinimalShellMode]),
|
||||
)
|
||||
|
||||
return (
|
||||
<View>
|
||||
<ViewHeader title={_(msg`Quotes`)} />
|
||||
<PostQuotesComponent uri={uri} />
|
||||
</View>
|
||||
)
|
||||
}
|
||||
Vendored
+2
@@ -12,6 +12,7 @@ import {findAllProfilesInQueryData as findAllProfilesInMyBlockedAccountsQueryDat
|
||||
import {findAllProfilesInQueryData as findAllProfilesInMyMutedAccountsQueryData} from '../queries/my-muted-accounts'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInFeedsQueryData} from '../queries/post-feed'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInPostLikedByQueryData} from '../queries/post-liked-by'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInPostQuotesQueryData} from '../queries/post-quotes'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInPostRepostedByQueryData} from '../queries/post-reposted-by'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInPostThreadQueryData} from '../queries/post-thread'
|
||||
import {findAllProfilesInQueryData as findAllProfilesInProfileQueryData} from '../queries/profile'
|
||||
@@ -104,6 +105,7 @@ function* findProfilesInCache(
|
||||
yield* findAllProfilesInMyMutedAccountsQueryData(queryClient, did)
|
||||
yield* findAllProfilesInPostLikedByQueryData(queryClient, did)
|
||||
yield* findAllProfilesInPostRepostedByQueryData(queryClient, did)
|
||||
yield* findAllProfilesInPostQuotesQueryData(queryClient, did)
|
||||
yield* findAllProfilesInProfileQueryData(queryClient, did)
|
||||
yield* findAllProfilesInProfileFollowersQueryData(queryClient, did)
|
||||
yield* findAllProfilesInProfileFollowsQueryData(queryClient, did)
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import {AppBskyActorDefs, AppBskyFeedGetQuotes} from '@atproto/api'
|
||||
import {
|
||||
InfiniteData,
|
||||
QueryClient,
|
||||
QueryKey,
|
||||
useInfiniteQuery,
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
import {useAgent} from '#/state/session'
|
||||
import {getEmbeddedPost} from './util'
|
||||
|
||||
const PAGE_SIZE = 30
|
||||
type RQPageParam = string | undefined
|
||||
|
||||
// TODO refactor invalidate on mutate?
|
||||
const RQKEY_ROOT = 'post-quotes'
|
||||
export const RQKEY = (resolvedUri: string) => [RQKEY_ROOT, resolvedUri]
|
||||
|
||||
export function usePostQuotesQuery(resolvedUri: string | undefined) {
|
||||
const agent = useAgent()
|
||||
return useInfiniteQuery<
|
||||
AppBskyFeedGetQuotes.OutputSchema,
|
||||
Error,
|
||||
InfiniteData<AppBskyFeedGetQuotes.OutputSchema>,
|
||||
QueryKey,
|
||||
RQPageParam
|
||||
>({
|
||||
queryKey: RQKEY(resolvedUri || ''),
|
||||
async queryFn({pageParam}: {pageParam: RQPageParam}) {
|
||||
const res = await agent.api.app.bsky.feed.getQuotes({
|
||||
uri: resolvedUri || '',
|
||||
limit: PAGE_SIZE,
|
||||
cursor: pageParam,
|
||||
})
|
||||
return res.data
|
||||
},
|
||||
initialPageParam: undefined,
|
||||
getNextPageParam: lastPage => lastPage.cursor,
|
||||
enabled: !!resolvedUri,
|
||||
})
|
||||
}
|
||||
|
||||
export function* findAllProfilesInQueryData(
|
||||
queryClient: QueryClient,
|
||||
did: string,
|
||||
): Generator<AppBskyActorDefs.ProfileView, void> {
|
||||
const queryDatas = queryClient.getQueriesData<
|
||||
InfiniteData<AppBskyFeedGetQuotes.OutputSchema>
|
||||
>({
|
||||
queryKey: [RQKEY_ROOT],
|
||||
})
|
||||
for (const [_queryKey, queryData] of queryDatas) {
|
||||
if (!queryData?.pages) {
|
||||
continue
|
||||
}
|
||||
for (const page of queryData?.pages) {
|
||||
for (const item of page.posts) {
|
||||
if (item.author.did === did) {
|
||||
yield item.author
|
||||
}
|
||||
const quotedPost = getEmbeddedPost(item.embed)
|
||||
if (quotedPost?.author.did === did) {
|
||||
yield quotedPost.author
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
import React, {useCallback, useMemo, useState} from 'react'
|
||||
import {ActivityIndicator, StyleSheet, View} from 'react-native'
|
||||
import {
|
||||
AppBskyFeedDefs,
|
||||
AppBskyFeedPost,
|
||||
ModerationDecision,
|
||||
} from '@atproto/api'
|
||||
|
||||
import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
|
||||
import {cleanError} from '#/lib/strings/errors'
|
||||
import {logger} from '#/logger'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {usePostQuotesQuery} from '#/state/queries/post-quotes'
|
||||
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
||||
import {FeedItem} from '../posts/FeedItem'
|
||||
import {ErrorMessage} from '../util/error/ErrorMessage'
|
||||
import {List} from '../util/List'
|
||||
import {LoadingScreen} from '../util/LoadingScreen'
|
||||
import {CenteredView} from '../util/Views'
|
||||
|
||||
export function PostQuotes({uri}: {uri: string}) {
|
||||
const [isPTRing, setIsPTRing] = useState(false)
|
||||
const {
|
||||
data: resolvedUri,
|
||||
error: resolveError,
|
||||
isFetching: isFetchingResolvedUri,
|
||||
} = useResolveUriQuery(uri)
|
||||
const {
|
||||
data,
|
||||
isFetching,
|
||||
isFetched,
|
||||
isFetchingNextPage,
|
||||
hasNextPage,
|
||||
fetchNextPage,
|
||||
isError,
|
||||
error,
|
||||
refetch,
|
||||
} = usePostQuotesQuery(resolvedUri?.uri)
|
||||
const moderationOpts = useModerationOpts()
|
||||
|
||||
const quotes = useMemo(() => {
|
||||
if (data?.pages) {
|
||||
return data.pages
|
||||
.flatMap(page => page.posts)
|
||||
.map(post => {
|
||||
if (!AppBskyFeedPost.isRecord(post.record) || !moderationOpts) {
|
||||
return null
|
||||
}
|
||||
const moderation = moderatePost(post, moderationOpts)
|
||||
return {post, record: post.record, moderation}
|
||||
})
|
||||
.filter(x => Boolean(x))
|
||||
}
|
||||
}, [data, moderationOpts])
|
||||
|
||||
const onRefresh = useCallback(async () => {
|
||||
setIsPTRing(true)
|
||||
try {
|
||||
await refetch()
|
||||
} catch (err) {
|
||||
logger.error('Failed to refresh quotes', {message: err})
|
||||
}
|
||||
setIsPTRing(false)
|
||||
}, [refetch, setIsPTRing])
|
||||
|
||||
const onEndReached = useCallback(async () => {
|
||||
if (isFetching || !hasNextPage || isError) return
|
||||
try {
|
||||
await fetchNextPage()
|
||||
} catch (err) {
|
||||
logger.error('Failed to load more quotes', {message: err})
|
||||
}
|
||||
}, [isFetching, hasNextPage, isError, fetchNextPage])
|
||||
|
||||
const renderItem = useCallback(
|
||||
({
|
||||
item,
|
||||
index,
|
||||
}: {
|
||||
item: {
|
||||
post: AppBskyFeedDefs.PostView
|
||||
moderation: ModerationDecision
|
||||
record: AppBskyFeedPost.Record
|
||||
}
|
||||
index: number
|
||||
}) => {
|
||||
return (
|
||||
<FeedItem
|
||||
key={item.post.uri}
|
||||
post={item.post}
|
||||
record={item.record}
|
||||
moderation={item.moderation}
|
||||
hideTopBorder={index === 0}
|
||||
reason={undefined}
|
||||
parentAuthor={undefined}
|
||||
feedContext={undefined}
|
||||
showReplyTo={false}
|
||||
/>
|
||||
)
|
||||
},
|
||||
[],
|
||||
)
|
||||
|
||||
if (isFetchingResolvedUri || !isFetched) {
|
||||
return <LoadingScreen />
|
||||
}
|
||||
|
||||
// error
|
||||
// =
|
||||
if (resolveError || isError) {
|
||||
return (
|
||||
<CenteredView>
|
||||
<ErrorMessage
|
||||
message={cleanError(resolveError || error)}
|
||||
onPressTryAgain={onRefresh}
|
||||
/>
|
||||
</CenteredView>
|
||||
)
|
||||
}
|
||||
|
||||
// loaded
|
||||
// =
|
||||
return (
|
||||
<List
|
||||
data={quotes}
|
||||
keyExtractor={item => item.did}
|
||||
refreshing={isPTRing}
|
||||
onRefresh={onRefresh}
|
||||
onEndReached={onEndReached}
|
||||
renderItem={renderItem}
|
||||
initialNumToRender={15}
|
||||
// FIXME(dan)
|
||||
// eslint-disable-next-line react/no-unstable-nested-components
|
||||
ListFooterComponent={() => (
|
||||
<View style={styles.footer}>
|
||||
{(isFetching || isFetchingNextPage) && <ActivityIndicator />}
|
||||
</View>
|
||||
)}
|
||||
// @ts-ignore our .web version only -prf
|
||||
desktopFixedHeight
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
footer: {
|
||||
height: 200,
|
||||
paddingTop: 20,
|
||||
},
|
||||
})
|
||||
@@ -199,6 +199,11 @@ let PostThreadItemLoaded = ({
|
||||
return makeProfileLink(post.author, 'post', urip.rkey, 'reposted-by')
|
||||
}, [post.uri, post.author])
|
||||
const repostsTitle = _(msg`Reposts of this post`)
|
||||
const quotesHref = React.useMemo(() => {
|
||||
const urip = new AtUri(post.uri)
|
||||
return makeProfileLink(post.author, 'post', urip.rkey, 'quotes')
|
||||
}, [post.uri, post.author])
|
||||
const quotesTitle = _(msg`Quotes of this post`)
|
||||
|
||||
const translatorUrl = getTranslatorLink(
|
||||
record?.text || '',
|
||||
@@ -343,7 +348,9 @@ let PostThreadItemLoaded = ({
|
||||
translatorUrl={translatorUrl}
|
||||
needsTranslation={needsTranslation}
|
||||
/>
|
||||
{post.repostCount !== 0 || post.likeCount !== 0 ? (
|
||||
{post.repostCount !== 0 ||
|
||||
post.likeCount !== 0 ||
|
||||
post.quoteCount !== 0 ? (
|
||||
// Show this section unless we're *sure* it has no engagement.
|
||||
<View style={[styles.expandedInfo, pal.border]}>
|
||||
{post.repostCount != null && post.repostCount !== 0 ? (
|
||||
@@ -382,6 +389,26 @@ let PostThreadItemLoaded = ({
|
||||
</Text>
|
||||
</Link>
|
||||
) : null}
|
||||
{post.quoteCount != null && post.quoteCount !== 0 ? (
|
||||
<Link
|
||||
style={styles.expandedInfoItem}
|
||||
href={quotesHref}
|
||||
title={quotesTitle}>
|
||||
<Text
|
||||
testID="quoteCount-expanded"
|
||||
type="lg"
|
||||
style={pal.textLight}>
|
||||
<Text type="xl-bold" style={pal.text}>
|
||||
{formatCount(post.quoteCount)}
|
||||
</Text>{' '}
|
||||
<Plural
|
||||
value={post.quoteCount}
|
||||
one="quote"
|
||||
other="quotes"
|
||||
/>
|
||||
</Text>
|
||||
</Link>
|
||||
) : null}
|
||||
</View>
|
||||
) : null}
|
||||
<View style={[s.pl10, s.pr10]}>
|
||||
|
||||
Reference in New Issue
Block a user