mirror of
https://github.com/lumina-rocks/lumina.git
synced 2026-04-08 22:46:49 +02:00
Feature: nsfw toggle (#130)
* feat: add sensitive content toggle in TrendingImageNew component and utility function for nsfw tag check * feat: add sensitive content toggle and nsfw content check in KIND20Card and QuickViewKind20NoteCard components * fix: improve styling and text for sensitive content button and message in QuickViewKind20NoteCard component * fix: remove unused EyeOff import in TrendingImageNew component * refactor: replace inline nsfw tag check with utility function in TrendingImageNew component --------- Co-authored-by: highperfocused <highperfocused@pm.me>
This commit is contained in:
@@ -14,7 +14,9 @@ import ZapButton from "./ZapButton"
|
||||
import Image from "next/image"
|
||||
import CardOptionsDropdown from "./CardOptionsDropdown"
|
||||
import { renderTextWithLinkedTags } from "@/utils/textUtils"
|
||||
import { getProxiedImageUrl } from "@/utils/utils"
|
||||
import { getProxiedImageUrl, hasNsfwContent } from "@/utils/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Eye } from "lucide-react"
|
||||
|
||||
// Function to extract all images from a kind 20 event's imeta tags
|
||||
const extractImagesFromEvent = (tags: string[][]): string[] => {
|
||||
@@ -54,8 +56,12 @@ const KIND20Card: React.FC<KIND20CardProps> = ({
|
||||
const [currentImage, setCurrentImage] = useState(0);
|
||||
const [imageErrors, setImageErrors] = useState<Record<string, boolean>>({});
|
||||
const [imagesWithoutProxy, setImagesWithoutProxy] = useState<Record<string, boolean>>({});
|
||||
const [showSensitiveContent, setShowSensitiveContent] = useState(false);
|
||||
const [api, setApi] = useState<any>(null);
|
||||
|
||||
// Check if the event has nsfw content
|
||||
const isNsfwContent = hasNsfwContent(tags);
|
||||
|
||||
// Extract all images from imeta tags
|
||||
const imetaImages = extractImagesFromEvent(tags);
|
||||
|
||||
@@ -82,6 +88,13 @@ const KIND20Card: React.FC<KIND20CardProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
// Toggle sensitive content visibility
|
||||
const toggleSensitiveContent = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setShowSensitiveContent(true);
|
||||
};
|
||||
|
||||
// Update current image index when carousel slides
|
||||
useEffect(() => {
|
||||
if (!api) return;
|
||||
@@ -142,47 +155,67 @@ const KIND20Card: React.FC<KIND20CardProps> = ({
|
||||
<CardContent className="p-0">
|
||||
<div className="w-full">
|
||||
{validImages.length > 0 && (
|
||||
<Carousel
|
||||
className="w-full"
|
||||
setApi={setApi}
|
||||
>
|
||||
<CarouselContent>
|
||||
{validImages.map((imageUrl, index) => {
|
||||
const shouldUseProxy = useImgProxy && !imagesWithoutProxy[imageUrl];
|
||||
const image = shouldUseProxy ? getProxiedImageUrl(imageUrl, 1200, 0) : imageUrl;
|
||||
return (
|
||||
<CarouselItem key={`${imageUrl}-${index}`}>
|
||||
<div className="w-full flex justify-center">
|
||||
<div className="relative w-full h-auto min-h-[300px] max-h-[80vh] flex justify-center">
|
||||
<img
|
||||
src={image}
|
||||
alt={text}
|
||||
className="rounded-lg w-full h-auto object-contain"
|
||||
onError={() => handleImageError(imageUrl)}
|
||||
loading="lazy"
|
||||
style={{
|
||||
maxHeight: "80vh",
|
||||
margin: "auto"
|
||||
}}
|
||||
/>
|
||||
<div className="relative">
|
||||
<Carousel
|
||||
className="w-full"
|
||||
setApi={setApi}
|
||||
>
|
||||
<CarouselContent>
|
||||
{validImages.map((imageUrl, index) => {
|
||||
const shouldUseProxy = useImgProxy && !imagesWithoutProxy[imageUrl];
|
||||
const image = shouldUseProxy ? getProxiedImageUrl(imageUrl, 1200, 0) : imageUrl;
|
||||
return (
|
||||
<CarouselItem key={`${imageUrl}-${index}`}>
|
||||
<div className="w-full flex justify-center">
|
||||
<div className="relative w-full h-auto min-h-[300px] max-h-[80vh] flex justify-center">
|
||||
<img
|
||||
src={image}
|
||||
alt={text}
|
||||
className={`rounded-lg w-full h-auto object-contain ${isNsfwContent && !showSensitiveContent ? 'blur-xl' : ''}`}
|
||||
onError={() => handleImageError(imageUrl)}
|
||||
loading="lazy"
|
||||
style={{
|
||||
maxHeight: "80vh",
|
||||
margin: "auto"
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CarouselItem>
|
||||
);
|
||||
})}
|
||||
</CarouselContent>
|
||||
{validImages.length > 1 && !isNsfwContent && (
|
||||
<>
|
||||
<CarouselPrevious className="left-2" />
|
||||
<CarouselNext className="right-2" />
|
||||
<div className="absolute bottom-4 left-0 right-0 flex justify-center">
|
||||
<div className="bg-black bg-opacity-50 text-white px-3 py-1 rounded-full text-sm">
|
||||
{`${currentImage + 1} / ${validImages.length}`}
|
||||
</div>
|
||||
</CarouselItem>
|
||||
);
|
||||
})}
|
||||
</CarouselContent>
|
||||
{validImages.length > 1 && (
|
||||
<>
|
||||
<CarouselPrevious className="left-2" />
|
||||
<CarouselNext className="right-2" />
|
||||
<div className="absolute bottom-4 left-0 right-0 flex justify-center">
|
||||
<div className="bg-black bg-opacity-50 text-white px-3 py-1 rounded-full text-sm">
|
||||
{`${currentImage + 1} / ${validImages.length}`}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
</>
|
||||
)}
|
||||
</Carousel>
|
||||
|
||||
{isNsfwContent && !showSensitiveContent && (
|
||||
<div
|
||||
className="absolute inset-0 flex flex-col items-center justify-center z-10"
|
||||
onClick={toggleSensitiveContent}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="bg-black bg-opacity-50 hover:bg-opacity-70 text-white px-4 py-2 rounded-md"
|
||||
onClick={toggleSensitiveContent}
|
||||
>
|
||||
<Eye className="h-4 w-4 mr-2" /> Show Sensitive Content
|
||||
</Button>
|
||||
<p className="mt-2 text-white text-sm bg-black bg-opacity-50 p-2 rounded">
|
||||
This image may contain sensitive content
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Carousel>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="p-4">
|
||||
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
} from "@/components/ui/card"
|
||||
import Link from 'next/link';
|
||||
import Image from 'next/image';
|
||||
import { extractDimensions, getProxiedImageUrl } from '@/utils/utils';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Eye } from 'lucide-react';
|
||||
import { extractDimensions, getProxiedImageUrl, hasNsfwContent } from '@/utils/utils';
|
||||
|
||||
interface QuickViewKind20NoteCardProps {
|
||||
pubkey: string;
|
||||
@@ -27,6 +29,10 @@ const QuickViewKind20NoteCard: React.FC<QuickViewKind20NoteCardProps> = ({ pubke
|
||||
});
|
||||
const [imageError, setImageError] = useState(false);
|
||||
const [tryWithoutProxy, setTryWithoutProxy] = useState(false);
|
||||
const [showSensitiveContent, setShowSensitiveContent] = useState(false);
|
||||
|
||||
// Check if the event has nsfw content
|
||||
const isNsfwContent = hasNsfwContent(tags);
|
||||
|
||||
if (!image || !image.startsWith("http")) return null;
|
||||
if (imageError && tryWithoutProxy) return null;
|
||||
@@ -40,6 +46,13 @@ const QuickViewKind20NoteCard: React.FC<QuickViewKind20NoteCardProps> = ({ pubke
|
||||
|
||||
const { width, height } = extractDimensions(event);
|
||||
|
||||
// Toggle sensitive content visibility
|
||||
const toggleSensitiveContent = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setShowSensitiveContent(true);
|
||||
};
|
||||
|
||||
const card = (
|
||||
<Card className="aspect-square overflow-hidden">
|
||||
<SmallCardContent className="h-full p-0">
|
||||
@@ -48,7 +61,7 @@ const QuickViewKind20NoteCard: React.FC<QuickViewKind20NoteCardProps> = ({ pubke
|
||||
<img
|
||||
src={image || "/placeholder.svg"}
|
||||
alt={text}
|
||||
className='w-full h-full rounded lg:rounded-lg object-cover'
|
||||
className={`w-full h-full rounded lg:rounded-lg object-cover ${isNsfwContent && !showSensitiveContent ? 'blur-xl' : ''}`}
|
||||
loading="lazy"
|
||||
onError={() => {
|
||||
if (tryWithoutProxy) {
|
||||
@@ -59,6 +72,23 @@ const QuickViewKind20NoteCard: React.FC<QuickViewKind20NoteCardProps> = ({ pubke
|
||||
}}
|
||||
style={{ objectPosition: 'center' }}
|
||||
/>
|
||||
{isNsfwContent && !showSensitiveContent && (
|
||||
<div
|
||||
className="absolute inset-0 flex flex-col items-center justify-center"
|
||||
onClick={toggleSensitiveContent}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="bg-black bg-opacity-50 hover:bg-opacity-70 text-white px-2 py-1 sm:px-4 sm:py-2 rounded-md text-xs sm:text-sm"
|
||||
onClick={toggleSensitiveContent}
|
||||
>
|
||||
<Eye className="h-3 w-3 sm:h-4 sm:w-4 mr-1 sm:mr-2" /> Show
|
||||
</Button>
|
||||
<p className="mt-1 sm:mt-2 text-white text-xs sm:text-sm bg-black bg-opacity-50 p-1 sm:p-2 rounded max-w-[80%] text-center">
|
||||
Sensitive Content
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SmallCardContent>
|
||||
@@ -68,7 +98,11 @@ const QuickViewKind20NoteCard: React.FC<QuickViewKind20NoteCardProps> = ({ pubke
|
||||
return (
|
||||
<>
|
||||
{linkToNote ? (
|
||||
<Link href={`/note/${encodedNoteId}`} className="block w-full aspect-square">
|
||||
<Link
|
||||
href={`/note/${encodedNoteId}`}
|
||||
className="block w-full aspect-square"
|
||||
onClick={isNsfwContent && !showSensitiveContent ? (e) => e.preventDefault() : undefined}
|
||||
>
|
||||
{card}
|
||||
</Link>
|
||||
) : (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from 'react';
|
||||
import React, { useState } from 'react';
|
||||
import { useProfile } from "nostr-react";
|
||||
import { nip19 } from "nostr-tools";
|
||||
import {
|
||||
@@ -10,6 +10,9 @@ import {
|
||||
import Link from 'next/link';
|
||||
import { Avatar } from './ui/avatar';
|
||||
import { AvatarImage } from '@radix-ui/react-avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Eye } from 'lucide-react';
|
||||
import { hasNsfwContent } from '@/utils/utils';
|
||||
|
||||
interface TrendingImageNewProps {
|
||||
event: {
|
||||
@@ -26,6 +29,12 @@ const TrendingImageNew: React.FC<TrendingImageNewProps> = ({ event }) => {
|
||||
pubkey: event.pubkey,
|
||||
});
|
||||
|
||||
// Check if the event has nsfw or sexy tags
|
||||
const hasNsfwTag = hasNsfwContent(event.tags);
|
||||
|
||||
// State to control image blur
|
||||
const [showSensitiveContent, setShowSensitiveContent] = useState(false);
|
||||
|
||||
const npubShortened = (() => {
|
||||
let encoded = nip19.npubEncode(event.pubkey);
|
||||
let parts = encoded.split('npub');
|
||||
@@ -43,6 +52,13 @@ const TrendingImageNew: React.FC<TrendingImageNewProps> = ({ event }) => {
|
||||
const hrefNote = `/note/${nip19.noteEncode(event.id)}`;
|
||||
const profileImageSrc = userData?.picture || "https://robohash.org/" + event.pubkey;
|
||||
|
||||
// Toggle sensitive content visibility
|
||||
const toggleSensitiveContent = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setShowSensitiveContent(!showSensitiveContent);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
@@ -62,14 +78,31 @@ const TrendingImageNew: React.FC<TrendingImageNewProps> = ({ event }) => {
|
||||
<div className='d-flex justify-content-center align-items-center'>
|
||||
{imageUrl && (
|
||||
<div style={{ position: 'relative', width: '100%', height: '100%', overflow: 'hidden' }}>
|
||||
<Link href={hrefNote}>
|
||||
<Link href={hrefNote} onClick={hasNsfwTag && !showSensitiveContent ? (e) => e.preventDefault() : undefined}>
|
||||
<img
|
||||
src={imageUrl}
|
||||
className='rounded lg:rounded-lg w-full h-full object-cover'
|
||||
className={`rounded lg:rounded-lg w-full h-full object-cover ${hasNsfwTag && !showSensitiveContent ? 'blur-xl' : ''}`}
|
||||
style={{ margin: 'auto' }}
|
||||
alt={text}
|
||||
loading="lazy"
|
||||
/>
|
||||
{hasNsfwTag && !showSensitiveContent && (
|
||||
<div
|
||||
className="absolute inset-0 flex flex-col items-center justify-center"
|
||||
onClick={toggleSensitiveContent}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
className="bg-black bg-opacity-50 hover:bg-opacity-70 text-white px-4 py-2 rounded-md"
|
||||
onClick={toggleSensitiveContent}
|
||||
>
|
||||
<Eye className="h-4 w-4 mr-2" /> Show Sensitive Content
|
||||
</Button>
|
||||
<p className="mt-2 text-white text-sm bg-black bg-opacity-50 p-2 rounded">
|
||||
This image may contain sensitive content
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -2,6 +2,14 @@ import { Event as NostrEvent, finalizeEvent} from "nostr-tools";
|
||||
import { hexToBytes } from "@noble/hashes/utils"
|
||||
import { signEventWithBunker } from "./bunkerUtils";
|
||||
|
||||
// Check if the event has nsfw or sexy tags
|
||||
export function hasNsfwContent(tags: string[][]): boolean {
|
||||
return tags.some(tag =>
|
||||
(tag[0] === 't' && (tag[1]?.toLowerCase() === 'nsfw' || tag[1]?.toLowerCase() === 'sexy')) ||
|
||||
(tag[0] === 'content-warning')
|
||||
);
|
||||
}
|
||||
|
||||
export function getImageUrl(tags: string[][]): string {
|
||||
const imetaTag = tags.find(tag => tag[0] === 'imeta');
|
||||
if (imetaTag) {
|
||||
|
||||
Reference in New Issue
Block a user