feat: support sending 1984 reports
This commit is contained in:
129
src/components/NoteOptions/ReportDialog.tsx
Normal file
129
src/components/NoteOptions/ReportDialog.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
36
src/components/ui/radio-group.tsx
Normal file
36
src/components/ui/radio-group.tsx
Normal 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 }
|
||||
Reference in New Issue
Block a user