diff --git a/package-lock.json b/package-lock.json index f7d73faa..a48c429c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@radix-ui/react-hover-card": "^1.1.4", "@radix-ui/react-label": "^2.1.1", "@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-select": "^2.1.4", "@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": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz", diff --git a/package.json b/package.json index 09e04637..da15a3e9 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@radix-ui/react-hover-card": "^1.1.4", "@radix-ui/react-label": "^2.1.1", "@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-select": "^2.1.4", "@radix-ui/react-separator": "^1.1.1", diff --git a/src/components/NoteOptions/ReportDialog.tsx b/src/components/NoteOptions/ReportDialog.tsx new file mode 100644 index 00000000..ffd9a742 --- /dev/null +++ b/src/components/NoteOptions/ReportDialog.tsx @@ -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 ( + { + if (!open) { + closeDialog() + } + }} + > + + + + + +
+ +
+
+
+ ) + } + + return ( + { + if (!open) { + closeDialog() + } + }} + > + + + + + + + + + ) +} + +function ReportContent({ event, closeDialog }: { event: NostrEvent; closeDialog: () => void }) { + const { t } = useTranslation() + const { pubkey, publish } = useNostr() + const [reason, setReason] = useState(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 ( +
+ + {['nudity', 'malware', 'profanity', 'illegal', 'spam', 'other'].map((item) => ( +
+ + +
+ ))} +
+ +
+ ) +} diff --git a/src/components/NoteOptions/index.tsx b/src/components/NoteOptions/index.tsx index 3f5f7ea9..eb4b7a6b 100644 --- a/src/components/NoteOptions/index.tsx +++ b/src/components/NoteOptions/index.tsx @@ -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([]) @@ -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)} /> + setIsReportDialogOpen(false)} + /> ) } diff --git a/src/components/NoteOptions/useMenuActions.tsx b/src/components/NoteOptions/useMenuActions.tsx index c4b69573..82cfe583 100644 --- a/src/components/NoteOptions/useMenuActions.tsx +++ b/src/components/NoteOptions/useMenuActions.tsx @@ -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({ diff --git a/src/components/ui/radio-group.tsx b/src/components/ui/radio-group.tsx new file mode 100644 index 00000000..d40f9c90 --- /dev/null +++ b/src/components/ui/radio-group.tsx @@ -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, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return +}) +RadioGroup.displayName = RadioGroupPrimitive.Root.displayName + +const RadioGroupItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + return ( + + + + + + ) +}) +RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName + +export { RadioGroup, RadioGroupItem } diff --git a/src/i18n/locales/ar.ts b/src/i18n/locales/ar.ts index 75a12e47..15abbb0c 100644 --- a/src/i18n/locales/ar.ts +++ b/src/i18n/locales/ar.ts @@ -383,6 +383,15 @@ export default { 'reposted your note': 'أعاد نشر ملاحظتك', 'zapped your note': 'زاب ملاحظتك', 'zapped you': 'زابك', - 'Mark as read': 'تعليم كمقروء' + 'Mark as read': 'تعليم كمقروء', + Report: 'تبليغ', + 'Successfully report': 'تم التبليغ بنجاح', + 'Failed to report': 'فشل في التبليغ', + nudity: 'عُري', + malware: 'برامج ضارة', + profanity: 'ألفاظ نابية', + illegal: 'محتوى غير قانوني', + spam: 'رسائل مزعجة', + other: 'أخرى' } } diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index bfaface4..3e69c55d 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -392,6 +392,15 @@ export default { 'reposted your note': 'hat Ihre Notiz geteilt', 'zapped your note': 'hat Ihre Notiz 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' } } diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 17329316..c93b6f12 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -382,6 +382,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' + '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' } } diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index d6b34a65..d12e7ed3 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -388,6 +388,15 @@ export default { 'reposted your note': 'reposteó tu nota', 'zapped your note': 'zappeó tu nota', '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' } } diff --git a/src/i18n/locales/fa.ts b/src/i18n/locales/fa.ts index 323e8adc..5dac2991 100644 --- a/src/i18n/locales/fa.ts +++ b/src/i18n/locales/fa.ts @@ -384,6 +384,15 @@ export default { 'reposted your note': 'یادداشت شما را بازنشر کرد', 'zapped your note': 'یادداشت شما را زپ کرد', 'zapped you': 'شما را زپ کرد', - 'Mark as read': 'علامت‌گذاری به عنوان خوانده شده' + 'Mark as read': 'علامت‌گذاری به عنوان خوانده شده', + Report: 'گزارش', + 'Successfully report': 'گزارش با موفقیت ارسال شد', + 'Failed to report': 'ارسال گزارش ناموفق بود', + nudity: 'برهنگی', + malware: 'بدافزار', + profanity: 'فحاشی', + illegal: 'محتوای غیرقانونی', + spam: 'اسپم', + other: 'سایر' } } diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index 21ff91b5..84e904c9 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -392,6 +392,15 @@ export default { 'reposted your note': 'a repartagé votre note', 'zapped your note': 'a zappé votre note', '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' } } diff --git a/src/i18n/locales/it.ts b/src/i18n/locales/it.ts index 31bbaf86..e651c159 100644 --- a/src/i18n/locales/it.ts +++ b/src/i18n/locales/it.ts @@ -388,6 +388,15 @@ export default { 'reposted your note': 'ha ricondiviso la tua nota', 'zapped your note': 'ha zappato la tua nota', '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' } } diff --git a/src/i18n/locales/ja.ts b/src/i18n/locales/ja.ts index bd36ec1d..a6b1df5b 100644 --- a/src/i18n/locales/ja.ts +++ b/src/i18n/locales/ja.ts @@ -385,6 +385,15 @@ export default { 'reposted your note': 'あなたのノートをリポストしました', 'zapped your note': 'あなたのノートにザップしました', 'zapped you': 'あなたにザップしました', - 'Mark as read': '既読にする' + 'Mark as read': '既読にする', + Report: '報告', + 'Successfully report': '報告が成功しました', + 'Failed to report': '報告に失敗しました', + nudity: 'ヌード', + malware: 'マルウェア', + profanity: '冒涜的な内容', + illegal: '違法コンテンツ', + spam: 'スパム', + other: 'その他' } } diff --git a/src/i18n/locales/ko.ts b/src/i18n/locales/ko.ts index 83f21785..66be0aa2 100644 --- a/src/i18n/locales/ko.ts +++ b/src/i18n/locales/ko.ts @@ -385,6 +385,15 @@ export default { 'reposted your note': '당신의 노트를 리포스트했습니다', 'zapped your note': '당신의 노트를 잽했습니다', 'zapped you': '당신을 잽했습니다', - 'Mark as read': '읽음으로 표시' + 'Mark as read': '읽음으로 표시', + Report: '신고', + 'Successfully report': '신고가 성공적으로 완료되었습니다', + 'Failed to report': '신고에 실패했습니다', + nudity: '음란물', + malware: '악성 소프트웨어', + profanity: '욕설', + illegal: '불법 콘텐츠', + spam: '스팸', + other: '기타' } } diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts index b70e3114..1b044851 100644 --- a/src/i18n/locales/pl.ts +++ b/src/i18n/locales/pl.ts @@ -389,6 +389,15 @@ export default { 'reposted your note': 'przepostował twoją notatkę', 'zapped your note': 'zappował twoją notatkę', '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' } } diff --git a/src/i18n/locales/pt-BR.ts b/src/i18n/locales/pt-BR.ts index 779dd836..07b84b58 100644 --- a/src/i18n/locales/pt-BR.ts +++ b/src/i18n/locales/pt-BR.ts @@ -385,6 +385,15 @@ export default { 'reposted your note': 'republicou sua nota', 'zapped your note': 'zappeou sua nota', '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' } } diff --git a/src/i18n/locales/pt-PT.ts b/src/i18n/locales/pt-PT.ts index ecc11b60..d8835c9a 100644 --- a/src/i18n/locales/pt-PT.ts +++ b/src/i18n/locales/pt-PT.ts @@ -388,6 +388,15 @@ export default { 'reposted your note': 'republicou a sua nota', 'zapped your note': 'zappeou a sua nota', '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' } } diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index 86ebf29f..8148036b 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -389,6 +389,15 @@ export default { 'reposted your note': 'репостнул вашу заметку', 'zapped your note': 'заппил вашу заметку', 'zapped you': 'заппил вас', - 'Mark as read': 'Отметить как прочитанное' + 'Mark as read': 'Отметить как прочитанное', + Report: 'Пожаловаться', + 'Successfully report': 'Жалоба успешно отправлена', + 'Failed to report': 'Не удалось отправить жалобу', + nudity: 'Обнаженность', + malware: 'Вредоносное ПО', + profanity: 'Ненормативная лексика', + illegal: 'Незаконный контент', + spam: 'Спам', + other: 'Другое' } } diff --git a/src/i18n/locales/th.ts b/src/i18n/locales/th.ts index 59b1d92f..9fb50fbb 100644 --- a/src/i18n/locales/th.ts +++ b/src/i18n/locales/th.ts @@ -380,6 +380,15 @@ export default { 'reposted your note': 'ได้รีโพสต์โน้ตของคุณ', 'zapped your note': 'ได้แซปโน้ตของคุณ', 'zapped you': 'ได้แซปคุณ', - 'Mark as read': 'ทำเครื่องหมายว่าอ่านแล้ว' + 'Mark as read': 'ทำเครื่องหมายว่าอ่านแล้ว', + Report: 'รายงาน', + 'Successfully report': 'รายงานสำเร็จ', + 'Failed to report': 'การรายงานล้มเหลว', + nudity: 'ภาพลามก', + malware: 'มัลแวร์', + profanity: 'คำหยาบคาย', + illegal: 'เนื้อหาผิดกฎหมาย', + spam: 'สแปม', + other: 'อื่นๆ' } } diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index 4371bda2..0c8d676f 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -378,6 +378,15 @@ export default { 'reposted your note': '转发了您的笔记', 'zapped your note': '打闪了您的笔记', 'zapped you': '给您打闪', - 'Mark as read': '标记为已读' + 'Mark as read': '标记为已读', + Report: '举报', + 'Successfully report': '举报成功', + 'Failed to report': '举报失败', + nudity: '色情内容', + malware: '恶意软件', + profanity: '亵渎言论', + illegal: '违法内容', + spam: '垃圾信息', + other: '其他' } } diff --git a/src/lib/draft-event.ts b/src/lib/draft-event.ts index d9fcc6c9..d4854862 100644 --- a/src/lib/draft-event.ts +++ b/src/lib/draft-event.ts @@ -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[]) { return imageUrls .map((imageUrl) => { diff --git a/src/services/client.service.ts b/src/services/client.service.ts index e1a25a26..699ec3cd 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -7,7 +7,7 @@ import { } from '@/lib/event' import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata' 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 { isSafari } from '@/lib/utils' import { ISigner, TProfile, TPublishOptions, TRelayList, TSubRequestFilter } from '@/types' @@ -87,6 +87,13 @@ class ClientService extends EventTarget { event: NEvent, { 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 ?? [] if (!specifiedRelayUrls?.length && ![kinds.Contacts, kinds.Mutelist].includes(event.kind)) { const mentions: string[] = []