feat: support choosing between public and private mute

This commit is contained in:
codytseng 2025-06-04 22:09:27 +08:00
parent 30a32ca94f
commit ec1692c066
19 changed files with 473 additions and 120 deletions

View file

@ -1,29 +1,43 @@
import { Button } from '@/components/ui/button'
import { Drawer, DrawerContent, DrawerTrigger } from '@/components/ui/drawer'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { useToast } from '@/hooks'
import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider'
import { Loader } from 'lucide-react'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { BellOff, Loader } from 'lucide-react'
import { useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
export default function MuteButton({ pubkey }: { pubkey: string }) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const { toast } = useToast()
const { pubkey: accountPubkey, checkLogin } = useNostr()
const { mutePubkeys, mutePubkey, unmutePubkey } = useMuteList()
const { mutePubkeys, changing, mutePubkeyPrivately, mutePubkeyPublicly, unmutePubkey } =
useMuteList()
const [updating, setUpdating] = useState(false)
const isMuted = useMemo(() => mutePubkeys.includes(pubkey), [mutePubkeys, pubkey])
if (!accountPubkey || (pubkey && pubkey === accountPubkey)) return null
const handleMute = async (e: React.MouseEvent) => {
const handleMute = async (e: React.MouseEvent, isPrivate = true) => {
e.stopPropagation()
checkLogin(async () => {
if (isMuted) return
setUpdating(true)
try {
await mutePubkey(pubkey)
if (isPrivate) {
await mutePubkeyPrivately(pubkey)
} else {
await mutePubkeyPublicly(pubkey)
}
} catch (error) {
toast({
title: t('Mute failed'),
@ -56,23 +70,76 @@ export default function MuteButton({ pubkey }: { pubkey: string }) {
})
}
return isMuted ? (
<Button
className="w-20 min-w-20 rounded-full"
variant="secondary"
onClick={handleUnmute}
disabled={updating}
>
{updating ? <Loader className="animate-spin" /> : t('Unmute')}
</Button>
) : (
if (isMuted) {
return (
<Button
className="w-20 min-w-20 rounded-full"
variant="secondary"
onClick={handleUnmute}
disabled={updating || changing}
>
{updating ? <Loader className="animate-spin" /> : t('Unmute')}
</Button>
)
}
const trigger = (
<Button
variant="destructive"
className="w-20 min-w-20 rounded-full"
onClick={handleMute}
disabled={updating}
disabled={updating || changing}
>
{updating ? <Loader className="animate-spin" /> : t('Mute')}
</Button>
)
if (isSmallScreen) {
return (
<Drawer>
<DrawerTrigger asChild>{trigger}</DrawerTrigger>
<DrawerContent>
<div className="py-2">
<Button
className="w-full p-6 justify-start text-destructive text-lg gap-4 [&_svg]:size-5 focus:text-destructive"
variant="ghost"
onClick={(e) => handleMute(e, true)}
disabled={updating || changing}
>
{updating ? <Loader className="animate-spin" /> : t('Mute user privately')}
</Button>
<Button
className="w-full p-6 justify-start text-destructive text-lg gap-4 [&_svg]:size-5 focus:text-destructive"
variant="ghost"
onClick={(e) => handleMute(e, false)}
disabled={updating || changing}
>
{updating ? <Loader className="animate-spin" /> : t('Mute user publicly')}
</Button>
</div>
</DrawerContent>
</Drawer>
)
}
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>{trigger}</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
onClick={(e) => handleMute(e, true)}
className="text-destructive focus:text-destructive"
>
<BellOff />
{t('Mute user privately')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={(e) => handleMute(e, false)}
className="text-destructive focus:text-destructive"
>
<BellOff />
{t('Mute user publicly')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

View file

@ -25,7 +25,7 @@ export default function NoteOptions({ event, className }: { event: Event; classN
const { pubkey } = useNostr()
const [isRawEventDialogOpen, setIsRawEventDialogOpen] = useState(false)
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
const { mutePubkey, unmutePubkey, mutePubkeys } = useMuteList()
const { mutePubkeyPublicly, mutePubkeyPrivately, unmutePubkey, mutePubkeys } = useMuteList()
const isMuted = useMemo(() => mutePubkeys.includes(event.pubkey), [mutePubkeys, event])
const trigger = (
@ -97,23 +97,45 @@ export default function NoteOptions({ event, className }: { event: Event; classN
<Code />
{t('View raw event')}
</Button>
{pubkey && (
<Button
onClick={() => {
setIsDrawerOpen(false)
if (isMuted) {
{pubkey &&
(isMuted ? (
<Button
onClick={() => {
setIsDrawerOpen(false)
unmutePubkey(event.pubkey)
} else {
mutePubkey(event.pubkey)
}
}}
className="w-full p-6 justify-start text-destructive text-lg gap-4 [&_svg]:size-5 focus:text-destructive"
variant="ghost"
>
{isMuted ? <Bell /> : <BellOff />}
{isMuted ? t('Unmute user') : t('Mute user')}
</Button>
)}
}}
className="w-full p-6 justify-start text-destructive text-lg gap-4 [&_svg]:size-5 focus:text-destructive"
variant="ghost"
>
<Bell />
{t('Unmute user')}
</Button>
) : (
<>
<Button
onClick={() => {
setIsDrawerOpen(false)
mutePubkeyPrivately(event.pubkey)
}}
className="w-full p-6 justify-start text-destructive text-lg gap-4 [&_svg]:size-5 focus:text-destructive"
variant="ghost"
>
<BellOff />
{t('Mute user privately')}
</Button>
<Button
onClick={() => {
setIsDrawerOpen(false)
mutePubkeyPublicly(event.pubkey)
}}
className="w-full p-6 justify-start text-destructive text-lg gap-4 [&_svg]:size-5 focus:text-destructive"
variant="ghost"
>
<BellOff />
{t('Mute user publicly')}
</Button>
</>
))}
</div>
</DrawerContent>
</Drawer>
@ -155,13 +177,32 @@ export default function NoteOptions({ event, className }: { event: Event; classN
{pubkey && (
<>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => (isMuted ? unmutePubkey(event.pubkey) : mutePubkey(event.pubkey))}
className="text-destructive focus:text-destructive"
>
{isMuted ? <Bell /> : <BellOff />}
{isMuted ? t('Unmute user') : t('Mute user')}
</DropdownMenuItem>
{isMuted ? (
<DropdownMenuItem
onClick={() => unmutePubkey(event.pubkey)}
className="text-destructive focus:text-destructive"
>
<Bell />
{t('Unmute user')}
</DropdownMenuItem>
) : (
<>
<DropdownMenuItem
onClick={() => mutePubkeyPrivately(event.pubkey)}
className="text-destructive focus:text-destructive"
>
<BellOff />
{t('Mute user privately')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => mutePubkeyPublicly(event.pubkey)}
className="text-destructive focus:text-destructive"
>
<BellOff />
{t('Mute user publicly')}
</DropdownMenuItem>
</>
)}
</>
)}
</DropdownMenuContent>

View file

@ -14,10 +14,12 @@ import { useTranslation } from 'react-i18next'
export default function ProfileOptions({ pubkey }: { pubkey: string }) {
const { t } = useTranslation()
const { pubkey: accountPubkey } = useNostr()
const { mutePubkeys, mutePubkey, unmutePubkey } = useMuteList()
const { mutePubkeys, mutePubkeyPrivately, mutePubkeyPublicly, unmutePubkey } = useMuteList()
if (pubkey === accountPubkey) return null
const isMuted = mutePubkeys.includes(pubkey)
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
@ -32,7 +34,7 @@ export default function ProfileOptions({ pubkey }: { pubkey: string }) {
<Copy />
{t('Copy user ID')}
</DropdownMenuItem>
{mutePubkeys.includes(pubkey) ? (
{isMuted ? (
<DropdownMenuItem
onClick={() => unmutePubkey(pubkey)}
className="text-destructive focus:text-destructive"
@ -41,13 +43,22 @@ export default function ProfileOptions({ pubkey }: { pubkey: string }) {
{t('Unmute user')}
</DropdownMenuItem>
) : (
<DropdownMenuItem
onClick={() => mutePubkey(pubkey)}
className="text-destructive focus:text-destructive"
>
<BellOff />
{t('Mute user')}
</DropdownMenuItem>
<>
<DropdownMenuItem
onClick={() => mutePubkeyPrivately(pubkey)}
className="text-destructive focus:text-destructive"
>
<BellOff />
{t('Mute user privately')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => mutePubkeyPublicly(pubkey)}
className="text-destructive focus:text-destructive"
>
<BellOff />
{t('Mute user publicly')}
</DropdownMenuItem>
</>
)}
</DropdownMenuContent>
</DropdownMenu>