diff --git a/src/pages/blog/action.ts b/src/pages/blog/action.ts new file mode 100644 index 0000000..696fcdd --- /dev/null +++ b/src/pages/blog/action.ts @@ -0,0 +1,264 @@ +import { NDKFilter } from '@nostr-dev-kit/ndk' +import { NDKContextType } from 'contexts/NDKContext' +import { kinds, nip19, UnsignedEvent } from 'nostr-tools' +import { ActionFunctionArgs } from 'react-router-dom' +import { toast } from 'react-toastify' +import { store } from 'store' +import { UserRelaysType } from 'types' +import { log, LogType, now, signAndPublish } from 'utils' + +export const blogRouteAction = + (ndkContext: NDKContextType) => + async ({ params, request }: ActionFunctionArgs) => { + const { naddr } = params + if (!naddr) { + log(true, LogType.Error, 'Required naddr.') + return null + } + + // Decode author from naddr + let aTag: string | undefined + try { + const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`) + const { identifier, kind, pubkey } = decoded.data + aTag = `${kind}:${pubkey}:${identifier}` + } catch (error) { + log(true, LogType.Error, 'Failed to decode naddr') + return null + } + + if (!aTag) { + log(true, LogType.Error, 'Missing #a Tag') + return null + } + + const userState = store.getState().user + let hexPubkey: string + if (userState.auth && userState.user?.pubkey) { + hexPubkey = userState.user.pubkey as string + } else { + hexPubkey = (await window.nostr?.getPublicKey()) as string + } + + if (!hexPubkey) { + toast.error('Failed to get the pubkey') + return null + } + + const isAdmin = + userState.user?.npub && + userState.user.npub === import.meta.env.VITE_REPORTING_NPUB + + const handleBlock = async () => { + // Define the event filter to search for the user's mute list events. + // We look for events of a specific kind (Mutelist) authored by the given hexPubkey. + const filter: NDKFilter = { + kinds: [kinds.Mutelist], + authors: [hexPubkey] + } + + // Fetch the mute list event from the relays. This returns the event containing the user's mute list. + const muteListEvent = await ndkContext.fetchEventFromUserRelays( + filter, + hexPubkey, + UserRelaysType.Write + ) + + let unsignedEvent: UnsignedEvent + if (muteListEvent) { + // get a list of tags + const tags = muteListEvent.tags + const alreadyExists = + tags.findIndex((item) => item[0] === 'a' && item[1] === aTag) !== -1 + + if (alreadyExists) { + toast.warn(`Blog reference is already in user's mute list`) + return null + } + + tags.push(['a', aTag]) + + unsignedEvent = { + pubkey: muteListEvent.pubkey, + kind: kinds.Mutelist, + content: muteListEvent.content, + created_at: now(), + tags: [...tags] + } + } else { + unsignedEvent = { + pubkey: hexPubkey, + kind: kinds.Mutelist, + content: '', + created_at: now(), + tags: [['a', aTag]] + } + } + + const isUpdated = await signAndPublish( + unsignedEvent, + ndkContext.ndk, + ndkContext.publish + ) + + if (!isUpdated) { + toast.error("Failed to update user's mute list") + } + return null + } + + const handleUnblock = async () => { + const filter: NDKFilter = { + kinds: [kinds.Mutelist], + authors: [hexPubkey] + } + const muteListEvent = await ndkContext.fetchEventFromUserRelays( + filter, + hexPubkey, + UserRelaysType.Write + ) + if (!muteListEvent) { + toast.error(`Couldn't get user's mute list event from relays`) + return null + } + + const tags = muteListEvent.tags + const unsignedEvent: UnsignedEvent = { + pubkey: muteListEvent.pubkey, + kind: kinds.Mutelist, + content: muteListEvent.content, + created_at: now(), + tags: tags.filter((item) => item[0] !== 'a' || item[1] !== aTag) + } + + const isUpdated = await signAndPublish( + unsignedEvent, + ndkContext.ndk, + ndkContext.publish + ) + if (!isUpdated) { + toast.error("Failed to update user's mute list") + } + return null + } + const handleAddNSFW = async () => { + const filter: NDKFilter = { + kinds: [kinds.Curationsets], + authors: [hexPubkey], + '#d': ['nsfw'] + } + + const nsfwListEvent = await ndkContext.fetchEventFromUserRelays( + filter, + hexPubkey, + UserRelaysType.Write + ) + + let unsignedEvent: UnsignedEvent + + if (nsfwListEvent) { + const tags = nsfwListEvent.tags + const alreadyExists = + tags.findIndex((item) => item[0] === 'a' && item[1] === aTag) !== -1 + + if (alreadyExists) { + toast.warn(`Blog reference is already in user's nsfw list`) + return null + } + + tags.push(['a', aTag]) + + unsignedEvent = { + pubkey: nsfwListEvent.pubkey, + kind: kinds.Curationsets, + content: nsfwListEvent.content, + created_at: now(), + tags: [...tags] + } + } else { + unsignedEvent = { + pubkey: hexPubkey, + kind: kinds.Curationsets, + content: '', + created_at: now(), + tags: [ + ['a', aTag], + ['d', 'nsfw'] + ] + } + } + + const isUpdated = await signAndPublish( + unsignedEvent, + ndkContext.ndk, + ndkContext.publish + ) + if (!isUpdated) { + toast.error("Failed to update user's nsfw list") + } + return null + } + const handleRemoveNSFW = async () => { + const filter: NDKFilter = { + kinds: [kinds.Curationsets], + authors: [hexPubkey], + '#d': ['nsfw'] + } + + const nsfwListEvent = await ndkContext.fetchEventFromUserRelays( + filter, + hexPubkey, + UserRelaysType.Write + ) + + if (!nsfwListEvent) { + toast.error(`Couldn't get nsfw list event from relays`) + return null + } + + const tags = nsfwListEvent.tags + + const unsignedEvent: UnsignedEvent = { + pubkey: nsfwListEvent.pubkey, + kind: kinds.Curationsets, + content: nsfwListEvent.content, + created_at: now(), + tags: tags.filter((item) => item[0] !== 'a' || item[1] !== aTag) + } + + const isUpdated = await signAndPublish( + unsignedEvent, + ndkContext.ndk, + ndkContext.publish + ) + if (!isUpdated) { + toast.error("Failed to update user's nsfw list") + } + return null + } + + const requestData = (await request.json()) as { + intent: 'nsfw' | 'block' + value: boolean + } + + switch (requestData.intent) { + case 'block': + await (requestData.value ? handleBlock() : handleUnblock()) + break + + case 'nsfw': + if (!isAdmin) { + log(true, LogType.Error, 'Unable to update NSFW list. No permission') + return null + } + await (requestData.value ? handleAddNSFW() : handleRemoveNSFW()) + break + + default: + log(true, LogType.Error, 'Missing intent for blog action') + break + } + + return null + } diff --git a/src/pages/blog/index.tsx b/src/pages/blog/index.tsx index 7fb5814..56d2b30 100644 --- a/src/pages/blog/index.tsx +++ b/src/pages/blog/index.tsx @@ -1,5 +1,10 @@ import { useState } from 'react' -import { useLoaderData } from 'react-router-dom' +import { + useLoaderData, + Link as ReactRouterLink, + useNavigation, + useSubmit +} from 'react-router-dom' import StarterKit from '@tiptap/starter-kit' import Link from '@tiptap/extension-link' import Image from '@tiptap/extension-image' @@ -14,9 +19,19 @@ import placeholder from '../../assets/img/DEGMods Placeholder Img.png' import { PublishDetails } from 'components/Internal/PublishDetails' import { Interactions } from 'components/Internal/Interactions' import { BlogCard } from 'components/BlogCard' +import { copyTextToClipboard } from 'utils' +import { toast } from 'react-toastify' +import { useAppSelector, useBodyScrollDisable } from 'hooks' +import { ReportPopup } from './report' export const BlogPage = () => { - const { blog, latest } = useLoaderData() as BlogPageLoaderResult + const { blog, latest, isAddedToNSFW, isBlocked } = + useLoaderData() as BlogPageLoaderResult + const userState = useAppSelector((state) => state.user) + const isAdmin = + userState.user?.npub && + userState.user.npub === import.meta.env.VITE_REPORTING_NPUB + const navigation = useNavigation() const [commentCount, setCommentCount] = useState(0) const html = marked.parse(blog?.content || '', { async: false }) const sanitized = DOMPurify.sanitize(html) @@ -38,6 +53,42 @@ export const BlogPage = () => { [sanitized] ) + const [showReportPopUp, setShowReportPopUp] = useState(false) + useBodyScrollDisable(showReportPopUp) + + const submit = useSubmit() + const handleBlock = () => { + if (navigation.state === 'idle') { + submit( + { + intent: 'block', + value: !isBlocked, + target: blog?.aTag || '' + }, + { + method: 'post', + encType: 'application/json' + } + ) + } + } + + const handleNSFW = () => { + if (navigation.state === 'idle') { + submit( + { + intent: 'nsfw', + value: !isAddedToNSFW, + target: blog?.aTag || '' + }, + { + method: 'post', + encType: 'application/json' + } + ) + } + } + return (
NSFW
-NSFW
+