From 5df33837ab211ac9ff14ebec55f16980bd72d791 Mon Sep 17 00:00:00 2001 From: codytseng Date: Fri, 27 Jun 2025 22:55:21 +0800 Subject: [PATCH] feat: add nsfw toggle to post editor --- src/components/Content/index.tsx | 15 +++-------- src/components/ImageCarousel/index.tsx | 12 ++------- src/components/ImageGallery/index.tsx | 12 +-------- src/components/Note/NsfwNote.tsx | 23 ++++++++++++++++ src/components/Note/index.tsx | 24 +++++++++++------ src/components/NsfwOverlay/index.tsx | 21 --------------- src/components/PictureContent/index.tsx | 9 +++---- src/components/PostEditor/PostContent.tsx | 9 +++++-- src/components/PostEditor/PostOptions.tsx | 33 ++++++++++++++++++----- src/components/VideoPlayer/index.tsx | 14 ++-------- src/lib/draft-event.ts | 10 +++++++ 11 files changed, 94 insertions(+), 88 deletions(-) create mode 100644 src/components/Note/NsfwNote.tsx delete mode 100644 src/components/NsfwOverlay/index.tsx diff --git a/src/components/Content/index.tsx b/src/components/Content/index.tsx index 9cb94921..b9b01ca0 100644 --- a/src/components/Content/index.tsx +++ b/src/components/Content/index.tsx @@ -11,7 +11,7 @@ import { EmbeddedWebsocketUrlParser, parseContent } from '@/lib/content-parser' -import { extractEmojiInfosFromTags, isNsfwEvent } from '@/lib/event' +import { extractEmojiInfosFromTags } from '@/lib/event' import { extractImageInfoFromTag } from '@/lib/tag' import { cn } from '@/lib/utils' import mediaUpload from '@/services/media-upload.service' @@ -88,20 +88,11 @@ const Content = memo(({ event, className }: { event: Event; className?: string } const end = imageIndex + (Array.isArray(node.data) ? node.data.length : 1) imageIndex = end return ( - + ) } if (node.type === 'video') { - return ( - - ) + return } if (node.type === 'url') { return diff --git a/src/components/ImageCarousel/index.tsx b/src/components/ImageCarousel/index.tsx index a69123b1..b0331fcb 100644 --- a/src/components/ImageCarousel/index.tsx +++ b/src/components/ImageCarousel/index.tsx @@ -6,15 +6,8 @@ import { useEffect, useState } from 'react' import Lightbox from 'yet-another-react-lightbox' import Zoom from 'yet-another-react-lightbox/plugins/zoom' import Image from '../Image' -import NsfwOverlay from '../NsfwOverlay' -export function ImageCarousel({ - images, - isNsfw = false -}: { - images: TImageInfo[] - isNsfw?: boolean -}) { +export function ImageCarousel({ images }: { images: TImageInfo[] }) { const [api, setApi] = useState() const [currentIndex, setCurrentIndex] = useState(0) const [lightboxIndex, setLightboxIndex] = useState(-1) @@ -42,7 +35,7 @@ export function ImageCarousel({ } return ( -
+
{images.map((image, index) => ( @@ -78,7 +71,6 @@ export function ImageCarousel({ }} styles={{ toolbar: { paddingTop: '2.25rem' } }} /> - {isNsfw && }
) } diff --git a/src/components/ImageGallery/index.tsx b/src/components/ImageGallery/index.tsx index f2afb33b..8b252495 100644 --- a/src/components/ImageGallery/index.tsx +++ b/src/components/ImageGallery/index.tsx @@ -7,18 +7,15 @@ import { createPortal } from 'react-dom' import Lightbox from 'yet-another-react-lightbox' import Zoom from 'yet-another-react-lightbox/plugins/zoom' import Image from '../Image' -import NsfwOverlay from '../NsfwOverlay' export default function ImageGallery({ className, images, - isNsfw = false, start = 0, end = images.length }: { className?: string images: TImageInfo[] - isNsfw?: boolean start?: number end?: number }) { @@ -83,13 +80,7 @@ export default function ImageGallery({ } return ( -
+
{imageContent} {index >= 0 && createPortal( @@ -112,7 +103,6 @@ export default function ImageGallery({
, document.body )} - {isNsfw && }
) } diff --git a/src/components/Note/NsfwNote.tsx b/src/components/Note/NsfwNote.tsx new file mode 100644 index 00000000..a0cb0391 --- /dev/null +++ b/src/components/Note/NsfwNote.tsx @@ -0,0 +1,23 @@ +import { Button } from '@/components/ui/button' +import { Eye } from 'lucide-react' +import { useTranslation } from 'react-i18next' + +export default function NsfwNote({ show }: { show: () => void }) { + const { t } = useTranslation() + + return ( +
+
{t('🔞 NSFW 🔞')}
+ +
+ ) +} diff --git a/src/components/Note/index.tsx b/src/components/Note/index.tsx index efa34d21..8c3c950b 100644 --- a/src/components/Note/index.tsx +++ b/src/components/Note/index.tsx @@ -3,13 +3,14 @@ import { extractImageInfosFromEventTags, getParentEventId, getUsingClient, + isNsfwEvent, isPictureEvent, isSupportedKind } from '@/lib/event' import { toNote } from '@/lib/link' import { useScreenSize } from '@/providers/ScreenSizeProvider' import { Event, kinds } from 'nostr-tools' -import { useMemo } from 'react' +import { useMemo, useState } from 'react' import Content from '../Content' import { FormattedTimestamp } from '../FormattedTimestamp' import ImageGallery from '../ImageGallery' @@ -21,6 +22,7 @@ import UserAvatar from '../UserAvatar' import Username from '../Username' import Highlight from './Highlight' import IValue from './IValue' +import NsfwNote from './NsfwNote' import { UnknownNote } from './UnknownNote' export default function Note({ @@ -45,6 +47,18 @@ export default function Note({ [event] ) const usingClient = useMemo(() => getUsingClient(event), [event]) + const [showNsfw, setShowNsfw] = useState(false) + + let content: React.ReactNode + if (!isSupportedKind(event.kind)) { + content = + } else if (isNsfwEvent(event) && !showNsfw) { + content = setShowNsfw(true)} /> + } else if (event.kind === kinds.Highlights) { + content = + } else { + content = + } return (
@@ -90,13 +104,7 @@ export default function Note({ /> )} - {event.kind === kinds.Highlights ? ( - - ) : isSupportedKind(event.kind) ? ( - - ) : ( - - )} + {content} {imageInfos.length > 0 && }
) diff --git a/src/components/NsfwOverlay/index.tsx b/src/components/NsfwOverlay/index.tsx deleted file mode 100644 index 895d05de..00000000 --- a/src/components/NsfwOverlay/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { cn } from '@/lib/utils' -import { useState } from 'react' - -export default function NsfwOverlay({ className }: { className?: string }) { - const [isHidden, setIsHidden] = useState(true) - - return ( - isHidden && ( -
{ - e.stopPropagation() - setIsHidden(false) - }} - /> - ) - ) -} diff --git a/src/components/PictureContent/index.tsx b/src/components/PictureContent/index.tsx index 276ed3b8..36e63614 100644 --- a/src/components/PictureContent/index.tsx +++ b/src/components/PictureContent/index.tsx @@ -1,13 +1,13 @@ import { EmbeddedEmojiParser, - EmbeddedLNInvoiceParser, EmbeddedHashtagParser, + EmbeddedLNInvoiceParser, EmbeddedMentionParser, EmbeddedNormalUrlParser, EmbeddedWebsocketUrlParser, parseContent } from '@/lib/content-parser' -import { extractEmojiInfosFromTags, extractImageInfosFromEventTags, isNsfwEvent } from '@/lib/event' +import { extractEmojiInfosFromTags, extractImageInfosFromEventTags } from '@/lib/event' import { cn } from '@/lib/utils' import { Event } from 'nostr-tools' import { memo, useMemo } from 'react' @@ -18,12 +18,11 @@ import { EmbeddedNormalUrl, EmbeddedWebsocketUrl } from '../Embedded' -import { ImageCarousel } from '../ImageCarousel' import Emoji from '../Emoji' +import { ImageCarousel } from '../ImageCarousel' const PictureContent = memo(({ event, className }: { event: Event; className?: string }) => { const images = useMemo(() => extractImageInfosFromEventTags(event), [event]) - const isNsfw = isNsfwEvent(event) const nodes = parseContent(event.content, [ EmbeddedNormalUrlParser, @@ -38,7 +37,7 @@ const PictureContent = memo(({ event, className }: { event: Event; className?: s return (
- +
{nodes.map((node, index) => { if (node.type === 'text') { diff --git a/src/components/PostEditor/PostContent.tsx b/src/components/PostEditor/PostContent.tsx index a7c4adcd..77607107 100644 --- a/src/components/PostEditor/PostContent.tsx +++ b/src/components/PostEditor/PostContent.tsx @@ -37,6 +37,7 @@ export default function PostContent({ const [addClientTag, setAddClientTag] = useState(false) const [specifiedRelayUrls, setSpecifiedRelayUrls] = useState(undefined) const [mentions, setMentions] = useState([]) + const [isNsfw, setIsNsfw] = useState(false) const canPost = !!text && !posting && !uploadingFiles const post = async (e?: React.MouseEvent) => { @@ -50,12 +51,14 @@ export default function PostContent({ parentEvent && parentEvent.kind !== kinds.ShortTextNote ? await createCommentDraftEvent(text, parentEvent, mentions, { addClientTag, - protectedEvent: !!specifiedRelayUrls + protectedEvent: !!specifiedRelayUrls, + isNsfw }) : await createShortTextNoteDraftEvent(text, mentions, { parentEvent, addClientTag, - protectedEvent: !!specifiedRelayUrls + protectedEvent: !!specifiedRelayUrls, + isNsfw }) await publish(draftEvent, { specifiedRelayUrls }) postContentCache.clearPostCache({ defaultContent, parentEvent }) @@ -159,6 +162,8 @@ export default function PostContent({ show={showMoreOptions} addClientTag={addClientTag} setAddClientTag={setAddClientTag} + isNsfw={isNsfw} + setIsNsfw={setIsNsfw} />