From 91053e3f701f46089ce0f86fe40e8365de7fe3c1 Mon Sep 17 00:00:00 2001 From: DS Boyce <260543580+ds-boyce@users.noreply.github.com> Date: Thu, 11 Jun 2026 09:48:23 -0700 Subject: [PATCH] Optimistically update chat invite button status (#10870) --- .../intents/GroupChatJoinDialog.tsx | 15 +++++- src/state/queries/join-links.ts | 49 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/components/intents/GroupChatJoinDialog.tsx b/src/components/intents/GroupChatJoinDialog.tsx index e3c20b0e59..287ccbca58 100644 --- a/src/components/intents/GroupChatJoinDialog.tsx +++ b/src/components/intents/GroupChatJoinDialog.tsx @@ -20,6 +20,7 @@ import {logger} from '#/logger' import {useModerationOpts} from '#/state/preferences/moderation-opts' import { invalidateJoinLinkPreviewsForCode, + setJoinLinkPreviewRequestedForCode, useJoinLinkPreviewsQuery, } from '#/state/queries/join-links' import {useRequestJoinGroupChat} from '#/state/queries/messages/request-join-group-chat' @@ -104,9 +105,14 @@ function GroupChatJoinDialogContent({code}: {code?: string}) { const {mutate: joinGroupChat, isPending: isJoinPending} = useRequestJoinGroupChat({ onSuccess: data => { - if (code) void invalidateJoinLinkPreviewsForCode(queryClient, code) switch (data.status) { case 'pending': + // Optimistically mark the link as requested so any invite cards + // backed by the preview cache (e.g. the DM embed) flip to + // "Requested" right away, rather than waiting on a server refetch + // that can lag behind the write. + if (code) + setJoinLinkPreviewRequestedForCode(queryClient, code, true) ax.metric('groupchat:inviteLink:redeem', {}) control.close(() => { Toast.show( @@ -116,6 +122,10 @@ function GroupChatJoinDialogContent({code}: {code?: string}) { break case 'joined': { if (data.convo && data.convo.id) { + // Refetch so invite cards pick up the resolved convo and switch + // their action to "Open chat". + if (code) + void invalidateJoinLinkPreviewsForCode(queryClient, code) ax.metric('groupchat:inviteLink:redeem', {}) control.close(() => { Toast.show(l`Successfully joined the group chat!`) @@ -172,6 +182,9 @@ function GroupChatJoinDialogContent({code}: {code?: string}) { const {mutate: withdrawRequest, isPending: isWithdrawPending} = useWithdrawJoinGroupChatRequest({ onSuccess: () => { + // Optimistically clear the requested state so invite cards backed by + // the preview cache flip back to "Request to join" right away. + if (code) setJoinLinkPreviewRequestedForCode(queryClient, code, false) control.close(() => { Toast.show(l`Join request rescinded.`) }) diff --git a/src/state/queries/join-links.ts b/src/state/queries/join-links.ts index 2381f467f4..ca1db05558 100644 --- a/src/state/queries/join-links.ts +++ b/src/state/queries/join-links.ts @@ -76,6 +76,55 @@ export function invalidateJoinLinkPreviewsForCode( }) } +/** + * Optimistically set whether the viewer has requested to join the link with the + * given code, across any cached join link preview queries. Used right after a + * successful join request (requested = true) or withdrawal (requested = false) + * so the UI ("Requested" vs "Request to join") updates immediately, without + * waiting on a server refetch that can lag behind the write. + */ +export function setJoinLinkPreviewRequestedForCode( + queryClient: QueryClient, + code: string, + requested: boolean, +) { + queryClient.setQueriesData( + { + predicate: query => { + const [root, args] = query.queryKey as Partial< + StructuredQueryKey<{codes?: string[]}> + > + return ( + root === joinLinkPreviewQueryKeyRoot && + Array.isArray(args?.codes) && + args.codes.includes(code) + ) + }, + }, + old => { + if (!old) return old + return { + ...old, + joinLinkPreviews: old.joinLinkPreviews.map(preview => { + if ( + ChatBskyGroupDefs.isJoinLinkPreviewView(preview) && + preview.code === code + ) { + return { + ...preview, + viewer: { + ...preview.viewer, + requestedAt: requested ? new Date().toISOString() : undefined, + }, + } + } + return preview + }), + } + }, + ) +} + /** * Invalidate any join link preview queries that resolved to the given convo. * The code isn't always known to the viewer (e.g. when they're a regular