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

306
package-lock.json generated
View File

@@ -23,6 +23,7 @@
"@radix-ui/react-hover-card": "^1.1.4", "@radix-ui/react-hover-card": "^1.1.4",
"@radix-ui/react-label": "^2.1.1", "@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-popover": "^1.1.4", "@radix-ui/react-popover": "^1.1.4",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "1.2.0", "@radix-ui/react-scroll-area": "1.2.0",
"@radix-ui/react-select": "^2.1.4", "@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-separator": "^1.1.1",
@@ -3288,6 +3289,311 @@
} }
} }
}, },
"node_modules/@radix-ui/react-radio-group": {
"version": "1.3.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.3.8.tgz",
"integrity": "sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-roving-focus": "1.1.11",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-previous": "1.1.1",
"@radix-ui/react-use-size": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-collection": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
"integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-presence": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
"integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-effect-event": "0.0.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-use-previous": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
"integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-use-size": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
"integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-roving-focus": { "node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz",

View File

@@ -33,6 +33,7 @@
"@radix-ui/react-hover-card": "^1.1.4", "@radix-ui/react-hover-card": "^1.1.4",
"@radix-ui/react-label": "^2.1.1", "@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-popover": "^1.1.4", "@radix-ui/react-popover": "^1.1.4",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-scroll-area": "1.2.0", "@radix-ui/react-scroll-area": "1.2.0",
"@radix-ui/react-select": "^2.1.4", "@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-separator": "^1.1.1",

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

View File

@@ -6,7 +6,18 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useMuteList } from '@/providers/MuteListProvider' import { useMuteList } from '@/providers/MuteListProvider'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import client from '@/services/client.service' 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 { Event } from 'nostr-tools'
import { useMemo } from 'react' import { useMemo } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@@ -34,6 +45,7 @@ interface UseMenuActionsProps {
closeDrawer: () => void closeDrawer: () => void
showSubMenuActions: (subMenu: SubMenuAction[], title: string) => void showSubMenuActions: (subMenu: SubMenuAction[], title: string) => void
setIsRawEventDialogOpen: (open: boolean) => void setIsRawEventDialogOpen: (open: boolean) => void
setIsReportDialogOpen: (open: boolean) => void
isSmallScreen: boolean isSmallScreen: boolean
} }
@@ -42,6 +54,7 @@ export function useMenuActions({
closeDrawer, closeDrawer,
showSubMenuActions, showSubMenuActions,
setIsRawEventDialogOpen, setIsRawEventDialogOpen,
setIsReportDialogOpen,
isSmallScreen isSmallScreen
}: UseMenuActionsProps) { }: UseMenuActionsProps) {
const { t } = useTranslation() 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 (pubkey && event.pubkey !== pubkey) {
if (isMuted) { if (isMuted) {
actions.push({ 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 }

View File

@@ -383,6 +383,15 @@ export default {
'reposted your note': 'أعاد نشر ملاحظتك', 'reposted your note': 'أعاد نشر ملاحظتك',
'zapped your note': 'زاب ملاحظتك', 'zapped your note': 'زاب ملاحظتك',
'zapped you': 'زابك', 'zapped you': 'زابك',
'Mark as read': 'تعليم كمقروء' 'Mark as read': 'تعليم كمقروء',
Report: 'تبليغ',
'Successfully report': 'تم التبليغ بنجاح',
'Failed to report': 'فشل في التبليغ',
nudity: 'عُري',
malware: 'برامج ضارة',
profanity: 'ألفاظ نابية',
illegal: 'محتوى غير قانوني',
spam: 'رسائل مزعجة',
other: 'أخرى'
} }
} }

View File

@@ -392,6 +392,15 @@ export default {
'reposted your note': 'hat Ihre Notiz geteilt', 'reposted your note': 'hat Ihre Notiz geteilt',
'zapped your note': 'hat Ihre Notiz gezappt', 'zapped your note': 'hat Ihre Notiz gezappt',
'zapped you': 'hat Sie gezappt', 'zapped you': 'hat Sie gezappt',
'Mark as read': 'Als gelesen markieren' 'Mark as read': 'Als gelesen markieren',
Report: 'Melden',
'Successfully report': 'Erfolgreich gemeldet',
'Failed to report': 'Meldung fehlgeschlagen',
nudity: 'Nacktheit',
malware: 'Schadsoftware',
profanity: 'Obszönität',
illegal: 'Illegaler Inhalt',
spam: 'Spam',
other: 'Sonstiges'
} }
} }

