Use post shadow to swap detached quotes

This commit is contained in:
Eric Bailey
2024-08-06 18:27:34 -05:00
parent 4b5952c08c
commit 413e87f4c5
5 changed files with 164 additions and 16 deletions
+10 -1
View File
@@ -1,5 +1,9 @@
import {useEffect, useMemo, useState} from 'react' import {useEffect, useMemo, useState} from 'react'
import {AppBskyFeedDefs} from '@atproto/api' import {
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyFeedDefs,
} from '@atproto/api'
import {QueryClient} from '@tanstack/react-query' import {QueryClient} from '@tanstack/react-query'
import EventEmitter from 'eventemitter3' import EventEmitter from 'eventemitter3'
@@ -15,6 +19,7 @@ export interface PostShadow {
likeUri: string | undefined likeUri: string | undefined
repostUri: string | undefined repostUri: string | undefined
isDeleted: boolean isDeleted: boolean
embed: AppBskyEmbedRecord.View | AppBskyEmbedRecordWithMedia.View | undefined
} }
export const POST_TOMBSTONE = Symbol('PostTombstone') export const POST_TOMBSTONE = Symbol('PostTombstone')
@@ -86,6 +91,10 @@ function mergeShadow(
repostCount = Math.max(0, repostCount) repostCount = Math.max(0, repostCount)
} }
if (shadow.embed !== undefined && post.embed) {
post.embed = shadow.embed
}
return castAsShadow({ return castAsShadow({
...post, ...post,
likeCount: likeCount, likeCount: likeCount,
+24
View File
@@ -73,6 +73,30 @@ export function useGetPost() {
) )
} }
export function useGetPosts() {
const queryClient = useQueryClient()
const agent = useAgent()
return useCallback(
async ({uris}: {uris: string[]}) => {
return queryClient.fetchQuery({
queryKey: RQKEY(uris.join(',') || ''),
async queryFn() {
const res = await agent.getPosts({
uris,
})
if (res.success) {
return res.data.posts
} else {
throw new Error('useGetPosts failed')
}
},
})
},
[queryClient, agent],
)
}
export function usePostLikeMutationQueue( export function usePostLikeMutationQueue(
post: Shadow<AppBskyFeedDefs.PostView>, post: Shadow<AppBskyFeedDefs.PostView>,
logContext: LogEvents['post:like']['logContext'] & logContext: LogEvents['post:like']['logContext'] &
+18 -1
View File
@@ -1,8 +1,11 @@
import {AppBskyFeedPostgate, AtUri, BskyAgent} from '@atproto/api' import {AppBskyFeedPostgate, AtUri, BskyAgent} from '@atproto/api'
import {useMutation} from '@tanstack/react-query' import {useMutation, useQueryClient} from '@tanstack/react-query'
import {networkRetry} from '#/lib/async/retry' import {networkRetry} from '#/lib/async/retry'
import {updatePostShadow} from '#/state/cache/post-shadow'
import {useGetPosts} from '#/state/queries/post'
import { import {
createEmbed,
createPostgateRecord, createPostgateRecord,
mergePostgateRecords, mergePostgateRecords,
POSTGATE_COLLECTION, POSTGATE_COLLECTION,
@@ -26,6 +29,7 @@ export async function getPostgateRecord({
} }
try { try {
// TODO don't retry on 404
const {data} = await networkRetry(2, () => const {data} = await networkRetry(2, () =>
agent.api.com.atproto.repo.getRecord({ agent.api.com.atproto.repo.getRecord({
repo: urip.host, repo: urip.host,
@@ -100,6 +104,8 @@ export async function upsertPostgate(
export function useToggleQuoteDetachmentMutation() { export function useToggleQuoteDetachmentMutation() {
const agent = useAgent() const agent = useAgent()
const queryClient = useQueryClient()
const getPosts = useGetPosts()
return useMutation({ return useMutation({
mutationFn: async ({ mutationFn: async ({
@@ -134,5 +140,16 @@ export function useToggleQuoteDetachmentMutation() {
} }
}) })
}, },
async onSuccess(_data, {postUri, quotedUri, action}) {
const [post, quotedPost] = await getPosts({uris: [postUri, quotedUri]})
updatePostShadow(queryClient, postUri, {
embed: createEmbed({
post,
embeddedPost: quotedPost,
detached: action === 'detach',
}),
})
},
}) })
} }
+63 -1
View File
@@ -1,4 +1,9 @@
import {AppBskyFeedPostgate} from '@atproto/api' import {
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyFeedDefs,
AppBskyFeedPostgate,
} from '@atproto/api'
import {ViewRemoved} from '@atproto/api/dist/client/types/app/bsky/embed/record' import {ViewRemoved} from '@atproto/api/dist/client/types/app/bsky/embed/record'
export const POSTGATE_COLLECTION = 'app.bsky.feed.postgate' export const POSTGATE_COLLECTION = 'app.bsky.feed.postgate'
@@ -42,3 +47,60 @@ export function createEmbedViewRemovedRecord({uri}: {uri: string}) {
record, record,
} }
} }
export function createEmbed({
post,
embeddedPost,
detached,
}: {
post: AppBskyFeedDefs.PostView
embeddedPost: AppBskyFeedDefs.PostView
detached: boolean
}) {
if (AppBskyEmbedRecord.isView(post.embed)) {
if (detached) return createEmbedViewRemovedRecord({uri: post.uri})
return createEmbedRecordView({post: embeddedPost})
} else if (AppBskyEmbedRecordWithMedia.isView(post.embed)) {
if (detached)
return {
...post.embed,
record: createEmbedViewRemovedRecord({uri: post.uri}),
}
return createEmbedRecordWithmediaView({post, embeddedPost})
}
}
export function createEmbedRecordView({
post,
}: {
post: AppBskyFeedDefs.PostView
}): AppBskyEmbedRecord.View {
return {
$type: 'app.bsky.embed.record#view',
record: {
$type: 'app.bsky.embed.record#viewRecord',
...post,
value: post.record,
},
}
}
export function createEmbedRecordWithmediaView({
post,
embeddedPost,
}: {
post: AppBskyFeedDefs.PostView
embeddedPost: AppBskyFeedDefs.PostView
}): AppBskyEmbedRecordWithMedia.View | undefined {
if (!AppBskyEmbedRecordWithMedia.isView(post.embed)) return
return {
...(post.embed || {}),
record: {
record: {
$type: 'app.bsky.embed.record#viewRecord',
...embeddedPost,
value: embeddedPost.record,
},
},
}
}
+49 -13
View File
@@ -8,6 +8,7 @@ import {
import * as Clipboard from 'expo-clipboard' import * as Clipboard from 'expo-clipboard'
import { import {
AppBskyEmbedRecord, AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyFeedDefs, AppBskyFeedDefs,
AppBskyFeedPost, AppBskyFeedPost,
AtUri, AtUri,
@@ -32,6 +33,7 @@ import {
usePostDeleteMutation, usePostDeleteMutation,
useThreadMuteMutationQueue, useThreadMuteMutationQueue,
} from '#/state/queries/post' } from '#/state/queries/post'
import {useToggleQuoteDetachmentMutation} from '#/state/queries/postgate'
import {useToggleReplyVisibilityMutation} from '#/state/queries/threadgate' import {useToggleReplyVisibilityMutation} from '#/state/queries/threadgate'
import {useThreadgateRecordQuery} from '#/state/queries/threadgate' import {useThreadgateRecordQuery} from '#/state/queries/threadgate'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
@@ -89,7 +91,6 @@ let PostDropdownBtn = ({
timestamp: string timestamp: string
rootPostUri?: string rootPostUri?: string
}): React.ReactNode => { }): React.ReactNode => {
console.log(record)
const {hasSession, currentAccount} = useSession() const {hasSession, currentAccount} = useSession()
const theme = useTheme() const theme = useTheme()
const alf = useAlf() const alf = useAlf()
@@ -118,18 +119,38 @@ let PostDropdownBtn = ({
const postAuthor = post.author const postAuthor = post.author
const quoteEmbed = React.useMemo(() => { const quoteEmbed = React.useMemo(() => {
if (!currentAccount) return if (!currentAccount) return
const quoteEmbed = if (!post.embed) return
record.embed && AppBskyEmbedRecord.isMain(record.embed)
? record.embed if (AppBskyEmbedRecord.isView(post.embed)) {
: undefined const isPost = AppBskyEmbedRecord.isViewRecord(post.embed.record)
if (!quoteEmbed) return const isDetachedPost = AppBskyEmbedRecord.isViewRemoved(post.embed.record)
const urip = new AtUri(quoteEmbed.record.uri)
if (urip.collection !== 'app.bsky.feed.post') return if (isPost || isDetachedPost) {
return { const urip = new AtUri(post.embed.record.uri)
uri: quoteEmbed.record.uri,
isOwnedByViewer: urip.host === currentAccount.did, return {
uri: urip.toString(),
isOwnedByViewer: urip.host === currentAccount.did,
isDetached: isDetachedPost,
}
}
} else if (AppBskyEmbedRecordWithMedia.isView(post.embed)) {
const isPost = AppBskyEmbedRecord.isViewRecord(post.embed.record.record)
const isDetachedPost = AppBskyEmbedRecord.isViewRemoved(
post.embed.record.record,
)
if (isPost || isDetachedPost) {
const urip = new AtUri(post.embed.record.record.uri)
return {
uri: urip.toString(),
isOwnedByViewer: urip.host === currentAccount.did,
isDetached: isDetachedPost,
}
}
} }
}, [record, currentAccount]) }, [post, currentAccount])
const rootUri = record.reply?.root?.uri || postUri const rootUri = record.reply?.root?.uri || postUri
const [isThreadMuted, muteThread, unmuteThread] = useThreadMuteMutationQueue( const [isThreadMuted, muteThread, unmuteThread] = useThreadMuteMutationQueue(
@@ -144,6 +165,9 @@ let PostDropdownBtn = ({
}) })
const isReplyHiddenByThreadgate = threadgate?.hiddenReplies?.includes(postUri) const isReplyHiddenByThreadgate = threadgate?.hiddenReplies?.includes(postUri)
const {mutateAsync: toggleQuoteDetachment} =
useToggleQuoteDetachmentMutation()
const href = React.useMemo(() => { const href = React.useMemo(() => {
const urip = new AtUri(postUri) const urip = new AtUri(postUri)
return makeProfileLink(postAuthor, 'post', urip.rkey) return makeProfileLink(postAuthor, 'post', urip.rkey)
@@ -273,6 +297,7 @@ let PostDropdownBtn = ({
const onToggleReplyVisibility = React.useCallback(() => { const onToggleReplyVisibility = React.useCallback(() => {
if (!rootPostUri) return if (!rootPostUri) return
// TODO handle failure
toggleReplyVisibility({ toggleReplyVisibility({
postUri: rootPostUri, postUri: rootPostUri,
replyUri: postUri, replyUri: postUri,
@@ -280,6 +305,17 @@ let PostDropdownBtn = ({
}) })
}, [isReplyHiddenByThreadgate, rootPostUri, postUri, toggleReplyVisibility]) }, [isReplyHiddenByThreadgate, rootPostUri, postUri, toggleReplyVisibility])
const onToggleQuotePostAttachment = React.useCallback(() => {
if (!quoteEmbed) return
// TODO handle failure
toggleQuoteDetachment({
postUri,
quotedUri: quoteEmbed.uri,
action: quoteEmbed.isDetached ? 'reattach' : 'detach',
})
}, [quoteEmbed, postUri, toggleQuoteDetachment])
const canEmbed = isWeb && gtMobile && !hideInPWI const canEmbed = isWeb && gtMobile && !hideInPWI
return ( return (
@@ -454,7 +490,7 @@ let PostDropdownBtn = ({
<Menu.Item <Menu.Item
testID="postDropdownHideBtn" testID="postDropdownHideBtn"
label={_(msg`Detach quote`)} label={_(msg`Detach quote`)}
onPress={onToggleReplyVisibility}> onPress={onToggleQuotePostAttachment}>
<Menu.ItemText>{_(msg`Detach quote`)}</Menu.ItemText> <Menu.ItemText>{_(msg`Detach quote`)}</Menu.ItemText>
<Menu.ItemIcon icon={EyeSlash} position="right" /> <Menu.ItemIcon icon={EyeSlash} position="right" />
</Menu.Item> </Menu.Item>