feat: support sending 1984 reports

This commit is contained in:
codytseng
2025-09-06 00:15:54 +08:00
parent 7562ae2c77
commit 71994be407
23 changed files with 685 additions and 17 deletions

View File

@@ -0,0 +1,129 @@
import { Button } from '@/components/ui/button'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog'
import {
Drawer,
DrawerContent,
DrawerDescription,
DrawerHeader,
DrawerTitle
} from '@/components/ui/drawer'
import { Label } from '@/components/ui/label'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'
import { createReportDraftEvent } from '@/lib/draft-event'
import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { Loader } from 'lucide-react'
import { NostrEvent } from 'nostr-tools'
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
export default function ReportDialog({
event,
isOpen,
closeDialog
}: {
event: NostrEvent
isOpen: boolean
closeDialog: () => void
}) {
const { isSmallScreen } = useScreenSize()
if (isSmallScreen) {
return (
<Drawer
open={isOpen}
onOpenChange={(open) => {
if (!open) {
closeDialog()
}
}}
>
<DrawerContent>
<DrawerHeader>
<DrawerTitle className="hidden" />
<DrawerDescription className="hidden" />
</DrawerHeader>
<div className="p-4">
<ReportContent event={event} closeDialog={closeDialog} />
</div>
</DrawerContent>
</Drawer>
)
}
return (
<Dialog
open={isOpen}
onOpenChange={(open) => {
if (!open) {
closeDialog()
}
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle className="hidden" />
<DialogDescription className="hidden" />
</DialogHeader>
<ReportContent event={event} closeDialog={closeDialog} />
</DialogContent>
</Dialog>
)
}
function ReportContent({ event, closeDialog }: { event: NostrEvent; closeDialog: () => void }) {
const { t } = useTranslation()
const { pubkey, publish } = useNostr()
const [reason, setReason] = useState<string | null>(null)
const [reporting, setReporting] = useState(false)
const handleReport = async () => {
if (!reason || !pubkey) return
try {
setReporting(true)
const draftEvent = createReportDraftEvent(event, reason)
await publish(draftEvent)
toast.success(t('Successfully report'))
closeDialog()
} catch (error) {
toast.error(t('Failed to report') + ': ' + (error as Error).message)
} finally {
setReporting(false)
}
}
return (
<div className="w-full space-y-4">
<RadioGroup value={reason} onValueChange={setReason} className="space-y-2">
{['nudity', 'malware', 'profanity', 'illegal', 'spam', 'other'].map((item) => (
<div key={item} className="flex items-center space-x-2">
<RadioGroupItem value={item} id={item} />
<Label htmlFor={item} className="text-base">
{t(item)}
</Label>
</div>
))}
</RadioGroup>
<Button
variant="destructive"
className="w-full"
disabled={!reason || reporting}
onClick={(e) => {
e.stopPropagation()
handleReport()
}}
>
{reporting && <Loader className="animate-spin" />}
{t('Report')}
</Button>
</div>
)
}

View File

@@ -5,11 +5,13 @@ import { useState } from 'react'
import { DesktopMenu } from './DesktopMenu'
import { MobileMenu } from './MobileMenu'
import RawEventDialog from './RawEventDialog'
import ReportDialog from './ReportDialog'
import { SubMenuAction, useMenuActions } from './useMenuActions'
export default function NoteOptions({ event, className }: { event: Event; className?: string }) {
const { isSmallScreen } = useScreenSize()
const [isRawEventDialogOpen, setIsRawEventDialogOpen] = useState(false)
const [isReportDialogOpen, setIsReportDialogOpen] = useState(false)
const [isDrawerOpen, setIsDrawerOpen] = useState(false)
const [showSubMenu, setShowSubMenu] = useState(false)
const [activeSubMenu, setActiveSubMenu] = useState<SubMenuAction[]>([])
@@ -35,6 +37,7 @@ export default function NoteOptions({ event, className }: { event: Event; classN
closeDrawer,
showSubMenuActions,
setIsRawEventDialogOpen,
setIsReportDialogOpen,
isSmallScreen
})
@@ -70,6 +73,11 @@ export default function NoteOptions({ event, className }: { event: Event; classN
isOpen={isRawEventDialogOpen}
onClose={() => setIsRawEventDialogOpen(false)}
/>
<ReportDialog
event={event}
isOpen={isReportDialogOpen}
closeDialog={() => setIsReportDialogOpen(false)}
/>
</div>
)
}

View File

@@ -6,7 +6,18 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider'
import client from '@/services/client.service'
import { Bell, BellOff, Code, Copy, Link, Mail, SatelliteDish, Server, Trash2 } from 'lucide-react'
import {
Bell,
BellOff,
Code,
Copy,
Link,
Mail,
SatelliteDish,
Server,
Trash2,
TriangleAlert
} from 'lucide-react'
import { Event } from 'nostr-tools'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
@@ -34,6 +45,7 @@ interface UseMenuActionsProps {
closeDrawer: () => void
showSubMenuActions: (subMenu: SubMenuAction[], title: string) => void
setIsRawEventDialogOpen: (open: boolean) => void
setIsReportDialogOpen: (open: boolean) => void
isSmallScreen: boolean
}
@@ -42,6 +54,7 @@ export function useMenuActions({
closeDrawer,
showSubMenuActions,
setIsRawEventDialogOpen,
setIsReportDialogOpen,
isSmallScreen
}: UseMenuActionsProps) {
const { t } = useTranslation()
@@ -198,6 +211,19 @@ export function useMenuActions({
})
}
if (pubkey && event.pubkey !== pubkey) {
actions.push({
icon: TriangleAlert,
label: t('Report'),
className: 'text-destructive focus:text-destructive',
onClick: () => {
closeDrawer()
setIsReportDialogOpen(true)
},
separator: true
})
}
if (pubkey && event.pubkey !== pubkey) {
if (isMuted) {
actions.push({

View File

@@ -0,0 +1,36 @@
import * as React from 'react'
import * as RadioGroupPrimitive from '@radix-ui/react-radio-group'
import { Circle } from 'lucide-react'
import { cn } from '@/lib/utils'
const RadioGroup = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>
>(({ className, ...props }, ref) => {
return <RadioGroupPrimitive.Root className={cn('grid gap-2', className)} {...props} ref={ref} />
})
RadioGroup.displayName = RadioGroupPrimitive.Root.displayName
const RadioGroupItem = React.forwardRef<
React.ElementRef<typeof RadioGroupPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Item>
>(({ className, ...props }, ref) => {
return (
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
'aspect-square h-4 w-4 rounded-full border focus:outline-none focus-visible:ring-1 focus-visible:ring-foreground disabled:cursor-not-allowed disabled:opacity-50',
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-foreground" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
})
RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName
export { RadioGroup, RadioGroupItem }