feat: mute

This commit is contained in:
codytseng
2025-01-19 14:40:05 +08:00
parent 34ff0cd314
commit cbae26e492
26 changed files with 564 additions and 45 deletions

View File

@@ -0,0 +1,78 @@
import { Button } from '@/components/ui/button'
import { useToast } from '@/hooks'
import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider'
import { 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 { toast } = useToast()
const { pubkey: accountPubkey, checkLogin } = useNostr()
const { mutePubkeys, mutePubkey, 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) => {
e.stopPropagation()
checkLogin(async () => {
if (isMuted) return
setUpdating(true)
try {
await mutePubkey(pubkey)
} catch (error) {
toast({
title: t('Mute failed'),
description: (error as Error).message,
variant: 'destructive'
})
} finally {
setUpdating(false)
}
})
}
const handleUnmute = async (e: React.MouseEvent) => {
e.stopPropagation()
checkLogin(async () => {
if (!isMuted) return
setUpdating(true)
try {
await unmutePubkey(pubkey)
} catch (error) {
toast({
title: t('Unmute failed'),
description: (error as Error).message,
variant: 'destructive'
})
} finally {
setUpdating(false)
}
})
}
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>
) : (
<Button
variant="destructive"
className="w-20 min-w-20 rounded-full"
onClick={handleMute}
disabled={updating}
>
{updating ? <Loader className="animate-spin" /> : t('Mute')}
</Button>
)
}

View File

@@ -3,6 +3,7 @@ import { PICTURE_EVENT_KIND } from '@/constants'
import { useFetchRelayInfos } from '@/hooks'
import { isReplyNoteEvent } from '@/lib/event'
import { cn } from '@/lib/utils'
import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import client from '@/services/client.service'
@@ -23,15 +24,18 @@ type TListMode = 'posts' | 'postsAndReplies' | 'pictures'
export default function NoteList({
relayUrls,
filter = {},
className
className,
filterMutedNotes = true
}: {
relayUrls: string[]
filter?: Filter
className?: string
filterMutedNotes?: boolean
}) {
const { t } = useTranslation()
const { isLargeScreen } = useScreenSize()
const { signEvent, checkLogin } = useNostr()
const { mutePubkeys } = useMuteList()
const { areAlgoRelays } = useFetchRelayInfos([...relayUrls])
const [refreshCount, setRefreshCount] = useState(0)
const [timelineKey, setTimelineKey] = useState<string | undefined>(undefined)
@@ -158,6 +162,13 @@ export default function NoteList({
setNewEvents([])
}
const eventFilter = (event: Event) => {
return (
(!filterMutedNotes || !mutePubkeys.includes(event.pubkey)) &&
(listMode !== 'posts' || !isReplyNoteEvent(event))
)
}
return (
<div className={cn('space-y-2 sm:space-y-2', className)}>
<ListModeSwitch listMode={listMode} setListMode={setListMode} />
@@ -169,8 +180,7 @@ export default function NoteList({
pullingContent=""
>
<div className="space-y-2 sm:space-y-2">
{newEvents.filter((event) => listMode !== 'posts' || !isReplyNoteEvent(event)).length >
0 && (
{newEvents.filter(eventFilter).length > 0 && (
<div className="flex justify-center w-full max-sm:mt-2">
<Button size="lg" onClick={showNewEvents}>
{t('show new notes')}
@@ -185,11 +195,9 @@ export default function NoteList({
/>
) : (
<div>
{events
.filter((event) => listMode === 'postsAndReplies' || !isReplyNoteEvent(event))
.map((event) => (
<NoteCard key={event.id} className="w-full" event={event} />
))}
{events.filter(eventFilter).map((event) => (
<NoteCard key={event.id} className="w-full" event={event} />
))}
</div>
)}
<div className="text-center text-sm text-muted-foreground">

View File

@@ -5,7 +5,9 @@ import {
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { getSharableEventId } from '@/lib/event'
import { Code, Copy, Ellipsis } from 'lucide-react'
import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider'
import { BellOff, Code, Copy, Ellipsis } from 'lucide-react'
import { Event } from 'nostr-tools'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
@@ -13,7 +15,9 @@ import RawEventDialog from './RawEventDialog'
export default function NoteOptions({ event }: { event: Event }) {
const { t } = useTranslation()
const { pubkey } = useNostr()
const [isRawEventDialogOpen, setIsRawEventDialogOpen] = useState(false)
const { mutePubkey } = useMuteList()
return (
<div className="h-4" onClick={(e) => e.stopPropagation()}>
@@ -35,6 +39,15 @@ export default function NoteOptions({ event }: { event: Event }) {
<Code />
{t('raw event')}
</DropdownMenuItem>
{pubkey && (
<DropdownMenuItem
onClick={() => mutePubkey(event.pubkey)}
className="text-destructive focus:text-destructive"
>
<BellOff />
{t('mute author')}
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
<RawEventDialog

View File

@@ -0,0 +1,55 @@
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { pubkeyToNpub } from '@/lib/pubkey'
import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider'
import { Bell, BellOff, Copy, Ellipsis } from 'lucide-react'
import { useTranslation } from 'react-i18next'
export default function ProfileOptions({ pubkey }: { pubkey: string }) {
const { t } = useTranslation()
const { pubkey: accountPubkey } = useNostr()
const { mutePubkeys, mutePubkey, unmutePubkey } = useMuteList()
if (pubkey === accountPubkey) return null
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="secondary" size="icon" className="rounded-full">
<Ellipsis className="text-muted-foreground hover:text-foreground cursor-pointer" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent collisionPadding={8}>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText('nostr:' + pubkeyToNpub(pubkey))}
>
<Copy />
{t('copy embedded code')}
</DropdownMenuItem>
{mutePubkeys.includes(pubkey) ? (
<DropdownMenuItem
onClick={() => unmutePubkey(pubkey)}
className="text-destructive focus:text-destructive"
>
<Bell />
{t('unmute user')}
</DropdownMenuItem>
) : (
<DropdownMenuItem
onClick={() => mutePubkey(pubkey)}
className="text-destructive focus:text-destructive"
>
<BellOff />
{t('mute user')}
</DropdownMenuItem>
)}
</DropdownMenuContent>
</DropdownMenu>
)
}