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 {AppBskyFeedDefs} from '@atproto/api'
import {
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyFeedDefs,
} from '@atproto/api'
import {QueryClient} from '@tanstack/react-query'
import EventEmitter from 'eventemitter3'
@@ -15,6 +19,7 @@ export interface PostShadow {
likeUri: string | undefined
repostUri: string | undefined
isDeleted: boolean
embed: AppBskyEmbedRecord.View | AppBskyEmbedRecordWithMedia.View | undefined
}
export const POST_TOMBSTONE = Symbol('PostTombstone')
@@ -86,6 +91,10 @@ function mergeShadow(
repostCount = Math.max(0, repostCount)
}
if (shadow.embed !== undefined && post.embed) {
post.embed = shadow.embed
}
return castAsShadow({
...post,
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(
post: Shadow<AppBskyFeedDefs.PostView>,
logContext: LogEvents['post:like']['logContext'] &
+18 -1
View File
@@ -1,8 +1,11 @@
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 {updatePostShadow} from '#/state/cache/post-shadow'
import {useGetPosts} from '#/state/queries/post'
import {
createEmbed,
createPostgateRecord,
mergePostgateRecords,
POSTGATE_COLLECTION,
@@ -26,6 +29,7 @@ export async function getPostgateRecord({
}
try {
// TODO don't retry on 404
const {data} = await networkRetry(2, () =>
agent.api.com.atproto.repo.getRecord({
repo: urip.host,
@@ -100,6 +104,8 @@ export async function upsertPostgate(
export function useToggleQuoteDetachmentMutation() {
const agent = useAgent()
const queryClient = useQueryClient()
const getPosts = useGetPosts()
return useMutation({
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'
export const POSTGATE_COLLECTION = 'app.bsky.feed.postgate'
@@ -42,3 +47,60 @@ export function createEmbedViewRemovedRecord({uri}: {uri: string}) {
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 {
AppBskyEmbedRecord,
AppBskyEmbedRecordWithMedia,
AppBskyFeedDefs,
AppBskyFeedPost,
AtUri,
@@ -32,6 +33,7 @@ import {
usePostDeleteMutation,
useThreadMuteMutationQueue,
} from '#/state/queries/post'
import {useToggleQuoteDetachmentMutation} from '#/state/queries/postgate'
import {useToggleReplyVisibilityMutation} from '#/state/queries/threadgate'
import {useThreadgateRecordQuery} from '#/state/queries/threadgate'
import {useSession} from '#/state/session'
@@ -89,7 +91,6 @@ let PostDropdownBtn = ({
timestamp: string
rootPostUri?: string
}): React.ReactNode => {
console.log(record)
const {hasSession, currentAccount} = useSession()
const theme = useTheme()
const alf = useAlf()
@@ -118,18 +119,38 @@ let PostDropdownBtn = ({
const postAuthor = post.author
const quoteEmbed = React.useMemo(() => {
if (!currentAccount) return
const quoteEmbed =
record.embed && AppBskyEmbedRecord.isMain(record.embed)
? record.embed
: undefined
if (!quoteEmbed) return
const urip = new AtUri(quoteEmbed.record.uri)
if (urip.collection !== 'app.bsky.feed.post') return
return {
uri: quoteEmbed.record.uri,
isOwnedByViewer: urip.host === currentAccount.did,
if (!post.embed) return
if (AppBskyEmbedRecord.isView(post.embed)) {
const isPost = AppBskyEmbedRecord.isViewRecord(post.embed.record)
const isDetachedPost = AppBskyEmbedRecord.isViewRemoved(post.embed.record)
if (isPost || isDetachedPost) {
const urip = new AtUri(post.embed.record.uri)
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 [isThreadMuted, muteThread, unmuteThread] = useThreadMuteMutationQueue(
@@ -144,6 +165,9 @@ let PostDropdownBtn = ({
})
const isReplyHiddenByThreadgate = threadgate?.hiddenReplies?.includes(postUri)
const {mutateAsync: toggleQuoteDetachment} =
useToggleQuoteDetachmentMutation()
const href = React.useMemo(() => {
const urip = new AtUri(postUri)
return makeProfileLink(postAuthor, 'post', urip.rkey)
@@ -273,6 +297,7 @@ let PostDropdownBtn = ({
const onToggleReplyVisibility = React.useCallback(() => {
if (!rootPostUri) return
// TODO handle failure
toggleReplyVisibility({
postUri: rootPostUri,
replyUri: postUri,
@@ -280,6 +305,17 @@ let PostDropdownBtn = ({
})
}, [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
return (
@@ -454,7 +490,7 @@ let PostDropdownBtn = ({
<Menu.Item
testID="postDropdownHideBtn"
label={_(msg`Detach quote`)}
onPress={onToggleReplyVisibility}>
onPress={onToggleQuotePostAttachment}>
<Menu.ItemText>{_(msg`Detach quote`)}</Menu.ItemText>
<Menu.ItemIcon icon={EyeSlash} position="right" />
</Menu.Item>