import { useCallback, useState } from 'react'; import { Check, ChevronDown, Loader2, UserPlus, VolumeX } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from '@/components/ui/alert-dialog'; import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useFollowActions } from '@/hooks/useFollowActions'; import { useMuteList } from '@/hooks/useMuteList'; import { useToast } from '@/hooks/useToast'; import { cn } from '@/lib/utils'; export interface FollowAllSplitButtonProps { /** Pubkeys (hex) to follow / mute in bulk. */ pubkeys: string[]; /** * Pubkeys the current user is already following, used to compute the * "Already following all" state and the "N new for you" counts. Optional — * if omitted, the button always shows "Follow All (N)" until pressed. */ followedPubkeys?: Set; /** * Human-readable noun for the list (e.g. "this list", "this pack", * "the team", "this badge"). Used in the Mute All confirmation copy. * Defaults to "this list". */ listNoun?: string; /** * If true, also add the pack/list author to the follow list. Used by kind 3 * follow-list views where the viewed event IS the author's own follow list. * Pass the author pubkey to include; ignored if omitted. */ includeAuthorPubkey?: string; /** * Optional className applied to the outer wrapper (controls layout, e.g. * "flex-1"). The split button itself is always an inline-flex group. */ className?: string; /** * Optional size to apply to the buttons. Defaults to "default". Use "sm" * for compact cards. */ size?: 'default' | 'sm' | 'lg'; /** * Optional variant for the main button when the user is already following * everyone in the list. Defaults to keeping the same "default" variant with * a check icon. Set to "outline" for a more subdued completed state. */ followedVariant?: 'default' | 'outline'; /** Optional toast title to show on successful Follow All. */ followSuccessTitle?: string; } /** * Split button that combines "Follow All" with a dropdown caret offering * "Mute all" — letting a viewer treat any list (NIP-02 follow list, NIP-51 * follow set, follow pack, badge awardees, etc.) as either a follow source * or a mute source. Mute All shows a confirmation AlertDialog before merging * the pubkeys into the user's NIP-51 kind 10000 mute list. * * Follow and mute are independent — a viewer can follow AND mute the same * pubkeys. Mute filtering in feed queries is what makes the second case * meaningful (mute wins). */ export function FollowAllSplitButton({ pubkeys, followedPubkeys, listNoun = 'this list', includeAuthorPubkey, className, size = 'default', followedVariant = 'default', followSuccessTitle, }: FollowAllSplitButtonProps) { const { user } = useCurrentUser(); const { followMany, isPending: isFollowing } = useFollowActions(); const { muteManyPubkeys } = useMuteList(); const { toast } = useToast(); const [muteDialogOpen, setMuteDialogOpen] = useState(false); // "Already following all" is only meaningful if the caller provided a // followedPubkeys set; otherwise we always show the active CTA. const allFollowed = !!followedPubkeys && pubkeys.length > 0 && pubkeys.every((pk) => followedPubkeys.has(pk)); const muteCount = pubkeys.filter((pk) => pk !== user?.pubkey).length; const isMuting = muteManyPubkeys.isPending; const isBusy = isFollowing || isMuting; const handleFollowAll = useCallback(async () => { if (!user) { toast({ title: 'Not logged in', description: 'Please log in to follow users.', variant: 'destructive', }); return; } try { const candidates = includeAuthorPubkey ? [...pubkeys, includeAuthorPubkey] : pubkeys; const added = await followMany(candidates); toast({ title: followSuccessTitle ?? 'Following all!', description: added > 0 ? `Added ${added} new account${added !== 1 ? 's' : ''} to your follow list.` : `You were already following everyone in ${listNoun}.`, }); } catch (error) { console.error('Failed to follow all:', error); toast({ title: 'Failed to follow', description: 'There was an error updating your follow list.', variant: 'destructive', }); } }, [user, pubkeys, includeAuthorPubkey, followMany, toast, listNoun, followSuccessTitle]); const handleMuteAll = useCallback(async () => { if (!user) { toast({ title: 'Not logged in', description: 'Please log in to mute users.', variant: 'destructive', }); return; } try { // Don't mute yourself even if the list happens to include you. const candidates = pubkeys.filter((pk) => pk !== user.pubkey); const added = await muteManyPubkeys.mutateAsync(candidates); toast({ title: 'Muted', description: added > 0 ? `Added ${added} account${added !== 1 ? 's' : ''} to your mute list.` : `Everyone in ${listNoun} was already muted.`, }); } catch (error) { console.error('Failed to mute all:', error); toast({ title: 'Failed to mute', description: 'There was an error updating your mute list.', variant: 'destructive', }); } finally { setMuteDialogOpen(false); } }, [user, pubkeys, muteManyPubkeys, toast, listNoun]); return (
{/* Main "Follow All" button — flush against the caret, with no rounded right corners */} {/* Caret dropdown — visible divider line on the left, no rounded left corners */} {/* * Use `onClick` (not `onSelect` + `e.preventDefault()`) so the menu * fully closes before the AlertDialog opens. Otherwise Radix's * DismissableLayer for the menu overlaps with the dialog's and can * leave `pointer-events: none` on the body, freezing the page until * a refresh. Defer the dialog open by a microtask so the menu's * unmount/cleanup runs first. */} { queueMicrotask(() => setMuteDialogOpen(true)); }} className="gap-2" > Mute all {/* Confirmation dialog before bulk-muting */} Mute {muteCount} account{muteCount !== 1 ? 's' : ''}? This will add everyone in {listNoun} to your mute list. Their posts won't appear in your feeds, even if you also follow them. You can unmute individual accounts later from Settings. Cancel { e.preventDefault(); void handleMuteAll(); }} disabled={isMuting} > {isMuting ? ( <> Muting… ) : ( <>Mute all )}
); }