diff --git a/src/pages/blog/loader.ts b/src/pages/blog/loader.ts index 27325be..e711c65 100644 --- a/src/pages/blog/loader.ts +++ b/src/pages/blog/loader.ts @@ -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( diff --git a/src/pages/home.tsx b/src/pages/home.tsx index 4b6c7d2..072cfc3 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -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, diff --git a/src/pages/profile/index.tsx b/src/pages/profile/index.tsx index 584c34f..3b6ec70 100644 --- a/src/pages/profile/index.tsx +++ b/src/pages/profile/index.tsx @@ -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)) { diff --git a/src/pages/write/action.tsx b/src/pages/write/action.tsx index 447a605..61bb7ad 100644 --- a/src/pages/write/action.tsx +++ b/src/pages/write/action.tsx @@ -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 { diff --git a/src/utils/blog.ts b/src/utils/blog.ts index df11b63..306620d 100644 --- a/src/utils/blog.ts +++ b/src/utils/blog.ts @@ -8,8 +8,10 @@ export const extractBlogDetails = (event: NDKEvent): Partial => ({ 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'),