refactor: mod page, add generic report popup, repost option
This commit is contained in:
parent
a241f90269
commit
c55dc03382
@ -15,9 +15,14 @@ import {
|
||||
signAndPublish
|
||||
} from 'utils'
|
||||
|
||||
export const blogReportRouteAction =
|
||||
export const reportRouteAction =
|
||||
(ndkContext: NDKContextType) =>
|
||||
async ({ params, request }: ActionFunctionArgs) => {
|
||||
// Check which post type is reported
|
||||
const url = new URL(request.url)
|
||||
const isModReport = url.pathname.startsWith('/mod/')
|
||||
const isBlogReport = url.pathname.startsWith('/blog/')
|
||||
const title = isModReport ? 'Mod' : isBlogReport ? 'Blog' : 'Post'
|
||||
const requestData = await request.formData()
|
||||
const { naddr } = params
|
||||
if (!naddr) {
|
||||
@ -30,7 +35,12 @@ export const blogReportRouteAction =
|
||||
try {
|
||||
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
|
||||
const { identifier, kind, pubkey } = decoded.data
|
||||
|
||||
aTag = `${kind}:${pubkey}:${identifier}`
|
||||
|
||||
if (isModReport) {
|
||||
aTag = identifier
|
||||
}
|
||||
} catch (error) {
|
||||
log(true, LogType.Error, 'Failed to decode naddr')
|
||||
return false
|
||||
@ -82,7 +92,7 @@ export const blogReportRouteAction =
|
||||
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`)
|
||||
toast.warn(`${title} reference is already in user's mute list`)
|
||||
return false
|
||||
}
|
||||
tags.push(['a', aTag])
|
||||
@ -109,10 +119,12 @@ export const blogReportRouteAction =
|
||||
log(
|
||||
true,
|
||||
LogType.Error,
|
||||
'Could not get pubkey for reporting blog!',
|
||||
`Could not get pubkey for reporting ${title.toLowerCase()}!`,
|
||||
error
|
||||
)
|
||||
toast.error('Could not get pubkey for reporting blog!')
|
||||
toast.error(
|
||||
`Could not get pubkey for reporting ${title.toLowerCase()}!`
|
||||
)
|
||||
return false
|
||||
}
|
||||
|
||||
@ -122,7 +134,7 @@ export const blogReportRouteAction =
|
||||
ndkContext.publish
|
||||
)
|
||||
return { isSent: isUpdated }
|
||||
} else {
|
||||
} else if (reportingPubkey) {
|
||||
const href = window.location.href
|
||||
let message = `I'd like to report ${href} due to following reasons:\n`
|
||||
Object.entries(formSubmit).forEach(([key, value]) => {
|
||||
@ -133,14 +145,26 @@ export const blogReportRouteAction =
|
||||
try {
|
||||
const isSent = await sendDMUsingRandomKey(
|
||||
message,
|
||||
reportingPubkey!,
|
||||
reportingPubkey,
|
||||
ndkContext.ndk,
|
||||
ndkContext.publish
|
||||
)
|
||||
return { isSent: isSent }
|
||||
} catch (error) {
|
||||
log(true, LogType.Error, 'Failed to send a blog report', error)
|
||||
log(
|
||||
true,
|
||||
LogType.Error,
|
||||
`Failed to send a ${title.toLowerCase()} report`,
|
||||
error
|
||||
)
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
log(
|
||||
true,
|
||||
LogType.Error,
|
||||
`Failed to send a ${title.toLowerCase()} report: VITE_REPORTING_NPUB missing`
|
||||
)
|
||||
return false
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
import { useNavigation } from 'react-router-dom'
|
||||
import styles from '../../styles/loadingSpinner.module.scss'
|
||||
|
||||
interface Props {
|
||||
@ -16,3 +17,14 @@ export const LoadingSpinner = (props: Props) => {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const RouterLoadingSpinner = () => {
|
||||
const navigation = useNavigation()
|
||||
|
||||
if (navigation.state === 'idle') return null
|
||||
|
||||
const desc =
|
||||
navigation.state.charAt(0).toUpperCase() + navigation.state.slice(1)
|
||||
|
||||
return <LoadingSpinner desc={`${desc}...`} />
|
||||
}
|
||||
|
@ -1,22 +1,23 @@
|
||||
import { useFetcher } from 'react-router-dom'
|
||||
import { CheckboxFieldUncontrolled } from 'components/Inputs'
|
||||
import { useEffect } from 'react'
|
||||
import { ReportReason } from 'types/report'
|
||||
import { LoadingSpinner } from './LoadingSpinner'
|
||||
|
||||
type ReportPopupProps = {
|
||||
openedAt: number
|
||||
reasons: ReportReason[]
|
||||
handleClose: () => void
|
||||
}
|
||||
|
||||
const BLOG_REPORT_REASONS = [
|
||||
{ label: 'Actually CP', key: 'actuallyCP' },
|
||||
{ label: 'Spam', key: 'spam' },
|
||||
{ label: 'Scam', key: 'scam' },
|
||||
{ label: 'Malware', key: 'malware' },
|
||||
{ label: `Wasn't tagged NSFW`, key: 'wasntTaggedNSFW' },
|
||||
{ label: 'Other', key: 'otherReason' }
|
||||
]
|
||||
|
||||
export const ReportPopup = ({ handleClose }: ReportPopupProps) => {
|
||||
const fetcher = useFetcher()
|
||||
export const ReportPopup = ({
|
||||
openedAt,
|
||||
reasons,
|
||||
handleClose
|
||||
}: ReportPopupProps) => {
|
||||
// Use openedAt to allow for multiple reports
|
||||
// by default, fetcher will remember the data
|
||||
const fetcher = useFetcher({ key: openedAt.toString() })
|
||||
|
||||
// Close automatically if action succeeds
|
||||
useEffect(() => {
|
||||
@ -30,6 +31,7 @@ export const ReportPopup = ({ handleClose }: ReportPopupProps) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
{fetcher.state !== 'idle' && <LoadingSpinner desc={''} />}
|
||||
<div className='popUpMain'>
|
||||
<div className='ContainerMain'>
|
||||
<div className='popUpMainCardWrapper'>
|
||||
@ -64,7 +66,7 @@ export const ReportPopup = ({ handleClose }: ReportPopupProps) => {
|
||||
>
|
||||
Why are you reporting this?
|
||||
</label>
|
||||
{BLOG_REPORT_REASONS.map((r) => (
|
||||
{reasons.map((r) => (
|
||||
<CheckboxFieldUncontrolled
|
||||
key={r.key}
|
||||
label={r.label}
|
@ -22,7 +22,16 @@ import { BlogCard } from 'components/BlogCard'
|
||||
import { copyTextToClipboard } from 'utils'
|
||||
import { toast } from 'react-toastify'
|
||||
import { useAppSelector, useBodyScrollDisable } from 'hooks'
|
||||
import { ReportPopup } from './report'
|
||||
import { ReportPopup } from 'components/ReportPopup'
|
||||
|
||||
const BLOG_REPORT_REASONS = [
|
||||
{ label: 'Actually CP', key: 'actuallyCP' },
|
||||
{ label: 'Spam', key: 'spam' },
|
||||
{ label: 'Scam', key: 'scam' },
|
||||
{ label: 'Malware', key: 'malware' },
|
||||
{ label: `Wasn't tagged NSFW`, key: 'wasntTaggedNSFW' },
|
||||
{ label: 'Other', key: 'otherReason' }
|
||||
]
|
||||
|
||||
export const BlogPage = () => {
|
||||
const { blog, latest, isAddedToNSFW, isBlocked } =
|
||||
@ -53,8 +62,8 @@ export const BlogPage = () => {
|
||||
[sanitized]
|
||||
)
|
||||
|
||||
const [showReportPopUp, setShowReportPopUp] = useState(false)
|
||||
useBodyScrollDisable(showReportPopUp)
|
||||
const [showReportPopUp, setShowReportPopUp] = useState<number>()
|
||||
useBodyScrollDisable(!!showReportPopUp)
|
||||
|
||||
const submit = useSubmit()
|
||||
const handleBlock = () => {
|
||||
@ -190,7 +199,7 @@ export const BlogPage = () => {
|
||||
<a
|
||||
className='dropdown-item dropdownMainMenuItem'
|
||||
id='reportPost'
|
||||
onClick={() => setShowReportPopUp(true)}
|
||||
onClick={() => setShowReportPopUp(Date.now())}
|
||||
>
|
||||
<svg
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
@ -309,8 +318,12 @@ export const BlogPage = () => {
|
||||
{navigation.state !== 'idle' && (
|
||||
<LoadingSpinner desc={'Loading...'} />
|
||||
)}
|
||||
{showReportPopUp && (
|
||||
<ReportPopup handleClose={() => setShowReportPopUp(false)} />
|
||||
{!!showReportPopUp && (
|
||||
<ReportPopup
|
||||
openedAt={showReportPopUp}
|
||||
reasons={BLOG_REPORT_REASONS}
|
||||
handleClose={() => setShowReportPopUp(undefined)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
233
src/pages/mod/action.ts
Normal file
233
src/pages/mod/action.ts
Normal file
@ -0,0 +1,233 @@
|
||||
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 {
|
||||
addToCurationSet,
|
||||
CurationSetIdentifiers,
|
||||
log,
|
||||
LogType,
|
||||
now,
|
||||
removeFromCurationSet,
|
||||
signAndPublish
|
||||
} from 'utils'
|
||||
|
||||
export const modRouteAction =
|
||||
(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}`)
|
||||
|
||||
// We encode mods naddr identifier as a whole aTag
|
||||
const { identifier } = decoded.data
|
||||
aTag = 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(`Mod 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 success = await addToCurationSet(
|
||||
{
|
||||
dTag: CurationSetIdentifiers.NSFW,
|
||||
pubkey: hexPubkey,
|
||||
ndkContext
|
||||
},
|
||||
aTag
|
||||
)
|
||||
log(true, LogType.Info, `ModAction - NSFW - Add - ${success}`)
|
||||
return null
|
||||
}
|
||||
const handleRemoveNSFW = async () => {
|
||||
const success = await removeFromCurationSet(
|
||||
{
|
||||
dTag: CurationSetIdentifiers.NSFW,
|
||||
pubkey: hexPubkey,
|
||||
ndkContext
|
||||
},
|
||||
aTag
|
||||
)
|
||||
log(true, LogType.Info, `ModAction - Repost - Remove - ${success}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const handleRepost = async () => {
|
||||
const success = await addToCurationSet(
|
||||
{
|
||||
dTag: CurationSetIdentifiers.Repost,
|
||||
pubkey: hexPubkey,
|
||||
ndkContext
|
||||
},
|
||||
aTag
|
||||
)
|
||||
log(true, LogType.Info, `ModAction - NSFW - Add - ${success}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const handleRemoveRepost = async () => {
|
||||
const success = await removeFromCurationSet(
|
||||
{
|
||||
dTag: CurationSetIdentifiers.Repost,
|
||||
pubkey: hexPubkey,
|
||||
ndkContext
|
||||
},
|
||||
aTag
|
||||
)
|
||||
log(true, LogType.Info, `ModAction - Repost - Remove - ${success}`)
|
||||
return null
|
||||
}
|
||||
|
||||
const requestData = (await request.json()) as {
|
||||
intent: 'nsfw' | 'block' | 'repost'
|
||||
value: boolean
|
||||
}
|
||||
|
||||
switch (requestData.intent) {
|
||||
case 'block':
|
||||
await (requestData.value ? handleBlock() : handleUnblock())
|
||||
break
|
||||
|
||||
case 'repost':
|
||||
await (requestData.value ? handleRepost() : handleRemoveRepost())
|
||||
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 mod action')
|
||||
break
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
File diff suppressed because it is too large
Load Diff
228
src/pages/mod/loader.ts
Normal file
228
src/pages/mod/loader.ts
Normal file
@ -0,0 +1,228 @@
|
||||
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'
|
||||
import { appRoutes } from 'routes'
|
||||
import { store } from 'store'
|
||||
import {
|
||||
FilterOptions,
|
||||
ModeratedFilter,
|
||||
ModPageLoaderResult,
|
||||
NSFWFilter
|
||||
} from 'types'
|
||||
import {
|
||||
DEFAULT_FILTER_OPTIONS,
|
||||
extractBlogCardDetails,
|
||||
extractModData,
|
||||
getLocalStorageItem,
|
||||
log,
|
||||
LogType
|
||||
} from 'utils'
|
||||
|
||||
export const modRouteLoader =
|
||||
(ndkContext: NDKContextType) =>
|
||||
async ({ params }: LoaderFunctionArgs) => {
|
||||
const { naddr } = params
|
||||
if (!naddr) {
|
||||
log(true, LogType.Error, 'Required naddr.')
|
||||
return redirect(appRoutes.blogs)
|
||||
}
|
||||
|
||||
// Decode from naddr
|
||||
let pubkey: string | undefined
|
||||
let identifier: string | undefined
|
||||
let kind: number | undefined
|
||||
try {
|
||||
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
|
||||
identifier = decoded.data.identifier
|
||||
kind = decoded.data.kind
|
||||
pubkey = decoded.data.pubkey
|
||||
} 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
|
||||
|
||||
try {
|
||||
// Set up the filters
|
||||
// Main mod content
|
||||
const modFilter: NDKFilter = {
|
||||
'#a': [identifier],
|
||||
authors: [pubkey],
|
||||
kinds: [kind]
|
||||
}
|
||||
|
||||
// Get the blog filter options for latest blogs
|
||||
const filterOptions = JSON.parse(
|
||||
getLocalStorageItem('filter-blog', DEFAULT_FILTER_OPTIONS)
|
||||
) as FilterOptions
|
||||
|
||||
// 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: PROFILE_BLOG_FILTER_LIMIT
|
||||
}
|
||||
// Add source filter
|
||||
if (filterOptions.source === window.location.host) {
|
||||
latestFilter['#r'] = [filterOptions.source]
|
||||
}
|
||||
// Filter by NSFW tag
|
||||
// NSFWFilter.Only_NSFW -> fetch with content-warning label
|
||||
// NSFWFilter.Show_NSFW -> filter not needed
|
||||
// NSFWFilter.Hide_NSFW -> up the limit and filter after fetch
|
||||
if (filterOptions.nsfw === NSFWFilter.Only_NSFW) {
|
||||
latestFilter['#L'] = ['content-warning']
|
||||
}
|
||||
|
||||
// Parallel fetch blog event, latest events, mute, and nsfw lists
|
||||
const settled = await Promise.allSettled([
|
||||
ndkContext.fetchEvent(modFilter),
|
||||
ndkContext.fetchEvents(latestFilter),
|
||||
ndkContext.getMuteLists(loggedInUserPubkey), // Pass pubkey for logged-in users
|
||||
ndkContext.getNSFWList()
|
||||
])
|
||||
|
||||
const result: ModPageLoaderResult = {
|
||||
mod: undefined,
|
||||
latest: [],
|
||||
isAddedToNSFW: false,
|
||||
isBlocked: false,
|
||||
isRepost: false
|
||||
}
|
||||
|
||||
// Check the mod event result
|
||||
const fetchEventResult = settled[0]
|
||||
if (fetchEventResult.status === 'fulfilled' && fetchEventResult.value) {
|
||||
// Extract the mod data from the event
|
||||
result.mod = extractModData(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 mod result
|
||||
// Handle it with the react-router's errorComponent
|
||||
if (!result.mod) {
|
||||
throw new Error('We are unable to find the mod 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)
|
||||
} else if (fetchEventsResult.status === 'rejected') {
|
||||
log(
|
||||
true,
|
||||
LogType.Error,
|
||||
'Unable to fetch the latest blog events.',
|
||||
fetchEventsResult.reason
|
||||
)
|
||||
}
|
||||
|
||||
const muteLists = settled[2]
|
||||
if (muteLists.status === 'fulfilled' && muteLists.value) {
|
||||
if (muteLists && muteLists.value) {
|
||||
if (result.mod && result.mod.aTag) {
|
||||
if (
|
||||
muteLists.value.admin.replaceableEvents.includes(
|
||||
result.mod.aTag
|
||||
) ||
|
||||
muteLists.value.user.replaceableEvents.includes(result.mod.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 (muteLists.status === 'rejected') {
|
||||
log(true, LogType.Error, 'Issue fetching mute list', muteLists.reason)
|
||||
}
|
||||
|
||||
const nsfwList = settled[3]
|
||||
if (nsfwList.status === 'fulfilled' && nsfwList.value) {
|
||||
// Check if the mod is marked as NSFW
|
||||
// Mark it as NSFW only if it's missing the tag
|
||||
if (result.mod) {
|
||||
const isMissingNsfwTag =
|
||||
!result.mod.nsfw &&
|
||||
result.mod.aTag &&
|
||||
nsfwList.value.includes(result.mod.aTag)
|
||||
|
||||
if (isMissingNsfwTag) {
|
||||
result.mod.nsfw = true
|
||||
}
|
||||
|
||||
if (result.mod.aTag && nsfwList.value.includes(result.mod.aTag)) {
|
||||
result.isAddedToNSFW = true
|
||||
}
|
||||
}
|
||||
// Check the latest blogs too
|
||||
result.latest = result.latest.map((b) => {
|
||||
// Add nsfw tag if it's missing
|
||||
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)
|
||||
}
|
||||
|
||||
// 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 mod details from relays'
|
||||
log(true, LogType.Error, message, error)
|
||||
if (error instanceof Error) {
|
||||
message = error.message
|
||||
throw new Error(message)
|
||||
}
|
||||
}
|
||||
}
|
@ -5,12 +5,14 @@ import { SearchPage } from '../pages/search'
|
||||
import { AboutPage } from '../pages/about'
|
||||
import { GamesPage } from '../pages/games'
|
||||
import { HomePage } from '../pages/home'
|
||||
import { ModPage } from '../pages/mod'
|
||||
import { ModsPage } from '../pages/mods'
|
||||
import { ModPage } from '../pages/mod'
|
||||
import { modRouteLoader } from '../pages/mod/loader'
|
||||
import { modRouteAction } from '../pages/mod/action'
|
||||
import { SubmitModPage } from '../pages/submitMod'
|
||||
import { ProfilePage } from '../pages/profile'
|
||||
import { profileRouteLoader } from 'pages/profile/loader'
|
||||
import { SettingsPage } from '../pages/settings'
|
||||
import { SubmitModPage } from '../pages/submitMod'
|
||||
import { GamePage } from '../pages/game'
|
||||
import { NotFoundPage } from '../pages/404'
|
||||
import { FeedLayout } from '../layout/feed'
|
||||
@ -18,12 +20,12 @@ import { FeedPage } from '../pages/feed'
|
||||
import { NotificationsPage } from '../pages/notifications'
|
||||
import { WritePage } from '../pages/write'
|
||||
import { writeRouteAction } from '../pages/write/action'
|
||||
import { BlogsPage } from 'pages/blogs'
|
||||
import { blogsRouteLoader } from 'pages/blogs/loader'
|
||||
import { BlogPage } from 'pages/blog'
|
||||
import { blogRouteLoader } from 'pages/blog/loader'
|
||||
import { blogRouteAction } from 'pages/blog/action'
|
||||
import { blogReportRouteAction } from 'pages/blog/reportAction'
|
||||
import { BlogsPage } from '../pages/blogs'
|
||||
import { blogsRouteLoader } from '../pages/blogs/loader'
|
||||
import { BlogPage } from '../pages/blog'
|
||||
import { blogRouteLoader } from '../pages/blog/loader'
|
||||
import { blogRouteAction } from '../pages/blog/action'
|
||||
import { reportRouteAction } from '../actions/report'
|
||||
|
||||
export const appRoutes = {
|
||||
index: '/',
|
||||
@ -32,6 +34,7 @@ export const appRoutes = {
|
||||
game: '/game/:name',
|
||||
mods: '/mods',
|
||||
mod: '/mod/:naddr',
|
||||
modReport_actionOnly: '/mod/:naddr/report',
|
||||
about: '/about',
|
||||
blogs: '/blog',
|
||||
blog: '/blog/:naddr',
|
||||
@ -88,7 +91,14 @@ export const routerWithNdkContext = (context: NDKContextType) =>
|
||||
},
|
||||
{
|
||||
path: appRoutes.mod,
|
||||
element: <ModPage />
|
||||
element: <ModPage />,
|
||||
loader: modRouteLoader(context),
|
||||
action: modRouteAction(context),
|
||||
errorElement: <NotFoundPage title={'Something went wrong.'} />
|
||||
},
|
||||
{
|
||||
path: appRoutes.modReport_actionOnly,
|
||||
action: reportRouteAction(context)
|
||||
},
|
||||
{
|
||||
path: appRoutes.about,
|
||||
@ -115,7 +125,7 @@ export const routerWithNdkContext = (context: NDKContextType) =>
|
||||
},
|
||||
{
|
||||
path: appRoutes.blogReport_actionOnly,
|
||||
action: blogReportRouteAction(context)
|
||||
action: reportRouteAction(context)
|
||||
},
|
||||
{
|
||||
path: appRoutes.submitMod,
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Event } from 'nostr-tools'
|
||||
import { BlogDetails } from 'types'
|
||||
|
||||
export enum CommentEventStatus {
|
||||
Publishing = 'Publishing comment...',
|
||||
@ -26,6 +27,7 @@ export interface ModFormState {
|
||||
featuredImageUrl: string
|
||||
summary: string
|
||||
nsfw: boolean
|
||||
repost: boolean
|
||||
screenshotsUrls: string[]
|
||||
tags: string
|
||||
downloadUrls: DownloadUrl[]
|
||||
@ -52,3 +54,11 @@ export interface MuteLists {
|
||||
authors: string[]
|
||||
replaceableEvents: string[]
|
||||
}
|
||||
|
||||
export interface ModPageLoaderResult {
|
||||
mod: ModDetails | undefined
|
||||
latest: Partial<BlogDetails>[]
|
||||
isAddedToNSFW: boolean
|
||||
isBlocked: boolean
|
||||
isRepost: boolean
|
||||
}
|
||||
|
4
src/types/report.ts
Normal file
4
src/types/report.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export interface ReportReason {
|
||||
label: string
|
||||
key: string
|
||||
}
|
159
src/utils/curationSets.ts
Normal file
159
src/utils/curationSets.ts
Normal file
@ -0,0 +1,159 @@
|
||||
import { NDKFilter } from '@nostr-dev-kit/ndk'
|
||||
import { NDKContextType } from 'contexts/NDKContext'
|
||||
import { UnsignedEvent, kinds } from 'nostr-tools'
|
||||
import { toast } from 'react-toastify'
|
||||
import { UserRelaysType } from 'types'
|
||||
import { now, signAndPublish } from './nostr'
|
||||
|
||||
interface CurationSetArgs {
|
||||
dTag: CurationSetIdentifiers
|
||||
pubkey: string
|
||||
ndkContext: NDKContextType
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for dTag when updating
|
||||
*/
|
||||
export enum CurationSetIdentifiers {
|
||||
NSFW = 'nsfw',
|
||||
Repost = 'repost'
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the article curation set (kind: 30004) for the user (pubkey)
|
||||
* with initial article (optional)
|
||||
* @see https://github.com/nostr-protocol/nips/blob/master/51.md#sets
|
||||
*/
|
||||
export async function createCurationSet(
|
||||
{ dTag, pubkey, ndkContext }: CurationSetArgs,
|
||||
initialArticle?: string
|
||||
) {
|
||||
const curationSetName = `${dTag} - Degmods's curation set`
|
||||
const tags = [
|
||||
['d', dTag],
|
||||
['title', curationSetName]
|
||||
]
|
||||
if (initialArticle) {
|
||||
tags.push(['a', initialArticle])
|
||||
}
|
||||
const unsignedEvent: UnsignedEvent = {
|
||||
pubkey: pubkey,
|
||||
kind: kinds.Curationsets,
|
||||
content: '',
|
||||
created_at: now(),
|
||||
tags
|
||||
}
|
||||
const isUpdated = await signAndPublish(
|
||||
unsignedEvent,
|
||||
ndkContext.ndk,
|
||||
ndkContext.publish
|
||||
)
|
||||
if (!isUpdated) {
|
||||
toast.error(`Failed to create user's ${dTag} curation set`)
|
||||
}
|
||||
return isUpdated
|
||||
}
|
||||
|
||||
export async function getCurationSet({
|
||||
dTag,
|
||||
pubkey,
|
||||
ndkContext
|
||||
}: CurationSetArgs) {
|
||||
const filter: NDKFilter = {
|
||||
kinds: [kinds.Curationsets],
|
||||
authors: [pubkey],
|
||||
'#d': [dTag]
|
||||
}
|
||||
|
||||
const nsfwListEvent = await ndkContext.fetchEventFromUserRelays(
|
||||
filter,
|
||||
pubkey,
|
||||
UserRelaysType.Write
|
||||
)
|
||||
|
||||
return nsfwListEvent
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the article curation set (kind: 30004) for the user (pubkey)
|
||||
* Add the aTag to the dTag list
|
||||
* @see https://github.com/nostr-protocol/nips/blob/master/51.md#sets
|
||||
*/
|
||||
export async function addToCurationSet(
|
||||
curationSetArgs: CurationSetArgs,
|
||||
aTag: string
|
||||
) {
|
||||
const curationSetEvent = await getCurationSet(curationSetArgs)
|
||||
|
||||
let isUpdated = false
|
||||
if (curationSetEvent) {
|
||||
const { dTag, pubkey, ndkContext } = curationSetArgs
|
||||
const tags = curationSetEvent.tags
|
||||
const alreadyExists =
|
||||
tags.findIndex((item) => item[0] === 'a' && item[1] === aTag) !== -1
|
||||
|
||||
if (alreadyExists) {
|
||||
toast.warn(`Already in user's ${dTag} list`)
|
||||
return false
|
||||
}
|
||||
|
||||
tags.push(['a', aTag])
|
||||
|
||||
const unsignedEvent = {
|
||||
pubkey: pubkey,
|
||||
kind: kinds.Curationsets,
|
||||
content: curationSetEvent.content,
|
||||
created_at: now(),
|
||||
tags: [...tags]
|
||||
}
|
||||
|
||||
isUpdated = await signAndPublish(
|
||||
unsignedEvent,
|
||||
ndkContext.ndk,
|
||||
ndkContext.publish
|
||||
)
|
||||
if (!isUpdated) {
|
||||
toast.error(`Failed to update user's ${dTag} list`)
|
||||
}
|
||||
} else {
|
||||
isUpdated = await createCurationSet(curationSetArgs, aTag)
|
||||
}
|
||||
|
||||
return isUpdated
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the article curation set (kind: 30004) for the user (pubkey)
|
||||
* Remove the aTag from the dTag list
|
||||
*/
|
||||
export async function removeFromCurationSet(
|
||||
curationSetArgs: CurationSetArgs,
|
||||
aTag: string
|
||||
) {
|
||||
const curationSetEvent = await getCurationSet(curationSetArgs)
|
||||
const { dTag, pubkey, ndkContext } = curationSetArgs
|
||||
|
||||
if (!curationSetEvent) {
|
||||
toast.error(`Couldn't get ${dTag} list event from relays`)
|
||||
return false
|
||||
}
|
||||
const tags = curationSetEvent.tags
|
||||
|
||||
const unsignedEvent: UnsignedEvent = {
|
||||
pubkey: pubkey,
|
||||
kind: kinds.Curationsets,
|
||||
content: curationSetEvent.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 ${dTag} list`)
|
||||
}
|
||||
return isUpdated
|
||||
}
|
@ -6,3 +6,4 @@ export * from './zap'
|
||||
export * from './localStorage'
|
||||
export * from './consts'
|
||||
export * from './blog'
|
||||
export * from './curationSets'
|
||||
|
@ -39,6 +39,7 @@ export const extractModData = (event: Event | NDKEvent): ModDetails => {
|
||||
featuredImageUrl: getFirstTagValue('featuredImageUrl'),
|
||||
summary: getFirstTagValue('summary'),
|
||||
nsfw: getFirstTagValue('nsfw') === 'true',
|
||||
repost: getFirstTagValue('repost') === 'true',
|
||||
screenshotsUrls: getTagValue(event, 'screenshotsUrls') || [],
|
||||
tags: getTagValue(event, 'tags') || [],
|
||||
downloadUrls: (getTagValue(event, 'downloadUrls') || []).map((item) =>
|
||||
@ -118,6 +119,7 @@ export const initializeFormState = (
|
||||
featuredImageUrl: existingModData?.featuredImageUrl || '',
|
||||
summary: existingModData?.summary || '',
|
||||
nsfw: existingModData?.nsfw || false,
|
||||
repost: existingModData?.repost || false,
|
||||
screenshotsUrls: existingModData?.screenshotsUrls || [''],
|
||||
tags: existingModData?.tags.join(',') || '',
|
||||
downloadUrls: existingModData?.downloadUrls || [
|
||||
|
Loading…
x
Reference in New Issue
Block a user