View File

@@ -382,6 +382,15 @@ export default {
'reposted your note': 'reposted your note', 'reposted your note': 'reposted your note',
'zapped your note': 'zapped your note', 'zapped your note': 'zapped your note',
'zapped you': 'zapped you', 'zapped you': 'zapped you',
'Mark as read': 'Mark as read' 'Mark as read': 'Mark as read',
Report: 'Report',
'Successfully report': 'Successfully reported',
'Failed to report': 'Failed to report',
nudity: 'Nudity',
malware: 'Malware',
profanity: 'Profanity',
illegal: 'Illegal content',
spam: 'Spam',
other: 'Other'
} }
} }

View File

@@ -388,6 +388,15 @@ export default {
'reposted your note': 'reposteó tu nota', 'reposted your note': 'reposteó tu nota',
'zapped your note': 'zappeó tu nota', 'zapped your note': 'zappeó tu nota',
'zapped you': 'te zappeó', 'zapped you': 'te zappeó',
'Mark as read': 'Marcar como leído' 'Mark as read': 'Marcar como leído',
Report: 'Reportar',
'Successfully report': 'Reporte exitoso',
'Failed to report': 'Fallo al reportar',
nudity: 'Desnudez',
malware: 'Software malicioso',
profanity: 'Blasfemia',
illegal: 'Contenido ilegal',
spam: 'Spam',
other: 'Otro'
} }
} }

View File

@@ -384,6 +384,15 @@ export default {
'reposted your note': 'یادداشت شما را بازنشر کرد', 'reposted your note': 'یادداشت شما را بازنشر کرد',
'zapped your note': 'یادداشت شما را زپ کرد', 'zapped your note': 'یادداشت شما را زپ کرد',
'zapped you': 'شما را زپ کرد', 'zapped you': 'شما را زپ کرد',
'Mark as read': 'علامت‌گذاری به عنوان خوانده شده' 'Mark as read': 'علامت‌گذاری به عنوان خوانده شده',
Report: 'گزارش',
'Successfully report': 'گزارش با موفقیت ارسال شد',
'Failed to report': 'ارسال گزارش ناموفق بود',
nudity: 'برهنگی',
malware: 'بدافزار',
profanity: 'فحاشی',
illegal: 'محتوای غیرقانونی',
spam: 'اسپم',
other: 'سایر'
} }
} }

View File

@@ -392,6 +392,15 @@ export default {
'reposted your note': 'a repartagé votre note', 'reposted your note': 'a repartagé votre note',
'zapped your note': 'a zappé votre note', 'zapped your note': 'a zappé votre note',
'zapped you': 'vous a zappé', 'zapped you': 'vous a zappé',
'Mark as read': 'Marquer comme lu' 'Mark as read': 'Marquer comme lu',
Report: 'Signaler',
'Successfully report': 'Signalement réussi',
'Failed to report': 'Échec du signalement',
nudity: 'Nudité',
malware: 'Logiciel malveillant',
profanity: 'Blasphème',
illegal: 'Contenu illégal',
spam: 'Spam',
other: 'Autre'
} }
} }

View File

@@ -388,6 +388,15 @@ export default {
'reposted your note': 'ha ricondiviso la tua nota', 'reposted your note': 'ha ricondiviso la tua nota',
'zapped your note': 'ha zappato la tua nota', 'zapped your note': 'ha zappato la tua nota',
'zapped you': 'ti ha zappato', 'zapped you': 'ti ha zappato',
'Mark as read': 'Segna come letto' 'Mark as read': 'Segna come letto',
Report: 'Segnala',
'Successfully report': 'Segnalazione riuscita',
'Failed to report': 'Segnalazione fallita',
nudity: 'Nudità',
malware: 'Malware',
profanity: 'Blasfemia',
illegal: 'Contenuto illegale',
spam: 'Spam',
other: 'Altro'
} }
} }

View File

@@ -385,6 +385,15 @@ export default {
'reposted your note': 'あなたのノートをリポストしました', 'reposted your note': 'あなたのノートをリポストしました',
'zapped your note': 'あなたのノートにザップしました', 'zapped your note': 'あなたのノートにザップしました',
'zapped you': 'あなたにザップしました', 'zapped you': 'あなたにザップしました',
'Mark as read': '既読にする' 'Mark as read': '既読にする',
Report: '報告',
'Successfully report': '報告が成功しました',
'Failed to report': '報告に失敗しました',
nudity: 'ヌード',
malware: 'マルウェア',
profanity: '冒涜的な内容',
illegal: '違法コンテンツ',
spam: 'スパム',
other: 'その他'
} }
} }

