diff --git a/package.json b/package.json index 2ec75ce..fcb7095 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "jank", - "version": "26.14.0", + "version": "26.14.1", "description": "A TweetDeck-style multi-column deck for Nostr — just another nostr klient", "type": "module", "author": "DocNR", diff --git a/src/components/Mute/MutedUserItem.tsx b/src/components/Mute/MutedUserItem.tsx index 096857a..4ef2ab3 100644 --- a/src/components/Mute/MutedUserItem.tsx +++ b/src/components/Mute/MutedUserItem.tsx @@ -1,21 +1,67 @@ import MuteButton from '@/components/MuteButton' import Nip05 from '@/components/Nip05' import { Button } from '@/components/ui/button' +import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import UserAvatar from '@/components/UserAvatar' import Username from '@/components/Username' import { useSecondaryPage } from '@/DeckManager' import { useFetchProfile } from '@/hooks' import { toProfile } from '@/lib/link' import { useMuteList } from '@/providers/UserListsProvider' -import { Loader, Lock, Unlock } from 'lucide-react' -import { useMemo, useState } from 'react' +import { Globe, Loader, Lock } from 'lucide-react' +import { useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' export default function MutedUserItem({ pubkey }: { pubkey: string }) { + const { t } = useTranslation() const { push } = useSecondaryPage() - const { changing, getMuteType, switchToPrivateMute, switchToPublicMute } = useMuteList() + const { getMuteType, switchToPrivateMute, switchToPublicMute } = useMuteList() const { profile } = useFetchProfile(pubkey) - const muteType = useMemo(() => getMuteType(pubkey), [pubkey, getMuteType]) - const [switching, setSwitching] = useState(false) + const realType = useMemo(() => getMuteType(pubkey), [pubkey, getMuteType]) + + // Coalesced optimistic visibility toggle. The label flips instantly on every + // click (`override`); underneath we publish at most one mutation per settled + // target — extra clicks while a publish is in flight just update `pendingRef`, + // so furious flipping never fans out into a cascade of signed kind-10000 + // events (each is a relay publish + a remote sign on a NIP-46 bunker). + const [override, setOverride] = useState<'public' | 'private' | null>(null) + const [saving, setSaving] = useState(false) + const pendingRef = useRef<'public' | 'private' | null>(null) + const runningRef = useRef(false) + + const shownType = override ?? realType + + const runCommit = async () => { + runningRef.current = true + setSaving(true) + try { + while (pendingRef.current !== null) { + const target = pendingRef.current + pendingRef.current = null + try { + await (target === 'public' ? switchToPublicMute(pubkey) : switchToPrivateMute(pubkey)) + } catch { + // The provider already surfaced a toast. Don't break: if a newer click + // queued a different target while this one was failing, honor it on the + // next iteration. Otherwise the loop exits and the label reconciles to + // the real state below. + } + } + } finally { + runningRef.current = false + setSaving(false) + // Drop the optimistic override: a successful switch leaves the real type + // at `target` (label unchanged); a no-op / failure reverts it. + setOverride(null) + } + } + + const handleToggleVisibility = () => { + const target = shownType === 'private' ? 'public' : 'private' + setOverride(target) + pendingRef.current = target + if (!runningRef.current) void runCommit() + } return (