feat: mute
This commit is contained in:
78
src/components/MuteButton/index.tsx
Normal file
78
src/components/MuteButton/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
55
src/components/ProfileOptions/index.tsx
Normal file
55
src/components/ProfileOptions/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user