Block posting of invalid chat invites (#10757)

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
DS Boyce
2026-06-08 05:28:24 -07:00
committed by GitHub
parent 16046cd85e
commit 5c0429a3f7
3 changed files with 33 additions and 20 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ import {type ButtonColor} from '#/components/Button'
import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow' import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow'
import {ArrowBoxRight_Stroke2_Corner3_Rounded as JoinIcon} from '#/components/icons/ArrowBoxRight' import {ArrowBoxRight_Stroke2_Corner3_Rounded as JoinIcon} from '#/components/icons/ArrowBoxRight'
import {ChainLink_Stroke2_Corner0_Rounded as LinkIcon} from '#/components/icons/ChainLink' import {ChainLink_Stroke2_Corner0_Rounded as LinkIcon} from '#/components/icons/ChainLink'
import {CheckThick_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check' import {Check_Stroke2_Corner0_Rounded as CheckIcon} from '#/components/icons/Check'
import {type Props as SVGIconProps} from '#/components/icons/common' import {type Props as SVGIconProps} from '#/components/icons/common'
import {RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon} from '#/components/icons/RaisingHand' import {RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon} from '#/components/icons/RaisingHand'
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
+13 -18
View File
@@ -1,5 +1,5 @@
import {type BskyAgent} from '@atproto/api' import {type AtpAgent} from '@atproto/api'
import {type QueryClient, useQuery} from '@tanstack/react-query' import {type QueryClient, queryOptions, useQuery} from '@tanstack/react-query'
import {type ResolvedLink, resolveGif, resolveLink} from '#/lib/api/resolve' import {type ResolvedLink, resolveGif, resolveLink} from '#/lib/api/resolve'
import {STALE} from '#/state/queries/index' import {STALE} from '#/state/queries/index'
@@ -12,29 +12,24 @@ export const RQKEY_LINK = (url: string) => [RQKEY_LINK_ROOT, url]
export const RQKEY_GIF_ROOT = 'resolve-gif' export const RQKEY_GIF_ROOT = 'resolve-gif'
export const RQKEY_GIF = (url: string) => [RQKEY_GIF_ROOT, url] export const RQKEY_GIF = (url: string) => [RQKEY_GIF_ROOT, url]
export function useResolveLinkQuery(url: string) { export function resolveLinkQueryOptions(agent: AtpAgent, url: string) {
const agent = useAgent() return queryOptions({
return useQuery({
staleTime: STALE.HOURS.ONE, staleTime: STALE.HOURS.ONE,
queryKey: RQKEY_LINK(url), queryKey: RQKEY_LINK(url),
queryFn: async () => { queryFn: () => resolveLink(agent, url),
return await resolveLink(agent, url)
},
}) })
} }
export function useResolveLinkQuery(url: string) {
const agent = useAgent()
return useQuery(resolveLinkQueryOptions(agent, url))
}
export function fetchResolveLinkQuery( export function fetchResolveLinkQuery(
queryClient: QueryClient, queryClient: QueryClient,
agent: BskyAgent, agent: AtpAgent,
url: string, url: string,
) { ) {
return queryClient.fetchQuery({ return queryClient.fetchQuery(resolveLinkQueryOptions(agent, url))
staleTime: STALE.HOURS.ONE,
queryKey: RQKEY_LINK(url),
queryFn: async () => {
return await resolveLink(agent, url)
},
})
} }
export function precacheResolveLinkQuery( export function precacheResolveLinkQuery(
queryClient: QueryClient, queryClient: QueryClient,
@@ -56,7 +51,7 @@ export function useResolveGifQuery(gif: Gif) {
} }
export function fetchResolveGifQuery( export function fetchResolveGifQuery(
queryClient: QueryClient, queryClient: QueryClient,
agent: BskyAgent, agent: AtpAgent,
gif: Gif, gif: Gif,
) { ) {
return queryClient.fetchQuery({ return queryClient.fetchQuery({
+19 -1
View File
@@ -57,7 +57,7 @@ import {
import {plural} from '@lingui/core/macro' import {plural} from '@lingui/core/macro'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {useNavigation} from '@react-navigation/native' import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query' import {useQueries, useQueryClient} from '@tanstack/react-query'
import * as apilib from '#/lib/api/index' import * as apilib from '#/lib/api/index'
import {EmbeddingDisabledError} from '#/lib/api/resolve' import {EmbeddingDisabledError} from '#/lib/api/resolve'
@@ -95,6 +95,7 @@ import {
} from '#/state/preferences/languages' } from '#/state/preferences/languages'
import {usePreferencesQuery} from '#/state/queries/preferences' import {usePreferencesQuery} from '#/state/queries/preferences'
import {useProfileQuery} from '#/state/queries/profile' import {useProfileQuery} from '#/state/queries/profile'
import {resolveLinkQueryOptions} from '#/state/queries/resolve-link'
import {useAgent, useSession} from '#/state/session' import {useAgent, useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer' import {useComposerControls} from '#/state/shell/composer'
import {type ComposerOpts, type OnPostSuccessData} from '#/state/shell/composer' import {type ComposerOpts, type OnPostSuccessData} from '#/state/shell/composer'
@@ -865,8 +866,25 @@ export const ComposePost = ({
} }
}, [thread, requireAltTextEnabled, l]) }, [thread, requireAltTextEnabled, l])
// Subscribe to the resolve-link cache for any link URIs in the thread so we
// can detect chat invites that resolved to no preview (revoked/expired) and
// block publishing - otherwise the post would go out without the embed.
const linkUris = thread.posts
.filter(post => post.embed.link)
.map(post => post.embed.link!.uri)
const linkQueries = useQueries({
queries: linkUris.map(uri => ({
...resolveLinkQueryOptions(agent, uri),
enabled: false,
})),
})
const hasUnavailableChatInvite = linkQueries.some(
q => q.data?.type === 'chat-invite' && !q.data.view,
)
const canPost = const canPost =
!missingAltError && !missingAltError &&
!hasUnavailableChatInvite &&
thread.posts.some(post => !isEmptyPost(post)) && thread.posts.some(post => !isEmptyPost(post)) &&
thread.posts.every( thread.posts.every(
post => post =>