View File

@@ -385,6 +385,15 @@ export default {
'reposted your note': '당신의 노트를 리포스트했습니다', 'reposted your note': '당신의 노트를 리포스트했습니다',
'zapped your note': '당신의 노트를 잽했습니다', 'zapped your note': '당신의 노트를 잽했습니다',
'zapped you': '당신을 잽했습니다', 'zapped you': '당신을 잽했습니다',
'Mark as read': '읽음으로 표시' 'Mark as read': '읽음으로 표시',
Report: '신고',
'Successfully report': '신고가 성공적으로 완료되었습니다',
'Failed to report': '신고에 실패했습니다',
nudity: '음란물',
malware: '악성 소프트웨어',
profanity: '욕설',
illegal: '불법 콘텐츠',
spam: '스팸',
other: '기타'
} }
} }

View File

@@ -389,6 +389,15 @@ export default {
'reposted your note': 'przepostował twoją notatkę', 'reposted your note': 'przepostował twoją notatkę',
'zapped your note': 'zappował twoją notatkę', 'zapped your note': 'zappował twoją notatkę',
'zapped you': 'zappował cię', 'zapped you': 'zappował cię',
'Mark as read': 'Oznacz jako przeczytane' 'Mark as read': 'Oznacz jako przeczytane',
Report: 'Zgłoś',
'Successfully report': 'Pomyślnie zgłoszono',
'Failed to report': 'Nie udało się zgłosić',
nudity: 'Nagość',
malware: 'Złośliwe oprogramowanie',
profanity: 'Wulgaryzmy',
illegal: 'Nielegalna treść',
spam: 'Spam',
other: 'Inne'
} }
} }

View File

@@ -385,6 +385,15 @@ export default {
'reposted your note': 'republicou sua nota', 'reposted your note': 'republicou sua nota',
'zapped your note': 'zappeou sua nota', 'zapped your note': 'zappeou sua nota',
'zapped you': 'zappeou você', 'zapped you': 'zappeou você',
'Mark as read': 'Marcar como lida' 'Mark as read': 'Marcar como lida',
Report: 'Denunciar',
'Successfully report': 'Denúncia enviada com sucesso',
'Failed to report': 'Falha ao enviar denúncia',
nudity: 'Nudez',
malware: 'Malware',
profanity: 'Blasfêmia',
illegal: 'Conteúdo ilegal',
spam: 'Spam',
other: 'Outro'
} }
} }

View File

@@ -388,6 +388,15 @@ export default {
'reposted your note': 'republicou a sua nota', 'reposted your note': 'republicou a sua nota',
'zapped your note': 'zappeou a sua nota', 'zapped your note': 'zappeou a sua nota',
'zapped you': 'zappeou-o', 'zapped you': 'zappeou-o',
'Mark as read': 'Marcar como lida' 'Mark as read': 'Marcar como lida',
Report: 'Denunciar',
'Successfully report': 'Denúncia enviada com sucesso',
'Failed to report': 'Falha ao enviar denúncia',
nudity: 'Nudez',
malware: 'Malware',
profanity: 'Blasfémia',
illegal: 'Conteúdo ilegal',
spam: 'Spam',
other: 'Outro'
} }
} }

View File

@@ -389,6 +389,15 @@ export default {
'reposted your note': 'репостнул вашу заметку', 'reposted your note': 'репостнул вашу заметку',
'zapped your note': 'заппил вашу заметку', 'zapped your note': 'заппил вашу заметку',
'zapped you': 'заппил вас', 'zapped you': 'заппил вас',
'Mark as read': 'Отметить как прочитанное' 'Mark as read': 'Отметить как прочитанное',
Report: 'Пожаловаться',
'Successfully report': 'Жалоба успешно отправлена',
'Failed to report': 'Не удалось отправить жалобу',
nudity: 'Обнаженность',
malware: 'Вредоносное ПО',
profanity: 'Ненормативная лексика',
illegal: 'Незаконный контент',
spam: 'Спам',
other: 'Другое'
} }
} }

View File

