fix(blog): nsfw filtering, use L tag instead nsfw
All checks were successful
Release to Staging / build_and_release (push) Successful in 54s

This commit is contained in:
enes 2024-11-12 20:15:27 +01:00
parent 352179f1d9
commit b49ae9537b
5 changed files with 72 additions and 64 deletions

View File

@ -1,4 +1,5 @@
import { NDKFilter } from '@nostr-dev-kit/ndk' import { NDKFilter } from '@nostr-dev-kit/ndk'
import { PROFILE_BLOG_FILTER_LIMIT } from '../../constants'
import { NDKContextType } from 'contexts/NDKContext' import { NDKContextType } from 'contexts/NDKContext'
import { kinds, nip19 } from 'nostr-tools' import { kinds, nip19 } from 'nostr-tools'
import { LoaderFunctionArgs, redirect } from 'react-router-dom' import { LoaderFunctionArgs, redirect } from 'react-router-dom'
@ -59,33 +60,33 @@ export const blogRouteLoader =
) as FilterOptions ) as FilterOptions
// Fetch 4 in case the current blog is included in the latest // Fetch 4 in case the current blog is included in the latest
const latestModsFilter: NDKFilter = { const latestFilter: NDKFilter = {
authors: [pubkey], authors: [pubkey],
kinds: [kinds.LongFormArticle], kinds: [kinds.LongFormArticle],
limit: 4 limit: 4
} }
// Add source filter // Add source filter
if (filterOptions.source === window.location.host) { if (filterOptions.source === window.location.host) {
latestModsFilter['#r'] = [filterOptions.source] latestFilter['#r'] = [filterOptions.source]
} }
// Filter by NSFW tag // Filter by NSFW tag
// NSFWFilter.Only_NSFW -> fetch with content-warning label
// NSFWFilter.Show_NSFW -> filter not needed // NSFWFilter.Show_NSFW -> filter not needed
// NSFWFilter.Only_NSFW -> true // NSFWFilter.Hide_NSFW -> up the limit and filter after fetch
// NSFWFilter.Hide_NSFW -> false if (filterOptions.nsfw === NSFWFilter.Only_NSFW) {
if (filterOptions.nsfw !== NSFWFilter.Show_NSFW) { latestFilter['#L'] = ['content-warning']
latestModsFilter['#nsfw'] = [ } else if (filterOptions.nsfw === NSFWFilter.Hide_NSFW) {
(filterOptions.nsfw === NSFWFilter.Only_NSFW).toString() // Up the limit in case we fetch multiple NSFW blogs
] latestFilter.limit = PROFILE_BLOG_FILTER_LIMIT
} }
// Parallel fetch blog event, latest events, mute, and nsfw lists in parallel // Parallel fetch blog event, latest events, mute, and nsfw lists in parallel
const settled = await Promise.allSettled([ const settled = await Promise.allSettled([
ndkContext.fetchEvent(filter), ndkContext.fetchEvent(filter),
ndkContext.fetchEvents(latestModsFilter), ndkContext.fetchEvents(latestFilter),
ndkContext.getMuteLists(loggedInUserPubkey), // Pass pubkey for logged-in users ndkContext.getMuteLists(loggedInUserPubkey), // Pass pubkey for logged-in users
ndkContext.getNSFWList() ndkContext.getNSFWList()
]) ])
const result: BlogPageLoaderResult = { const result: BlogPageLoaderResult = {
blog: undefined, blog: undefined,
latest: [], latest: [],
@ -120,6 +121,9 @@ export const blogRouteLoader =
result.latest = fetchEventsResult.value result.latest = fetchEventsResult.value
.map(extractBlogCardDetails) .map(extractBlogCardDetails)
.filter((b) => b.id !== result.blog?.id) // Filter out current blog if present .filter((b) => b.id !== result.blog?.id) // Filter out current blog if present
.filter(
(b) => !(b.nsfw && filterOptions.nsfw === NSFWFilter.Hide_NSFW)
) // Filter out the NSFW if selected
.slice(0, 3) // Take only three .slice(0, 3) // Take only three
} else if (fetchEventsResult.status === 'rejected') { } else if (fetchEventsResult.status === 'rejected') {
log( log(

View File

@ -6,7 +6,7 @@ import { Swiper, SwiperSlide } from 'swiper/react'
import { BlogCard } from '../components/BlogCard' import { BlogCard } from '../components/BlogCard'
import { GameCard } from '../components/GameCard' import { GameCard } from '../components/GameCard'
import { ModCard } from '../components/ModCard' import { ModCard } from '../components/ModCard'
import { LANDING_PAGE_DATA } from '../constants' import { LANDING_PAGE_DATA, PROFILE_BLOG_FILTER_LIMIT } from '../constants'
import { import {
useDidMount, useDidMount,
useGames, useGames,
@ -31,11 +31,7 @@ import '../styles/SimpleSlider.css'
import '../styles/styles.css' import '../styles/styles.css'
// Import Swiper styles // Import Swiper styles
import { import { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk'
filterForEventsTaggingId,
NDKEvent,
NDKFilter
} from '@nostr-dev-kit/ndk'
import 'swiper/css' import 'swiper/css'
import 'swiper/css/navigation' import 'swiper/css/navigation'
import 'swiper/css/pagination' import 'swiper/css/pagination'
@ -332,38 +328,34 @@ const DisplayLatestBlogs = () => {
// Show maximum of 4 blog posts // Show maximum of 4 blog posts
// 2 should be featured and the most recent 2 from blog npubs // 2 should be featured and the most recent 2 from blog npubs
// Populate the filter from known naddr (constants.ts) // Populate the filter from known naddr (constants.ts)
const filters: NDKFilter[] = [] const filter: NDKFilter = {
kinds: [kinds.LongFormArticle],
authors: [],
'#d': []
}
for (let i = 0; i < LANDING_PAGE_DATA.featuredBlogPosts.length; i++) { for (let i = 0; i < LANDING_PAGE_DATA.featuredBlogPosts.length; i++) {
try { try {
const naddr = LANDING_PAGE_DATA.featuredBlogPosts[i] const naddr = LANDING_PAGE_DATA.featuredBlogPosts[i]
const filterId = filterForEventsTaggingId(naddr) const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
if (filterId) { const { pubkey, identifier } = decoded.data
filters.push(filterId) if (!filter.authors?.includes(pubkey)) {
filter.authors?.push(pubkey)
}
if (!filter.authors?.includes(identifier)) {
filter['#d']?.push(identifier)
} }
} catch (error) { } catch (error) {
// Silently ignore // Silently ignore
} }
} }
// Create a single filter based on multiple #a's
const filter = filters.reduce(
(filter, id) => {
const a = id['#a']
if (a) {
filter['#a']?.push(a[0])
}
return filter
},
{
'#a': []
} as NDKFilter
)
// Prepare filter for the latest // Prepare filter for the latest
const blogNpubs = import.meta.env.VITE_BLOG_NPUBS.split(',') const blogNpubs = import.meta.env.VITE_BLOG_NPUBS.split(',')
const blogHexkeys = blogNpubs const blogHexkeys = blogNpubs
.map(npubToHex) .map(npubToHex)
.filter((hexkey) => hexkey !== null) .filter((hexkey) => hexkey !== null)
// We fetch 4 posts in case of duplicates (from featured) // We fetch more posts in case of duplicates (from featured)
const latestFilter: NDKFilter = { const latestFilter: NDKFilter = {
authors: blogHexkeys, authors: blogHexkeys,
kinds: [kinds.LongFormArticle], kinds: [kinds.LongFormArticle],
@ -371,17 +363,15 @@ const DisplayLatestBlogs = () => {
} }
// Filter by NSFW tag // Filter by NSFW tag
// NSFWFilter.Show_NSFW -> filter not needed if (filterOptions.nsfw === NSFWFilter.Only_NSFW) {
// NSFWFilter.Only_NSFW -> true latestFilter['#L'] = ['content-warning']
// NSFWFilter.Hide_NSFW -> false } else if (filterOptions.nsfw === NSFWFilter.Hide_NSFW) {
if (filterOptions.nsfw !== NSFWFilter.Show_NSFW) { // Up the limit in case we fetch multiple NSFW blogs
latestFilter['#nsfw'] = [ latestFilter.limit = PROFILE_BLOG_FILTER_LIMIT
(filterOptions.nsfw === NSFWFilter.Only_NSFW).toString()
]
} }
const results = await Promise.allSettled([ const results = await Promise.allSettled([
fetchEvents({ ...filter, kinds: [kinds.LongFormArticle] }), fetchEvents(filter),
fetchEvents(latestFilter) fetchEvents(latestFilter)
]) ])
@ -403,9 +393,13 @@ const DisplayLatestBlogs = () => {
}, new Map()) }, new Map())
.values() .values()
) )
const latest = unique.slice(0, 4) .map(extractBlogCardDetails)
.filter(
(b) => !(b.nsfw && filterOptions.nsfw === NSFWFilter.Hide_NSFW)
)
setBlogs(latest.map(extractBlogCardDetails)) const latest = unique.slice(0, 4)
setBlogs(latest)
} catch (error) { } catch (error) {
log( log(
true, true,

View File

@ -704,10 +704,8 @@ const ProfileTabBlogs = () => {
filter['#r'] = [host] filter['#r'] = [host]
} }
if (filterOptions.nsfw !== NSFWFilter.Show_NSFW) { if (filterOptions.nsfw === NSFWFilter.Only_NSFW) {
filter['#nsfw'] = [ filter['#L'] = ['content-warning']
(filterOptions.nsfw === NSFWFilter.Only_NSFW).toString()
]
} }
return filter return filter
@ -725,7 +723,7 @@ const ProfileTabBlogs = () => {
} }
fetchEvents(filter) fetchEvents(filter)
.then((events) => { .then((events) => {
setBlogs(events.map(extractBlogCardDetails).filter((e) => e.naddr)) setBlogs(events.map(extractBlogCardDetails).filter((b) => b.naddr))
setHasMore(events.length > PROFILE_BLOG_FILTER_LIMIT) setHasMore(events.length > PROFILE_BLOG_FILTER_LIMIT)
}) })
.finally(() => { .finally(() => {
@ -752,7 +750,7 @@ const ProfileTabBlogs = () => {
setHasMore(nextBlogs.length > PROFILE_BLOG_FILTER_LIMIT) setHasMore(nextBlogs.length > PROFILE_BLOG_FILTER_LIMIT)
setPage((prev) => prev + 1) setPage((prev) => prev + 1)
setBlogs( setBlogs(
nextBlogs.slice(0, PROFILE_BLOG_FILTER_LIMIT).filter((e) => e.naddr) nextBlogs.slice(0, PROFILE_BLOG_FILTER_LIMIT).filter((b) => b.naddr)
) )
}) })
.finally(() => setIsLoading(false)) .finally(() => setIsLoading(false))
@ -775,7 +773,7 @@ const ProfileTabBlogs = () => {
.then((events) => { .then((events) => {
setHasMore(true) setHasMore(true)
setPage((prev) => prev - 1) setPage((prev) => prev - 1)
setBlogs(events.map(extractBlogCardDetails).filter((e) => e.naddr)) setBlogs(events.map(extractBlogCardDetails).filter((b) => b.naddr))
}) })
.finally(() => setIsLoading(false)) .finally(() => setIsLoading(false))
} }
@ -799,6 +797,11 @@ const ProfileTabBlogs = () => {
}) })
} }
// Filter nsfw (Hide_NSFW option)
_blogs = _blogs.filter(
(b) => !(b.nsfw && filterOptions.nsfw === NSFWFilter.Hide_NSFW)
)
// Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully" // Only apply filtering if the user is not an admin or the admin has not selected "Unmoderated Fully"
// Allow "Unmoderated Fully" when author visits own profile // Allow "Unmoderated Fully" when author visits own profile
if (!((isAdmin || isOwner) && isUnmoderatedFully)) { if (!((isAdmin || isOwner) && isUnmoderatedFully)) {

View File

@ -84,22 +84,27 @@ export const writeRouteAction =
.split(',') .split(',')
.map((t) => ['t', t]) .map((t) => ['t', t])
const tags = [
['d', uuid],
['a', aTag],
['r', rTag],
['published_at', published_at.toString()],
['title', formSubmit.title!],
['image', formSubmit.image!],
['summary', formSubmit.summary!],
...tTags
]
// Add NSFW tag, L label namespace standardized tag
// https://github.com/nostr-protocol/nips/blob/2838e3bd51ac00bd63c4cef1601ae09935e7dd56/README.md#standardized-tags
if (formSubmit.nsfw === 'on') tags.push(['L', 'content-warning'])
const unsignedEvent: UnsignedEvent = { const unsignedEvent: UnsignedEvent = {
kind: kinds.LongFormArticle, kind: kinds.LongFormArticle,
created_at: currentTimeStamp, created_at: currentTimeStamp,
pubkey: hexPubkey, pubkey: hexPubkey,
content: content, content: content,
tags: [ tags: tags
['d', uuid],
['a', aTag],
['r', rTag],
['published_at', published_at.toString()],
['title', formSubmit.title!],
['image', formSubmit.image!],
['summary', formSubmit.summary!],
['nsfw', (formSubmit.nsfw === 'on').toString()],
...tTags
]
} }
try { try {

View File

@ -8,8 +8,10 @@ export const extractBlogDetails = (event: NDKEvent): Partial<BlogDetails> => ({
content: event.content, content: event.content,
summary: getFirstTagValue(event, 'summary'), summary: getFirstTagValue(event, 'summary'),
image: getFirstTagValue(event, 'image'), image: getFirstTagValue(event, 'image'),
nsfw: getFirstTagValue(event, 'nsfw') === 'true', // Check L label namespace for content warning or nsfw (backwards compatibility)
nsfw:
getFirstTagValue(event, 'L') === 'content-warning' ||
getFirstTagValue(event, 'nsfw') === 'true',
id: event.id, id: event.id,
author: event.pubkey, author: event.pubkey,
published_at: getFirstTagValueAsInt(event, 'published_at'), published_at: getFirstTagValueAsInt(event, 'published_at'),