Use post shadow to swap detached quotes
This commit is contained in:
Vendored
+10
-1
@@ -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,
|
||||||
|
|||||||
@@ -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'] &
|
||||||
|
|||||||
@@ -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',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user