@@ -380,6 +380,15 @@ export default {
'reposted your note': 'ได้รีโพสต์โน้ตของคุณ', 'reposted your note': 'ได้รีโพสต์โน้ตของคุณ',
'zapped your note': 'ได้แซปโน้ตของคุณ', 'zapped your note': 'ได้แซปโน้ตของคุณ',
'zapped you': 'ได้แซปคุณ', 'zapped you': 'ได้แซปคุณ',
'Mark as read': 'ทำเครื่องหมายว่าอ่านแล้ว' 'Mark as read': 'ทำเครื่องหมายว่าอ่านแล้ว',
Report: 'รายงาน',
'Successfully report': 'รายงานสำเร็จ',
'Failed to report': 'การรายงานล้มเหลว',
nudity: 'ภาพลามก',
malware: 'มัลแวร์',
profanity: 'คำหยาบคาย',
illegal: 'เนื้อหาผิดกฎหมาย',
spam: 'สแปม',
other: 'อื่นๆ'
} }
} }

View File

@@ -378,6 +378,15 @@ export default {
'reposted your note': '转发了您的笔记', 'reposted your note': '转发了您的笔记',
'zapped your note': '打闪了您的笔记', 'zapped your note': '打闪了您的笔记',
'zapped you': '给您打闪', 'zapped you': '给您打闪',
'Mark as read': '标记为已读' 'Mark as read': '标记为已读',
Report: '举报',
'Successfully report': '举报成功',
'Failed to report': '举报失败',
nudity: '色情内容',
malware: '恶意软件',
profanity: '亵渎言论',
illegal: '违法内容',
spam: '垃圾信息',
other: '其他'
} }
} }

View File

@@ -432,6 +432,26 @@ export function createDeletionRequestDraftEvent(event: Event): TDraftEvent {
} }
} }
export function createReportDraftEvent(event: Event, reason: string): TDraftEvent {
const tags: string[][] = []
if (event.kind === kinds.Metadata) {
tags.push(['p', event.pubkey, reason])
} else {
tags.push(['p', event.pubkey])
tags.push(['e', event.id, reason])
if (isReplaceableEvent(event.kind)) {
tags.push(['a', getReplaceableCoordinateFromEvent(event), reason])
}
}
return {
kind: kinds.Report,
content: '',
tags,
created_at: dayjs().unix()
}
}
function generateImetaTags(imageUrls: string[]) { function generateImetaTags(imageUrls: string[]) {
return imageUrls return imageUrls
.map((imageUrl) => { .map((imageUrl) => {

View File

@@ -7,7 +7,7 @@ import {
} from '@/lib/event' } from '@/lib/event'
import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata' import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata'
import { formatPubkey, isValidPubkey, pubkeyToNpub, userIdToPubkey } from '@/lib/pubkey' import { formatPubkey, isValidPubkey, pubkeyToNpub, userIdToPubkey } from '@/lib/pubkey'
import { getPubkeysFromPTags, getServersFromServerTags } from '@/lib/tag' import { getPubkeysFromPTags, getServersFromServerTags, tagNameEquals } from '@/lib/tag'
import { isLocalNetworkUrl, isWebsocketUrl, normalizeUrl } from '@/lib/url' import { isLocalNetworkUrl, isWebsocketUrl, normalizeUrl } from '@/lib/url'
import { isSafari } from '@/lib/utils' import { isSafari } from '@/lib/utils'
import { ISigner, TProfile, TPublishOptions, TRelayList, TSubRequestFilter } from '@/types' import { ISigner, TProfile, TPublishOptions, TRelayList, TSubRequestFilter } from '@/types'
@@ -87,6 +87,13 @@ class ClientService extends EventTarget {
event: NEvent, event: NEvent,
{ specifiedRelayUrls, additionalRelayUrls }: TPublishOptions = {} { specifiedRelayUrls, additionalRelayUrls }: TPublishOptions = {}
) { ) {
if (event.kind === kinds.Report) {
const targetEventId = event.tags.find(tagNameEquals('e'))?.[1]
if (targetEventId) {
return this.getSeenEventRelayUrls(targetEventId)
}
}
const _additionalRelayUrls: string[] = additionalRelayUrls ?? [] const _additionalRelayUrls: string[] = additionalRelayUrls ?? []
if (!specifiedRelayUrls?.length && ![kinds.Contacts, kinds.Mutelist].includes(event.kind)) { if (!specifiedRelayUrls?.length && ![kinds.Contacts, kinds.Mutelist].includes(event.kind)) {
const mentions: string[] = [] const mentions: string[] = []