Compare commits
No commits in common. "cd150142f80a2aed0874c9c98458523bc6b791b2" and "e06a8d6e9601bbafdae326dc0caa2157f3a56487" have entirely different histories.
cd150142f8
...
e06a8d6e96
@ -1,8 +1,4 @@
|
|||||||
# This relay will be used to publish/retrieve events along with other relays (user's relays, admin relays)
|
# This relay will be used to publish/retrieve events along with other relays (user's relays, admin relays)
|
||||||
VITE_APP_RELAY=wss://relay.degmods.com
|
VITE_APP_RELAY=wss://relay.degmods.com
|
||||||
|
|
||||||
# A comma separated list of npubs, Relay list will be extracted for these npubs and this relay list will be used to publish event
|
# A comma separated list of npubs, Relay list will be extracted for these npubs and this relay list will be used to publish event
|
||||||
VITE_ADMIN_NPUBS= <A comma separated list of npubs>
|
VITE_ADMIN_NPUBS= <A comma separated list of npubs>
|
||||||
|
|
||||||
# A dedicated npub used for reporting mods, blogs, profile and etc.
|
|
||||||
VITE_REPORTING_NPUB= <npub1...>
|
|
@ -24,7 +24,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "VITE_APP_RELAY=${{ vars.VITE_APP_RELAY }}" >> .env
|
echo "VITE_APP_RELAY=${{ vars.VITE_APP_RELAY }}" >> .env
|
||||||
echo "VITE_ADMIN_NPUBS=${{ vars.VITE_ADMIN_NPUBS }}" >> .env
|
echo "VITE_ADMIN_NPUBS=${{ vars.VITE_ADMIN_NPUBS }}" >> .env
|
||||||
echo "VITE_REPORTING_NPUB=${{ vars.VITE_REPORTING_NPUB }}" >> .env
|
|
||||||
cat .env
|
cat .env
|
||||||
|
|
||||||
- name: Create Build
|
- name: Create Build
|
||||||
|
@ -24,7 +24,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "VITE_APP_RELAY=${{ vars.VITE_APP_RELAY }}" >> .env
|
echo "VITE_APP_RELAY=${{ vars.VITE_APP_RELAY }}" >> .env
|
||||||
echo "VITE_ADMIN_NPUBS=${{ vars.VITE_ADMIN_NPUBS }}" >> .env
|
echo "VITE_ADMIN_NPUBS=${{ vars.VITE_ADMIN_NPUBS }}" >> .env
|
||||||
echo "VITE_REPORTING_NPUB=${{ vars.VITE_REPORTING_NPUB }}" >> .env
|
|
||||||
cat .env
|
cat .env
|
||||||
|
|
||||||
- name: Create Build
|
- name: Create Build
|
||||||
|
6
package-lock.json
generated
6
package-lock.json
generated
@ -3808,9 +3808,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/micromatch": {
|
"node_modules/micromatch": {
|
||||||
"version": "4.0.8",
|
"version": "4.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
|
||||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"braces": "^3.0.3",
|
"braces": "^3.0.3",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import Link from '@tiptap/extension-link'
|
import Link from '@tiptap/extension-link'
|
||||||
import { Editor, EditorContent, useEditor } from '@tiptap/react'
|
import { EditorProvider, useCurrentEditor } from '@tiptap/react'
|
||||||
import StarterKit from '@tiptap/starter-kit'
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
import React, { useEffect } from 'react'
|
import React from 'react'
|
||||||
import '../styles/styles.css'
|
import '../styles/styles.css'
|
||||||
import '../styles/tiptap.scss'
|
import '../styles/tiptap.scss'
|
||||||
|
|
||||||
@ -112,56 +112,34 @@ type RichTextEditorProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const RichTextEditor = ({ content, updateContent }: RichTextEditorProps) => {
|
const RichTextEditor = ({ content, updateContent }: RichTextEditorProps) => {
|
||||||
const editor = useEditor({
|
|
||||||
extensions: [StarterKit, Link],
|
|
||||||
onUpdate: ({ editor }) => {
|
|
||||||
// Update the state when the editor content changes
|
|
||||||
updateContent(editor.getHTML())
|
|
||||||
},
|
|
||||||
content
|
|
||||||
})
|
|
||||||
|
|
||||||
// Update editor content when the `content` prop changes
|
|
||||||
useEffect(() => {
|
|
||||||
if (editor && editor.getHTML() !== content) {
|
|
||||||
editor.commands.setContent(content, false)
|
|
||||||
}
|
|
||||||
}, [content, editor])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='inputMain'>
|
<div className='inputMain'>
|
||||||
{editor && (
|
<EditorProvider
|
||||||
<>
|
slotBefore={<MenuBar />}
|
||||||
<MenuBar editor={editor} />
|
extensions={[StarterKit, Link]}
|
||||||
<EditorContent editor={editor} />
|
content={content}
|
||||||
</>
|
onUpdate={({ editor }) => {
|
||||||
)}
|
// Update the state when the editor content changes
|
||||||
|
updateContent(editor.getHTML())
|
||||||
|
}}
|
||||||
|
></EditorProvider>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type MenuBarProps = {
|
const MenuBar = () => {
|
||||||
editor: Editor
|
const { editor } = useCurrentEditor()
|
||||||
}
|
|
||||||
|
if (!editor) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
const MenuBar = ({ editor }: MenuBarProps) => {
|
|
||||||
const setLink = () => {
|
const setLink = () => {
|
||||||
// Prompt the user to enter a URL
|
const url = prompt('URL')
|
||||||
let url = prompt('URL')
|
|
||||||
|
|
||||||
// Check if the user provided a URL
|
|
||||||
if (url) {
|
if (url) {
|
||||||
// If the URL doesn't start with 'http://' or 'https://',
|
|
||||||
// prepend 'https://' to the URL
|
|
||||||
if (!/^(http|https):\/\//i.test(url)) {
|
|
||||||
url = `https://${url}`
|
|
||||||
}
|
|
||||||
|
|
||||||
return editor.chain().focus().setLink({ href: url }).run()
|
return editor.chain().focus().setLink({ href: url }).run()
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no URL was provided (e.g., the user cancels the prompt),
|
|
||||||
// return false, indicating that the link was not set.
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,7 +4,6 @@ type ModCardProps = {
|
|||||||
title: string
|
title: string
|
||||||
summary: string
|
summary: string
|
||||||
backgroundLink: string
|
backgroundLink: string
|
||||||
link: string
|
|
||||||
handleClick: () => void
|
handleClick: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -12,18 +11,10 @@ export const ModCard = ({
|
|||||||
title,
|
title,
|
||||||
summary,
|
summary,
|
||||||
backgroundLink,
|
backgroundLink,
|
||||||
link,
|
|
||||||
handleClick
|
handleClick
|
||||||
}: ModCardProps) => {
|
}: ModCardProps) => {
|
||||||
return (
|
return (
|
||||||
<a
|
<a className='cardModMainWrapperLink' onClick={handleClick}>
|
||||||
className='cardModMainWrapperLink'
|
|
||||||
href={link}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.preventDefault()
|
|
||||||
handleClick()
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className='cardModMain'>
|
<div className='cardModMain'>
|
||||||
<div
|
<div
|
||||||
className='cMMPicture'
|
className='cMMPicture'
|
||||||
|
@ -19,7 +19,6 @@ export class MetadataController {
|
|||||||
private usersMetadata = new Map<string, UserProfile>()
|
private usersMetadata = new Map<string, UserProfile>()
|
||||||
public adminNpubs: string[]
|
public adminNpubs: string[]
|
||||||
public adminRelays = new Set<string>()
|
public adminRelays = new Set<string>()
|
||||||
public reportingNpub: string
|
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this.ndk = new NDK({
|
this.ndk = new NDK({
|
||||||
@ -41,7 +40,6 @@ export class MetadataController {
|
|||||||
})
|
})
|
||||||
|
|
||||||
this.adminNpubs = import.meta.env.VITE_ADMIN_NPUBS.split(',')
|
this.adminNpubs = import.meta.env.VITE_ADMIN_NPUBS.split(',')
|
||||||
this.reportingNpub = import.meta.env.VITE_REPORTING_NPUB
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private setAdminRelays = async () => {
|
private setAdminRelays = async () => {
|
||||||
@ -131,110 +129,38 @@ export class MetadataController {
|
|||||||
return ndkRelayList[userRelaysType]
|
return ndkRelayList[userRelaysType]
|
||||||
}
|
}
|
||||||
|
|
||||||
public getMuteLists = async (
|
public getAdminsMuteLists = async (): Promise<MuteLists> => {
|
||||||
pubkey?: string
|
// Create a Set to collect all unique muted authors
|
||||||
): Promise<{
|
|
||||||
admin: MuteLists
|
|
||||||
user: MuteLists
|
|
||||||
}> => {
|
|
||||||
const adminMutedAuthors = new Set<string>()
|
|
||||||
const adminMutedPosts = new Set<string>()
|
|
||||||
|
|
||||||
const adminHexKey = npubToHex(this.reportingNpub)
|
const mutedAuthors = new Set<string>()
|
||||||
|
|
||||||
|
// Create an array of promises to fetch mute lists for each npub
|
||||||
|
const promises = this.adminNpubs.map(async (npub) => {
|
||||||
|
const hexKey = npubToHex(npub)
|
||||||
|
if (!hexKey) return
|
||||||
|
|
||||||
if (adminHexKey) {
|
|
||||||
const muteListEvent = await this.ndk.fetchEvent({
|
const muteListEvent = await this.ndk.fetchEvent({
|
||||||
kinds: [kinds.Mutelist],
|
kinds: [kinds.Mutelist],
|
||||||
authors: [adminHexKey]
|
authors: [hexKey]
|
||||||
})
|
})
|
||||||
|
|
||||||
if (muteListEvent) {
|
if (muteListEvent) {
|
||||||
const list = NDKList.from(muteListEvent)
|
const list = NDKList.from(muteListEvent)
|
||||||
|
|
||||||
list.items.forEach((item) => {
|
list.items.forEach((item) => {
|
||||||
|
// Add muted authors to the Set directly
|
||||||
if (item[0] === 'p') {
|
if (item[0] === 'p') {
|
||||||
adminMutedAuthors.add(item[1])
|
mutedAuthors.add(item[1])
|
||||||
} else if (item[0] === 'a') {
|
|
||||||
adminMutedPosts.add(item[1])
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
const userMutedAuthors = new Set<string>()
|
await Promise.allSettled(promises)
|
||||||
const userMutedPosts = new Set<string>()
|
|
||||||
|
|
||||||
if (pubkey) {
|
|
||||||
const userHexKey = npubToHex(pubkey)
|
|
||||||
|
|
||||||
if (userHexKey) {
|
|
||||||
const muteListEvent = await this.ndk.fetchEvent({
|
|
||||||
kinds: [kinds.Mutelist],
|
|
||||||
authors: [userHexKey]
|
|
||||||
})
|
|
||||||
|
|
||||||
if (muteListEvent) {
|
|
||||||
const list = NDKList.from(muteListEvent)
|
|
||||||
|
|
||||||
list.items.forEach((item) => {
|
|
||||||
if (item[0] === 'p') {
|
|
||||||
userMutedAuthors.add(item[1])
|
|
||||||
} else if (item[0] === 'a') {
|
|
||||||
userMutedPosts.add(item[1])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
admin: {
|
authors: Array.from(mutedAuthors),
|
||||||
authors: Array.from(adminMutedAuthors),
|
eventIds: []
|
||||||
replaceableEvents: Array.from(adminMutedPosts)
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
authors: Array.from(userMutedAuthors),
|
|
||||||
replaceableEvents: Array.from(userMutedPosts)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves a list of NSFW (Not Safe For Work) posts that were not specified as NSFW by post author but marked as NSFW by admin.
|
|
||||||
*
|
|
||||||
* @returns {Promise<string[]>} - A promise that resolves to an array of NSFW post identifiers (e.g., URLs or IDs).
|
|
||||||
*/
|
|
||||||
public getNSFWList = async (): Promise<string[]> => {
|
|
||||||
// Initialize an array to store the NSFW post identifiers
|
|
||||||
const nsfwPosts: string[] = []
|
|
||||||
|
|
||||||
// Convert the public key (npub) to a hexadecimal format
|
|
||||||
const hexKey = npubToHex(this.reportingNpub)
|
|
||||||
|
|
||||||
// If the conversion is successful and we have a hexKey
|
|
||||||
if (hexKey) {
|
|
||||||
// Fetch the event that contains the NSFW list
|
|
||||||
const nsfwListEvent = await this.ndk.fetchEvent({
|
|
||||||
kinds: [kinds.Curationsets],
|
|
||||||
authors: [hexKey],
|
|
||||||
'#d': ['nsfw']
|
|
||||||
})
|
|
||||||
|
|
||||||
if (nsfwListEvent) {
|
|
||||||
// Convert the event data to an NDKList, which is a structured list format
|
|
||||||
const list = NDKList.from(nsfwListEvent)
|
|
||||||
|
|
||||||
// Iterate through the items in the list
|
|
||||||
list.items.forEach((item) => {
|
|
||||||
if (item[0] === 'a') {
|
|
||||||
// Add the identifier of the NSFW post to the nsfwPosts array
|
|
||||||
nsfwPosts.push(item[1])
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return the array of NSFW post identifiers
|
|
||||||
return nsfwPosts
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { Event, Filter, kinds, Relay } from 'nostr-tools'
|
import { Event, Filter, kinds, Relay } from 'nostr-tools'
|
||||||
import { ModDetails } from '../types'
|
|
||||||
import {
|
import {
|
||||||
extractZapAmount,
|
extractZapAmount,
|
||||||
log,
|
log,
|
||||||
@ -8,6 +7,7 @@ import {
|
|||||||
timeout
|
timeout
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { MetadataController, UserRelaysType } from './metadata'
|
import { MetadataController, UserRelaysType } from './metadata'
|
||||||
|
import { ModDetails } from '../types'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Singleton class to manage relay operations.
|
* Singleton class to manage relay operations.
|
||||||
@ -155,103 +155,6 @@ export class RelayController {
|
|||||||
return publishedOnRelays
|
return publishedOnRelays
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Publishes an encrypted DM to receiver's read relays.
|
|
||||||
*
|
|
||||||
* This method connects to the application relay and a set of receiver's read relays
|
|
||||||
* obtained from the `MetadataController`. It then publishes the event to
|
|
||||||
* all connected relays and returns a list of relays where the event was successfully published.
|
|
||||||
*
|
|
||||||
* @param event - The event to be published.
|
|
||||||
* @returns A promise that resolves to an array of URLs of relays where the event was published,
|
|
||||||
* or an empty array if no relays were connected or the event could not be published.
|
|
||||||
*/
|
|
||||||
publishDM = async (event: Event, receiver: string): Promise<string[]> => {
|
|
||||||
// Connect to the application relay specified by environment variable
|
|
||||||
const appRelayPromise = this.connectRelay(import.meta.env.VITE_APP_RELAY)
|
|
||||||
|
|
||||||
// todo: window.nostr.getRelays() is not implemented yet in nostr-login, implement the logic once its done
|
|
||||||
|
|
||||||
const metadataController = await MetadataController.getInstance()
|
|
||||||
|
|
||||||
// Retrieve the list of read relays for the receiver
|
|
||||||
// Use a timeout to handle cases where retrieving read relays takes too long
|
|
||||||
const readRelaysPromise = metadataController.findUserRelays(
|
|
||||||
receiver,
|
|
||||||
UserRelaysType.Read
|
|
||||||
)
|
|
||||||
|
|
||||||
log(this.debug, LogType.Info, `ℹ Finding receiver's read relays`)
|
|
||||||
|
|
||||||
// Use Promise.race to either get the read relay URLs or timeout
|
|
||||||
const readRelayUrls = await Promise.race([
|
|
||||||
readRelaysPromise,
|
|
||||||
timeout() // This is a custom timeout function that rejects the promise after a specified time
|
|
||||||
]).catch((err) => {
|
|
||||||
log(this.debug, LogType.Error, err)
|
|
||||||
return [] as string[] // Return an empty array if an error occurs
|
|
||||||
})
|
|
||||||
|
|
||||||
// push admin relay urls obtained from metadata controller to readRelayUrls list
|
|
||||||
metadataController.adminRelays.forEach((url) => {
|
|
||||||
readRelayUrls.push(url)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Connect to all write relays obtained from MetadataController
|
|
||||||
const relayPromises = readRelayUrls.map((relayUrl) =>
|
|
||||||
this.connectRelay(relayUrl)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Wait for all relay connections to settle (either fulfilled or rejected)
|
|
||||||
await Promise.allSettled([appRelayPromise, ...relayPromises])
|
|
||||||
|
|
||||||
// Check if any relays are connected; if not, log an error and return null
|
|
||||||
if (this.connectedRelays.length === 0) {
|
|
||||||
log(this.debug, LogType.Error, 'No relay is connected!')
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
const publishedOnRelays: string[] = [] // List to track which relays successfully published the event
|
|
||||||
|
|
||||||
// Create a promise for publishing the event to each connected relay
|
|
||||||
const publishPromises = this.connectedRelays.map((relay) => {
|
|
||||||
log(
|
|
||||||
this.debug,
|
|
||||||
LogType.Info,
|
|
||||||
`⬆️ nostr (${relay.url}): Sending event:`,
|
|
||||||
event
|
|
||||||
)
|
|
||||||
|
|
||||||
return Promise.race([
|
|
||||||
relay.publish(event), // Publish the event to the relay
|
|
||||||
timeout(30000) // Set a timeout to handle cases where publishing takes too long
|
|
||||||
])
|
|
||||||
.then((res) => {
|
|
||||||
log(
|
|
||||||
this.debug,
|
|
||||||
LogType.Info,
|
|
||||||
`⬆️ nostr (${relay.url}): Publish result:`,
|
|
||||||
res
|
|
||||||
)
|
|
||||||
publishedOnRelays.push(relay.url) // Add the relay URL to the list of successfully published relays
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
log(
|
|
||||||
this.debug,
|
|
||||||
LogType.Error,
|
|
||||||
`❌ nostr (${relay.url}): Publish error!`,
|
|
||||||
err
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Wait for all publish operations to complete (either fulfilled or rejected)
|
|
||||||
await Promise.allSettled(publishPromises)
|
|
||||||
|
|
||||||
// Return the list of relay URLs where the event was published
|
|
||||||
return publishedOnRelays
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asynchronously retrieves multiple event from a set of relays based on a provided filter.
|
* Asynchronously retrieves multiple event from a set of relays based on a provided filter.
|
||||||
* If no relays are specified, it defaults to using connected relays.
|
* If no relays are specified, it defaults to using connected relays.
|
||||||
@ -344,46 +247,6 @@ export class RelayController {
|
|||||||
return events[0] || null
|
return events[0] || null
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches an event from the user's relays based on a specified filter.
|
|
||||||
* The function first retrieves the user's relays, and then fetches the event using the provided filter.
|
|
||||||
*
|
|
||||||
* @param filter - The event filter to use when fetching the event (e.g., kinds, authors).
|
|
||||||
* @param hexKey - The hexadecimal representation of the user's public key.
|
|
||||||
* @param userRelaysType - The type of relays to search (e.g., write, read).
|
|
||||||
* @returns A promise that resolves to the fetched event or null if the operation fails.
|
|
||||||
*/
|
|
||||||
fetchEventFromUserRelays = async (
|
|
||||||
filter: Filter,
|
|
||||||
hexKey: string,
|
|
||||||
userRelaysType: UserRelaysType
|
|
||||||
) => {
|
|
||||||
// Get an instance of the MetadataController, which manages user metadata and relays
|
|
||||||
const metadataController = await MetadataController.getInstance()
|
|
||||||
|
|
||||||
// Find the user's relays using the MetadataController. This is an asynchronous operation.
|
|
||||||
const usersRelaysPromise = metadataController.findUserRelays(
|
|
||||||
hexKey,
|
|
||||||
userRelaysType
|
|
||||||
)
|
|
||||||
|
|
||||||
log(true, LogType.Info, `ℹ Finding user's relays`)
|
|
||||||
|
|
||||||
// Use Promise.race to attempt to resolve the user's relays, or timeout if it takes too long
|
|
||||||
const relayUrls = await Promise.race([
|
|
||||||
usersRelaysPromise,
|
|
||||||
timeout() // This is a custom timeout function that rejects the promise after a specified time
|
|
||||||
]).catch((err) => {
|
|
||||||
// Log an error if the relay fetching operation fails
|
|
||||||
log(true, LogType.Error, err)
|
|
||||||
// Return an empty array to indicate failure in retrieving relay URLs
|
|
||||||
return [] as string[]
|
|
||||||
})
|
|
||||||
|
|
||||||
// Fetch the event from the user's relays using the provided filter and relay URLs
|
|
||||||
return this.fetchEvent(filter, relayUrls)
|
|
||||||
}
|
|
||||||
|
|
||||||
getTotalZapAmount = async (
|
getTotalZapAmount = async (
|
||||||
modDetails: ModDetails,
|
modDetails: ModDetails,
|
||||||
currentLoggedInUser?: string
|
currentLoggedInUser?: string
|
||||||
|
@ -154,7 +154,6 @@ export const HomePage = () => {
|
|||||||
title='Placeholder'
|
title='Placeholder'
|
||||||
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
|
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
|
||||||
backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png'
|
backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png'
|
||||||
link=''
|
|
||||||
handleClick={() => {
|
handleClick={() => {
|
||||||
alert(
|
alert(
|
||||||
'these are dummy mods. So navigation on these are not implemented yet'
|
'these are dummy mods. So navigation on these are not implemented yet'
|
||||||
@ -165,7 +164,6 @@ export const HomePage = () => {
|
|||||||
title='Placeholder'
|
title='Placeholder'
|
||||||
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
|
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
|
||||||
backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png'
|
backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png'
|
||||||
link=''
|
|
||||||
handleClick={() => {
|
handleClick={() => {
|
||||||
alert(
|
alert(
|
||||||
'these are dummy mods. So navigation on these are not implemented yet'
|
'these are dummy mods. So navigation on these are not implemented yet'
|
||||||
@ -176,7 +174,6 @@ export const HomePage = () => {
|
|||||||
title='Placeholder'
|
title='Placeholder'
|
||||||
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
|
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
|
||||||
backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png'
|
backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png'
|
||||||
link=''
|
|
||||||
handleClick={() => {
|
handleClick={() => {
|
||||||
alert(
|
alert(
|
||||||
'these are dummy mods. So navigation on these are not implemented yet'
|
'these are dummy mods. So navigation on these are not implemented yet'
|
||||||
@ -203,7 +200,6 @@ export const HomePage = () => {
|
|||||||
title='Placeholder'
|
title='Placeholder'
|
||||||
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
|
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
|
||||||
backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png'
|
backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png'
|
||||||
link=''
|
|
||||||
handleClick={() => {
|
handleClick={() => {
|
||||||
alert(
|
alert(
|
||||||
'these are dummy mods. So navigation on these are not implemented yet'
|
'these are dummy mods. So navigation on these are not implemented yet'
|
||||||
@ -214,7 +210,6 @@ export const HomePage = () => {
|
|||||||
title='Placeholder'
|
title='Placeholder'
|
||||||
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
|
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
|
||||||
backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png'
|
backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png'
|
||||||
link=''
|
|
||||||
handleClick={() => {
|
handleClick={() => {
|
||||||
alert(
|
alert(
|
||||||
'these are dummy mods. So navigation on these are not implemented yet'
|
'these are dummy mods. So navigation on these are not implemented yet'
|
||||||
@ -225,7 +220,6 @@ export const HomePage = () => {
|
|||||||
title='Placeholder'
|
title='Placeholder'
|
||||||
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
|
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
|
||||||
backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png'
|
backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png'
|
||||||
link=''
|
|
||||||
handleClick={() => {
|
handleClick={() => {
|
||||||
alert(
|
alert(
|
||||||
'these are dummy mods. So navigation on these are not implemented yet'
|
'these are dummy mods. So navigation on these are not implemented yet'
|
||||||
@ -236,7 +230,6 @@ export const HomePage = () => {
|
|||||||
title='Placeholder'
|
title='Placeholder'
|
||||||
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
|
summary='Lorem ipsum dolor sit amet, consectetur adipiscing elit.'
|
||||||
backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png'
|
backgroundLink='/assets/img/DEGMods%20Placeholder%20Img.png'
|
||||||
link=''
|
|
||||||
handleClick={() => {
|
handleClick={() => {
|
||||||
alert(
|
alert(
|
||||||
'these are dummy mods. So navigation on these are not implemented yet'
|
'these are dummy mods. So navigation on these are not implemented yet'
|
||||||
|
@ -2,15 +2,8 @@ import Link from '@tiptap/extension-link'
|
|||||||
import { EditorContent, useEditor } from '@tiptap/react'
|
import { EditorContent, useEditor } from '@tiptap/react'
|
||||||
import StarterKit from '@tiptap/starter-kit'
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
import { formatDate } from 'date-fns'
|
import { formatDate } from 'date-fns'
|
||||||
import { Filter, kinds, nip19, UnsignedEvent } from 'nostr-tools'
|
import { Filter, nip19 } from 'nostr-tools'
|
||||||
import {
|
import { Dispatch, SetStateAction, useCallback, useRef, useState } from 'react'
|
||||||
Dispatch,
|
|
||||||
SetStateAction,
|
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState
|
|
||||||
} from 'react'
|
|
||||||
import { useNavigate, useParams } from 'react-router-dom'
|
import { useNavigate, useParams } from 'react-router-dom'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { BlogCard } from '../components/BlogCard'
|
import { BlogCard } from '../components/BlogCard'
|
||||||
@ -20,7 +13,6 @@ import { ZapButtons, ZapPresets, ZapQR } from '../components/Zap'
|
|||||||
import {
|
import {
|
||||||
MetadataController,
|
MetadataController,
|
||||||
RelayController,
|
RelayController,
|
||||||
UserRelaysType,
|
|
||||||
ZapController
|
ZapController
|
||||||
} from '../controllers'
|
} from '../controllers'
|
||||||
import { useAppSelector, useDidMount } from '../hooks'
|
import { useAppSelector, useDidMount } from '../hooks'
|
||||||
@ -45,11 +37,7 @@ import {
|
|||||||
getFilenameFromUrl,
|
getFilenameFromUrl,
|
||||||
log,
|
log,
|
||||||
LogType,
|
LogType,
|
||||||
now,
|
unformatNumber
|
||||||
npubToHex,
|
|
||||||
sendDMUsingRandomKey,
|
|
||||||
unformatNumber,
|
|
||||||
signAndPublish
|
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
|
|
||||||
export const InnerModPage = () => {
|
export const InnerModPage = () => {
|
||||||
@ -126,7 +114,6 @@ export const InnerModPage = () => {
|
|||||||
naddr={naddr!}
|
naddr={naddr!}
|
||||||
game={modData.game}
|
game={modData.game}
|
||||||
author={modData.author}
|
author={modData.author}
|
||||||
aTag={modData.aTag}
|
|
||||||
/>
|
/>
|
||||||
<Body
|
<Body
|
||||||
featuredImageUrl={modData.featuredImageUrl}
|
featuredImageUrl={modData.featuredImageUrl}
|
||||||
@ -210,708 +197,128 @@ type GameProps = {
|
|||||||
naddr: string
|
naddr: string
|
||||||
game: string
|
game: string
|
||||||
author: string
|
author: string
|
||||||
aTag: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const Game = ({ naddr, game, author, aTag }: GameProps) => {
|
const Game = ({ naddr, game, author }: GameProps) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const userState = useAppSelector((state) => state.user)
|
const userState = useAppSelector((state) => state.user)
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
|
||||||
const [showReportPopUp, setShowReportPopUp] = useState(false)
|
|
||||||
const [isBlocked, setIsBlocked] = useState(false)
|
|
||||||
const [isAddedToNSFW, setIsAddedToNSFW] = useState(false)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (userState.auth && userState.user?.pubkey) {
|
|
||||||
const pubkey = userState.user.pubkey as string
|
|
||||||
|
|
||||||
const muteListFilter: Filter = {
|
|
||||||
kinds: [kinds.Mutelist],
|
|
||||||
authors: [pubkey]
|
|
||||||
}
|
|
||||||
|
|
||||||
RelayController.getInstance()
|
|
||||||
.fetchEventFromUserRelays(muteListFilter, pubkey, UserRelaysType.Write)
|
|
||||||
.then((event) => {
|
|
||||||
if (event) {
|
|
||||||
// get a list of tags
|
|
||||||
const tags = event.tags
|
|
||||||
const blocked =
|
|
||||||
tags.findIndex((item) => item[0] === 'a' && item[1] === aTag) !==
|
|
||||||
-1
|
|
||||||
|
|
||||||
setIsBlocked(blocked)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (
|
|
||||||
userState.user.npub &&
|
|
||||||
userState.user.npub === import.meta.env.VITE_REPORTING_NPUB
|
|
||||||
) {
|
|
||||||
const nsfwListFilter: Filter = {
|
|
||||||
kinds: [kinds.Curationsets],
|
|
||||||
authors: [pubkey],
|
|
||||||
'#d': ['nsfw']
|
|
||||||
}
|
|
||||||
|
|
||||||
RelayController.getInstance()
|
|
||||||
.fetchEventFromUserRelays(
|
|
||||||
nsfwListFilter,
|
|
||||||
pubkey,
|
|
||||||
UserRelaysType.Write
|
|
||||||
)
|
|
||||||
.then((event) => {
|
|
||||||
if (event) {
|
|
||||||
// get a list of tags
|
|
||||||
const tags = event.tags
|
|
||||||
const existsInNSFWList =
|
|
||||||
tags.findIndex(
|
|
||||||
(item) => item[0] === 'a' && item[1] === aTag
|
|
||||||
) !== -1
|
|
||||||
|
|
||||||
setIsAddedToNSFW(existsInNSFWList)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [userState, aTag])
|
|
||||||
|
|
||||||
const handleBlock = async () => {
|
|
||||||
let hexPubkey: string
|
|
||||||
|
|
||||||
setIsLoading(true)
|
|
||||||
setLoadingSpinnerDesc('Getting user pubkey')
|
|
||||||
|
|
||||||
if (userState.auth && userState.user?.pubkey) {
|
|
||||||
hexPubkey = userState.user.pubkey as string
|
|
||||||
} else {
|
|
||||||
hexPubkey = (await window.nostr?.getPublicKey()) as string
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hexPubkey) {
|
|
||||||
toast.error('Could not get pubkey for updating mute list')
|
|
||||||
setIsLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc(`Finding user's mute list`)
|
|
||||||
|
|
||||||
// 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: Filter = {
|
|
||||||
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 RelayController.getInstance().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) {
|
|
||||||
setIsLoading(false)
|
|
||||||
setIsBlocked(true)
|
|
||||||
return toast.warn(`Mod reference is already in user's mute list`)
|
|
||||||
}
|
|
||||||
|
|
||||||
tags.push(['a', aTag])
|
|
||||||
|
|
||||||
unsignedEvent = {
|
|
||||||
pubkey: muteListEvent.pubkey,
|
|
||||||
kind: muteListEvent.kind,
|
|
||||||
content: muteListEvent.content,
|
|
||||||
created_at: now(),
|
|
||||||
tags: [...tags]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
unsignedEvent = {
|
|
||||||
pubkey: hexPubkey,
|
|
||||||
kind: kinds.Mutelist,
|
|
||||||
content: '',
|
|
||||||
created_at: now(),
|
|
||||||
tags: [['a', aTag]]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Updating mute list event')
|
|
||||||
|
|
||||||
const isUpdated = await signAndPublish(unsignedEvent)
|
|
||||||
if (isUpdated) {
|
|
||||||
setIsBlocked(true)
|
|
||||||
}
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUnblock = async () => {
|
|
||||||
const pubkey = userState.user?.pubkey as string
|
|
||||||
|
|
||||||
const filter: Filter = {
|
|
||||||
kinds: [kinds.Mutelist],
|
|
||||||
authors: [pubkey]
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true)
|
|
||||||
setLoadingSpinnerDesc(`Finding user's mute list`)
|
|
||||||
|
|
||||||
// Fetch the mute list event from the relays. This returns the event containing the user's mute list.
|
|
||||||
const muteListEvent =
|
|
||||||
await RelayController.getInstance().fetchEventFromUserRelays(
|
|
||||||
filter,
|
|
||||||
pubkey,
|
|
||||||
UserRelaysType.Write
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!muteListEvent) {
|
|
||||||
toast.error(`Couldn't get user's mute list event from relays`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const tags = muteListEvent.tags
|
|
||||||
|
|
||||||
const unsignedEvent: UnsignedEvent = {
|
|
||||||
pubkey: muteListEvent.pubkey,
|
|
||||||
kind: muteListEvent.kind,
|
|
||||||
content: muteListEvent.content,
|
|
||||||
created_at: now(),
|
|
||||||
tags: tags.filter((item) => item[0] !== 'a' || item[1] !== aTag)
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Updating mute list event')
|
|
||||||
const isUpdated = await signAndPublish(unsignedEvent)
|
|
||||||
if (isUpdated) {
|
|
||||||
setIsBlocked(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleBlockNSFW = async () => {
|
|
||||||
const pubkey = userState.user?.pubkey as string | undefined
|
|
||||||
|
|
||||||
if (!pubkey) return
|
|
||||||
|
|
||||||
const filter: Filter = {
|
|
||||||
kinds: [kinds.Curationsets],
|
|
||||||
authors: [pubkey],
|
|
||||||
'#d': ['nsfw']
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true)
|
|
||||||
setLoadingSpinnerDesc('Finding NSFW list')
|
|
||||||
|
|
||||||
const nsfwListEvent =
|
|
||||||
await RelayController.getInstance().fetchEventFromUserRelays(
|
|
||||||
filter,
|
|
||||||
pubkey,
|
|
||||||
UserRelaysType.Write
|
|
||||||
)
|
|
||||||
|
|
||||||
let unsignedEvent: UnsignedEvent
|
|
||||||
|
|
||||||
if (nsfwListEvent) {
|
|
||||||
// get a list of tags
|
|
||||||
const tags = nsfwListEvent.tags
|
|
||||||
const alreadyExists =
|
|
||||||
tags.findIndex((item) => item[0] === 'a' && item[1] === aTag) !== -1
|
|
||||||
|
|
||||||
if (alreadyExists) {
|
|
||||||
setIsLoading(false)
|
|
||||||
setIsAddedToNSFW(true)
|
|
||||||
return toast.warn(`Mod reference is already in user's nsfw list`)
|
|
||||||
}
|
|
||||||
|
|
||||||
tags.push(['a', aTag])
|
|
||||||
|
|
||||||
unsignedEvent = {
|
|
||||||
pubkey: nsfwListEvent.pubkey,
|
|
||||||
kind: nsfwListEvent.kind,
|
|
||||||
content: nsfwListEvent.content,
|
|
||||||
created_at: now(),
|
|
||||||
tags: [...tags]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
unsignedEvent = {
|
|
||||||
pubkey: pubkey,
|
|
||||||
kind: kinds.Curationsets,
|
|
||||||
content: '',
|
|
||||||
created_at: now(),
|
|
||||||
tags: [
|
|
||||||
['a', aTag],
|
|
||||||
['d', 'nsfw']
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Updating nsfw list event')
|
|
||||||
|
|
||||||
const isUpdated = await signAndPublish(unsignedEvent)
|
|
||||||
if (isUpdated) {
|
|
||||||
setIsAddedToNSFW(true)
|
|
||||||
}
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleUnblockNSFW = async () => {
|
|
||||||
const pubkey = userState.user?.pubkey as string
|
|
||||||
|
|
||||||
const filter: Filter = {
|
|
||||||
kinds: [kinds.Curationsets],
|
|
||||||
authors: [pubkey],
|
|
||||||
'#d': ['nsfw']
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(true)
|
|
||||||
setLoadingSpinnerDesc('Finding NSFW list')
|
|
||||||
|
|
||||||
const nsfwListEvent =
|
|
||||||
await RelayController.getInstance().fetchEventFromUserRelays(
|
|
||||||
filter,
|
|
||||||
pubkey,
|
|
||||||
UserRelaysType.Write
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!nsfwListEvent) {
|
|
||||||
toast.error(`Couldn't get nsfw list event from relays`)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const tags = nsfwListEvent.tags
|
|
||||||
|
|
||||||
const unsignedEvent: UnsignedEvent = {
|
|
||||||
pubkey: nsfwListEvent.pubkey,
|
|
||||||
kind: nsfwListEvent.kind,
|
|
||||||
content: nsfwListEvent.content,
|
|
||||||
created_at: now(),
|
|
||||||
tags: tags.filter((item) => item[0] !== 'a' || item[1] !== aTag)
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Updating nsfw list event')
|
|
||||||
const isUpdated = await signAndPublish(unsignedEvent)
|
|
||||||
if (isUpdated) {
|
|
||||||
setIsAddedToNSFW(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
const isAdmin =
|
|
||||||
userState.user?.npub &&
|
|
||||||
userState.user.npub === import.meta.env.VITE_REPORTING_NPUB
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className='IBMSMSMBSSModFor'>
|
||||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
<p className='IBMSMSMBSSModForPara'>
|
||||||
<div className='IBMSMSMBSSModFor'>
|
Mod for:
|
||||||
<p className='IBMSMSMBSSModForPara'>
|
<a className='IBMSMSMBSSModForLink' href='search.html'>
|
||||||
Mod for:
|
{game}
|
||||||
<a className='IBMSMSMBSSModForLink' href='search.html'>
|
</a>
|
||||||
{game}
|
</p>
|
||||||
</a>
|
<div className='dropdown dropdownMain' style={{ flexGrow: 'unset' }}>
|
||||||
</p>
|
<button
|
||||||
<div className='dropdown dropdownMain' style={{ flexGrow: 'unset' }}>
|
className='btn btnMain btnMainDropdown'
|
||||||
<button
|
aria-expanded='false'
|
||||||
className='btn btnMain btnMainDropdown'
|
data-bs-toggle='dropdown'
|
||||||
aria-expanded='false'
|
type='button'
|
||||||
data-bs-toggle='dropdown'
|
style={{
|
||||||
type='button'
|
borderRadius: '5px',
|
||||||
style={{
|
background: 'unset',
|
||||||
borderRadius: '5px',
|
padding: '5px'
|
||||||
background: 'unset',
|
}}
|
||||||
padding: '5px'
|
>
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='-192 0 512 512'
|
||||||
|
width='1em'
|
||||||
|
height='1em'
|
||||||
|
fill='currentColor'
|
||||||
|
>
|
||||||
|
<path d='M64 360C94.93 360 120 385.1 120 416C120 446.9 94.93 472 64 472C33.07 472 8 446.9 8 416C8 385.1 33.07 360 64 360zM64 200C94.93 200 120 225.1 120 256C120 286.9 94.93 312 64 312C33.07 312 8 286.9 8 256C8 225.1 33.07 200 64 200zM64 152C33.07 152 8 126.9 8 96C8 65.07 33.07 40 64 40C94.93 40 120 65.07 120 96C120 126.9 94.93 152 64 152z'></path>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<div className={`dropdown-menu dropdown-menu-end dropdownMainMenu`}>
|
||||||
|
{userState.auth && userState.user?.pubkey === author && (
|
||||||
|
<a
|
||||||
|
className='dropdown-item dropdownMainMenuItem'
|
||||||
|
onClick={() => navigate(getModsEditPageRoute(naddr))}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='0 0 512 512'
|
||||||
|
width='1em'
|
||||||
|
height='1em'
|
||||||
|
fill='currentColor'
|
||||||
|
className='IBMSMSMSSS_Author_Top_Icon'
|
||||||
|
>
|
||||||
|
<path d='M362.7 19.32C387.7-5.678 428.3-5.678 453.3 19.32L492.7 58.75C517.7 83.74 517.7 124.3 492.7 149.3L444.3 197.7L314.3 67.72L362.7 19.32zM421.7 220.3L188.5 453.4C178.1 463.8 165.2 471.5 151.1 475.6L30.77 511C22.35 513.5 13.24 511.2 7.03 504.1C.8198 498.8-1.502 489.7 .976 481.2L36.37 360.9C40.53 346.8 48.16 333.9 58.57 323.5L291.7 90.34L421.7 220.3z'></path>
|
||||||
|
</svg>
|
||||||
|
Edit
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<a
|
||||||
|
className='dropdown-item dropdownMainMenuItem'
|
||||||
|
onClick={() => {
|
||||||
|
copyTextToClipboard(window.location.href).then((isCopied) => {
|
||||||
|
if (isCopied) toast.success('Url copied to clipboard!')
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
viewBox='-192 0 512 512'
|
viewBox='0 0 512 512'
|
||||||
width='1em'
|
width='1em'
|
||||||
height='1em'
|
height='1em'
|
||||||
fill='currentColor'
|
fill='currentColor'
|
||||||
|
className='IBMSMSMSSS_Author_Top_Icon'
|
||||||
>
|
>
|
||||||
<path d='M64 360C94.93 360 120 385.1 120 416C120 446.9 94.93 472 64 472C33.07 472 8 446.9 8 416C8 385.1 33.07 360 64 360zM64 200C94.93 200 120 225.1 120 256C120 286.9 94.93 312 64 312C33.07 312 8 286.9 8 256C8 225.1 33.07 200 64 200zM64 152C33.07 152 8 126.9 8 96C8 65.07 33.07 40 64 40C94.93 40 120 65.07 120 96C120 126.9 94.93 152 64 152z'></path>
|
<path d='M384 96L384 0h-112c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48H464c26.51 0 48-21.49 48-48V128h-95.1C398.4 128 384 113.6 384 96zM416 0v96h96L416 0zM192 352V128h-144c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48h192c26.51 0 48-21.49 48-48L288 416h-32C220.7 416 192 387.3 192 352z'></path>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
Copy URL
|
||||||
<div className={`dropdown-menu dropdown-menu-end dropdownMainMenu`}>
|
</a>
|
||||||
{userState.auth && userState.user?.pubkey === author && (
|
<a className='dropdown-item dropdownMainMenuItem' href='#'>
|
||||||
<a
|
<svg
|
||||||
className='dropdown-item dropdownMainMenuItem'
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
onClick={() => navigate(getModsEditPageRoute(naddr))}
|
viewBox='0 0 512 512'
|
||||||
>
|
width='1em'
|
||||||
<svg
|
height='1em'
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
fill='currentColor'
|
||||||
viewBox='0 0 512 512'
|
className='IBMSMSMSSS_Author_Top_Icon'
|
||||||
width='1em'
|
|
||||||
height='1em'
|
|
||||||
fill='currentColor'
|
|
||||||
className='IBMSMSMSSS_Author_Top_Icon'
|
|
||||||
>
|
|
||||||
<path d='M362.7 19.32C387.7-5.678 428.3-5.678 453.3 19.32L492.7 58.75C517.7 83.74 517.7 124.3 492.7 149.3L444.3 197.7L314.3 67.72L362.7 19.32zM421.7 220.3L188.5 453.4C178.1 463.8 165.2 471.5 151.1 475.6L30.77 511C22.35 513.5 13.24 511.2 7.03 504.1C.8198 498.8-1.502 489.7 .976 481.2L36.37 360.9C40.53 346.8 48.16 333.9 58.57 323.5L291.7 90.34L421.7 220.3z'></path>
|
|
||||||
</svg>
|
|
||||||
Edit
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<a
|
|
||||||
className='dropdown-item dropdownMainMenuItem'
|
|
||||||
onClick={() => {
|
|
||||||
copyTextToClipboard(window.location.href).then((isCopied) => {
|
|
||||||
if (isCopied) toast.success('Url copied to clipboard!')
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<svg
|
<path d='M503.7 226.2l-176 151.1c-15.38 13.3-39.69 2.545-39.69-18.16V272.1C132.9 274.3 66.06 312.8 111.4 457.8c5.031 16.09-14.41 28.56-28.06 18.62C39.59 444.6 0 383.8 0 322.3c0-152.2 127.4-184.4 288-186.3V56.02c0-20.67 24.28-31.46 39.69-18.16l176 151.1C514.8 199.4 514.8 216.6 503.7 226.2z'></path>
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
</svg>
|
||||||
viewBox='0 0 512 512'
|
Share
|
||||||
width='1em'
|
</a>
|
||||||
height='1em'
|
<a
|
||||||
fill='currentColor'
|
className='dropdown-item dropdownMainMenuItem'
|
||||||
className='IBMSMSMSSS_Author_Top_Icon'
|
id='reportPost'
|
||||||
>
|
href='#'
|
||||||
<path d='M384 96L384 0h-112c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48H464c26.51 0 48-21.49 48-48V128h-95.1C398.4 128 384 113.6 384 96zM416 0v96h96L416 0zM192 352V128h-144c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48h192c26.51 0 48-21.49 48-48L288 416h-32C220.7 416 192 387.3 192 352z'></path>
|
>
|
||||||
</svg>
|
<svg
|
||||||
Copy URL
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
</a>
|
viewBox='0 0 512 512'
|
||||||
<a className='dropdown-item dropdownMainMenuItem' href='#'>
|
width='1em'
|
||||||
<svg
|
height='1em'
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
fill='currentColor'
|
||||||
viewBox='0 0 512 512'
|
className='IBMSMSMSSS_Author_Top_Icon'
|
||||||
width='1em'
|
|
||||||
height='1em'
|
|
||||||
fill='currentColor'
|
|
||||||
className='IBMSMSMSSS_Author_Top_Icon'
|
|
||||||
>
|
|
||||||
<path d='M503.7 226.2l-176 151.1c-15.38 13.3-39.69 2.545-39.69-18.16V272.1C132.9 274.3 66.06 312.8 111.4 457.8c5.031 16.09-14.41 28.56-28.06 18.62C39.59 444.6 0 383.8 0 322.3c0-152.2 127.4-184.4 288-186.3V56.02c0-20.67 24.28-31.46 39.69-18.16l176 151.1C514.8 199.4 514.8 216.6 503.7 226.2z'></path>
|
|
||||||
</svg>
|
|
||||||
Share
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
className='dropdown-item dropdownMainMenuItem'
|
|
||||||
id='reportPost'
|
|
||||||
onClick={() => setShowReportPopUp(true)}
|
|
||||||
>
|
>
|
||||||
<svg
|
<path d='M506.3 417l-213.3-364c-16.33-28-57.54-28-73.98 0l-213.2 364C-10.59 444.9 9.849 480 42.74 480h426.6C502.1 480 522.6 445 506.3 417zM232 168c0-13.25 10.75-24 24-24S280 154.8 280 168v128c0 13.25-10.75 24-23.1 24S232 309.3 232 296V168zM256 416c-17.36 0-31.44-14.08-31.44-31.44c0-17.36 14.07-31.44 31.44-31.44s31.44 14.08 31.44 31.44C287.4 401.9 273.4 416 256 416z'></path>
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
</svg>
|
||||||
viewBox='0 0 512 512'
|
Report
|
||||||
width='1em'
|
</a>
|
||||||
height='1em'
|
<a className='dropdown-item dropdownMainMenuItem' href='#'>
|
||||||
fill='currentColor'
|
<svg
|
||||||
className='IBMSMSMSSS_Author_Top_Icon'
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
>
|
viewBox='-32 0 512 512'
|
||||||
<path d='M506.3 417l-213.3-364c-16.33-28-57.54-28-73.98 0l-213.2 364C-10.59 444.9 9.849 480 42.74 480h426.6C502.1 480 522.6 445 506.3 417zM232 168c0-13.25 10.75-24 24-24S280 154.8 280 168v128c0 13.25-10.75 24-23.1 24S232 309.3 232 296V168zM256 416c-17.36 0-31.44-14.08-31.44-31.44c0-17.36 14.07-31.44 31.44-31.44s31.44 14.08 31.44 31.44C287.4 401.9 273.4 416 256 416z'></path>
|
width='1em'
|
||||||
</svg>
|
height='1em'
|
||||||
Report
|
fill='currentColor'
|
||||||
</a>
|
className='IBMSMSMSSS_Author_Top_Icon'
|
||||||
<a
|
|
||||||
className='dropdown-item dropdownMainMenuItem'
|
|
||||||
onClick={isBlocked ? handleUnblock : handleBlock}
|
|
||||||
>
|
>
|
||||||
<svg
|
<path d='M323.5 51.25C302.8 70.5 284 90.75 267.4 111.1C240.1 73.62 206.2 35.5 168 0C69.75 91.12 0 210 0 281.6C0 408.9 100.2 512 224 512s224-103.1 224-230.4C448 228.4 396 118.5 323.5 51.25zM304.1 391.9C282.4 407 255.8 416 226.9 416c-72.13 0-130.9-47.73-130.9-125.2c0-38.63 24.24-72.64 65.13-83.3c10.14-2.656 19.94 4.78 19.94 15.27c0 6.941-4.469 13.16-11.16 15.19c-17.5 4.578-34.41 23.94-34.41 52.84c0 50.81 39.31 94.81 91.41 94.81c24.66 0 45.22-6.5 63.19-18.75c11.75-8 27.91 3.469 23.91 16.69C314.6 384.7 309.8 388.4 304.1 391.9z'></path>
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
</svg>
|
||||||
viewBox='-32 0 512 512'
|
Block Post
|
||||||
width='1em'
|
</a>
|
||||||
height='1em'
|
|
||||||
fill='currentColor'
|
|
||||||
className='IBMSMSMSSS_Author_Top_Icon'
|
|
||||||
>
|
|
||||||
<path d='M323.5 51.25C302.8 70.5 284 90.75 267.4 111.1C240.1 73.62 206.2 35.5 168 0C69.75 91.12 0 210 0 281.6C0 408.9 100.2 512 224 512s224-103.1 224-230.4C448 228.4 396 118.5 323.5 51.25zM304.1 391.9C282.4 407 255.8 416 226.9 416c-72.13 0-130.9-47.73-130.9-125.2c0-38.63 24.24-72.64 65.13-83.3c10.14-2.656 19.94 4.78 19.94 15.27c0 6.941-4.469 13.16-11.16 15.19c-17.5 4.578-34.41 23.94-34.41 52.84c0 50.81 39.31 94.81 91.41 94.81c24.66 0 45.22-6.5 63.19-18.75c11.75-8 27.91 3.469 23.91 16.69C314.6 384.7 309.8 388.4 304.1 391.9z'></path>
|
|
||||||
</svg>
|
|
||||||
{isBlocked ? 'Unblock' : 'Block'} Post
|
|
||||||
</a>
|
|
||||||
{isAdmin && (
|
|
||||||
<a
|
|
||||||
className='dropdown-item dropdownMainMenuItem'
|
|
||||||
onClick={isAddedToNSFW ? handleUnblockNSFW : handleBlockNSFW}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
|
||||||
viewBox='-32 0 512 512'
|
|
||||||
width='1em'
|
|
||||||
height='1em'
|
|
||||||
fill='currentColor'
|
|
||||||
className='IBMSMSMSSS_Author_Top_Icon'
|
|
||||||
>
|
|
||||||
<path d='M323.5 51.25C302.8 70.5 284 90.75 267.4 111.1C240.1 73.62 206.2 35.5 168 0C69.75 91.12 0 210 0 281.6C0 408.9 100.2 512 224 512s224-103.1 224-230.4C448 228.4 396 118.5 323.5 51.25zM304.1 391.9C282.4 407 255.8 416 226.9 416c-72.13 0-130.9-47.73-130.9-125.2c0-38.63 24.24-72.64 65.13-83.3c10.14-2.656 19.94 4.78 19.94 15.27c0 6.941-4.469 13.16-11.16 15.19c-17.5 4.578-34.41 23.94-34.41 52.84c0 50.81 39.31 94.81 91.41 94.81c24.66 0 45.22-6.5 63.19-18.75c11.75-8 27.91 3.469 23.91 16.69C314.6 384.7 309.8 388.4 304.1 391.9z'></path>
|
|
||||||
</svg>
|
|
||||||
{isAddedToNSFW ? 'Un-mark' : 'Mark'} as NSFW
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{showReportPopUp && (
|
</div>
|
||||||
<ReportPopup
|
|
||||||
aTag={aTag}
|
|
||||||
handleClose={() => setShowReportPopUp(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReportPopupProps = {
|
|
||||||
aTag: string
|
|
||||||
handleClose: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReportPopup = ({ aTag, handleClose }: ReportPopupProps) => {
|
|
||||||
const userState = useAppSelector((state) => state.user)
|
|
||||||
const [selectedOptions, setSelectedOptions] = useState({
|
|
||||||
actuallyCP: false,
|
|
||||||
spam: false,
|
|
||||||
scam: false,
|
|
||||||
notAGameMod: false,
|
|
||||||
stolenGameMod: false,
|
|
||||||
wasntTaggedNSFW: false,
|
|
||||||
otherReason: false
|
|
||||||
})
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
|
||||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
|
||||||
|
|
||||||
const handleCheckboxChange = (option: keyof typeof selectedOptions) => {
|
|
||||||
setSelectedOptions((prevState) => ({
|
|
||||||
...prevState,
|
|
||||||
[option]: !prevState[option]
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
|
||||||
const selectedOptionsCount = Object.values(selectedOptions).filter(
|
|
||||||
(isSelected) => isSelected
|
|
||||||
).length
|
|
||||||
|
|
||||||
if (selectedOptionsCount === 0) {
|
|
||||||
toast.error('At least one option should be checked!')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let hexPubkey: string
|
|
||||||
|
|
||||||
setIsLoading(true)
|
|
||||||
setLoadingSpinnerDesc('Getting user pubkey')
|
|
||||||
|
|
||||||
if (userState.auth && userState.user?.pubkey) {
|
|
||||||
hexPubkey = userState.user.pubkey as string
|
|
||||||
} else {
|
|
||||||
hexPubkey = (await window.nostr?.getPublicKey()) as string
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hexPubkey) {
|
|
||||||
toast.error('Could not get pubkey for reporting mod!')
|
|
||||||
setIsLoading(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const metadataController = await MetadataController.getInstance()
|
|
||||||
const reportingPubkey = npubToHex(metadataController.reportingNpub)
|
|
||||||
|
|
||||||
if (reportingPubkey === hexPubkey) {
|
|
||||||
setLoadingSpinnerDesc(`Finding user's mute list`)
|
|
||||||
// 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: Filter = {
|
|
||||||
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 RelayController.getInstance().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) {
|
|
||||||
setIsLoading(false)
|
|
||||||
return toast.warn(`Mod reference is already in user's mute list`)
|
|
||||||
}
|
|
||||||
|
|
||||||
tags.push(['a', aTag])
|
|
||||||
|
|
||||||
unsignedEvent = {
|
|
||||||
pubkey: muteListEvent.pubkey,
|
|
||||||
kind: muteListEvent.kind,
|
|
||||||
content: muteListEvent.content,
|
|
||||||
created_at: now(),
|
|
||||||
tags: [...tags]
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
unsignedEvent = {
|
|
||||||
pubkey: hexPubkey,
|
|
||||||
kind: kinds.Mutelist,
|
|
||||||
content: '',
|
|
||||||
created_at: now(),
|
|
||||||
tags: [['a', aTag]]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Updating mute list event')
|
|
||||||
const isUpdated = await signAndPublish(unsignedEvent)
|
|
||||||
if (isUpdated) handleClose()
|
|
||||||
} else {
|
|
||||||
const href = window.location.href
|
|
||||||
let message = `I'd like to report ${href} due to following reasons:\n`
|
|
||||||
|
|
||||||
Object.entries(selectedOptions).forEach(([key, value]) => {
|
|
||||||
if (value) {
|
|
||||||
message += `* ${key}\n`
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
setLoadingSpinnerDesc('Sending report')
|
|
||||||
const isSent = await sendDMUsingRandomKey(message, reportingPubkey!)
|
|
||||||
if (isSent) handleClose()
|
|
||||||
}
|
|
||||||
setIsLoading(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
|
|
||||||
<div className='popUpMain'>
|
|
||||||
<div className='ContainerMain'>
|
|
||||||
<div className='popUpMainCardWrapper'>
|
|
||||||
<div className='popUpMainCard popUpMainCardQR'>
|
|
||||||
<div className='popUpMainCardTop'>
|
|
||||||
<div className='popUpMainCardTopInfo'>
|
|
||||||
<h3>Report Post</h3>
|
|
||||||
</div>
|
|
||||||
<div className='popUpMainCardTopClose' onClick={handleClose}>
|
|
||||||
<svg
|
|
||||||
xmlns='http://www.w3.org/2000/svg'
|
|
||||||
viewBox='-96 0 512 512'
|
|
||||||
width='1em'
|
|
||||||
height='1em'
|
|
||||||
fill='currentColor'
|
|
||||||
style={{ zIndex: 1 }}
|
|
||||||
>
|
|
||||||
<path d='M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z'></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className='pUMCB_Zaps'>
|
|
||||||
<div className='pUMCB_ZapsInside'>
|
|
||||||
<div className='inputLabelWrapperMain'>
|
|
||||||
<label
|
|
||||||
className='form-label labelMain'
|
|
||||||
style={{ fontWeight: 'bold' }}
|
|
||||||
>
|
|
||||||
Why are you reporting this?
|
|
||||||
</label>
|
|
||||||
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt'>
|
|
||||||
<label className='form-label labelMain'>
|
|
||||||
Actually CP
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type='checkbox'
|
|
||||||
className='CheckboxMain'
|
|
||||||
name='reportOption'
|
|
||||||
checked={selectedOptions.actuallyCP}
|
|
||||||
onChange={() => handleCheckboxChange('actuallyCP')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt'>
|
|
||||||
<label className='form-label labelMain'>Spam</label>
|
|
||||||
<input
|
|
||||||
type='checkbox'
|
|
||||||
className='CheckboxMain'
|
|
||||||
name='reportOption'
|
|
||||||
checked={selectedOptions.spam}
|
|
||||||
onChange={() => handleCheckboxChange('spam')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt'>
|
|
||||||
<label className='form-label labelMain'>Scam</label>
|
|
||||||
<input
|
|
||||||
type='checkbox'
|
|
||||||
className='CheckboxMain'
|
|
||||||
name='reportOption'
|
|
||||||
checked={selectedOptions.scam}
|
|
||||||
onChange={() => handleCheckboxChange('scam')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt'>
|
|
||||||
<label className='form-label labelMain'>
|
|
||||||
Not a game mod
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type='checkbox'
|
|
||||||
className='CheckboxMain'
|
|
||||||
name='reportOption'
|
|
||||||
checked={selectedOptions.notAGameMod}
|
|
||||||
onChange={() => handleCheckboxChange('notAGameMod')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt'>
|
|
||||||
<label className='form-label labelMain'>
|
|
||||||
Stolen game mod
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type='checkbox'
|
|
||||||
className='CheckboxMain'
|
|
||||||
name='reportOption'
|
|
||||||
checked={selectedOptions.stolenGameMod}
|
|
||||||
onChange={() => handleCheckboxChange('stolenGameMod')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt'>
|
|
||||||
<label className='form-label labelMain'>
|
|
||||||
Wasn't tagged NSFW
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type='checkbox'
|
|
||||||
className='CheckboxMain'
|
|
||||||
name='reportOption'
|
|
||||||
checked={selectedOptions.wasntTaggedNSFW}
|
|
||||||
onChange={() => handleCheckboxChange('wasntTaggedNSFW')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className='inputLabelWrapperMain inputLabelWrapperMainAlt'>
|
|
||||||
<label className='form-label labelMain'>
|
|
||||||
Other reason
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type='checkbox'
|
|
||||||
className='CheckboxMain'
|
|
||||||
name='reportOption'
|
|
||||||
checked={selectedOptions.otherReason}
|
|
||||||
onChange={() => handleCheckboxChange('otherReason')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
className='btn btnMain pUMCB_Report'
|
|
||||||
type='button'
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
onClick={handleSubmit}
|
|
||||||
>
|
|
||||||
Submit Report
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ import { useNavigate } from 'react-router-dom'
|
|||||||
import { LoadingSpinner } from '../components/LoadingSpinner'
|
import { LoadingSpinner } from '../components/LoadingSpinner'
|
||||||
import { ModCard } from '../components/ModCard'
|
import { ModCard } from '../components/ModCard'
|
||||||
import { MetadataController } from '../controllers'
|
import { MetadataController } from '../controllers'
|
||||||
import { useAppSelector, useDidMount } from '../hooks'
|
import { useDidMount } from '../hooks'
|
||||||
import { getModsInnerPageRoute } from '../routes'
|
import { getModsInnerPageRoute } from '../routes'
|
||||||
import '../styles/filters.css'
|
import '../styles/filters.css'
|
||||||
import '../styles/pagination.css'
|
import '../styles/pagination.css'
|
||||||
@ -36,8 +36,7 @@ enum NSFWFilter {
|
|||||||
|
|
||||||
enum ModeratedFilter {
|
enum ModeratedFilter {
|
||||||
Moderated = 'Moderated',
|
Moderated = 'Moderated',
|
||||||
Unmoderated = 'Unmoderated',
|
Unmoderated = 'Unmoderated'
|
||||||
Unmoderated_Fully = 'Unmoderated Fully'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FilterOptions {
|
interface FilterOptions {
|
||||||
@ -57,36 +56,18 @@ export const ModsPage = () => {
|
|||||||
source: window.location.host,
|
source: window.location.host,
|
||||||
moderated: ModeratedFilter.Moderated
|
moderated: ModeratedFilter.Moderated
|
||||||
})
|
})
|
||||||
const [muteLists, setMuteLists] = useState<{
|
const [muteLists, setMuteLists] = useState<MuteLists>({
|
||||||
admin: MuteLists
|
authors: [],
|
||||||
user: MuteLists
|
eventIds: []
|
||||||
}>({
|
|
||||||
admin: {
|
|
||||||
authors: [],
|
|
||||||
replaceableEvents: []
|
|
||||||
},
|
|
||||||
user: {
|
|
||||||
authors: [],
|
|
||||||
replaceableEvents: []
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
const [nsfwList, setNSFWList] = useState<string[]>([])
|
|
||||||
|
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
|
|
||||||
const userState = useAppSelector((state) => state.user)
|
|
||||||
|
|
||||||
useDidMount(async () => {
|
useDidMount(async () => {
|
||||||
const pubkey = userState.user?.pubkey as string | undefined
|
|
||||||
|
|
||||||
const metadataController = await MetadataController.getInstance()
|
const metadataController = await MetadataController.getInstance()
|
||||||
metadataController.getMuteLists(pubkey).then((lists) => {
|
metadataController.getAdminsMuteLists().then((lists) => {
|
||||||
setMuteLists(lists)
|
setMuteLists(lists)
|
||||||
})
|
})
|
||||||
|
|
||||||
metadataController.getNSFWList().then((list) => {
|
|
||||||
setNSFWList(list)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -104,7 +85,7 @@ export const ModsPage = () => {
|
|||||||
setIsFetching(true)
|
setIsFetching(true)
|
||||||
|
|
||||||
const until =
|
const until =
|
||||||
mods.length > 0 ? mods[mods.length - 1].published_at - 1 : undefined
|
mods.length > 0 ? mods[mods.length - 1].edited_at - 1 : undefined
|
||||||
|
|
||||||
fetchMods(filterOptions.source, until)
|
fetchMods(filterOptions.source, until)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
@ -119,7 +100,7 @@ export const ModsPage = () => {
|
|||||||
const handlePrev = useCallback(() => {
|
const handlePrev = useCallback(() => {
|
||||||
setIsFetching(true)
|
setIsFetching(true)
|
||||||
|
|
||||||
const since = mods.length > 0 ? mods[0].published_at + 1 : undefined
|
const since = mods.length > 0 ? mods[0].edited_at + 1 : undefined
|
||||||
|
|
||||||
fetchMods(filterOptions.source, undefined, since)
|
fetchMods(filterOptions.source, undefined, since)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
@ -137,54 +118,39 @@ export const ModsPage = () => {
|
|||||||
switch (filterOptions.nsfw) {
|
switch (filterOptions.nsfw) {
|
||||||
case NSFWFilter.Hide_NSFW:
|
case NSFWFilter.Hide_NSFW:
|
||||||
// If 'Hide_NSFW' is selected, filter out NSFW mods
|
// If 'Hide_NSFW' is selected, filter out NSFW mods
|
||||||
return mods.filter((mod) => !mod.nsfw && !nsfwList.includes(mod.aTag))
|
return mods.filter((mod) => !mod.nsfw)
|
||||||
case NSFWFilter.Show_NSFW:
|
case NSFWFilter.Show_NSFW:
|
||||||
// If 'Show_NSFW' is selected, return all mods (no filtering)
|
// If 'Show_NSFW' is selected, return all mods (no filtering)
|
||||||
return mods
|
return mods
|
||||||
case NSFWFilter.Only_NSFW:
|
case NSFWFilter.Only_NSFW:
|
||||||
// If 'Only_NSFW' is selected, filter to show only NSFW mods
|
// If 'Only_NSFW' is selected, filter to show only NSFW mods
|
||||||
return mods.filter((mod) => mod.nsfw || nsfwList.includes(mod.aTag))
|
return mods.filter((mod) => mod.nsfw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let filtered = nsfwFilter(mods)
|
let filtered = nsfwFilter(mods)
|
||||||
|
|
||||||
const isAdmin = userState.user?.npub === import.meta.env.VITE_REPORTING_NPUB
|
|
||||||
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"
|
|
||||||
if (!(isAdmin && isUnmoderatedFully)) {
|
|
||||||
filtered = filtered.filter(
|
|
||||||
(mod) =>
|
|
||||||
!muteLists.admin.authors.includes(mod.author) &&
|
|
||||||
!muteLists.admin.replaceableEvents.includes(mod.aTag)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (filterOptions.moderated === ModeratedFilter.Moderated) {
|
if (filterOptions.moderated === ModeratedFilter.Moderated) {
|
||||||
filtered = filtered.filter(
|
filtered = filtered.filter(
|
||||||
(mod) =>
|
(mod) =>
|
||||||
!muteLists.user.authors.includes(mod.author) &&
|
!muteLists.authors.includes(mod.author) &&
|
||||||
!muteLists.user.replaceableEvents.includes(mod.aTag)
|
!muteLists.eventIds.includes(mod.id)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterOptions.sort === SortBy.Latest) {
|
if (filterOptions.sort === SortBy.Latest) {
|
||||||
filtered.sort((a, b) => b.published_at - a.published_at)
|
filtered.sort((a, b) => b.edited_at - a.edited_at)
|
||||||
} else if (filterOptions.sort === SortBy.Oldest) {
|
} else if (filterOptions.sort === SortBy.Oldest) {
|
||||||
filtered.sort((a, b) => a.published_at - b.published_at)
|
filtered.sort((a, b) => a.edited_at - b.edited_at)
|
||||||
}
|
}
|
||||||
|
|
||||||
return filtered
|
return filtered
|
||||||
}, [
|
}, [
|
||||||
userState.user?.npub,
|
|
||||||
filterOptions.sort,
|
filterOptions.sort,
|
||||||
filterOptions.moderated,
|
filterOptions.moderated,
|
||||||
filterOptions.nsfw,
|
filterOptions.nsfw,
|
||||||
mods,
|
mods,
|
||||||
muteLists,
|
muteLists
|
||||||
nsfwList
|
|
||||||
])
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -201,26 +167,25 @@ export const ModsPage = () => {
|
|||||||
|
|
||||||
<div className='IBMSecMain IBMSMListWrapper'>
|
<div className='IBMSecMain IBMSMListWrapper'>
|
||||||
<div className='IBMSMList'>
|
<div className='IBMSMList'>
|
||||||
{filteredModList.map((mod) => {
|
{filteredModList.map((mod) => (
|
||||||
const route = getModsInnerPageRoute(
|
<ModCard
|
||||||
nip19.naddrEncode({
|
key={mod.id}
|
||||||
identifier: mod.aTag,
|
title={mod.title}
|
||||||
pubkey: mod.author,
|
summary={mod.summary}
|
||||||
kind: kinds.ClassifiedListing
|
backgroundLink={mod.featuredImageUrl}
|
||||||
})
|
handleClick={() =>
|
||||||
)
|
navigate(
|
||||||
|
getModsInnerPageRoute(
|
||||||
return (
|
nip19.naddrEncode({
|
||||||
<ModCard
|
identifier: mod.aTag,
|
||||||
key={mod.id}
|
pubkey: mod.author,
|
||||||
title={mod.title}
|
kind: kinds.ClassifiedListing
|
||||||
summary={mod.summary}
|
})
|
||||||
backgroundLink={mod.featuredImageUrl}
|
)
|
||||||
link={`#${route}`}
|
)
|
||||||
handleClick={() => navigate(route)}
|
}
|
||||||
/>
|
/>
|
||||||
)
|
))}
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -274,8 +239,6 @@ type FiltersProps = {
|
|||||||
|
|
||||||
const Filters = React.memo(
|
const Filters = React.memo(
|
||||||
({ filterOptions, setFilterOptions }: FiltersProps) => {
|
({ filterOptions, setFilterOptions }: FiltersProps) => {
|
||||||
const userState = useAppSelector((state) => state.user)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='IBMSecMain'>
|
<div className='IBMSecMain'>
|
||||||
<div className='FiltersMain'>
|
<div className='FiltersMain'>
|
||||||
@ -319,30 +282,20 @@ const Filters = React.memo(
|
|||||||
{filterOptions.moderated}
|
{filterOptions.moderated}
|
||||||
</button>
|
</button>
|
||||||
<div className='dropdown-menu dropdownMainMenu'>
|
<div className='dropdown-menu dropdownMainMenu'>
|
||||||
{Object.values(ModeratedFilter).map((item, index) => {
|
{Object.values(ModeratedFilter).map((item, index) => (
|
||||||
if (item === ModeratedFilter.Unmoderated_Fully) {
|
<div
|
||||||
const isAdmin =
|
key={`moderatedFilterItem-${index}`}
|
||||||
userState.user?.npub ===
|
className='dropdown-item dropdownMainMenuItem'
|
||||||
import.meta.env.VITE_REPORTING_NPUB
|
onClick={() =>
|
||||||
|
setFilterOptions((prev) => ({
|
||||||
if (!isAdmin) return null
|
...prev,
|
||||||
}
|
moderated: item
|
||||||
|
}))
|
||||||
return (
|
}
|
||||||
<div
|
>
|
||||||
key={`moderatedFilterItem-${index}`}
|
{item}
|
||||||
className='dropdown-item dropdownMainMenuItem'
|
</div>
|
||||||
onClick={() =>
|
))}
|
||||||
setFilterOptions((prev) => ({
|
|
||||||
...prev,
|
|
||||||
moderated: item
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{item}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -56,8 +56,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.IBMSMSMSSS_Author_Top_Icon {
|
.IBMSMSMSSS_Author_Top_Icon {
|
||||||
min-width: 16px;
|
|
||||||
min-height: 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.IBMSMSMSSS_Author_Top_Address {
|
.IBMSMSMSSS_Author_Top_Address {
|
||||||
|
@ -32,5 +32,5 @@ export interface ModDetails extends Omit<ModFormState, 'tags'> {
|
|||||||
|
|
||||||
export interface MuteLists {
|
export interface MuteLists {
|
||||||
authors: string[]
|
authors: string[]
|
||||||
replaceableEvents: string[]
|
eventIds: string[]
|
||||||
}
|
}
|
||||||
|
@ -167,6 +167,8 @@ export const fetchMods = async (
|
|||||||
return RelayController.getInstance()
|
return RelayController.getInstance()
|
||||||
.fetchEvents(filter, []) // Pass the filter and an empty array of options
|
.fetchEvents(filter, []) // Pass the filter and an empty array of options
|
||||||
.then((events) => {
|
.then((events) => {
|
||||||
|
console.log('events :>> ', events)
|
||||||
|
|
||||||
// Convert the fetched events into a list of mods
|
// Convert the fetched events into a list of mods
|
||||||
const modList = constructModListFromEvents(events)
|
const modList = constructModListFromEvents(events)
|
||||||
return modList // Return the list of mods
|
return modList // Return the list of mods
|
||||||
|
@ -1,16 +1,4 @@
|
|||||||
import {
|
import { nip19, Event } from 'nostr-tools'
|
||||||
Event,
|
|
||||||
finalizeEvent,
|
|
||||||
generateSecretKey,
|
|
||||||
getPublicKey,
|
|
||||||
kinds,
|
|
||||||
nip04,
|
|
||||||
nip19,
|
|
||||||
UnsignedEvent
|
|
||||||
} from 'nostr-tools'
|
|
||||||
import { toast } from 'react-toastify'
|
|
||||||
import { RelayController } from '../controllers'
|
|
||||||
import { log, LogType } from './utils'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current time in seconds since the Unix epoch (January 1, 1970).
|
* Get the current time in seconds since the Unix epoch (January 1, 1970).
|
||||||
@ -133,107 +121,3 @@ export const extractZapAmount = (event: Event): number => {
|
|||||||
// Return 0 if the zap amount cannot be determined
|
// Return 0 if the zap amount cannot be determined
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Signs and publishes an event to user's relays.
|
|
||||||
*
|
|
||||||
* @param unsignedEvent - The event object which needs to be signed before publishing.
|
|
||||||
* @returns - A promise that resolves to boolean indicating whether the event was successfully signed and published
|
|
||||||
*/
|
|
||||||
export const signAndPublish = async (unsignedEvent: UnsignedEvent) => {
|
|
||||||
// Sign the event. This returns a signed event or null if signing fails.
|
|
||||||
const signedEvent = await window.nostr
|
|
||||||
?.signEvent(unsignedEvent)
|
|
||||||
.then((event) => event as Event)
|
|
||||||
.catch((err) => {
|
|
||||||
// If signing the event fails, display an error toast and log the error.
|
|
||||||
toast.error('Failed to sign the event!')
|
|
||||||
log(true, LogType.Error, 'Failed to sign the event!', err)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
// If the event couldn't be signed, exit the function and return null.
|
|
||||||
if (!signedEvent) return false
|
|
||||||
|
|
||||||
// Publish the signed event to the relays using the RelayController.
|
|
||||||
// This returns an array of relay URLs where the event was successfully published.
|
|
||||||
const publishedOnRelays = await RelayController.getInstance().publish(
|
|
||||||
signedEvent as Event
|
|
||||||
)
|
|
||||||
|
|
||||||
// Handle cases where publishing to the relays failed
|
|
||||||
if (publishedOnRelays.length === 0) {
|
|
||||||
// Display an error toast if the event could not be published to any relay.
|
|
||||||
toast.error('Failed to publish event on any relay')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display a success toast with the list of relays where the event was successfully published.
|
|
||||||
toast.success(
|
|
||||||
`Event published successfully to the following relays\n\n${publishedOnRelays.join(
|
|
||||||
'\n'
|
|
||||||
)}`
|
|
||||||
)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sends an encrypted direct message (DM) to a receiver using a randomly generated secret key.
|
|
||||||
*
|
|
||||||
* @param message - The plaintext message content to be sent.
|
|
||||||
* @param receiver - The public key of the receiver to whom the message is being sent.
|
|
||||||
* @returns - A promise that resolves to true if the message was successfully sent, or false if an error occurred.
|
|
||||||
*/
|
|
||||||
export const sendDMUsingRandomKey = async (
|
|
||||||
message: string,
|
|
||||||
receiver: string
|
|
||||||
) => {
|
|
||||||
// Generate a random secret key for encrypting the message
|
|
||||||
const secretKey = generateSecretKey()
|
|
||||||
|
|
||||||
// Encrypt the message using the generated secret key and the receiver's public key
|
|
||||||
const encryptedMessage = await nip04
|
|
||||||
.encrypt(secretKey, receiver, message)
|
|
||||||
.catch((err) => {
|
|
||||||
// If encryption fails, display an error toast
|
|
||||||
toast.error(
|
|
||||||
`An error occurred in encrypting message content: ${err.message || err}`
|
|
||||||
)
|
|
||||||
return null
|
|
||||||
})
|
|
||||||
|
|
||||||
// If encryption failed, exit the function and return false
|
|
||||||
if (!encryptedMessage) return false
|
|
||||||
|
|
||||||
// Construct the unsigned event containing the encrypted message and relevant metadata
|
|
||||||
const unsignedEvent: UnsignedEvent = {
|
|
||||||
pubkey: getPublicKey(secretKey),
|
|
||||||
kind: kinds.EncryptedDirectMessage,
|
|
||||||
created_at: now(),
|
|
||||||
tags: [['p', receiver]],
|
|
||||||
content: encryptedMessage
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finalize and sign the event using the generated secret key
|
|
||||||
const signedEvent = finalizeEvent(unsignedEvent, secretKey)
|
|
||||||
|
|
||||||
// Publish the signed event (the encrypted DM) to the relays
|
|
||||||
const publishedOnRelays = await RelayController.getInstance().publishDM(
|
|
||||||
signedEvent,
|
|
||||||
receiver
|
|
||||||
)
|
|
||||||
|
|
||||||
// Handle cases where publishing to the relays failed
|
|
||||||
if (publishedOnRelays.length === 0) {
|
|
||||||
// Display an error toast if the event could not be published to any relay
|
|
||||||
toast.error('Failed to publish encrypted direct message on any relay')
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display a success toast if the event was successfully published to one or more relays
|
|
||||||
toast.success(`Report successfully submitted!`)
|
|
||||||
|
|
||||||
// Return true indicating that the DM was successfully sent
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
1
src/vite-env.d.ts
vendored
1
src/vite-env.d.ts
vendored
@ -3,7 +3,6 @@
|
|||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_APP_RELAY: string
|
readonly VITE_APP_RELAY: string
|
||||||
readonly VITE_ADMIN_NPUBS: string
|
readonly VITE_ADMIN_NPUBS: string
|
||||||
readonly VITE_REPORTING_NPUB: string
|
|
||||||
// more env variables...
|
// more env variables...
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user