feat: add nsfw toggle to post editor
This commit is contained in:
@@ -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 (
|
||||
<ImageGallery
|
||||
className="mt-2"
|
||||
key={index}
|
||||
images={allImages}
|
||||
isNsfw={isNsfwEvent(event)}
|
||||
start={start}
|
||||
end={end}
|
||||
/>
|
||||
<ImageGallery className="mt-2" key={index} images={allImages} start={start} end={end} />
|
||||
)
|
||||
}
|
||||
if (node.type === 'video') {
|
||||
return (
|
||||
<VideoPlayer className="mt-2" key={index} src={node.data} isNsfw={isNsfwEvent(event)} />
|
||||
)
|
||||
return <VideoPlayer className="mt-2" key={index} src={node.data} />
|
||||
}
|
||||
if (node.type === 'url') {
|
||||
return <EmbeddedNormalUrl url={node.data} key={index} />
|
||||
|
||||
@@ -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<CarouselApi>()
|
||||
const [currentIndex, setCurrentIndex] = useState(0)
|
||||
const [lightboxIndex, setLightboxIndex] = useState(-1)
|
||||
@@ -42,7 +35,7 @@ export function ImageCarousel({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative space-y-2">
|
||||
<div className="space-y-2">
|
||||
<Carousel className="w-full" setApi={setApi}>
|
||||
<CarouselContent className="xl:px-4">
|
||||
{images.map((image, index) => (
|
||||
@@ -78,7 +71,6 @@ export function ImageCarousel({
|
||||
}}
|
||||
styles={{ toolbar: { paddingTop: '2.25rem' } }}
|
||||
/>
|
||||
{isNsfw && <NsfwOverlay className="rounded-lg" />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
className={cn(
|
||||
'relative',
|
||||
displayImages.length === 1 ? 'w-fit max-w-full' : 'w-full',
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className={cn(displayImages.length === 1 ? 'w-fit max-w-full' : 'w-full', className)}>
|
||||
{imageContent}
|
||||
{index >= 0 &&
|
||||
createPortal(
|
||||
@@ -112,7 +103,6 @@ export default function ImageGallery({
|
||||
</div>,
|
||||
document.body
|
||||
)}
|
||||
{isNsfw && <NsfwOverlay className="rounded-lg" />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
23
src/components/Note/NsfwNote.tsx
Normal file
23
src/components/Note/NsfwNote.tsx
Normal file
@@ -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 (
|
||||
<div className="flex flex-col gap-2 items-center text-muted-foreground font-medium my-4">
|
||||
<div>{t('🔞 NSFW 🔞')}</div>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
show()
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
<Eye />
|
||||
{t('Temporarily display this note')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -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 = <UnknownNote className="mt-2" event={event} />
|
||||
} else if (isNsfwEvent(event) && !showNsfw) {
|
||||
content = <NsfwNote show={() => setShowNsfw(true)} />
|
||||
} else if (event.kind === kinds.Highlights) {
|
||||
content = <Highlight className="mt-2" event={event} />
|
||||
} else {
|
||||
content = <Content className="mt-2" event={event} />
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
@@ -90,13 +104,7 @@ export default function Note({
|
||||
/>
|
||||
)}
|
||||
<IValue event={event} className="mt-2" />
|
||||
{event.kind === kinds.Highlights ? (
|
||||
<Highlight className="mt-2" event={event} />
|
||||
) : isSupportedKind(event.kind) ? (
|
||||
<Content className="mt-2" event={event} />
|
||||
) : (
|
||||
<UnknownNote className="mt-2" event={event} />
|
||||
)}
|
||||
{content}
|
||||
{imageInfos.length > 0 && <ImageGallery images={imageInfos} />}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -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 && (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute top-0 left-0 backdrop-blur-3xl w-full h-full cursor-pointer',
|
||||
className
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setIsHidden(false)
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -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 (
|
||||
<div className={cn('text-wrap break-words whitespace-pre-wrap space-y-2', className)}>
|
||||
<ImageCarousel images={images} isNsfw={isNsfw} />
|
||||
<ImageCarousel images={images} />
|
||||
<div className="px-4">
|
||||
{nodes.map((node, index) => {
|
||||
if (node.type === 'text') {
|
||||
|
||||
@@ -37,6 +37,7 @@ export default function PostContent({
|
||||
const [addClientTag, setAddClientTag] = useState(false)
|
||||
const [specifiedRelayUrls, setSpecifiedRelayUrls] = useState<string[] | undefined>(undefined)
|
||||
const [mentions, setMentions] = useState<string[]>([])
|
||||
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}
|
||||
/>
|
||||
<div className="flex gap-2 items-center justify-around sm:hidden">
|
||||
<Button
|
||||
|
||||
@@ -7,11 +7,15 @@ import { useTranslation } from 'react-i18next'
|
||||
export default function PostOptions({
|
||||
show,
|
||||
addClientTag,
|
||||
setAddClientTag
|
||||
setAddClientTag,
|
||||
isNsfw,
|
||||
setIsNsfw
|
||||
}: {
|
||||
show: boolean
|
||||
addClientTag: boolean
|
||||
setAddClientTag: Dispatch<SetStateAction<boolean>>
|
||||
isNsfw: boolean
|
||||
setIsNsfw: Dispatch<SetStateAction<boolean>>
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
@@ -26,14 +30,29 @@ export default function PostOptions({
|
||||
window.localStorage.setItem(StorageKey.ADD_CLIENT_TAG, checked.toString())
|
||||
}
|
||||
|
||||
const onNsfwChange = (checked: boolean) => {
|
||||
setIsNsfw(checked)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="add-client-tag">{t('Add client tag')}</Label>
|
||||
<Switch id="add-client-tag" checked={addClientTag} onCheckedChange={onAddClientTagChange} />
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="add-client-tag">{t('Add client tag')}</Label>
|
||||
<Switch
|
||||
id="add-client-tag"
|
||||
checked={addClientTag}
|
||||
onCheckedChange={onAddClientTagChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{t('Show others this was sent via Jumble')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs">
|
||||
{t('Show others this was sent via Jumble')}
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="add-nsfw-tag">{t('NSFW')}</Label>
|
||||
<Switch id="add-nsfw-tag" checked={isNsfw} onCheckedChange={onNsfwChange} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,17 +2,8 @@ import { cn, isInViewport } from '@/lib/utils'
|
||||
import { useAutoplay } from '@/providers/AutoplayProvider'
|
||||
import videoManager from '@/services/video-manager.service'
|
||||
import { useEffect, useRef } from 'react'
|
||||
import NsfwOverlay from '../NsfwOverlay'
|
||||
|
||||
export default function VideoPlayer({
|
||||
src,
|
||||
className,
|
||||
isNsfw = false
|
||||
}: {
|
||||
src: string
|
||||
className?: string
|
||||
isNsfw?: boolean
|
||||
}) {
|
||||
export default function VideoPlayer({ src, className }: { src: string; className?: string }) {
|
||||
const { autoplay } = useAutoplay()
|
||||
const videoRef = useRef<HTMLVideoElement>(null)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
@@ -48,7 +39,7 @@ export default function VideoPlayer({
|
||||
}, [autoplay])
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="relative">
|
||||
<div ref={containerRef}>
|
||||
<video
|
||||
ref={videoRef}
|
||||
controls
|
||||
@@ -61,7 +52,6 @@ export default function VideoPlayer({
|
||||
}}
|
||||
muted
|
||||
/>
|
||||
{isNsfw && <NsfwOverlay className="rounded-lg" />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ export async function createShortTextNoteDraftEvent(
|
||||
parentEvent?: Event
|
||||
addClientTag?: boolean
|
||||
protectedEvent?: boolean
|
||||
isNsfw?: boolean
|
||||
} = {}
|
||||
): Promise<TDraftEvent> {
|
||||
const { quoteEventIds, rootETag, parentETag } = await extractRelatedEventIds(
|
||||
@@ -103,6 +104,10 @@ export async function createShortTextNoteDraftEvent(
|
||||
tags.push(['client', 'jumble'])
|
||||
}
|
||||
|
||||
if (options.isNsfw) {
|
||||
tags.push(['content-warning', 'NSFW'])
|
||||
}
|
||||
|
||||
if (options.protectedEvent) {
|
||||
tags.push(['-'])
|
||||
}
|
||||
@@ -182,6 +187,7 @@ export async function createCommentDraftEvent(
|
||||
options: {
|
||||
addClientTag?: boolean
|
||||
protectedEvent?: boolean
|
||||
isNsfw?: boolean
|
||||
} = {}
|
||||
): Promise<TDraftEvent> {
|
||||
const {
|
||||
@@ -241,6 +247,10 @@ export async function createCommentDraftEvent(
|
||||
tags.push(['client', 'jumble'])
|
||||
}
|
||||
|
||||
if (options.isNsfw) {
|
||||
tags.push(['content-warning', 'NSFW'])
|
||||
}
|
||||
|
||||
if (options.protectedEvent) {
|
||||
tags.push(['-'])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user