Compare commits
3 Commits
7aef0b3456
...
3f9e39a92a
Author | SHA1 | Date | |
---|---|---|---|
|
3f9e39a92a | ||
|
8529a95718 | ||
|
44acba8d26 |
64
package-lock.json
generated
64
package-lock.json
generated
@ -10,6 +10,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@getalby/lightning-tools": "5.0.3",
|
"@getalby/lightning-tools": "5.0.3",
|
||||||
"@nostr-dev-kit/ndk": "2.10.0",
|
"@nostr-dev-kit/ndk": "2.10.0",
|
||||||
|
"@nostr-dev-kit/ndk-cache-dexie": "2.5.1",
|
||||||
"@reduxjs/toolkit": "2.2.6",
|
"@reduxjs/toolkit": "2.2.6",
|
||||||
"@tiptap/core": "2.6.6",
|
"@tiptap/core": "2.6.6",
|
||||||
"@tiptap/extension-link": "2.6.6",
|
"@tiptap/extension-link": "2.6.6",
|
||||||
@ -19,6 +20,7 @@
|
|||||||
"bech32": "2.0.0",
|
"bech32": "2.0.0",
|
||||||
"buffer": "6.0.3",
|
"buffer": "6.0.3",
|
||||||
"date-fns": "3.6.0",
|
"date-fns": "3.6.0",
|
||||||
|
"dexie": "4.0.8",
|
||||||
"dompurify": "3.1.6",
|
"dompurify": "3.1.6",
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
"fslightbox-react": "1.7.6",
|
"fslightbox-react": "1.7.6",
|
||||||
@ -1043,22 +1045,25 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@noble/curves": {
|
"node_modules/@noble/curves": {
|
||||||
"version": "1.4.2",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.6.0.tgz",
|
||||||
"integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==",
|
"integrity": "sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@noble/hashes": "1.4.0"
|
"@noble/hashes": "1.5.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^14.21.3 || >=16"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@noble/hashes": {
|
"node_modules/@noble/hashes": {
|
||||||
"version": "1.4.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.5.0.tgz",
|
||||||
"integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==",
|
"integrity": "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 16"
|
"node": "^14.21.3 || >=16"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
@ -1129,6 +1134,18 @@
|
|||||||
"node": ">=16"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@nostr-dev-kit/ndk-cache-dexie": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@nostr-dev-kit/ndk-cache-dexie/-/ndk-cache-dexie-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-tUwEy68bd9GL5JVuZIjcpdwuDEBnaXen3WJ64/GRDtbyE1RB01Y6hHC7IQC9bcQ6SC7XBGyPd+2nuTyR7+Mffg==",
|
||||||
|
"dependencies": {
|
||||||
|
"@nostr-dev-kit/ndk": "2.10.0",
|
||||||
|
"debug": "^4.3.4",
|
||||||
|
"dexie": "^4.0.2",
|
||||||
|
"nostr-tools": "^2.4.0",
|
||||||
|
"typescript-lru-cache": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@popperjs/core": {
|
"node_modules/@popperjs/core": {
|
||||||
"version": "2.11.8",
|
"version": "2.11.8",
|
||||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||||
@ -1383,9 +1400,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@scure/base": {
|
"node_modules/@scure/base": {
|
||||||
"version": "1.1.7",
|
"version": "1.1.9",
|
||||||
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.7.tgz",
|
"resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz",
|
||||||
"integrity": "sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g==",
|
"integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==",
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://paulmillr.com/funding/"
|
"url": "https://paulmillr.com/funding/"
|
||||||
}
|
}
|
||||||
@ -2670,11 +2687,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.5",
|
"version": "4.3.7",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
||||||
"integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==",
|
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "2.1.2"
|
"ms": "^2.1.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0"
|
"node": ">=6.0"
|
||||||
@ -2699,6 +2716,11 @@
|
|||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dexie": {
|
||||||
|
"version": "4.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/dexie/-/dexie-4.0.8.tgz",
|
||||||
|
"integrity": "sha512-1G6cJevS17KMDK847V3OHvK2zei899GwpDiqfEXHP1ASvme6eWJmAp9AU4s1son2TeGkWmC0g3y8ezOBPnalgQ=="
|
||||||
|
},
|
||||||
"node_modules/diff": {
|
"node_modules/diff": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||||
@ -3718,9 +3740,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/light-bolt11-decoder": {
|
"node_modules/light-bolt11-decoder": {
|
||||||
"version": "3.1.1",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/light-bolt11-decoder/-/light-bolt11-decoder-3.2.0.tgz",
|
||||||
"integrity": "sha512-sLg/KCwYkgsHWkefWd6KqpCHrLFWWaXTOX3cf6yD2hAzL0SLpX+lFcaFK2spkjbgzG6hhijKfORDc9WoUHwX0A==",
|
"integrity": "sha512-3QEofgiBOP4Ehs9BI+RkZdXZNtSys0nsJ6fyGeSiAGCBsMwHGUDS/JQlY/sTnWs91A2Nh0S9XXfA8Sy9g6QpuQ==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@scure/base": "1.1.1"
|
"@scure/base": "1.1.1"
|
||||||
}
|
}
|
||||||
@ -3884,9 +3906,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.2",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
|
||||||
},
|
},
|
||||||
"node_modules/nanoid": {
|
"node_modules/nanoid": {
|
||||||
"version": "3.3.7",
|
"version": "3.3.7",
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@getalby/lightning-tools": "5.0.3",
|
"@getalby/lightning-tools": "5.0.3",
|
||||||
"@nostr-dev-kit/ndk": "2.10.0",
|
"@nostr-dev-kit/ndk": "2.10.0",
|
||||||
|
"@nostr-dev-kit/ndk-cache-dexie": "2.5.1",
|
||||||
"@reduxjs/toolkit": "2.2.6",
|
"@reduxjs/toolkit": "2.2.6",
|
||||||
"@tiptap/core": "2.6.6",
|
"@tiptap/core": "2.6.6",
|
||||||
"@tiptap/extension-link": "2.6.6",
|
"@tiptap/extension-link": "2.6.6",
|
||||||
@ -21,6 +22,7 @@
|
|||||||
"bech32": "2.0.0",
|
"bech32": "2.0.0",
|
||||||
"buffer": "6.0.3",
|
"buffer": "6.0.3",
|
||||||
"date-fns": "3.6.0",
|
"date-fns": "3.6.0",
|
||||||
|
"dexie": "4.0.8",
|
||||||
"dompurify": "3.1.6",
|
"dompurify": "3.1.6",
|
||||||
"file-saver": "2.0.5",
|
"file-saver": "2.0.5",
|
||||||
"fslightbox-react": "1.7.6",
|
"fslightbox-react": "1.7.6",
|
||||||
|
@ -4,12 +4,8 @@ import { QRCodeSVG } from 'qrcode.react'
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Link } from 'react-router-dom'
|
import { Link } from 'react-router-dom'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import {
|
import { RelayController, UserRelaysType } from '../controllers'
|
||||||
MetadataController,
|
import { useAppSelector, useDidMount, useNDKContext } from '../hooks'
|
||||||
RelayController,
|
|
||||||
UserRelaysType
|
|
||||||
} from '../controllers'
|
|
||||||
import { useAppSelector, useDidMount } from '../hooks'
|
|
||||||
import { appRoutes, getProfilePageRoute } from '../routes'
|
import { appRoutes, getProfilePageRoute } from '../routes'
|
||||||
import '../styles/author.css'
|
import '../styles/author.css'
|
||||||
import '../styles/innerPage.css'
|
import '../styles/innerPage.css'
|
||||||
@ -31,11 +27,11 @@ type Props = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const ProfileSection = ({ pubkey }: Props) => {
|
export const ProfileSection = ({ pubkey }: Props) => {
|
||||||
|
const { findMetadata } = useNDKContext()
|
||||||
const [profile, setProfile] = useState<UserProfile>()
|
const [profile, setProfile] = useState<UserProfile>()
|
||||||
|
|
||||||
useDidMount(async () => {
|
useDidMount(() => {
|
||||||
const metadataController = await MetadataController.getInstance()
|
findMetadata(pubkey).then((res) => {
|
||||||
metadataController.findMetadata(pubkey).then((res) => {
|
|
||||||
setProfile(res)
|
setProfile(res)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -371,6 +367,7 @@ type FollowButtonProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const FollowButton = ({ pubkey }: FollowButtonProps) => {
|
const FollowButton = ({ pubkey }: FollowButtonProps) => {
|
||||||
|
const { fetchEventFromUserRelays } = useNDKContext()
|
||||||
const [isFollowing, setIsFollowing] = useState(false)
|
const [isFollowing, setIsFollowing] = useState(false)
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
@ -409,8 +406,7 @@ const FollowButton = ({ pubkey }: FollowButtonProps) => {
|
|||||||
authors: [userHexKey]
|
authors: [userHexKey]
|
||||||
}
|
}
|
||||||
|
|
||||||
const contactListEvent =
|
const contactListEvent = await fetchEventFromUserRelays(
|
||||||
await RelayController.getInstance().fetchEventFromUserRelays(
|
|
||||||
filter,
|
filter,
|
||||||
userHexKey,
|
userHexKey,
|
||||||
UserRelaysType.Both
|
UserRelaysType.Both
|
||||||
@ -513,8 +509,7 @@ const FollowButton = ({ pubkey }: FollowButtonProps) => {
|
|||||||
authors: [userHexKey]
|
authors: [userHexKey]
|
||||||
}
|
}
|
||||||
|
|
||||||
const contactListEvent =
|
const contactListEvent = await fetchEventFromUserRelays(
|
||||||
await RelayController.getInstance().fetchEventFromUserRelays(
|
|
||||||
filter,
|
filter,
|
||||||
userHexKey,
|
userHexKey,
|
||||||
UserRelaysType.Both
|
UserRelaysType.Both
|
||||||
|
@ -10,7 +10,7 @@ import React, {
|
|||||||
import Countdown, { CountdownRenderProps } from 'react-countdown'
|
import Countdown, { CountdownRenderProps } from 'react-countdown'
|
||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { MetadataController, ZapController } from '../controllers'
|
import { MetadataController, ZapController } from '../controllers'
|
||||||
import { useAppSelector, useDidMount } from '../hooks'
|
import { useAppSelector, useDidMount, useNDKContext } from '../hooks'
|
||||||
import '../styles/popup.css'
|
import '../styles/popup.css'
|
||||||
import { PaymentRequest, UserProfile } from '../types'
|
import { PaymentRequest, UserProfile } from '../types'
|
||||||
import {
|
import {
|
||||||
@ -133,9 +133,11 @@ export const ZapQR = React.memo(
|
|||||||
setTotalZapAmount,
|
setTotalZapAmount,
|
||||||
setHasZapped
|
setHasZapped
|
||||||
}: ZapQRProps) => {
|
}: ZapQRProps) => {
|
||||||
|
const { ndk } = useNDKContext()
|
||||||
|
|
||||||
useDidMount(() => {
|
useDidMount(() => {
|
||||||
ZapController.getInstance()
|
ZapController.getInstance()
|
||||||
.pollZapReceipt(paymentRequest)
|
.pollZapReceipt(paymentRequest, ndk)
|
||||||
.then((zapReceipt) => {
|
.then((zapReceipt) => {
|
||||||
toast.success(`Successfully sent sats!`)
|
toast.success(`Successfully sent sats!`)
|
||||||
if (setTotalZapAmount) {
|
if (setTotalZapAmount) {
|
||||||
@ -249,6 +251,7 @@ export const ZapPopUp = ({
|
|||||||
setHasZapped,
|
setHasZapped,
|
||||||
handleClose
|
handleClose
|
||||||
}: ZapPopUpProps) => {
|
}: ZapPopUpProps) => {
|
||||||
|
const { findMetadata } = useNDKContext()
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||||
const [amount, setAmount] = useState<number>(0)
|
const [amount, setAmount] = useState<number>(0)
|
||||||
@ -282,9 +285,8 @@ export const ZapPopUp = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
setLoadingSpinnerDesc('finding receiver metadata')
|
setLoadingSpinnerDesc('finding receiver metadata')
|
||||||
const metadataController = await MetadataController.getInstance()
|
|
||||||
|
|
||||||
const receiverMetadata = await metadataController.findMetadata(receiver)
|
const receiverMetadata = await findMetadata(receiver)
|
||||||
|
|
||||||
if (!receiverMetadata?.lud16) {
|
if (!receiverMetadata?.lud16) {
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
@ -480,6 +482,7 @@ export const ZapSplit = ({
|
|||||||
setHasZapped,
|
setHasZapped,
|
||||||
handleClose
|
handleClose
|
||||||
}: ZapSplitProps) => {
|
}: ZapSplitProps) => {
|
||||||
|
const { findMetadata } = useNDKContext()
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||||
const [amount, setAmount] = useState<number>(0)
|
const [amount, setAmount] = useState<number>(0)
|
||||||
@ -495,12 +498,12 @@ export const ZapSplit = ({
|
|||||||
const [invoices, setInvoices] = useState<Map<string, PaymentRequest>>()
|
const [invoices, setInvoices] = useState<Map<string, PaymentRequest>>()
|
||||||
|
|
||||||
useDidMount(async () => {
|
useDidMount(async () => {
|
||||||
const metadataController = await MetadataController.getInstance()
|
findMetadata(pubkey).then((res) => {
|
||||||
metadataController.findMetadata(pubkey).then((res) => {
|
|
||||||
setAuthor(res)
|
setAuthor(res)
|
||||||
})
|
})
|
||||||
|
|
||||||
metadataController.findAdminMetadata().then((res) => {
|
const metadataController = await MetadataController.getInstance()
|
||||||
|
findMetadata(metadataController.adminNpubs[0]).then((res) => {
|
||||||
setAdmin(res)
|
setAdmin(res)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
347
src/contexts/NDKContext.tsx
Normal file
347
src/contexts/NDKContext.tsx
Normal file
@ -0,0 +1,347 @@
|
|||||||
|
import NDK, {
|
||||||
|
getRelayListForUser,
|
||||||
|
NDKEvent,
|
||||||
|
NDKFilter,
|
||||||
|
NDKKind,
|
||||||
|
NDKRelaySet,
|
||||||
|
NDKSubscriptionCacheUsage,
|
||||||
|
NDKUser
|
||||||
|
} from '@nostr-dev-kit/ndk'
|
||||||
|
import NDKCacheAdapterDexie from '@nostr-dev-kit/ndk-cache-dexie'
|
||||||
|
import { MOD_FILTER_LIMIT, T_TAG_VALUE } from 'constants.ts'
|
||||||
|
import { UserRelaysType } from 'controllers'
|
||||||
|
import { Dexie } from 'dexie'
|
||||||
|
import { createContext, ReactNode, useEffect, useMemo } from 'react'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { ModDetails, UserProfile } from 'types'
|
||||||
|
import {
|
||||||
|
constructModListFromEvents,
|
||||||
|
hexToNpub,
|
||||||
|
log,
|
||||||
|
LogType,
|
||||||
|
npubToHex,
|
||||||
|
orderEventsChronologically
|
||||||
|
} from 'utils'
|
||||||
|
|
||||||
|
type FetchModsOptions = {
|
||||||
|
source?: string
|
||||||
|
until?: number
|
||||||
|
since?: number
|
||||||
|
limit?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NDKContextType {
|
||||||
|
ndk: NDK
|
||||||
|
fetchMods: (opts: FetchModsOptions) => Promise<ModDetails[]>
|
||||||
|
fetchEvents: (filter: NDKFilter, relayUrls?: string[]) => Promise<NDKEvent[]>
|
||||||
|
fetchEvent: (
|
||||||
|
filter: NDKFilter,
|
||||||
|
relayUrls?: string[]
|
||||||
|
) => Promise<NDKEvent | null>
|
||||||
|
|
||||||
|
fetchEventsFromUserRelays: (
|
||||||
|
filter: NDKFilter,
|
||||||
|
hexKey: string,
|
||||||
|
userRelaysType: UserRelaysType
|
||||||
|
) => Promise<NDKEvent[]>
|
||||||
|
fetchEventFromUserRelays: (
|
||||||
|
filter: NDKFilter,
|
||||||
|
hexKey: string,
|
||||||
|
userRelaysType: UserRelaysType
|
||||||
|
) => Promise<NDKEvent | null>
|
||||||
|
findMetadata: (pubkey: string) => Promise<UserProfile>
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the context with an initial value of `null`
|
||||||
|
export const NDKContext = createContext<NDKContextType | null>(null)
|
||||||
|
|
||||||
|
// Create a provider component to wrap around parts of your app
|
||||||
|
export const NDKContextProvider = ({ children }: { children: ReactNode }) => {
|
||||||
|
useEffect(() => {
|
||||||
|
window.onunhandledrejection = async (event: PromiseRejectionEvent) => {
|
||||||
|
event.preventDefault()
|
||||||
|
console.log(event.reason)
|
||||||
|
if (event.reason?.name === Dexie.errnames.DatabaseClosed) {
|
||||||
|
console.log(
|
||||||
|
'Could not open Dexie DB, probably version change. Deleting old DB and reloading...'
|
||||||
|
)
|
||||||
|
await Dexie.delete('degmod-db')
|
||||||
|
// Must reload to open a brand new DB
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const ndk = useMemo(() => {
|
||||||
|
localStorage.setItem('debug', '*')
|
||||||
|
const dexieAdapter = new NDKCacheAdapterDexie({ dbName: 'degmod-db' })
|
||||||
|
dexieAdapter.locking = true
|
||||||
|
const ndk = new NDK({
|
||||||
|
enableOutboxModel: true,
|
||||||
|
autoConnectUserRelays: true,
|
||||||
|
autoFetchUserMutelist: true,
|
||||||
|
explicitRelayUrls: [
|
||||||
|
'wss://user.kindpag.es',
|
||||||
|
'wss://purplepag.es',
|
||||||
|
'wss://relay.damus.io/',
|
||||||
|
import.meta.env.VITE_APP_RELAY
|
||||||
|
],
|
||||||
|
cacheAdapter: dexieAdapter
|
||||||
|
})
|
||||||
|
|
||||||
|
ndk.connect()
|
||||||
|
|
||||||
|
return ndk
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a list of mods based on the provided source.
|
||||||
|
*
|
||||||
|
* @param source - The source URL to filter the mods. If it matches the current window location,
|
||||||
|
* it adds a filter condition to the request.
|
||||||
|
* @param until - Optional timestamp to filter events until this time.
|
||||||
|
* @param since - Optional timestamp to filter events from this time.
|
||||||
|
* @returns A promise that resolves to an array of `ModDetails` objects. In case of an error,
|
||||||
|
* it logs the error and shows a notification, then returns an empty array.
|
||||||
|
*/
|
||||||
|
const fetchMods = async ({
|
||||||
|
source,
|
||||||
|
until,
|
||||||
|
since,
|
||||||
|
limit
|
||||||
|
}: FetchModsOptions): Promise<ModDetails[]> => {
|
||||||
|
const relays = new Set<string>()
|
||||||
|
relays.add(import.meta.env.VITE_APP_RELAY)
|
||||||
|
|
||||||
|
const adminNpubs = import.meta.env.VITE_ADMIN_NPUBS.split(',')
|
||||||
|
|
||||||
|
const promises = adminNpubs.map((npub) => {
|
||||||
|
const hexKey = npubToHex(npub)
|
||||||
|
if (!hexKey) return null
|
||||||
|
|
||||||
|
return getRelayListForUser(hexKey, ndk)
|
||||||
|
.then((ndkRelayList) => {
|
||||||
|
if (ndkRelayList) {
|
||||||
|
ndkRelayList.writeRelayUrls.forEach((url) => relays.add(url))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
log(
|
||||||
|
true,
|
||||||
|
LogType.Error,
|
||||||
|
`❌ Error occurred in getting the instance of NDKRelayList for npub: ${npub}`,
|
||||||
|
err
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.allSettled(promises)
|
||||||
|
|
||||||
|
// Define the filter criteria for fetching mods
|
||||||
|
const filter: NDKFilter = {
|
||||||
|
kinds: [NDKKind.Classified], // Specify the kind of events to fetch
|
||||||
|
limit: limit || MOD_FILTER_LIMIT, // Limit the number of events fetched to 20
|
||||||
|
'#t': [T_TAG_VALUE],
|
||||||
|
until, // Optional filter to fetch events until this timestamp
|
||||||
|
since // Optional filter to fetch events from this timestamp
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the source matches the current window location, add a filter condition
|
||||||
|
if (source === window.location.host) {
|
||||||
|
filter['#r'] = [window.location.host] // Add a tag filter for the current host
|
||||||
|
}
|
||||||
|
|
||||||
|
return ndk
|
||||||
|
.fetchEvents(
|
||||||
|
filter,
|
||||||
|
{ closeOnEose: true, cacheUsage: NDKSubscriptionCacheUsage.PARALLEL },
|
||||||
|
NDKRelaySet.fromRelayUrls(Array.from(relays), ndk, true)
|
||||||
|
)
|
||||||
|
.then((ndkEventSet) => {
|
||||||
|
const ndkEvents = Array.from(ndkEventSet)
|
||||||
|
orderEventsChronologically(ndkEvents)
|
||||||
|
|
||||||
|
// Convert the fetched events into a list of mods
|
||||||
|
const modList = constructModListFromEvents(ndkEvents)
|
||||||
|
return modList // Return the list of mods
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
// Log the error and show a notification if fetching fails
|
||||||
|
log(
|
||||||
|
true,
|
||||||
|
LogType.Error,
|
||||||
|
'An error occurred in fetching mods from relays',
|
||||||
|
err
|
||||||
|
)
|
||||||
|
toast.error('An error occurred in fetching mods from relays') // Show error notification
|
||||||
|
return [] // Return an empty array in case of an error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* @param filter - The filter criteria to find the event.
|
||||||
|
* @param relays - An optional array of relay URLs to search for the event.
|
||||||
|
* @returns Returns a promise that resolves to the found event or null if not found.
|
||||||
|
*/
|
||||||
|
const fetchEvents = async (
|
||||||
|
filter: NDKFilter,
|
||||||
|
relayUrls: string[] = []
|
||||||
|
): Promise<NDKEvent[]> => {
|
||||||
|
const relays = new Set<string>()
|
||||||
|
|
||||||
|
// add all the relays passed to relay set
|
||||||
|
relayUrls.forEach((relayUrl) => {
|
||||||
|
relays.add(relayUrl)
|
||||||
|
})
|
||||||
|
|
||||||
|
relays.add(import.meta.env.VITE_APP_RELAY)
|
||||||
|
|
||||||
|
const adminNpubs = import.meta.env.VITE_ADMIN_NPUBS.split(',')
|
||||||
|
|
||||||
|
const promises = adminNpubs.map((npub) => {
|
||||||
|
const hexKey = npubToHex(npub)
|
||||||
|
if (!hexKey) return null
|
||||||
|
|
||||||
|
return getRelayListForUser(hexKey, ndk)
|
||||||
|
.then((ndkRelayList) => {
|
||||||
|
if (ndkRelayList) {
|
||||||
|
ndkRelayList.writeRelayUrls.forEach((url) => relays.add(url))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
log(
|
||||||
|
true,
|
||||||
|
LogType.Error,
|
||||||
|
`❌ Error occurred in getting the instance of NDKRelayList for npub: ${npub}`,
|
||||||
|
err
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
await Promise.allSettled(promises)
|
||||||
|
|
||||||
|
return ndk
|
||||||
|
.fetchEvents(
|
||||||
|
filter,
|
||||||
|
{ closeOnEose: true, cacheUsage: NDKSubscriptionCacheUsage.PARALLEL },
|
||||||
|
NDKRelaySet.fromRelayUrls(Array.from(relays), ndk, true)
|
||||||
|
)
|
||||||
|
.then((ndkEventSet) => {
|
||||||
|
const ndkEvents = Array.from(ndkEventSet)
|
||||||
|
return orderEventsChronologically(ndkEvents)
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
// Log the error and show a notification if fetching fails
|
||||||
|
log(true, LogType.Error, 'An error occurred in fetching events', err)
|
||||||
|
toast.error('An error occurred in fetching events') // Show error notification
|
||||||
|
return [] // Return an empty array in case of an error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously retrieves an event from a set of relays based on a provided filter.
|
||||||
|
* If no relays are specified, it defaults to using connected relays.
|
||||||
|
*
|
||||||
|
* @param filter - The filter criteria to find the event.
|
||||||
|
* @param relaysUrls - An optional array of relay URLs to search for the event.
|
||||||
|
* @returns Returns a promise that resolves to the found event or null if not found.
|
||||||
|
*/
|
||||||
|
const fetchEvent = async (filter: NDKFilter, relayUrls: string[] = []) => {
|
||||||
|
const events = await fetchEvents(filter, relayUrls)
|
||||||
|
if (events.length === 0) return null
|
||||||
|
return events[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asynchronously retrieves multiple events from the user's relays based on a specified filter.
|
||||||
|
* The function first retrieves the user's relays, and then fetches the events 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 with an array of events.
|
||||||
|
*/
|
||||||
|
const fetchEventsFromUserRelays = async (
|
||||||
|
filter: NDKFilter,
|
||||||
|
hexKey: string,
|
||||||
|
userRelaysType: UserRelaysType
|
||||||
|
) => {
|
||||||
|
// Find the user's relays.
|
||||||
|
const relayUrls = await getRelayListForUser(hexKey, ndk)
|
||||||
|
.then((ndkRelayList) => {
|
||||||
|
if (ndkRelayList) return ndkRelayList[userRelaysType]
|
||||||
|
return [] // Return an empty array if ndkRelayList is undefined
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
log(
|
||||||
|
true,
|
||||||
|
LogType.Error,
|
||||||
|
`An error occurred in fetching user's (${hexKey}) ${userRelaysType}`,
|
||||||
|
err
|
||||||
|
)
|
||||||
|
return [] as string[]
|
||||||
|
})
|
||||||
|
|
||||||
|
// Fetch the event from the user's relays using the provided filter and relay URLs
|
||||||
|
return fetchEvents(filter, relayUrls)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
const fetchEventFromUserRelays = async (
|
||||||
|
filter: NDKFilter,
|
||||||
|
hexKey: string,
|
||||||
|
userRelaysType: UserRelaysType
|
||||||
|
) => {
|
||||||
|
const events = await fetchEventsFromUserRelays(
|
||||||
|
filter,
|
||||||
|
hexKey,
|
||||||
|
userRelaysType
|
||||||
|
)
|
||||||
|
if (events.length === 0) return null
|
||||||
|
return events[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds metadata for a given pubkey.
|
||||||
|
*
|
||||||
|
* @param hexKey - The pubkey to search for metadata.
|
||||||
|
* @returns A promise that resolves to the metadata event.
|
||||||
|
*/
|
||||||
|
const findMetadata = async (pubkey: string): Promise<UserProfile> => {
|
||||||
|
const npub = hexToNpub(pubkey)
|
||||||
|
|
||||||
|
const user = new NDKUser({ npub })
|
||||||
|
user.ndk = ndk
|
||||||
|
|
||||||
|
const userProfile = await user.fetchProfile()
|
||||||
|
|
||||||
|
return userProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<NDKContext.Provider
|
||||||
|
value={{
|
||||||
|
ndk,
|
||||||
|
fetchMods,
|
||||||
|
fetchEvents,
|
||||||
|
fetchEvent,
|
||||||
|
fetchEventsFromUserRelays,
|
||||||
|
fetchEventFromUserRelays,
|
||||||
|
findMetadata
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</NDKContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
@ -1,8 +1,7 @@
|
|||||||
import NDK, { getRelayListForUser, NDKList, NDKUser } from '@nostr-dev-kit/ndk'
|
import NDK, { getRelayListForUser, NDKList } from '@nostr-dev-kit/ndk'
|
||||||
import { kinds } from 'nostr-tools'
|
import { kinds } from 'nostr-tools'
|
||||||
import { MuteLists } from '../types'
|
import { MuteLists } from '../types'
|
||||||
import { UserProfile } from '../types/user'
|
import { log, LogType, npubToHex, timeout } from '../utils'
|
||||||
import { hexToNpub, log, LogType, npubToHex, timeout } from '../utils'
|
|
||||||
|
|
||||||
export enum UserRelaysType {
|
export enum UserRelaysType {
|
||||||
Read = 'readRelayUrls',
|
Read = 'readRelayUrls',
|
||||||
@ -16,7 +15,6 @@ export enum UserRelaysType {
|
|||||||
export class MetadataController {
|
export class MetadataController {
|
||||||
private static instance: MetadataController
|
private static instance: MetadataController
|
||||||
private ndk: NDK
|
private ndk: NDK
|
||||||
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
|
public reportingNpub: string
|
||||||
@ -84,40 +82,6 @@ export class MetadataController {
|
|||||||
return MetadataController.instance
|
return MetadataController.instance
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds metadata for a given pubkey.
|
|
||||||
*
|
|
||||||
* @param hexKey - The pubkey to search for metadata.
|
|
||||||
* @returns A promise that resolves to the metadata event.
|
|
||||||
*/
|
|
||||||
public findMetadata = async (pubkey: string): Promise<UserProfile> => {
|
|
||||||
const npub = hexToNpub(pubkey)
|
|
||||||
|
|
||||||
const cachedMetadata = this.usersMetadata.get(npub)
|
|
||||||
if (cachedMetadata) {
|
|
||||||
return cachedMetadata
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = new NDKUser({ npub })
|
|
||||||
user.ndk = this.ndk
|
|
||||||
|
|
||||||
const userProfile = await user.fetchProfile()
|
|
||||||
if (userProfile) {
|
|
||||||
this.usersMetadata.set(npub, userProfile)
|
|
||||||
}
|
|
||||||
|
|
||||||
return userProfile
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Finds metadata for admin user.
|
|
||||||
*
|
|
||||||
* @returns A promise that resolves to the metadata event.
|
|
||||||
*/
|
|
||||||
public findAdminMetadata = async (): Promise<UserProfile> => {
|
|
||||||
return this.findMetadata(this.adminNpubs[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
public findUserRelays = async (
|
public findUserRelays = async (
|
||||||
hexKey: string,
|
hexKey: string,
|
||||||
userRelaysType: UserRelaysType = UserRelaysType.Both
|
userRelaysType: UserRelaysType = UserRelaysType.Both
|
||||||
|
@ -372,217 +372,6 @@ export class RelayController {
|
|||||||
return publishedOnRelays
|
return publishedOnRelays
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 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.
|
|
||||||
*
|
|
||||||
* @param {Filter} filter - The filter criteria to find the event.
|
|
||||||
* @param {string[]} [relays] - An optional array of relay URLs to search for the event.
|
|
||||||
* @returns {Promise<Event | null>} - Returns a promise that resolves to the found event or null if not found.
|
|
||||||
*/
|
|
||||||
fetchEvents = async (
|
|
||||||
filter: Filter,
|
|
||||||
relayUrls: string[] = []
|
|
||||||
): Promise<Event[]> => {
|
|
||||||
const relaySet = new Set<string>()
|
|
||||||
|
|
||||||
// add all the relays passed to relay set
|
|
||||||
relayUrls.forEach((relayUrl) => {
|
|
||||||
relaySet.add(relayUrl)
|
|
||||||
})
|
|
||||||
|
|
||||||
relaySet.add(import.meta.env.VITE_APP_RELAY)
|
|
||||||
|
|
||||||
const metadataController = await MetadataController.getInstance()
|
|
||||||
// add admin relays to relays array
|
|
||||||
metadataController.adminRelays.forEach((relayUrl) => {
|
|
||||||
relaySet.add(relayUrl)
|
|
||||||
})
|
|
||||||
|
|
||||||
relayUrls = Array.from(relaySet)
|
|
||||||
|
|
||||||
// Connect to all specified relays
|
|
||||||
const relayPromises = relayUrls.map((relayUrl) =>
|
|
||||||
this.connectRelay(relayUrl)
|
|
||||||
)
|
|
||||||
|
|
||||||
// Use Promise.allSettled to wait for all promises to settle
|
|
||||||
const results = await Promise.allSettled(relayPromises)
|
|
||||||
|
|
||||||
// Extract non-null values from fulfilled promises in a single pass
|
|
||||||
const relays = results.reduce<Relay[]>((acc, result) => {
|
|
||||||
if (result.status === 'fulfilled') {
|
|
||||||
const value = result.value
|
|
||||||
if (value) {
|
|
||||||
acc.push(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return acc
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// Check if any relays are connected
|
|
||||||
if (relays.length === 0) {
|
|
||||||
throw new Error('No relay is connected to fetch events!')
|
|
||||||
}
|
|
||||||
|
|
||||||
const events: Event[] = []
|
|
||||||
const eventIds = new Set<string>() // To keep track of event IDs and avoid duplicates
|
|
||||||
|
|
||||||
// Create a promise for each relay subscription
|
|
||||||
const subPromises = relays.map((relay) => {
|
|
||||||
return new Promise<void>((resolve) => {
|
|
||||||
// Subscribe to the relay with the specified filter
|
|
||||||
const sub = relay.subscribe([filter], {
|
|
||||||
// Handle incoming events
|
|
||||||
onevent: (e) => {
|
|
||||||
// Add the event to the array if it's not a duplicate
|
|
||||||
if (!eventIds.has(e.id)) {
|
|
||||||
eventIds.add(e.id) // Record the event ID
|
|
||||||
events.push(e) // Add the event to the array
|
|
||||||
}
|
|
||||||
},
|
|
||||||
// Handle the End-Of-Stream (EOSE) message
|
|
||||||
oneose: () => {
|
|
||||||
sub.close() // Close the subscription
|
|
||||||
resolve() // Resolve the promise when EOSE is received
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Wait for all subscriptions to complete
|
|
||||||
await Promise.allSettled(subPromises)
|
|
||||||
|
|
||||||
// It is possible that different relays will send different events and events array may contain more events then specified limit in filter
|
|
||||||
// To fix this issue we'll first sort these events and then return only limited events
|
|
||||||
if (filter.limit) {
|
|
||||||
// Sort events by creation date in descending order
|
|
||||||
events.sort((a, b) => b.created_at - a.created_at)
|
|
||||||
|
|
||||||
return events.slice(0, filter.limit)
|
|
||||||
}
|
|
||||||
|
|
||||||
return events
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronously retrieves an event from a set of relays based on a provided filter.
|
|
||||||
* If no relays are specified, it defaults to using connected relays.
|
|
||||||
*
|
|
||||||
* @param {Filter} filter - The filter criteria to find the event.
|
|
||||||
* @param {string[]} [relays] - An optional array of relay URLs to search for the event.
|
|
||||||
* @returns {Promise<Event | null>} - Returns a promise that resolves to the found event or null if not found.
|
|
||||||
*/
|
|
||||||
fetchEvent = async (
|
|
||||||
filter: Filter,
|
|
||||||
relays: string[] = []
|
|
||||||
): Promise<Event | null> => {
|
|
||||||
// first check if event is present in cached map then return that
|
|
||||||
// otherwise query relays
|
|
||||||
if (filter['#a']) {
|
|
||||||
const aTag = filter['#a'][0]
|
|
||||||
const cachedEvent = this.events.get(aTag)
|
|
||||||
|
|
||||||
if (cachedEvent) return cachedEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
const events = await this.fetchEvents(filter, relays)
|
|
||||||
|
|
||||||
// Sort events by creation date in descending order
|
|
||||||
events.sort((a, b) => b.created_at - a.created_at)
|
|
||||||
|
|
||||||
if (events.length > 0) {
|
|
||||||
const event = events[0]
|
|
||||||
|
|
||||||
// if the aTag was specified in filter then cache the fetched event before returning
|
|
||||||
if (filter['#a']) {
|
|
||||||
const aTag = filter['#a'][0]
|
|
||||||
this.events.set(aTag, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
// return the event
|
|
||||||
return event
|
|
||||||
}
|
|
||||||
|
|
||||||
// return null if event array is empty
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Asynchronously retrieves multiple events from the user's relays based on a specified filter.
|
|
||||||
* The function first retrieves the user's relays, and then fetches the events 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 with an array of events.
|
|
||||||
*/
|
|
||||||
fetchEventsFromUserRelays = async (
|
|
||||||
filter: Filter,
|
|
||||||
hexKey: string,
|
|
||||||
userRelaysType: UserRelaysType
|
|
||||||
): Promise<Event[]> => {
|
|
||||||
// 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.
|
|
||||||
const relayUrls = await metadataController.findUserRelays(
|
|
||||||
hexKey,
|
|
||||||
userRelaysType
|
|
||||||
)
|
|
||||||
|
|
||||||
// Fetch the event from the user's relays using the provided filter and relay URLs
|
|
||||||
return this.fetchEvents(filter, relayUrls)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
): Promise<Event | null> => {
|
|
||||||
// first check if event is present in cached map then return that
|
|
||||||
// otherwise query relays
|
|
||||||
if (filter['#a']) {
|
|
||||||
const aTag = filter['#a'][0]
|
|
||||||
const cachedEvent = this.events.get(aTag)
|
|
||||||
|
|
||||||
if (cachedEvent) return cachedEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
const events = await this.fetchEventsFromUserRelays(
|
|
||||||
filter,
|
|
||||||
hexKey,
|
|
||||||
userRelaysType
|
|
||||||
)
|
|
||||||
// Sort events by creation date in descending order
|
|
||||||
events.sort((a, b) => b.created_at - a.created_at)
|
|
||||||
|
|
||||||
if (events.length > 0) {
|
|
||||||
const event = events[0]
|
|
||||||
|
|
||||||
// if the aTag was specified in filter then cache the fetched event before returning
|
|
||||||
if (filter['#a']) {
|
|
||||||
const aTag = filter['#a'][0]
|
|
||||||
this.events.set(aTag, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
// return the event
|
|
||||||
return event
|
|
||||||
}
|
|
||||||
|
|
||||||
// return null if event array is empty
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribes to events from multiple relays.
|
* Subscribes to events from multiple relays.
|
||||||
*
|
*
|
||||||
|
@ -1,6 +1,12 @@
|
|||||||
import { Invoice } from '@getalby/lightning-tools'
|
import { Invoice } from '@getalby/lightning-tools'
|
||||||
|
import NDK, {
|
||||||
|
NDKFilter,
|
||||||
|
NDKKind,
|
||||||
|
NDKRelaySet,
|
||||||
|
NDKSubscriptionCacheUsage
|
||||||
|
} from '@nostr-dev-kit/ndk'
|
||||||
import axios, { AxiosInstance } from 'axios'
|
import axios, { AxiosInstance } from 'axios'
|
||||||
import { Filter, kinds } from 'nostr-tools'
|
import { kinds } from 'nostr-tools'
|
||||||
import { requestProvider, SendPaymentResponse, WebLNProvider } from 'webln'
|
import { requestProvider, SendPaymentResponse, WebLNProvider } from 'webln'
|
||||||
import {
|
import {
|
||||||
isLnurlResponse,
|
isLnurlResponse,
|
||||||
@ -11,7 +17,6 @@ import {
|
|||||||
ZapRequest
|
ZapRequest
|
||||||
} from '../types'
|
} from '../types'
|
||||||
import { log, LogType, npubToHex } from '../utils'
|
import { log, LogType, npubToHex } from '../utils'
|
||||||
import { RelayController } from './relay'
|
|
||||||
import { MetadataController, UserRelaysType } from './metadata'
|
import { MetadataController, UserRelaysType } from './metadata'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -134,6 +139,7 @@ export class ZapController {
|
|||||||
*/
|
*/
|
||||||
async pollZapReceipt(
|
async pollZapReceipt(
|
||||||
paymentRequest: PaymentRequest,
|
paymentRequest: PaymentRequest,
|
||||||
|
ndk: NDK,
|
||||||
pollingTimeout?: number
|
pollingTimeout?: number
|
||||||
) {
|
) {
|
||||||
const { pr, ...zapRequest } = paymentRequest
|
const { pr, ...zapRequest } = paymentRequest
|
||||||
@ -148,7 +154,7 @@ export class ZapController {
|
|||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
clearTimeout(timeout)
|
clearTimeout(timeout)
|
||||||
|
|
||||||
subscriptions.forEach((subscription) => subscription.close())
|
subscription.stop()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Polling timeout
|
// Polling timeout
|
||||||
@ -168,32 +174,35 @@ export class ZapController {
|
|||||||
const relayUrls = relaysTag.slice(1)
|
const relayUrls = relaysTag.slice(1)
|
||||||
|
|
||||||
// filter relay for event of kind 9735
|
// filter relay for event of kind 9735
|
||||||
const filter: Filter = {
|
const filter: NDKFilter = {
|
||||||
kinds: [kinds.Zap],
|
kinds: [NDKKind.Zap],
|
||||||
since: created_at
|
since: created_at
|
||||||
}
|
}
|
||||||
|
|
||||||
const subscriptions =
|
const subscription = ndk.subscribe(
|
||||||
await RelayController.getInstance().subscribeForEvents(
|
|
||||||
filter,
|
filter,
|
||||||
relayUrls,
|
{
|
||||||
async (event) => {
|
closeOnEose: false,
|
||||||
// get description tag of the event
|
cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY
|
||||||
const description = event.tags.filter(
|
},
|
||||||
(tag) => tag[0] === 'description'
|
NDKRelaySet.fromRelayUrls(relayUrls, ndk, true)
|
||||||
)[0]
|
)
|
||||||
|
|
||||||
|
subscription.on('event', async (ndkEvent) => {
|
||||||
// compare description tag of the event with stringified zap request
|
// compare description tag of the event with stringified zap request
|
||||||
if (description[1] === zapRequestStringified) {
|
if (ndkEvent.tagValue('description') === zapRequestStringified) {
|
||||||
// validate zap receipt
|
// validate zap receipt
|
||||||
if (await this.validateZapReceipt(pr, event as ZapReceipt)) {
|
if (
|
||||||
|
await this.validateZapReceipt(pr, ndkEvent.rawEvent() as ZapReceipt)
|
||||||
|
) {
|
||||||
cleanup()
|
cleanup()
|
||||||
|
|
||||||
resolve(event as ZapReceipt)
|
resolve(ndkEvent.rawEvent() as ZapReceipt)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
)
|
|
||||||
|
subscription.start()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,3 +5,4 @@ export * from './useGames'
|
|||||||
export * from './useMuteLists'
|
export * from './useMuteLists'
|
||||||
export * from './useNSFWList'
|
export * from './useNSFWList'
|
||||||
export * from './useReactions'
|
export * from './useReactions'
|
||||||
|
export * from './useNDKContext'
|
||||||
|
@ -1,43 +1,87 @@
|
|||||||
import {
|
import {
|
||||||
MetadataController,
|
getRelayListForUser,
|
||||||
RelayController,
|
NDKFilter,
|
||||||
UserRelaysType
|
NDKKind,
|
||||||
} from 'controllers'
|
NDKRelaySet,
|
||||||
import { Filter, kinds } from 'nostr-tools'
|
NDKSubscription,
|
||||||
import { useState } from 'react'
|
NDKSubscriptionCacheUsage
|
||||||
|
} from '@nostr-dev-kit/ndk'
|
||||||
|
import { UserRelaysType } from 'controllers'
|
||||||
|
import { useEffect, useState } from 'react'
|
||||||
import { CommentEvent, ModDetails } from 'types'
|
import { CommentEvent, ModDetails } from 'types'
|
||||||
import { useDidMount } from './useDidMount'
|
import { log, LogType } from 'utils'
|
||||||
|
import { useNDKContext } from './useNDKContext'
|
||||||
|
|
||||||
export const useComments = (mod: ModDetails) => {
|
export const useComments = (mod: ModDetails) => {
|
||||||
|
const { ndk } = useNDKContext()
|
||||||
const [commentEvents, setCommentEvents] = useState<CommentEvent[]>([])
|
const [commentEvents, setCommentEvents] = useState<CommentEvent[]>([])
|
||||||
|
|
||||||
useDidMount(async () => {
|
useEffect(() => {
|
||||||
const metadataController = await MetadataController.getInstance()
|
let subscription: NDKSubscription // Define the subscription variable here for cleanup
|
||||||
|
|
||||||
const authorReadRelays = await metadataController.findUserRelays(
|
const setupSubscription = async () => {
|
||||||
mod.author,
|
// Find the mod author's relays.
|
||||||
UserRelaysType.Read
|
const authorReadRelays = await getRelayListForUser(mod.author, ndk)
|
||||||
|
.then((ndkRelayList) => {
|
||||||
|
if (ndkRelayList) return ndkRelayList[UserRelaysType.Read]
|
||||||
|
return [] // Return an empty array if ndkRelayList is undefined
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
log(
|
||||||
|
true,
|
||||||
|
LogType.Error,
|
||||||
|
`An error occurred in fetching user's (${mod.author}) ${UserRelaysType.Read}`,
|
||||||
|
err
|
||||||
)
|
)
|
||||||
|
return [] as string[]
|
||||||
|
})
|
||||||
|
|
||||||
const filter: Filter = {
|
const filter: NDKFilter = {
|
||||||
kinds: [kinds.ShortTextNote],
|
kinds: [NDKKind.Text],
|
||||||
'#a': [mod.aTag]
|
'#a': [mod.aTag]
|
||||||
}
|
}
|
||||||
|
|
||||||
RelayController.getInstance().subscribeForEvents(
|
subscription = ndk.subscribe(
|
||||||
filter,
|
filter,
|
||||||
authorReadRelays,
|
{
|
||||||
(event) => {
|
closeOnEose: false,
|
||||||
|
cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST
|
||||||
|
},
|
||||||
|
NDKRelaySet.fromRelayUrls(authorReadRelays, ndk, true)
|
||||||
|
)
|
||||||
|
|
||||||
|
subscription.on('event', (ndkEvent) => {
|
||||||
setCommentEvents((prev) => {
|
setCommentEvents((prev) => {
|
||||||
if (prev.find((e) => e.id === event.id)) {
|
if (prev.find((e) => e.id === ndkEvent.id)) {
|
||||||
return [...prev]
|
return [...prev]
|
||||||
}
|
}
|
||||||
|
|
||||||
return [event, ...prev]
|
const commentEvent: CommentEvent = {
|
||||||
})
|
kind: NDKKind.Text,
|
||||||
|
tags: ndkEvent.tags,
|
||||||
|
content: ndkEvent.content,
|
||||||
|
created_at: ndkEvent.created_at!,
|
||||||
|
pubkey: ndkEvent.pubkey,
|
||||||
|
id: ndkEvent.id,
|
||||||
|
sig: ndkEvent.sig!
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
return [commentEvent, ...prev]
|
||||||
})
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
subscription.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
setupSubscription()
|
||||||
|
|
||||||
|
// Cleanup function to stop the subscription on unmount
|
||||||
|
return () => {
|
||||||
|
if (subscription) {
|
||||||
|
subscription.stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [mod.aTag, mod.author, ndk])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
commentEvents,
|
commentEvents,
|
||||||
|
31
src/hooks/useNDKContext.ts
Normal file
31
src/hooks/useNDKContext.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { NDKContext } from 'contexts/NDKContext'
|
||||||
|
import { useContext } from 'react'
|
||||||
|
|
||||||
|
export const useNDKContext = () => {
|
||||||
|
const ndkContext = useContext(NDKContext)
|
||||||
|
|
||||||
|
if (!ndkContext)
|
||||||
|
throw new Error(
|
||||||
|
'NDKContext should not be used in out component tree hierarchy'
|
||||||
|
)
|
||||||
|
|
||||||
|
const {
|
||||||
|
ndk,
|
||||||
|
fetchEvents,
|
||||||
|
fetchEvent,
|
||||||
|
fetchEventsFromUserRelays,
|
||||||
|
fetchEventFromUserRelays,
|
||||||
|
fetchMods,
|
||||||
|
findMetadata
|
||||||
|
} = ndkContext
|
||||||
|
|
||||||
|
return {
|
||||||
|
ndk,
|
||||||
|
fetchEvents,
|
||||||
|
fetchEvent,
|
||||||
|
fetchEventsFromUserRelays,
|
||||||
|
fetchEventFromUserRelays,
|
||||||
|
fetchMods,
|
||||||
|
findMetadata
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,10 @@
|
|||||||
import { useState, useMemo } from 'react'
|
import { NDKEvent, NDKFilter } from '@nostr-dev-kit/ndk'
|
||||||
import { toast } from 'react-toastify'
|
|
||||||
import { REACTIONS } from 'constants.ts'
|
import { REACTIONS } from 'constants.ts'
|
||||||
import { RelayController, UserRelaysType } from 'controllers'
|
import { RelayController, UserRelaysType } from 'controllers'
|
||||||
import { useAppSelector, useDidMount } from 'hooks'
|
import { useAppSelector, useDidMount, useNDKContext } from 'hooks'
|
||||||
import { Event, Filter, UnsignedEvent, kinds } from 'nostr-tools'
|
import { Event, kinds, UnsignedEvent } from 'nostr-tools'
|
||||||
|
import { useMemo, useState } from 'react'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
import { abbreviateNumber, log, LogType, now } from 'utils'
|
import { abbreviateNumber, log, LogType, now } from 'utils'
|
||||||
|
|
||||||
type UseReactionsParams = {
|
type UseReactionsParams = {
|
||||||
@ -13,14 +14,15 @@ type UseReactionsParams = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useReactions = (params: UseReactionsParams) => {
|
export const useReactions = (params: UseReactionsParams) => {
|
||||||
|
const { ndk, fetchEventsFromUserRelays } = useNDKContext()
|
||||||
const [isReactionInProgress, setIsReactionInProgress] = useState(false)
|
const [isReactionInProgress, setIsReactionInProgress] = useState(false)
|
||||||
const [isDataLoaded, setIsDataLoaded] = useState(false)
|
const [isDataLoaded, setIsDataLoaded] = useState(false)
|
||||||
const [reactionEvents, setReactionEvents] = useState<Event[]>([])
|
const [reactionEvents, setReactionEvents] = useState<NDKEvent[]>([])
|
||||||
|
|
||||||
const userState = useAppSelector((state) => state.user)
|
const userState = useAppSelector((state) => state.user)
|
||||||
|
|
||||||
useDidMount(() => {
|
useDidMount(() => {
|
||||||
const filter: Filter = {
|
const filter: NDKFilter = {
|
||||||
kinds: [kinds.Reaction]
|
kinds: [kinds.Reaction]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -30,8 +32,7 @@ export const useReactions = (params: UseReactionsParams) => {
|
|||||||
filter['#e'] = [params.eTag]
|
filter['#e'] = [params.eTag]
|
||||||
}
|
}
|
||||||
|
|
||||||
RelayController.getInstance()
|
fetchEventsFromUserRelays(filter, params.pubkey, UserRelaysType.Read)
|
||||||
.fetchEventsFromUserRelays(filter, params.pubkey, UserRelaysType.Read)
|
|
||||||
.then((events) => {
|
.then((events) => {
|
||||||
setReactionEvents(events)
|
setReactionEvents(events)
|
||||||
})
|
})
|
||||||
@ -118,7 +119,7 @@ export const useReactions = (params: UseReactionsParams) => {
|
|||||||
|
|
||||||
if (!signedEvent) return
|
if (!signedEvent) return
|
||||||
|
|
||||||
setReactionEvents((prev) => [...prev, signedEvent])
|
setReactionEvents((prev) => [...prev, new NDKEvent(ndk, signedEvent)])
|
||||||
|
|
||||||
const publishedOnRelays = await RelayController.getInstance().publish(
|
const publishedOnRelays = await RelayController.getInstance().publish(
|
||||||
signedEvent as Event,
|
signedEvent as Event,
|
||||||
|
@ -7,7 +7,12 @@ import { Link } from 'react-router-dom'
|
|||||||
import { Banner } from '../components/Banner'
|
import { Banner } from '../components/Banner'
|
||||||
import { ZapPopUp } from '../components/Zap'
|
import { ZapPopUp } from '../components/Zap'
|
||||||
import { MetadataController } from '../controllers'
|
import { MetadataController } from '../controllers'
|
||||||
import { useAppDispatch, useAppSelector, useDidMount } from '../hooks'
|
import {
|
||||||
|
useAppDispatch,
|
||||||
|
useAppSelector,
|
||||||
|
useDidMount,
|
||||||
|
useNDKContext
|
||||||
|
} from '../hooks'
|
||||||
import { appRoutes } from '../routes'
|
import { appRoutes } from '../routes'
|
||||||
import { setAuth, setUser } from '../store/reducers/user'
|
import { setAuth, setUser } from '../store/reducers/user'
|
||||||
import mainStyles from '../styles//main.module.scss'
|
import mainStyles from '../styles//main.module.scss'
|
||||||
@ -17,6 +22,7 @@ import { npubToHex } from '../utils'
|
|||||||
|
|
||||||
export const Header = () => {
|
export const Header = () => {
|
||||||
const dispatch = useAppDispatch()
|
const dispatch = useAppDispatch()
|
||||||
|
const { findMetadata } = useNDKContext()
|
||||||
const userState = useAppSelector((state) => state.user)
|
const userState = useAppSelector((state) => state.user)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -41,8 +47,7 @@ export const Header = () => {
|
|||||||
pubkey: npubToHex(npub)!
|
pubkey: npubToHex(npub)!
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
MetadataController.getInstance().then((metadataController) => {
|
findMetadata(npub).then((userProfile) => {
|
||||||
metadataController.findMetadata(npub).then((userProfile) => {
|
|
||||||
if (userProfile) {
|
if (userProfile) {
|
||||||
dispatch(
|
dispatch(
|
||||||
setUser({
|
setUser({
|
||||||
@ -53,11 +58,10 @@ export const Header = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [dispatch])
|
}, [dispatch, findMetadata])
|
||||||
|
|
||||||
const handleLogin = () => {
|
const handleLogin = () => {
|
||||||
launchNostrLoginDialog()
|
launchNostrLoginDialog()
|
||||||
@ -357,8 +361,14 @@ const RegisterButtonWithDialog = () => {
|
|||||||
</label>
|
</label>
|
||||||
<p className='labelDescriptionMain'>
|
<p className='labelDescriptionMain'>
|
||||||
Once you create your "account" on any of these (
|
Once you create your "account" on any of these (
|
||||||
<a href="https://video.nostr.build/765aa9bf16dd58bca701efee2572f7e77f29b2787cddd2bee8bbbdea35798153.mp4" target="blank_">Here's a quick video guide</a>
|
<a
|
||||||
), come back and click login, then sign-in with extension.
|
href='https://video.nostr.build/765aa9bf16dd58bca701efee2572f7e77f29b2787cddd2bee8bbbdea35798153.mp4'
|
||||||
|
target='blank_'
|
||||||
|
>
|
||||||
|
Here's a quick video guide
|
||||||
|
</a>
|
||||||
|
), come back and click login, then sign-in with
|
||||||
|
extension.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<a
|
<a
|
||||||
|
@ -7,12 +7,15 @@ import 'react-toastify/dist/ReactToastify.css'
|
|||||||
import App from './App.tsx'
|
import App from './App.tsx'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import { store } from './store/index.ts'
|
import { store } from './store/index.ts'
|
||||||
|
import { NDKContextProvider } from 'contexts/NDKContext.tsx'
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<Provider store={store}>
|
<Provider store={store}>
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
|
<NDKContextProvider>
|
||||||
<App />
|
<App />
|
||||||
|
</NDKContextProvider>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
</HashRouter>
|
</HashRouter>
|
||||||
</Provider>
|
</Provider>
|
||||||
|
@ -1,20 +1,21 @@
|
|||||||
import { LoadingSpinner } from 'components/LoadingSpinner'
|
import {
|
||||||
|
NDKFilter,
|
||||||
|
NDKKind,
|
||||||
|
NDKSubscriptionCacheUsage
|
||||||
|
} from '@nostr-dev-kit/ndk'
|
||||||
import { ModCard } from 'components/ModCard'
|
import { ModCard } from 'components/ModCard'
|
||||||
import { ModFilter } from 'components/ModsFilter'
|
import { ModFilter } from 'components/ModsFilter'
|
||||||
import { PaginationWithPageNumbers } from 'components/Pagination'
|
import { PaginationWithPageNumbers } from 'components/Pagination'
|
||||||
import { MAX_MODS_PER_PAGE, T_TAG_VALUE } from 'constants.ts'
|
import { MAX_MODS_PER_PAGE, T_TAG_VALUE } from 'constants.ts'
|
||||||
import { RelayController } from 'controllers'
|
|
||||||
import {
|
import {
|
||||||
useAppSelector,
|
useAppSelector,
|
||||||
useFilteredMods,
|
useFilteredMods,
|
||||||
useMuteLists,
|
useMuteLists,
|
||||||
|
useNDKContext,
|
||||||
useNSFWList
|
useNSFWList
|
||||||
} from 'hooks'
|
} from 'hooks'
|
||||||
import { Filter, kinds } from 'nostr-tools'
|
import { useEffect, useState } from 'react'
|
||||||
import { Subscription } from 'nostr-tools/abstract-relay'
|
|
||||||
import { useEffect, useRef, useState } from 'react'
|
|
||||||
import { useParams } from 'react-router-dom'
|
import { useParams } from 'react-router-dom'
|
||||||
import { toast } from 'react-toastify'
|
|
||||||
import {
|
import {
|
||||||
FilterOptions,
|
FilterOptions,
|
||||||
ModDetails,
|
ModDetails,
|
||||||
@ -22,11 +23,12 @@ import {
|
|||||||
NSFWFilter,
|
NSFWFilter,
|
||||||
SortBy
|
SortBy
|
||||||
} from 'types'
|
} from 'types'
|
||||||
import { extractModData, isModDataComplete, log, LogType } from 'utils'
|
import { extractModData, isModDataComplete } from 'utils'
|
||||||
|
|
||||||
export const GamePage = () => {
|
export const GamePage = () => {
|
||||||
const params = useParams()
|
const params = useParams()
|
||||||
const { name: gameName } = params
|
const { name: gameName } = params
|
||||||
|
const { ndk } = useNDKContext()
|
||||||
const muteLists = useMuteLists()
|
const muteLists = useMuteLists()
|
||||||
const nsfwList = useNSFWList()
|
const nsfwList = useNSFWList()
|
||||||
|
|
||||||
@ -38,8 +40,6 @@ export const GamePage = () => {
|
|||||||
})
|
})
|
||||||
const [mods, setMods] = useState<ModDetails[]>([])
|
const [mods, setMods] = useState<ModDetails[]>([])
|
||||||
|
|
||||||
const hasEffectRun = useRef(false)
|
|
||||||
const [isSubscribing, setIsSubscribing] = useState(false)
|
|
||||||
const [currentPage, setCurrentPage] = useState(1)
|
const [currentPage, setCurrentPage] = useState(1)
|
||||||
|
|
||||||
const userState = useAppSelector((state) => state.user)
|
const userState = useAppSelector((state) => state.user)
|
||||||
@ -66,57 +66,40 @@ export const GamePage = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasEffectRun.current) {
|
const filter: NDKFilter = {
|
||||||
return
|
kinds: [NDKKind.Classified],
|
||||||
}
|
|
||||||
|
|
||||||
hasEffectRun.current = true // Set it so the effect doesn't run again
|
|
||||||
|
|
||||||
const filter: Filter = {
|
|
||||||
kinds: [kinds.ClassifiedListing],
|
|
||||||
'#t': [T_TAG_VALUE]
|
'#t': [T_TAG_VALUE]
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsSubscribing(true)
|
const subscription = ndk.subscribe(filter, {
|
||||||
|
cacheUsage: NDKSubscriptionCacheUsage.PARALLEL,
|
||||||
|
closeOnEose: true
|
||||||
|
})
|
||||||
|
|
||||||
let subscriptions: Subscription[] = []
|
subscription.on('event', (ndkEvent) => {
|
||||||
|
if (isModDataComplete(ndkEvent)) {
|
||||||
|
const mod = extractModData(ndkEvent)
|
||||||
|
if (mod.game === gameName)
|
||||||
|
setMods((prev) => {
|
||||||
|
if (prev.find((e) => e.aTag === mod.aTag)) return [...prev]
|
||||||
|
|
||||||
RelayController.getInstance()
|
return [...prev, mod]
|
||||||
.subscribeForEvents(filter, [], (event) => {
|
})
|
||||||
if (isModDataComplete(event)) {
|
|
||||||
const mod = extractModData(event)
|
|
||||||
if (mod.game === gameName) setMods((prev) => [...prev, mod])
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((subs) => {
|
|
||||||
subscriptions = subs
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
log(
|
|
||||||
true,
|
|
||||||
LogType.Error,
|
|
||||||
'An error occurred in subscribing to relays.',
|
|
||||||
err
|
|
||||||
)
|
|
||||||
toast.error(err.message || err)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsSubscribing(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Cleanup function to stop all subscriptions
|
subscription.start()
|
||||||
|
|
||||||
|
// Cleanup function to stop subscription
|
||||||
return () => {
|
return () => {
|
||||||
subscriptions.forEach((sub) => sub.close()) // close each subscription
|
subscription.stop()
|
||||||
}
|
}
|
||||||
}, [gameName])
|
}, [gameName, ndk])
|
||||||
|
|
||||||
if (!gameName) return null
|
if (!gameName) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isSubscribing && (
|
|
||||||
<LoadingSpinner desc='Subscribing to relays for mods' />
|
|
||||||
)}
|
|
||||||
<div className='InnerBodyMain'>
|
<div className='InnerBodyMain'>
|
||||||
<div className='ContainerMain'>
|
<div className='ContainerMain'>
|
||||||
<div className='IBMSecMainGroup IBMSecMainGroupAlt'>
|
<div className='IBMSecMainGroup IBMSecMainGroupAlt'>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { PaginationWithPageNumbers } from 'components/Pagination'
|
import { PaginationWithPageNumbers } from 'components/Pagination'
|
||||||
import { MAX_GAMES_PER_PAGE } from 'constants.ts'
|
import { MAX_GAMES_PER_PAGE } from 'constants.ts'
|
||||||
import { useDidMount, useGames } from 'hooks'
|
import { useDidMount, useGames, useNDKContext } from 'hooks'
|
||||||
import { useMemo, useRef, useState } from 'react'
|
import { useMemo, useRef, useState } from 'react'
|
||||||
import { GameCard } from '../components/GameCard'
|
import { GameCard } from '../components/GameCard'
|
||||||
import '../styles/pagination.css'
|
import '../styles/pagination.css'
|
||||||
@ -8,10 +8,10 @@ import '../styles/search.css'
|
|||||||
import '../styles/styles.css'
|
import '../styles/styles.css'
|
||||||
import { createSearchParams, useNavigate } from 'react-router-dom'
|
import { createSearchParams, useNavigate } from 'react-router-dom'
|
||||||
import { appRoutes } from 'routes'
|
import { appRoutes } from 'routes'
|
||||||
import { fetchMods } from 'utils'
|
|
||||||
|
|
||||||
export const GamesPage = () => {
|
export const GamesPage = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { fetchMods } = useNDKContext()
|
||||||
const searchTermRef = useRef<HTMLInputElement>(null)
|
const searchTermRef = useRef<HTMLInputElement>(null)
|
||||||
const games = useGames()
|
const games = useGames()
|
||||||
const [gamesWithMods, setGamesWithMods] = useState<string[]>([])
|
const [gamesWithMods, setGamesWithMods] = useState<string[]>([])
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Filter, nip19 } from 'nostr-tools'
|
import { nip19 } from 'nostr-tools'
|
||||||
import { useMemo, useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import { A11y, Autoplay, Navigation, Pagination } from 'swiper/modules'
|
import { A11y, Autoplay, Navigation, Pagination } from 'swiper/modules'
|
||||||
@ -7,23 +7,23 @@ import { BlogCard } from '../components/BlogCard'
|
|||||||
import { GameCard } from '../components/GameCard'
|
import { GameCard } from '../components/GameCard'
|
||||||
import { ModCard } from '../components/ModCard'
|
import { ModCard } from '../components/ModCard'
|
||||||
import { LANDING_PAGE_DATA } from '../constants'
|
import { LANDING_PAGE_DATA } from '../constants'
|
||||||
import { RelayController } from '../controllers'
|
import {
|
||||||
import { useDidMount, useGames, useMuteLists, useNSFWList } from '../hooks'
|
useDidMount,
|
||||||
|
useGames,
|
||||||
|
useMuteLists,
|
||||||
|
useNDKContext,
|
||||||
|
useNSFWList
|
||||||
|
} from '../hooks'
|
||||||
import { appRoutes, getModPageRoute } from '../routes'
|
import { appRoutes, getModPageRoute } from '../routes'
|
||||||
import { ModDetails } from '../types'
|
import { ModDetails } from '../types'
|
||||||
import {
|
import { extractModData, handleModImageError, log, LogType } from '../utils'
|
||||||
extractModData,
|
|
||||||
fetchMods,
|
|
||||||
handleModImageError,
|
|
||||||
log,
|
|
||||||
LogType
|
|
||||||
} from '../utils'
|
|
||||||
|
|
||||||
import '../styles/cardLists.css'
|
import '../styles/cardLists.css'
|
||||||
import '../styles/SimpleSlider.css'
|
import '../styles/SimpleSlider.css'
|
||||||
import '../styles/styles.css'
|
import '../styles/styles.css'
|
||||||
|
|
||||||
// Import Swiper styles
|
// Import Swiper styles
|
||||||
|
import { NDKFilter } from '@nostr-dev-kit/ndk'
|
||||||
import 'swiper/css'
|
import 'swiper/css'
|
||||||
import 'swiper/css/navigation'
|
import 'swiper/css/navigation'
|
||||||
import 'swiper/css/pagination'
|
import 'swiper/css/pagination'
|
||||||
@ -146,23 +146,23 @@ type SlideContentProps = {
|
|||||||
|
|
||||||
const SlideContent = ({ naddr }: SlideContentProps) => {
|
const SlideContent = ({ naddr }: SlideContentProps) => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { fetchEvent } = useNDKContext()
|
||||||
const [mod, setMod] = useState<ModDetails>()
|
const [mod, setMod] = useState<ModDetails>()
|
||||||
|
|
||||||
useDidMount(() => {
|
useDidMount(() => {
|
||||||
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
|
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
|
||||||
const { identifier, kind, pubkey, relays = [] } = decoded.data
|
const { identifier, kind, pubkey, relays = [] } = decoded.data
|
||||||
|
|
||||||
const filter: Filter = {
|
const ndkFilter: NDKFilter = {
|
||||||
'#a': [identifier],
|
'#a': [identifier],
|
||||||
authors: [pubkey],
|
authors: [pubkey],
|
||||||
kinds: [kind]
|
kinds: [kind]
|
||||||
}
|
}
|
||||||
|
|
||||||
RelayController.getInstance()
|
fetchEvent(ndkFilter, relays)
|
||||||
.fetchEvent(filter, relays)
|
.then((ndkEvent) => {
|
||||||
.then((event) => {
|
if (ndkEvent) {
|
||||||
if (event) {
|
const extracted = extractModData(ndkEvent)
|
||||||
const extracted = extractModData(event)
|
|
||||||
setMod(extracted)
|
setMod(extracted)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -220,21 +220,22 @@ type DisplayModProps = {
|
|||||||
const DisplayMod = ({ naddr }: DisplayModProps) => {
|
const DisplayMod = ({ naddr }: DisplayModProps) => {
|
||||||
const [mod, setMod] = useState<ModDetails>()
|
const [mod, setMod] = useState<ModDetails>()
|
||||||
|
|
||||||
|
const { fetchEvent } = useNDKContext()
|
||||||
|
|
||||||
useDidMount(() => {
|
useDidMount(() => {
|
||||||
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
|
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
|
||||||
const { identifier, kind, pubkey, relays = [] } = decoded.data
|
const { identifier, kind, pubkey, relays = [] } = decoded.data
|
||||||
|
|
||||||
const filter: Filter = {
|
const ndkFilter: NDKFilter = {
|
||||||
'#a': [identifier],
|
'#a': [identifier],
|
||||||
authors: [pubkey],
|
authors: [pubkey],
|
||||||
kinds: [kind]
|
kinds: [kind]
|
||||||
}
|
}
|
||||||
|
|
||||||
RelayController.getInstance()
|
fetchEvent(ndkFilter, relays)
|
||||||
.fetchEvent(filter, relays)
|
.then((ndkEvent) => {
|
||||||
.then((event) => {
|
if (ndkEvent) {
|
||||||
if (event) {
|
const extracted = extractModData(ndkEvent)
|
||||||
const extracted = extractModData(event)
|
|
||||||
setMod(extracted)
|
setMod(extracted)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -255,6 +256,7 @@ const DisplayMod = ({ naddr }: DisplayModProps) => {
|
|||||||
|
|
||||||
const DisplayLatestMods = () => {
|
const DisplayLatestMods = () => {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const { fetchMods } = useNDKContext()
|
||||||
const [isFetchingLatestMods, setIsFetchingLatestMods] = useState(true)
|
const [isFetchingLatestMods, setIsFetchingLatestMods] = useState(true)
|
||||||
const [latestMods, setLatestMods] = useState<ModDetails[]>([])
|
const [latestMods, setLatestMods] = useState<ModDetails[]>([])
|
||||||
|
|
||||||
@ -263,8 +265,7 @@ const DisplayLatestMods = () => {
|
|||||||
|
|
||||||
useDidMount(() => {
|
useDidMount(() => {
|
||||||
fetchMods({ source: window.location.host })
|
fetchMods({ source: window.location.host })
|
||||||
.then((res) => {
|
.then((mods) => {
|
||||||
const mods = res.sort((a, b) => b.published_at - a.published_at)
|
|
||||||
setLatestMods(mods)
|
setLatestMods(mods)
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
|
@ -1,21 +1,18 @@
|
|||||||
|
import { NDKFilter, NDKKind } from '@nostr-dev-kit/ndk'
|
||||||
import Link from '@tiptap/extension-link'
|
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 FsLightbox from 'fslightbox-react'
|
import FsLightbox from 'fslightbox-react'
|
||||||
import { Filter, kinds, nip19, UnsignedEvent } from 'nostr-tools'
|
import { nip19, UnsignedEvent } from 'nostr-tools'
|
||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { Link as ReactRouterLink, useParams } from 'react-router-dom'
|
import { Link as ReactRouterLink, 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'
|
||||||
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
import { LoadingSpinner } from '../../components/LoadingSpinner'
|
||||||
import { ProfileSection } from '../../components/ProfileSection'
|
import { ProfileSection } from '../../components/ProfileSection'
|
||||||
import {
|
import { MetadataController, UserRelaysType } from '../../controllers'
|
||||||
MetadataController,
|
import { useAppSelector, useDidMount, useNDKContext } from '../../hooks'
|
||||||
RelayController,
|
|
||||||
UserRelaysType
|
|
||||||
} from '../../controllers'
|
|
||||||
import { useAppSelector, useDidMount } from '../../hooks'
|
|
||||||
import { getGamePageRoute, getModsEditPageRoute } from '../../routes'
|
import { getGamePageRoute, getModsEditPageRoute } from '../../routes'
|
||||||
import '../../styles/comments.css'
|
import '../../styles/comments.css'
|
||||||
import '../../styles/downloads.css'
|
import '../../styles/downloads.css'
|
||||||
@ -47,6 +44,7 @@ import { Zap } from './internal/zap'
|
|||||||
|
|
||||||
export const ModPage = () => {
|
export const ModPage = () => {
|
||||||
const { naddr } = useParams()
|
const { naddr } = useParams()
|
||||||
|
const { fetchEvent } = useNDKContext()
|
||||||
const [modData, setModData] = useState<ModDetails>()
|
const [modData, setModData] = useState<ModDetails>()
|
||||||
const [isFetching, setIsFetching] = useState(true)
|
const [isFetching, setIsFetching] = useState(true)
|
||||||
const [commentCount, setCommentCount] = useState(0)
|
const [commentCount, setCommentCount] = useState(0)
|
||||||
@ -56,14 +54,13 @@ export const ModPage = () => {
|
|||||||
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
|
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
|
||||||
const { identifier, kind, pubkey, relays = [] } = decoded.data
|
const { identifier, kind, pubkey, relays = [] } = decoded.data
|
||||||
|
|
||||||
const filter: Filter = {
|
const filter: NDKFilter = {
|
||||||
'#a': [identifier],
|
'#a': [identifier],
|
||||||
authors: [pubkey],
|
authors: [pubkey],
|
||||||
kinds: [kind]
|
kinds: [kind]
|
||||||
}
|
}
|
||||||
|
|
||||||
RelayController.getInstance()
|
fetchEvent(filter, relays)
|
||||||
.fetchEvent(filter, relays)
|
|
||||||
.then((event) => {
|
.then((event) => {
|
||||||
if (event) {
|
if (event) {
|
||||||
const extracted = extractModData(event)
|
const extracted = extractModData(event)
|
||||||
@ -214,6 +211,8 @@ type GameProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const Game = ({ naddr, game, author, aTag }: GameProps) => {
|
const Game = ({ naddr, game, author, aTag }: GameProps) => {
|
||||||
|
const { fetchEventFromUserRelays } = useNDKContext()
|
||||||
|
|
||||||
const userState = useAppSelector((state) => state.user)
|
const userState = useAppSelector((state) => state.user)
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
|
||||||
@ -225,20 +224,21 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => {
|
|||||||
if (userState.auth && userState.user?.pubkey) {
|
if (userState.auth && userState.user?.pubkey) {
|
||||||
const pubkey = userState.user.pubkey as string
|
const pubkey = userState.user.pubkey as string
|
||||||
|
|
||||||
const muteListFilter: Filter = {
|
const muteListFilter: NDKFilter = {
|
||||||
kinds: [kinds.Mutelist],
|
kinds: [NDKKind.MuteList],
|
||||||
authors: [pubkey]
|
authors: [pubkey]
|
||||||
}
|
}
|
||||||
|
|
||||||
RelayController.getInstance()
|
fetchEventFromUserRelays(
|
||||||
.fetchEventFromUserRelays(muteListFilter, pubkey, UserRelaysType.Write)
|
muteListFilter,
|
||||||
.then((event) => {
|
pubkey,
|
||||||
|
UserRelaysType.Write
|
||||||
|
).then((event) => {
|
||||||
if (event) {
|
if (event) {
|
||||||
// get a list of tags
|
// get a list of tags
|
||||||
const tags = event.tags
|
const tags = event.tags
|
||||||
const blocked =
|
const blocked =
|
||||||
tags.findIndex((item) => item[0] === 'a' && item[1] === aTag) !==
|
tags.findIndex((item) => item[0] === 'a' && item[1] === aTag) !== -1
|
||||||
-1
|
|
||||||
|
|
||||||
setIsBlocked(blocked)
|
setIsBlocked(blocked)
|
||||||
}
|
}
|
||||||
@ -248,33 +248,30 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => {
|
|||||||
userState.user.npub &&
|
userState.user.npub &&
|
||||||
userState.user.npub === import.meta.env.VITE_REPORTING_NPUB
|
userState.user.npub === import.meta.env.VITE_REPORTING_NPUB
|
||||||
) {
|
) {
|
||||||
const nsfwListFilter: Filter = {
|
const nsfwListFilter: NDKFilter = {
|
||||||
kinds: [kinds.Curationsets],
|
kinds: [NDKKind.ArticleCurationSet],
|
||||||
authors: [pubkey],
|
authors: [pubkey],
|
||||||
'#d': ['nsfw']
|
'#d': ['nsfw']
|
||||||
}
|
}
|
||||||
|
|
||||||
RelayController.getInstance()
|
fetchEventFromUserRelays(
|
||||||
.fetchEventFromUserRelays(
|
|
||||||
nsfwListFilter,
|
nsfwListFilter,
|
||||||
pubkey,
|
pubkey,
|
||||||
UserRelaysType.Write
|
UserRelaysType.Write
|
||||||
)
|
).then((event) => {
|
||||||
.then((event) => {
|
|
||||||
if (event) {
|
if (event) {
|
||||||
// get a list of tags
|
// get a list of tags
|
||||||
const tags = event.tags
|
const tags = event.tags
|
||||||
const existsInNSFWList =
|
const existsInNSFWList =
|
||||||
tags.findIndex(
|
tags.findIndex((item) => item[0] === 'a' && item[1] === aTag) !==
|
||||||
(item) => item[0] === 'a' && item[1] === aTag
|
-1
|
||||||
) !== -1
|
|
||||||
|
|
||||||
setIsAddedToNSFW(existsInNSFWList)
|
setIsAddedToNSFW(existsInNSFWList)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [userState, aTag])
|
}, [userState, aTag, fetchEventFromUserRelays])
|
||||||
|
|
||||||
const handleBlock = async () => {
|
const handleBlock = async () => {
|
||||||
let hexPubkey: string
|
let hexPubkey: string
|
||||||
@ -298,14 +295,13 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => {
|
|||||||
|
|
||||||
// Define the event filter to search for the user's mute list events.
|
// 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.
|
// We look for events of a specific kind (Mutelist) authored by the given hexPubkey.
|
||||||
const filter: Filter = {
|
const filter: NDKFilter = {
|
||||||
kinds: [kinds.Mutelist],
|
kinds: [NDKKind.MuteList],
|
||||||
authors: [hexPubkey]
|
authors: [hexPubkey]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the mute list event from the relays. This returns the event containing the user's mute list.
|
// Fetch the mute list event from the relays. This returns the event containing the user's mute list.
|
||||||
const muteListEvent =
|
const muteListEvent = await fetchEventFromUserRelays(
|
||||||
await RelayController.getInstance().fetchEventFromUserRelays(
|
|
||||||
filter,
|
filter,
|
||||||
hexPubkey,
|
hexPubkey,
|
||||||
UserRelaysType.Write
|
UserRelaysType.Write
|
||||||
@ -329,7 +325,7 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => {
|
|||||||
|
|
||||||
unsignedEvent = {
|
unsignedEvent = {
|
||||||
pubkey: muteListEvent.pubkey,
|
pubkey: muteListEvent.pubkey,
|
||||||
kind: muteListEvent.kind,
|
kind: NDKKind.MuteList,
|
||||||
content: muteListEvent.content,
|
content: muteListEvent.content,
|
||||||
created_at: now(),
|
created_at: now(),
|
||||||
tags: [...tags]
|
tags: [...tags]
|
||||||
@ -337,7 +333,7 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => {
|
|||||||
} else {
|
} else {
|
||||||
unsignedEvent = {
|
unsignedEvent = {
|
||||||
pubkey: hexPubkey,
|
pubkey: hexPubkey,
|
||||||
kind: kinds.Mutelist,
|
kind: NDKKind.MuteList,
|
||||||
content: '',
|
content: '',
|
||||||
created_at: now(),
|
created_at: now(),
|
||||||
tags: [['a', aTag]]
|
tags: [['a', aTag]]
|
||||||
@ -356,8 +352,8 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => {
|
|||||||
const handleUnblock = async () => {
|
const handleUnblock = async () => {
|
||||||
const pubkey = userState.user?.pubkey as string
|
const pubkey = userState.user?.pubkey as string
|
||||||
|
|
||||||
const filter: Filter = {
|
const filter: NDKFilter = {
|
||||||
kinds: [kinds.Mutelist],
|
kinds: [NDKKind.MuteList],
|
||||||
authors: [pubkey]
|
authors: [pubkey]
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -365,8 +361,7 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => {
|
|||||||
setLoadingSpinnerDesc(`Finding user's mute list`)
|
setLoadingSpinnerDesc(`Finding user's mute list`)
|
||||||
|
|
||||||
// Fetch the mute list event from the relays. This returns the event containing the user's mute list.
|
// Fetch the mute list event from the relays. This returns the event containing the user's mute list.
|
||||||
const muteListEvent =
|
const muteListEvent = await fetchEventFromUserRelays(
|
||||||
await RelayController.getInstance().fetchEventFromUserRelays(
|
|
||||||
filter,
|
filter,
|
||||||
pubkey,
|
pubkey,
|
||||||
UserRelaysType.Write
|
UserRelaysType.Write
|
||||||
@ -381,7 +376,7 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => {
|
|||||||
|
|
||||||
const unsignedEvent: UnsignedEvent = {
|
const unsignedEvent: UnsignedEvent = {
|
||||||
pubkey: muteListEvent.pubkey,
|
pubkey: muteListEvent.pubkey,
|
||||||
kind: muteListEvent.kind,
|
kind: NDKKind.MuteList,
|
||||||
content: muteListEvent.content,
|
content: muteListEvent.content,
|
||||||
created_at: now(),
|
created_at: now(),
|
||||||
tags: tags.filter((item) => item[0] !== 'a' || item[1] !== aTag)
|
tags: tags.filter((item) => item[0] !== 'a' || item[1] !== aTag)
|
||||||
@ -401,8 +396,8 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => {
|
|||||||
|
|
||||||
if (!pubkey) return
|
if (!pubkey) return
|
||||||
|
|
||||||
const filter: Filter = {
|
const filter: NDKFilter = {
|
||||||
kinds: [kinds.Curationsets],
|
kinds: [NDKKind.ArticleCurationSet],
|
||||||
authors: [pubkey],
|
authors: [pubkey],
|
||||||
'#d': ['nsfw']
|
'#d': ['nsfw']
|
||||||
}
|
}
|
||||||
@ -410,8 +405,7 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => {
|
|||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setLoadingSpinnerDesc('Finding NSFW list')
|
setLoadingSpinnerDesc('Finding NSFW list')
|
||||||
|
|
||||||
const nsfwListEvent =
|
const nsfwListEvent = await fetchEventFromUserRelays(
|
||||||
await RelayController.getInstance().fetchEventFromUserRelays(
|
|
||||||
filter,
|
filter,
|
||||||
pubkey,
|
pubkey,
|
||||||
UserRelaysType.Write
|
UserRelaysType.Write
|
||||||
@ -435,7 +429,7 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => {
|
|||||||
|
|
||||||
unsignedEvent = {
|
unsignedEvent = {
|
||||||
pubkey: nsfwListEvent.pubkey,
|
pubkey: nsfwListEvent.pubkey,
|
||||||
kind: nsfwListEvent.kind,
|
kind: NDKKind.ArticleCurationSet,
|
||||||
content: nsfwListEvent.content,
|
content: nsfwListEvent.content,
|
||||||
created_at: now(),
|
created_at: now(),
|
||||||
tags: [...tags]
|
tags: [...tags]
|
||||||
@ -443,7 +437,7 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => {
|
|||||||
} else {
|
} else {
|
||||||
unsignedEvent = {
|
unsignedEvent = {
|
||||||
pubkey: pubkey,
|
pubkey: pubkey,
|
||||||
kind: kinds.Curationsets,
|
kind: NDKKind.ArticleCurationSet,
|
||||||
content: '',
|
content: '',
|
||||||
created_at: now(),
|
created_at: now(),
|
||||||
tags: [
|
tags: [
|
||||||
@ -465,8 +459,8 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => {
|
|||||||
const handleUnblockNSFW = async () => {
|
const handleUnblockNSFW = async () => {
|
||||||
const pubkey = userState.user?.pubkey as string
|
const pubkey = userState.user?.pubkey as string
|
||||||
|
|
||||||
const filter: Filter = {
|
const filter: NDKFilter = {
|
||||||
kinds: [kinds.Curationsets],
|
kinds: [NDKKind.ArticleCurationSet],
|
||||||
authors: [pubkey],
|
authors: [pubkey],
|
||||||
'#d': ['nsfw']
|
'#d': ['nsfw']
|
||||||
}
|
}
|
||||||
@ -474,8 +468,7 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => {
|
|||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
setLoadingSpinnerDesc('Finding NSFW list')
|
setLoadingSpinnerDesc('Finding NSFW list')
|
||||||
|
|
||||||
const nsfwListEvent =
|
const nsfwListEvent = await fetchEventFromUserRelays(
|
||||||
await RelayController.getInstance().fetchEventFromUserRelays(
|
|
||||||
filter,
|
filter,
|
||||||
pubkey,
|
pubkey,
|
||||||
UserRelaysType.Write
|
UserRelaysType.Write
|
||||||
@ -490,7 +483,7 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => {
|
|||||||
|
|
||||||
const unsignedEvent: UnsignedEvent = {
|
const unsignedEvent: UnsignedEvent = {
|
||||||
pubkey: nsfwListEvent.pubkey,
|
pubkey: nsfwListEvent.pubkey,
|
||||||
kind: nsfwListEvent.kind,
|
kind: NDKKind.ArticleCurationSet,
|
||||||
content: nsfwListEvent.content,
|
content: nsfwListEvent.content,
|
||||||
created_at: now(),
|
created_at: now(),
|
||||||
tags: tags.filter((item) => item[0] !== 'a' || item[1] !== aTag)
|
tags: tags.filter((item) => item[0] !== 'a' || item[1] !== aTag)
|
||||||
@ -667,6 +660,7 @@ type ReportPopupProps = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ReportPopup = ({ aTag, handleClose }: ReportPopupProps) => {
|
const ReportPopup = ({ aTag, handleClose }: ReportPopupProps) => {
|
||||||
|
const { fetchEventFromUserRelays } = useNDKContext()
|
||||||
const userState = useAppSelector((state) => state.user)
|
const userState = useAppSelector((state) => state.user)
|
||||||
const [selectedOptions, setSelectedOptions] = useState({
|
const [selectedOptions, setSelectedOptions] = useState({
|
||||||
actuallyCP: false,
|
actuallyCP: false,
|
||||||
@ -720,14 +714,13 @@ const ReportPopup = ({ aTag, handleClose }: ReportPopupProps) => {
|
|||||||
setLoadingSpinnerDesc(`Finding user's mute list`)
|
setLoadingSpinnerDesc(`Finding user's mute list`)
|
||||||
// Define the event filter to search for the user's mute list events.
|
// 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.
|
// We look for events of a specific kind (Mutelist) authored by the given hexPubkey.
|
||||||
const filter: Filter = {
|
const filter: NDKFilter = {
|
||||||
kinds: [kinds.Mutelist],
|
kinds: [NDKKind.MuteList],
|
||||||
authors: [hexPubkey]
|
authors: [hexPubkey]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch the mute list event from the relays. This returns the event containing the user's mute list.
|
// Fetch the mute list event from the relays. This returns the event containing the user's mute list.
|
||||||
const muteListEvent =
|
const muteListEvent = await fetchEventFromUserRelays(
|
||||||
await RelayController.getInstance().fetchEventFromUserRelays(
|
|
||||||
filter,
|
filter,
|
||||||
hexPubkey,
|
hexPubkey,
|
||||||
UserRelaysType.Write
|
UserRelaysType.Write
|
||||||
@ -750,7 +743,7 @@ const ReportPopup = ({ aTag, handleClose }: ReportPopupProps) => {
|
|||||||
|
|
||||||
unsignedEvent = {
|
unsignedEvent = {
|
||||||
pubkey: muteListEvent.pubkey,
|
pubkey: muteListEvent.pubkey,
|
||||||
kind: muteListEvent.kind,
|
kind: NDKKind.MuteList,
|
||||||
content: muteListEvent.content,
|
content: muteListEvent.content,
|
||||||
created_at: now(),
|
created_at: now(),
|
||||||
tags: [...tags]
|
tags: [...tags]
|
||||||
@ -758,7 +751,7 @@ const ReportPopup = ({ aTag, handleClose }: ReportPopupProps) => {
|
|||||||
} else {
|
} else {
|
||||||
unsignedEvent = {
|
unsignedEvent = {
|
||||||
pubkey: hexPubkey,
|
pubkey: hexPubkey,
|
||||||
kind: kinds.Mutelist,
|
kind: NDKKind.MuteList,
|
||||||
content: '',
|
content: '',
|
||||||
created_at: now(),
|
created_at: now(),
|
||||||
tags: [['a', aTag]]
|
tags: [['a', aTag]]
|
||||||
|
@ -5,7 +5,7 @@ import {
|
|||||||
UserRelaysType
|
UserRelaysType
|
||||||
} from 'controllers'
|
} from 'controllers'
|
||||||
import { formatDate } from 'date-fns'
|
import { formatDate } from 'date-fns'
|
||||||
import { useAppSelector, useDidMount, useReactions } from 'hooks'
|
import { useAppSelector, useDidMount, useNDKContext, useReactions } from 'hooks'
|
||||||
import { useComments } from 'hooks/useComments'
|
import { useComments } from 'hooks/useComments'
|
||||||
import { Event, kinds, nip19, UnsignedEvent } from 'nostr-tools'
|
import { Event, kinds, nip19, UnsignedEvent } from 'nostr-tools'
|
||||||
import React, {
|
import React, {
|
||||||
@ -322,11 +322,11 @@ const Filter = React.memo(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const Comment = (props: CommentEvent) => {
|
const Comment = (props: CommentEvent) => {
|
||||||
|
const { findMetadata } = useNDKContext()
|
||||||
const [profile, setProfile] = useState<UserProfile>()
|
const [profile, setProfile] = useState<UserProfile>()
|
||||||
|
|
||||||
useDidMount(async () => {
|
useDidMount(() => {
|
||||||
const metadataController = await MetadataController.getInstance()
|
findMetadata(props.pubkey).then((res) => {
|
||||||
metadataController.findMetadata(props.pubkey).then((res) => {
|
|
||||||
setProfile(res)
|
setProfile(res)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
useAppSelector,
|
useAppSelector,
|
||||||
useFilteredMods,
|
useFilteredMods,
|
||||||
useMuteLists,
|
useMuteLists,
|
||||||
|
useNDKContext,
|
||||||
useNSFWList
|
useNSFWList
|
||||||
} from '../hooks'
|
} from '../hooks'
|
||||||
import { appRoutes } from '../routes'
|
import { appRoutes } from '../routes'
|
||||||
@ -23,9 +24,9 @@ import {
|
|||||||
NSFWFilter,
|
NSFWFilter,
|
||||||
SortBy
|
SortBy
|
||||||
} from '../types'
|
} from '../types'
|
||||||
import { fetchMods } from '../utils'
|
|
||||||
|
|
||||||
export const ModsPage = () => {
|
export const ModsPage = () => {
|
||||||
|
const { fetchMods } = useNDKContext()
|
||||||
const [isFetching, setIsFetching] = useState(false)
|
const [isFetching, setIsFetching] = useState(false)
|
||||||
const [mods, setMods] = useState<ModDetails[]>([])
|
const [mods, setMods] = useState<ModDetails[]>([])
|
||||||
const [filterOptions, setFilterOptions] = useState<FilterOptions>({
|
const [filterOptions, setFilterOptions] = useState<FilterOptions>({
|
||||||
@ -50,7 +51,7 @@ export const ModsPage = () => {
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsFetching(false)
|
setIsFetching(false)
|
||||||
})
|
})
|
||||||
}, [filterOptions.source])
|
}, [filterOptions.source, fetchMods])
|
||||||
|
|
||||||
const handleNext = useCallback(() => {
|
const handleNext = useCallback(() => {
|
||||||
setIsFetching(true)
|
setIsFetching(true)
|
||||||
@ -69,7 +70,7 @@ export const ModsPage = () => {
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsFetching(false)
|
setIsFetching(false)
|
||||||
})
|
})
|
||||||
}, [filterOptions.source, mods])
|
}, [filterOptions.source, mods, fetchMods])
|
||||||
|
|
||||||
const handlePrev = useCallback(() => {
|
const handlePrev = useCallback(() => {
|
||||||
setIsFetching(true)
|
setIsFetching(true)
|
||||||
@ -87,7 +88,7 @@ export const ModsPage = () => {
|
|||||||
.finally(() => {
|
.finally(() => {
|
||||||
setIsFetching(false)
|
setIsFetching(false)
|
||||||
})
|
})
|
||||||
}, [filterOptions.source, mods])
|
}, [filterOptions.source, mods, fetchMods])
|
||||||
|
|
||||||
const filteredModList = useFilteredMods(
|
const filteredModList = useFilteredMods(
|
||||||
mods,
|
mods,
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
import { NDKEvent, NDKUserProfile, profileFromEvent } from '@nostr-dev-kit/ndk'
|
import {
|
||||||
|
NDKEvent,
|
||||||
|
NDKFilter,
|
||||||
|
NDKKind,
|
||||||
|
NDKSubscriptionCacheUsage,
|
||||||
|
NDKUserProfile,
|
||||||
|
profileFromEvent
|
||||||
|
} from '@nostr-dev-kit/ndk'
|
||||||
import { ErrorBoundary } from 'components/ErrorBoundary'
|
import { ErrorBoundary } from 'components/ErrorBoundary'
|
||||||
import { GameCard } from 'components/GameCard'
|
import { GameCard } from 'components/GameCard'
|
||||||
import { LoadingSpinner } from 'components/LoadingSpinner'
|
import { LoadingSpinner } from 'components/LoadingSpinner'
|
||||||
@ -11,16 +18,14 @@ import {
|
|||||||
MAX_MODS_PER_PAGE,
|
MAX_MODS_PER_PAGE,
|
||||||
T_TAG_VALUE
|
T_TAG_VALUE
|
||||||
} from 'constants.ts'
|
} from 'constants.ts'
|
||||||
import { RelayController } from 'controllers'
|
|
||||||
import {
|
import {
|
||||||
useAppSelector,
|
useAppSelector,
|
||||||
useFilteredMods,
|
useFilteredMods,
|
||||||
useGames,
|
useGames,
|
||||||
useMuteLists,
|
useMuteLists,
|
||||||
|
useNDKContext,
|
||||||
useNSFWList
|
useNSFWList
|
||||||
} from 'hooks'
|
} from 'hooks'
|
||||||
import { Filter, kinds } from 'nostr-tools'
|
|
||||||
import { Subscription } from 'nostr-tools/abstract-relay'
|
|
||||||
import React, {
|
import React, {
|
||||||
Dispatch,
|
Dispatch,
|
||||||
SetStateAction,
|
SetStateAction,
|
||||||
@ -30,7 +35,6 @@ import React, {
|
|||||||
useState
|
useState
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { useSearchParams } from 'react-router-dom'
|
import { useSearchParams } from 'react-router-dom'
|
||||||
import { toast } from 'react-toastify'
|
|
||||||
import {
|
import {
|
||||||
FilterOptions,
|
FilterOptions,
|
||||||
ModDetails,
|
ModDetails,
|
||||||
@ -267,56 +271,38 @@ const ModsResult = ({
|
|||||||
muteLists,
|
muteLists,
|
||||||
nsfwList
|
nsfwList
|
||||||
}: ModsResultProps) => {
|
}: ModsResultProps) => {
|
||||||
const hasEffectRun = useRef(false)
|
const { ndk } = useNDKContext()
|
||||||
const [isSubscribing, setIsSubscribing] = useState(false)
|
|
||||||
const [mods, setMods] = useState<ModDetails[]>([])
|
const [mods, setMods] = useState<ModDetails[]>([])
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const userState = useAppSelector((state) => state.user)
|
const userState = useAppSelector((state) => state.user)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasEffectRun.current) {
|
const filter: NDKFilter = {
|
||||||
return
|
kinds: [NDKKind.Classified],
|
||||||
}
|
|
||||||
|
|
||||||
hasEffectRun.current = true // Set it so the effect doesn't run again
|
|
||||||
|
|
||||||
const filter: Filter = {
|
|
||||||
kinds: [kinds.ClassifiedListing],
|
|
||||||
'#t': [T_TAG_VALUE]
|
'#t': [T_TAG_VALUE]
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsSubscribing(true)
|
const subscription = ndk.subscribe(filter, {
|
||||||
|
cacheUsage: NDKSubscriptionCacheUsage.PARALLEL,
|
||||||
|
closeOnEose: true
|
||||||
|
})
|
||||||
|
|
||||||
let subscriptions: Subscription[] = []
|
subscription.on('event', (ndkEvent) => {
|
||||||
|
if (isModDataComplete(ndkEvent)) {
|
||||||
|
const mod = extractModData(ndkEvent)
|
||||||
|
setMods((prev) => {
|
||||||
|
if (prev.find((e) => e.aTag === mod.aTag)) return [...prev]
|
||||||
|
|
||||||
RelayController.getInstance()
|
return [...prev, mod]
|
||||||
.subscribeForEvents(filter, [], (event) => {
|
})
|
||||||
if (isModDataComplete(event)) {
|
|
||||||
const mod = extractModData(event)
|
|
||||||
setMods((prev) => [...prev, mod])
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.then((subs) => {
|
|
||||||
subscriptions = subs
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
log(
|
|
||||||
true,
|
|
||||||
LogType.Error,
|
|
||||||
'An error occurred in subscribing to relays.',
|
|
||||||
err
|
|
||||||
)
|
|
||||||
toast.error(err.message || err)
|
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setIsSubscribing(false)
|
|
||||||
})
|
|
||||||
|
|
||||||
// Cleanup function to stop all subscriptions
|
// Cleanup function to stop all subscriptions
|
||||||
return () => {
|
return () => {
|
||||||
subscriptions.forEach((sub) => sub.close()) // close each subscription
|
subscription.stop()
|
||||||
}
|
}
|
||||||
}, [])
|
}, [ndk])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setPage(1)
|
setPage(1)
|
||||||
@ -357,9 +343,6 @@ const ModsResult = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isSubscribing && (
|
|
||||||
<LoadingSpinner desc='Subscribing to relays for mods' />
|
|
||||||
)}
|
|
||||||
<div className='IBMSecMain IBMSMListWrapper'>
|
<div className='IBMSecMain IBMSMListWrapper'>
|
||||||
<div className='IBMSMList'>
|
<div className='IBMSMList'>
|
||||||
{filteredModList
|
{filteredModList
|
||||||
@ -393,6 +376,7 @@ const UsersResult = ({
|
|||||||
moderationFilter,
|
moderationFilter,
|
||||||
muteLists
|
muteLists
|
||||||
}: UsersResultProps) => {
|
}: UsersResultProps) => {
|
||||||
|
const { fetchEvents } = useNDKContext()
|
||||||
const [isFetching, setIsFetching] = useState(false)
|
const [isFetching, setIsFetching] = useState(false)
|
||||||
const [profiles, setProfiles] = useState<NDKUserProfile[]>([])
|
const [profiles, setProfiles] = useState<NDKUserProfile[]>([])
|
||||||
|
|
||||||
@ -402,14 +386,13 @@ const UsersResult = ({
|
|||||||
if (searchTerm === '') {
|
if (searchTerm === '') {
|
||||||
setProfiles([])
|
setProfiles([])
|
||||||
} else {
|
} else {
|
||||||
const filter: Filter = {
|
const filter: NDKFilter = {
|
||||||
kinds: [kinds.Metadata],
|
kinds: [NDKKind.Metadata],
|
||||||
search: searchTerm
|
search: searchTerm
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsFetching(true)
|
setIsFetching(true)
|
||||||
RelayController.getInstance()
|
fetchEvents(filter, ['wss://purplepag.es', 'wss://user.kindpag.es'])
|
||||||
.fetchEvents(filter, ['wss://purplepag.es', 'wss://user.kindpag.es'])
|
|
||||||
.then((events) => {
|
.then((events) => {
|
||||||
const results = events.map((event) => {
|
const results = events.map((event) => {
|
||||||
const ndkEvent = new NDKEvent(undefined, event)
|
const ndkEvent = new NDKEvent(undefined, event)
|
||||||
@ -425,7 +408,7 @@ const UsersResult = ({
|
|||||||
setIsFetching(false)
|
setIsFetching(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [searchTerm])
|
}, [searchTerm, fetchEvents])
|
||||||
|
|
||||||
const filteredProfiles = useMemo(() => {
|
const filteredProfiles = useMemo(() => {
|
||||||
let filtered = [...profiles]
|
let filtered = [...profiles]
|
||||||
|
@ -1,21 +1,22 @@
|
|||||||
|
import { NDKFilter } from '@nostr-dev-kit/ndk'
|
||||||
|
import { nip19 } from 'nostr-tools'
|
||||||
|
import { useState } from 'react'
|
||||||
import { useLocation, useParams } from 'react-router-dom'
|
import { useLocation, useParams } from 'react-router-dom'
|
||||||
|
import { toast } from 'react-toastify'
|
||||||
|
import { LoadingSpinner } from '../components/LoadingSpinner'
|
||||||
import { ModForm } from '../components/ModForm'
|
import { ModForm } from '../components/ModForm'
|
||||||
import { ProfileSection } from '../components/ProfileSection'
|
import { ProfileSection } from '../components/ProfileSection'
|
||||||
|
import { useAppSelector, useDidMount, useNDKContext } from '../hooks'
|
||||||
import '../styles/innerPage.css'
|
import '../styles/innerPage.css'
|
||||||
import '../styles/styles.css'
|
import '../styles/styles.css'
|
||||||
import '../styles/write.css'
|
import '../styles/write.css'
|
||||||
import { Filter, nip19 } from 'nostr-tools'
|
|
||||||
import { RelayController } from '../controllers'
|
|
||||||
import { extractModData, log, LogType } from '../utils'
|
|
||||||
import { ModDetails } from '../types'
|
import { ModDetails } from '../types'
|
||||||
import { toast } from 'react-toastify'
|
import { extractModData, log, LogType } from '../utils'
|
||||||
import { useState } from 'react'
|
|
||||||
import { LoadingSpinner } from '../components/LoadingSpinner'
|
|
||||||
import { useAppSelector, useDidMount } from '../hooks'
|
|
||||||
|
|
||||||
export const SubmitModPage = () => {
|
export const SubmitModPage = () => {
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const { naddr } = useParams()
|
const { naddr } = useParams()
|
||||||
|
const { fetchEvent } = useNDKContext()
|
||||||
const [modData, setModData] = useState<ModDetails>()
|
const [modData, setModData] = useState<ModDetails>()
|
||||||
const [isFetching, setIsFetching] = useState(false)
|
const [isFetching, setIsFetching] = useState(false)
|
||||||
|
|
||||||
@ -30,15 +31,15 @@ export const SubmitModPage = () => {
|
|||||||
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
|
const decoded = nip19.decode<'naddr'>(naddr as `naddr1${string}`)
|
||||||
const { identifier, kind, pubkey, relays = [] } = decoded.data
|
const { identifier, kind, pubkey, relays = [] } = decoded.data
|
||||||
|
|
||||||
const filter: Filter = {
|
const filter: NDKFilter = {
|
||||||
'#a': [identifier],
|
'#a': [identifier],
|
||||||
authors: [pubkey],
|
authors: [pubkey],
|
||||||
kinds: [kind]
|
kinds: [kind]
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsFetching(true)
|
setIsFetching(true)
|
||||||
RelayController.getInstance()
|
|
||||||
.fetchEvent(filter, relays)
|
fetchEvent(filter, relays)
|
||||||
.then((event) => {
|
.then((event) => {
|
||||||
if (event) {
|
if (event) {
|
||||||
const extracted = extractModData(event)
|
const extracted = extractModData(event)
|
||||||
|
106
src/utils/mod.ts
106
src/utils/mod.ts
@ -1,11 +1,7 @@
|
|||||||
import { Event, Filter, kinds } from 'nostr-tools'
|
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||||
|
import { Event } from 'nostr-tools'
|
||||||
|
import { ModDetails, ModFormState } from '../types'
|
||||||
import { getTagValue } from './nostr'
|
import { getTagValue } from './nostr'
|
||||||
import { ModFormState, ModDetails } from '../types'
|
|
||||||
import { RelayController } from '../controllers'
|
|
||||||
import { log, LogType } from './utils'
|
|
||||||
import { toast } from 'react-toastify'
|
|
||||||
import { MOD_FILTER_LIMIT, T_TAG_VALUE } from '../constants'
|
|
||||||
import DOMPurify from 'dompurify'
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extracts and normalizes mod data from an event.
|
* Extracts and normalizes mod data from an event.
|
||||||
@ -16,7 +12,7 @@ import DOMPurify from 'dompurify'
|
|||||||
* @param event - The event object from which to extract data.
|
* @param event - The event object from which to extract data.
|
||||||
* @returns A `Partial<PageData>` object containing extracted data.
|
* @returns A `Partial<PageData>` object containing extracted data.
|
||||||
*/
|
*/
|
||||||
export const extractModData = (event: Event): ModDetails => {
|
export const extractModData = (event: Event | NDKEvent): ModDetails => {
|
||||||
// Helper function to safely get the first value of a tag or return a default value
|
// Helper function to safely get the first value of a tag or return a default value
|
||||||
const getFirstTagValue = (tagIdentifier: string, defaultValue = '') => {
|
const getFirstTagValue = (tagIdentifier: string, defaultValue = '') => {
|
||||||
const tagValue = getTagValue(event, tagIdentifier)
|
const tagValue = getTagValue(event, tagIdentifier)
|
||||||
@ -35,7 +31,7 @@ export const extractModData = (event: Event): ModDetails => {
|
|||||||
aTag: getFirstTagValue('a'),
|
aTag: getFirstTagValue('a'),
|
||||||
rTag: getFirstTagValue('r'),
|
rTag: getFirstTagValue('r'),
|
||||||
author: event.pubkey,
|
author: event.pubkey,
|
||||||
edited_at: event.created_at,
|
edited_at: event.created_at!,
|
||||||
body: event.content,
|
body: event.content,
|
||||||
published_at: getIntTagValue('published_at'),
|
published_at: getIntTagValue('published_at'),
|
||||||
game: getFirstTagValue('game'),
|
game: getFirstTagValue('game'),
|
||||||
@ -61,7 +57,9 @@ export const extractModData = (event: Event): ModDetails => {
|
|||||||
* @param events - The array of event objects to be processed.
|
* @param events - The array of event objects to be processed.
|
||||||
* @returns An array of `ModDetails` objects constructed from valid events.
|
* @returns An array of `ModDetails` objects constructed from valid events.
|
||||||
*/
|
*/
|
||||||
export const constructModListFromEvents = (events: Event[]): ModDetails[] => {
|
export const constructModListFromEvents = (
|
||||||
|
events: Event[] | NDKEvent[]
|
||||||
|
): ModDetails[] => {
|
||||||
// Filter and extract mod details from events
|
// Filter and extract mod details from events
|
||||||
const modDetailsList: ModDetails[] = events
|
const modDetailsList: ModDetails[] = events
|
||||||
.filter(isModDataComplete) // Filter out incomplete events
|
.filter(isModDataComplete) // Filter out incomplete events
|
||||||
@ -78,7 +76,7 @@ export const constructModListFromEvents = (events: Event[]): ModDetails[] => {
|
|||||||
* @param event - The event object to be checked.
|
* @param event - The event object to be checked.
|
||||||
* @returns `true` if the event contains all required data; `false` otherwise.
|
* @returns `true` if the event contains all required data; `false` otherwise.
|
||||||
*/
|
*/
|
||||||
export const isModDataComplete = (event: Event): boolean => {
|
export const isModDataComplete = (event: Event | NDKEvent): boolean => {
|
||||||
// Helper function to check if a tag value is present and not empty
|
// Helper function to check if a tag value is present and not empty
|
||||||
const hasTagValue = (tagIdentifier: string): boolean => {
|
const hasTagValue = (tagIdentifier: string): boolean => {
|
||||||
const value = getTagValue(event, tagIdentifier)
|
const value = getTagValue(event, tagIdentifier)
|
||||||
@ -133,89 +131,3 @@ export const initializeFormState = (
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
interface FetchModsOptions {
|
|
||||||
source?: string
|
|
||||||
until?: number
|
|
||||||
since?: number
|
|
||||||
limit?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetches a list of mods based on the provided source.
|
|
||||||
*
|
|
||||||
* @param source - The source URL to filter the mods. If it matches the current window location,
|
|
||||||
* it adds a filter condition to the request.
|
|
||||||
* @param until - Optional timestamp to filter events until this time.
|
|
||||||
* @param since - Optional timestamp to filter events from this time.
|
|
||||||
* @returns A promise that resolves to an array of `ModDetails` objects. In case of an error,
|
|
||||||
* it logs the error and shows a notification, then returns an empty array.
|
|
||||||
*/
|
|
||||||
export const fetchMods = async ({
|
|
||||||
source,
|
|
||||||
until,
|
|
||||||
since,
|
|
||||||
limit
|
|
||||||
}: FetchModsOptions): Promise<ModDetails[]> => {
|
|
||||||
// Define the filter criteria for fetching mods
|
|
||||||
const filter: Filter = {
|
|
||||||
kinds: [kinds.ClassifiedListing], // Specify the kind of events to fetch
|
|
||||||
limit: limit || MOD_FILTER_LIMIT, // Limit the number of events fetched to 20
|
|
||||||
'#t': [T_TAG_VALUE],
|
|
||||||
until, // Optional filter to fetch events until this timestamp
|
|
||||||
since // Optional filter to fetch events from this timestamp
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the source matches the current window location, add a filter condition
|
|
||||||
if (source === window.location.host) {
|
|
||||||
filter['#r'] = [window.location.host] // Add a tag filter for the current host
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch events from the relay using the defined filter
|
|
||||||
return RelayController.getInstance()
|
|
||||||
.fetchEvents(filter, []) // Pass the filter and an empty array of options
|
|
||||||
.then((events) => {
|
|
||||||
// Convert the fetched events into a list of mods
|
|
||||||
const modList = constructModListFromEvents(events)
|
|
||||||
return modList // Return the list of mods
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
// Log the error and show a notification if fetching fails
|
|
||||||
log(
|
|
||||||
true,
|
|
||||||
LogType.Error,
|
|
||||||
'An error occurred in fetching mods from relays',
|
|
||||||
err
|
|
||||||
)
|
|
||||||
toast.error('An error occurred in fetching mods from relays') // Show error notification
|
|
||||||
return [] as ModDetails[] // Return an empty array in case of an error
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sanitizes the given HTML string and adds target="_blank" to all <a> tags.
|
|
||||||
*
|
|
||||||
* @param htmlString - The HTML string to sanitize and modify.
|
|
||||||
* @returns The modified HTML string with sanitized content and updated links.
|
|
||||||
*/
|
|
||||||
export const sanitizeAndAddTargetBlank = (htmlString: string) => {
|
|
||||||
// Step 1: Sanitize the HTML string using DOMPurify.
|
|
||||||
// This removes any potentially dangerous content and ensures that the HTML is safe to use.
|
|
||||||
const sanitizedHtml = DOMPurify.sanitize(htmlString, { ADD_ATTR: ['target'] })
|
|
||||||
|
|
||||||
// Step 2: Create a temporary container (a <div> element) to parse the sanitized HTML.
|
|
||||||
// This allows us to manipulate the HTML content in a safe and controlled manner.
|
|
||||||
const tempDiv = document.createElement('div')
|
|
||||||
tempDiv.innerHTML = sanitizedHtml
|
|
||||||
|
|
||||||
// Step 3: Add target="_blank" to all <a> tags within the temporary container.
|
|
||||||
// This ensures that all links open in a new tab when clicked.
|
|
||||||
const links = tempDiv.querySelectorAll('a')
|
|
||||||
links.forEach((link) => {
|
|
||||||
link.setAttribute('target', '_blank')
|
|
||||||
})
|
|
||||||
|
|
||||||
// Step 4: Convert the manipulated DOM back to an HTML string.
|
|
||||||
// This string contains the sanitized content with the target="_blank" attribute added to all links.
|
|
||||||
return tempDiv.innerHTML
|
|
||||||
}
|
|
||||||
|
@ -11,6 +11,7 @@ import {
|
|||||||
import { toast } from 'react-toastify'
|
import { toast } from 'react-toastify'
|
||||||
import { RelayController } from '../controllers'
|
import { RelayController } from '../controllers'
|
||||||
import { log, LogType } from './utils'
|
import { log, LogType } from './utils'
|
||||||
|
import { NDKEvent } from '@nostr-dev-kit/ndk'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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).
|
||||||
@ -50,7 +51,7 @@ export const hexToNpub = (hexPubkey: string): `npub1${string}` => {
|
|||||||
* @returns {string | null} The value(s) associated with the specified tag identifier, or `null` if the tag is not found.
|
* @returns {string | null} The value(s) associated with the specified tag identifier, or `null` if the tag is not found.
|
||||||
*/
|
*/
|
||||||
export const getTagValue = (
|
export const getTagValue = (
|
||||||
event: Event,
|
event: Event | NDKEvent,
|
||||||
tagIdentifier: string
|
tagIdentifier: string
|
||||||
): string[] | null => {
|
): string[] | null => {
|
||||||
// Find the tag in the event's tags array where the first element matches the tagIdentifier.
|
// Find the tag in the event's tags array where the first element matches the tagIdentifier.
|
||||||
@ -219,3 +220,24 @@ export const sendDMUsingRandomKey = async (
|
|||||||
// Return true indicating that the DM was successfully sent
|
// Return true indicating that the DM was successfully sent
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Orders an array of NDKEvent objects chronologically based on their `created_at` property.
|
||||||
|
*
|
||||||
|
* @param events - The array of NDKEvent objects to be sorted.
|
||||||
|
* @param reverse - Optional flag to reverse the sorting order.
|
||||||
|
* If true, sorts in ascending order (oldest first), otherwise sorts in descending order (newest first).
|
||||||
|
*
|
||||||
|
* @returns The sorted array of events.
|
||||||
|
*/
|
||||||
|
export function orderEventsChronologically(
|
||||||
|
events: NDKEvent[],
|
||||||
|
reverse: boolean = false
|
||||||
|
): NDKEvent[] {
|
||||||
|
events.sort((e1: NDKEvent, e2: NDKEvent) => {
|
||||||
|
if (reverse) return e1.created_at! - e2.created_at!
|
||||||
|
else return e2.created_at! - e1.created_at!
|
||||||
|
})
|
||||||
|
|
||||||
|
return events
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user