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 { PROFILE_BLOG_FILTER_LIMIT } from '../../constants'
import { NDKContextType } from 'contexts/NDKContext'
import { kinds, nip19 } from 'nostr-tools'
import { LoaderFunctionArgs, redirect } from 'react-router-dom'
@ -59,33 +60,33 @@ export const blogRouteLoader =
) as FilterOptions
// Fetch 4 in case the current blog is included in the latest
const latestModsFilter: NDKFilter = {
const latestFilter: NDKFilter = {
authors: [pubkey],
kinds: [kinds.LongFormArticle],
limit: 4
}
// Add source filter
if (filterOptions.source === window.location.host) {
latestModsFilter['#r'] = [filterOptions.source]
latestFilter['#r'] = [filterOptions.source]
}
// Filter by NSFW tag
// NSFWFilter.Only_NSFW -> fetch with content-warning label
// NSFWFilter.Show_NSFW -> filter not needed
// NSFWFilter.Only_NSFW -> true
// NSFWFilter.Hide_NSFW -> false
if (filterOptions.nsfw !== NSFWFilter.Show_NSFW) {
latestModsFilter['#nsfw'] = [
(filterOptions.nsfw === NSFWFilter.Only_NSFW).toString()
]
// NSFWFilter.Hide_NSFW -> up the limit and filter after fetch
if (filterOptions.nsfw === NSFWFilter.Only_NSFW) {
latestFilter['#L'] = ['content-warning']
} else if (filterOptions.nsfw === NSFWFilter.Hide_NSFW) {
// 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
const settled = await Promise.allSettled([
ndkContext.fetchEvent(filter),
ndkContext.fetchEvents(latestModsFilter),
ndkContext.fetchEvents(latestFilter),
ndkContext.getMuteLists(loggedInUserPubkey), // Pass pubkey for logged-in users
ndkContext.getNSFWList()
])
const result: BlogPageLoaderResult = {
blog: undefined,
latest: [],
@ -120,6 +121,9 @@ export const blogRouteLoader =
result.latest = fetchEventsResult.value
.map(extractBlogCardDetails)
.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
} else if (fetchEventsResult.status === 'rejected') {
log(

View File

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

View File

@ -704,10 +704,8 @@ const ProfileTabBlogs = () => {
filter['#r'] = [host]
}
if (filterOptions.nsfw !== NSFWFilter.Show_NSFW) {
filter['#nsfw'] = [
(filterOptions.nsfw === NSFWFilter.Only_NSFW).toString()
]
if (filterOptions.nsfw === NSFWFilter.Only_NSFW) {
filter['#L'] = ['content-warning']
}
return filter
@ -725,7 +723,7 @@ const ProfileTabBlogs = () => {
}
fetchEvents(filter)
.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)
})
.finally(() => {
@ -752,7 +750,7 @@ const ProfileTabBlogs = () => {
setHasMore(nextBlogs.length > PROFILE_BLOG_FILTER_LIMIT)
setPage((prev) => prev + 1)
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))
@ -775,7 +773,7 @@ const ProfileTabBlogs = () => {
.then((events) => {
setHasMore(true)
setPage((prev) => prev - 1)
setBlogs(events.map(extractBlogCardDetails).filter((e) => e.naddr))
setBlogs(events.map(extractBlogCardDetails).filter((b) => b.naddr))
})
.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"
// Allow "Unmoderated Fully" when author visits own profile
if (!((isAdmin || isOwner) && isUnmoderatedFully)) {

View File

@ -84,22 +84,27 @@ export const writeRouteAction =
.split(',')
.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 = {
kind: kinds.LongFormArticle,
created_at: currentTimeStamp,
pubkey: hexPubkey,
content: content,
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
]
tags: tags
}
try {

View File

@ -8,8 +8,10 @@ export const extractBlogDetails = (event: NDKEvent): Partial<BlogDetails> => ({
content: event.content,
summary: getFirstTagValue(event, 'summary'),
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,
author: event.pubkey,
published_at: getFirstTagValueAsInt(event, 'published_at'),