import { NDKFilter } from '@nostr-dev-kit/ndk' import { NDKContextType } from 'contexts/NDKContext' 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 { DEFAULT_FILTER_OPTIONS, getLocalStorageItem, log, LogType } from 'utils' import { extractBlogCardDetails, extractBlogDetails } from 'utils/blog' export const blogRouteLoader = (ndkContext: NDKContextType) => async ({ params, request }: LoaderFunctionArgs) => { const { naddr } = params if (!naddr) { log(true, LogType.Error, 'Required naddr.') return redirect(appRoutes.blogs) } // Decode author and identifier from naddr let pubkey: string | undefined let identifier: string | undefined try { const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`) pubkey = decoded.data.pubkey identifier = decoded.data.identifier } catch (error) { log(true, LogType.Error, `Failed to decode naddr: ${naddr}`, error) throw new Error('Failed to fetch the blog. The address might be wrong') } const userState = store.getState().user const loggedInUserPubkey = userState?.user?.pubkey as string | undefined // Check if editing and the user is the original author // Redirect if NOT const url = new URL(request.url) const isEditMode = url.pathname.endsWith('/edit') if (isEditMode && loggedInUserPubkey !== pubkey) { return redirect(appRoutes.blogs) } try { // Set the filter for the main blog content const filter = { kinds: [kinds.LongFormArticle], authors: [pubkey], '#d': [identifier] } // Get the blog filter options for latest blogs const filterOptions = JSON.parse( getLocalStorageItem('filter-blog', DEFAULT_FILTER_OPTIONS) ) as FilterOptions // Fetch 4 in case the current blog is included in the latest const latestModsFilter: NDKFilter = { authors: [pubkey], kinds: [kinds.LongFormArticle], limit: 4 } // Add source filter if (filterOptions.source === window.location.host) { latestModsFilter['#r'] = [filterOptions.source] } // Filter by NSFW tag // 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() ] } // Parallel fetch blog event, latest events, mute, and nsfw lists in parallel const settled = await Promise.allSettled([ ndkContext.fetchEvent(filter), ndkContext.fetchEvents(latestModsFilter), ndkContext.getMuteLists(loggedInUserPubkey), // Pass pubkey for logged-in users ndkContext.getNSFWList() ]) const result: BlogPageLoaderResult = { blog: undefined, latest: [], isAddedToNSFW: false, isBlocked: false } // Check the blog event result const fetchEventResult = settled[0] if (fetchEventResult.status === 'fulfilled' && fetchEventResult.value) { // Extract the blog details from the event result.blog = extractBlogDetails(fetchEventResult.value) } else if (fetchEventResult.status === 'rejected') { log( true, LogType.Error, 'Unable to fetch the blog event.', fetchEventResult.reason ) } // Throw an error if we are missing the main blog result // Handle it with the react-router's errorComponent if (!result.blog) { throw new Error('We are unable to find the blog on the relays') } // Check the lateast blog events const fetchEventsResult = settled[1] if (fetchEventsResult.status === 'fulfilled' && fetchEventsResult.value) { // Extract the blog card details from the events result.latest = fetchEventsResult.value .map(extractBlogCardDetails) .filter((b) => b.id !== result.blog?.id) // Filter out current blog if present .slice(0, 3) // Take only three } else if (fetchEventsResult.status === 'rejected') { log( true, LogType.Error, 'Unable to fetch the latest blog events.', fetchEventsResult.reason ) } const muteList = settled[2] if (muteList.status === 'fulfilled' && muteList.value) { if (muteList && muteList.value) { if (result.blog && result.blog.aTag) { if ( muteList.value.admin.replaceableEvents.includes( result.blog.aTag ) || muteList.value.user.replaceableEvents.includes(result.blog.aTag) ) { result.isBlocked = true } } } } else if (muteList.status === 'rejected') { log(true, LogType.Error, 'Issue fetching mute list', muteList.reason) } const nsfwList = settled[3] if (nsfwList.status === 'fulfilled' && nsfwList.value) { // Check if the blog is marked as NSFW // Mark it as NSFW only if it's missing the tag if (result.blog) { const isMissingNsfwTag = !result.blog.nsfw && result.blog.aTag && nsfwList.value.includes(result.blog.aTag) if (isMissingNsfwTag) { result.blog.nsfw = true } if (result.blog.aTag && nsfwList.value.includes(result.blog.aTag)) { result.isAddedToNSFW = true } } // Check if the the latest blogs too result.latest = result.latest.map((b) => { if (b) { const isMissingNsfwTag = !b.nsfw && b.aTag && nsfwList.value.includes(b.aTag) if (isMissingNsfwTag) { b.nsfw = true } } return b }) } else if (nsfwList.status === 'rejected') { log(true, LogType.Error, 'Issue fetching nsfw list', nsfwList.reason) } return result } catch (error) { let message = 'An error occurred in fetching blog details from relays' log(true, LogType.Error, message, error) if (error instanceof Error) { message = error.message throw new Error(message) } } }