Animate the tag follow toggle while the request settles
The chip toggle swaps its icon for a tiny current-color spinner from the click until the follows list reflects the new state, covering the refetch gap so '+' never flashes back before the checkmark.
This commit is contained in:
@@ -11,6 +11,8 @@ export function TagFollowToggle({ tag }: { tag: string }) {
|
||||
const user = useAuth((state) => state.user);
|
||||
const queryClient = useQueryClient();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// What the chip should show once the request and refetch settle.
|
||||
const [target, setTarget] = useState<boolean | null>(null);
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: ["follows-tags"],
|
||||
@@ -37,9 +39,17 @@ export function TagFollowToggle({ tag }: { tag: string }) {
|
||||
void queryClient.invalidateQueries({ queryKey: ["follows-feed"] });
|
||||
void queryClient.invalidateQueries({ queryKey: ["follows-cloud"] });
|
||||
},
|
||||
onError: (err) => setError(errorMessage(err)),
|
||||
onError: (err) => {
|
||||
setTarget(null);
|
||||
setError(errorMessage(err));
|
||||
},
|
||||
});
|
||||
|
||||
// Keep the spinner up until the refetched list reflects the new state.
|
||||
// A stale target that matches `followed` is harmless: the next click
|
||||
// recomputes it before mutating.
|
||||
const busy = mutation.isPending || (target !== null && followed !== target);
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
function handleClick(event: MouseEvent<HTMLButtonElement>) {
|
||||
@@ -47,6 +57,8 @@ export function TagFollowToggle({ tag }: { tag: string }) {
|
||||
// action and must not trigger them.
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (busy) return;
|
||||
setTarget(!followed);
|
||||
mutation.mutate();
|
||||
}
|
||||
|
||||
@@ -54,16 +66,21 @@ export function TagFollowToggle({ tag }: { tag: string }) {
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
disabled={mutation.isPending || query.isPending}
|
||||
disabled={busy || query.isPending}
|
||||
title={error ?? (followed ? `Unfollow ${tag}` : `Follow ${tag}`)}
|
||||
aria-label={followed ? `Unfollow ${tag}` : `Follow ${tag}`}
|
||||
className={cn(
|
||||
"-mr-0.5 inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded-sm transition",
|
||||
error ? "text-ctp-red" : "opacity-70 hover:opacity-100",
|
||||
mutation.isPending && "opacity-40",
|
||||
query.isPending && "opacity-40",
|
||||
)}
|
||||
>
|
||||
{followed ? (
|
||||
{busy ? (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="h-3 w-3 animate-spin rounded-full border-[1.5px] border-current border-t-transparent"
|
||||
/>
|
||||
) : followed ? (
|
||||
<Check className="h-3 w-3" strokeWidth={3} />
|
||||
) : (
|
||||
<Plus className="h-3 w-3" strokeWidth={3} />
|
||||
|
||||
Reference in New Issue
Block a user