From 718350d2bc1613ff977d7569d5de9fbb734945d0 Mon Sep 17 00:00:00 2001 From: enes Date: Wed, 13 Nov 2024 14:09:13 +0100 Subject: [PATCH] fix(blogs): moderation and missing aTag --- src/pages/blog/loader.ts | 84 +++++++++++++++++++++++++++------------ src/pages/blogs/loader.ts | 45 ++++++++++++++++++--- src/utils/blog.ts | 48 +++++++++++++--------- 3 files changed, 128 insertions(+), 49 deletions(-) diff --git a/src/pages/blog/loader.ts b/src/pages/blog/loader.ts index e711c65..0b06b50 100644 --- a/src/pages/blog/loader.ts +++ b/src/pages/blog/loader.ts @@ -5,7 +5,12 @@ import { kinds, nip19 } from 'nostr-tools' import { LoaderFunctionArgs, redirect } from 'react-router-dom' import { appRoutes } from 'routes' import { store } from 'store' -import { BlogPageLoaderResult, FilterOptions, NSFWFilter } from 'types' +import { + BlogPageLoaderResult, + FilterOptions, + ModeratedFilter, + NSFWFilter +} from 'types' import { DEFAULT_FILTER_OPTIONS, getLocalStorageItem, @@ -59,11 +64,11 @@ export const blogRouteLoader = getLocalStorageItem('filter-blog', DEFAULT_FILTER_OPTIONS) ) as FilterOptions - // Fetch 4 in case the current blog is included in the latest + // Fetch more in case the current blog is included in the latest and filters remove some const latestFilter: NDKFilter = { authors: [pubkey], kinds: [kinds.LongFormArticle], - limit: 4 + limit: PROFILE_BLOG_FILTER_LIMIT } // Add source filter if (filterOptions.source === window.location.host) { @@ -75,12 +80,9 @@ export const blogRouteLoader = // 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 + // Parallel fetch blog event, latest events, mute, and nsfw lists const settled = await Promise.allSettled([ ndkContext.fetchEvent(filter), ndkContext.fetchEvents(latestFilter), @@ -121,10 +123,6 @@ 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( true, @@ -134,22 +132,48 @@ export const blogRouteLoader = ) } - const muteList = settled[2] - if (muteList.status === 'fulfilled' && muteList.value) { - if (muteList && muteList.value) { + const muteLists = settled[2] + if (muteLists.status === 'fulfilled' && muteLists.value) { + if (muteLists && muteLists.value) { if (result.blog && result.blog.aTag) { if ( - muteList.value.admin.replaceableEvents.includes( + muteLists.value.admin.replaceableEvents.includes( result.blog.aTag ) || - muteList.value.user.replaceableEvents.includes(result.blog.aTag) + muteLists.value.user.replaceableEvents.includes(result.blog.aTag) ) { result.isBlocked = true } } + + // Moderate the latest + const isAdmin = + userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB + const isOwner = + userState.user?.pubkey && userState.user.pubkey === pubkey + const isUnmoderatedFully = + filterOptions.moderated === ModeratedFilter.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 + if (!((isAdmin || isOwner) && isUnmoderatedFully)) { + result.latest = result.latest.filter( + (b) => + !muteLists.value.admin.authors.includes(b.author!) && + !muteLists.value.admin.replaceableEvents.includes(b.aTag!) + ) + } + + if (filterOptions.moderated === ModeratedFilter.Moderated) { + result.latest = result.latest.filter( + (b) => + !muteLists.value.user.authors.includes(b.author!) && + !muteLists.value.user.replaceableEvents.includes(b.aTag!) + ) + } } - } else if (muteList.status === 'rejected') { - log(true, LogType.Error, 'Issue fetching mute list', muteList.reason) + } else if (muteLists.status === 'rejected') { + log(true, LogType.Error, 'Issue fetching mute list', muteLists.reason) } const nsfwList = settled[3] @@ -171,15 +195,14 @@ export const blogRouteLoader = } } - // Check if the the latest blogs too + // Check the latest blogs too result.latest = result.latest.map((b) => { - if (b) { - const isMissingNsfwTag = - !b.nsfw && b.aTag && nsfwList.value.includes(b.aTag) + // Add nsfw tag if it's missing + const isMissingNsfwTag = + !b.nsfw && b.aTag && nsfwList.value.includes(b.aTag) - if (isMissingNsfwTag) { - b.nsfw = true - } + if (isMissingNsfwTag) { + b.nsfw = true } return b }) @@ -187,6 +210,17 @@ export const blogRouteLoader = log(true, LogType.Error, 'Issue fetching nsfw list', nsfwList.reason) } + // Filter latest, sort and take only three + result.latest = result.latest + .filter( + // Filter out the NSFW if selected + (b) => !(b.nsfw && filterOptions.nsfw === NSFWFilter.Hide_NSFW) + ) + .sort((a, b) => + a.published_at && b.published_at ? b.published_at - a.published_at : 0 + ) + .slice(0, 3) + return result } catch (error) { let message = 'An error occurred in fetching blog details from relays' diff --git a/src/pages/blogs/loader.ts b/src/pages/blogs/loader.ts index f2ab84d..c550999 100644 --- a/src/pages/blogs/loader.ts +++ b/src/pages/blogs/loader.ts @@ -1,6 +1,7 @@ import { NDKFilter } from '@nostr-dev-kit/ndk' import { NDKContextType } from 'contexts/NDKContext' import { kinds } from 'nostr-tools' +import { BlogCardDetails } from 'types' import { log, LogType, npubToHex } from 'utils' import { extractBlogCardDetails } from 'utils/blog' @@ -15,14 +16,46 @@ export const blogsRouteLoader = (ndkContext: NDKContextType) => async () => { authors: blogHexkeys, kinds: [kinds.LongFormArticle] } - const events = await ndkContext.fetchEvents(filter) - if (!events) { - log(true, LogType.Error, 'Unable to fetch the blog events.') - return null + const settled = await Promise.allSettled([ + ndkContext.fetchEvents(filter), + ndkContext.getMuteLists() + ]) + + let blogs: Partial[] = [] + const fetchEventsResult = settled[0] + if (fetchEventsResult.status === 'fulfilled' && fetchEventsResult.value) { + // Extract the blog card details from the events + blogs = fetchEventsResult.value + .map(extractBlogCardDetails) + .filter((b) => b.naddr) + } else if (fetchEventsResult.status === 'rejected') { + log( + true, + LogType.Error, + 'Unable to fetch the blog events.', + fetchEventsResult.reason + ) + return [] } - return events.map(extractBlogCardDetails).filter((e) => e.naddr) + const muteListResult = settled[1] + if (muteListResult.status === 'fulfilled' && muteListResult.value) { + // Filter out the blocked events + blogs = blogs.filter( + (b) => + b.aTag && + !muteListResult.value.admin.replaceableEvents.includes(b.aTag) + ) + } else if (muteListResult.status === 'rejected') { + log( + true, + LogType.Error, + 'Failed to fetch mutelists.', + muteListResult.reason + ) + } + return blogs } catch (error) { log( true, @@ -30,6 +63,6 @@ export const blogsRouteLoader = (ndkContext: NDKContextType) => async () => { 'An error occurred in fetching blog details from relays', error ) - return null + return [] } } diff --git a/src/utils/blog.ts b/src/utils/blog.ts index 306620d..089e058 100644 --- a/src/utils/blog.ts +++ b/src/utils/blog.ts @@ -3,24 +3,36 @@ import { BlogCardDetails, BlogDetails } from 'types' import { getFirstTagValue, getFirstTagValueAsInt, getTagValues } from './nostr' import { kinds, nip19 } from 'nostr-tools' -export const extractBlogDetails = (event: NDKEvent): Partial => ({ - title: getFirstTagValue(event, 'title'), - content: event.content, - summary: getFirstTagValue(event, 'summary'), - image: getFirstTagValue(event, 'image'), - // 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'), - edited_at: event.created_at, - rTag: getFirstTagValue(event, 'r') || 'N/A', - dTag: getFirstTagValue(event, 'd'), - aTag: getFirstTagValue(event, 'a'), - tTags: getTagValues(event, 't') || [] -}) +export const extractBlogDetails = (event: NDKEvent): Partial => { + const dTag = getFirstTagValue(event, 'd') + + // Check if the aTag exists on the blog + let aTag = getFirstTagValue(event, 'a') + + // Create aTag from components if aTag is not included + if (typeof aTag === 'undefined' && event.pubkey && dTag) { + aTag = `${kinds.LongFormArticle}:${event.pubkey}:${dTag}` + } + + return { + title: getFirstTagValue(event, 'title'), + content: event.content, + summary: getFirstTagValue(event, 'summary'), + image: getFirstTagValue(event, 'image'), + // 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'), + edited_at: event.created_at, + rTag: getFirstTagValue(event, 'r') || 'N/A', + dTag: dTag, + aTag: aTag, + tTags: getTagValues(event, 't') || [] + } +} export const extractBlogCardDetails = ( event: NDKEvent