relay management (settings), profile box display fix, /games mod fetch filter by current site source, game in mod post to redirect to game search for that game #49

Merged
freakoverse merged 4 commits from staging into master 2024-09-25 16:22:46 +00:00
9 changed files with 576 additions and 215 deletions

View File

@ -1,7 +1,7 @@
import { useNavigate } from 'react-router-dom' import { Link } from 'react-router-dom'
import { getGamePageRoute } from 'routes'
import '../styles/cardGames.css' import '../styles/cardGames.css'
import { handleGameImageError } from '../utils' import { handleGameImageError } from '../utils'
import { getGamePageRoute } from 'routes'
type GameCardProps = { type GameCardProps = {
title: string title: string
@ -9,13 +9,10 @@ type GameCardProps = {
} }
export const GameCard = ({ title, imageUrl }: GameCardProps) => { export const GameCard = ({ title, imageUrl }: GameCardProps) => {
const navigate = useNavigate() const route = getGamePageRoute(title)
return ( return (
<div <Link className='cardGameMainWrapperLink' to={route}>
className='cardGameMainWrapperLink'
onClick={() => navigate(getGamePageRoute(title))}
>
<div className='cardGameMainWrapper'> <div className='cardGameMainWrapper'>
<img <img
src={imageUrl} src={imageUrl}
@ -26,6 +23,6 @@ export const GameCard = ({ title, imageUrl }: GameCardProps) => {
<div className='cardGameMainTitle'> <div className='cardGameMainTitle'>
<p>{title}</p> <p>{title}</p>
</div> </div>
</div> </Link>
) )
} }

View File

@ -1,3 +1,4 @@
import { FALLBACK_PROFILE_IMAGE } from 'constants.ts'
import { Event, Filter, kinds, nip19, UnsignedEvent } from 'nostr-tools' import { Event, Filter, kinds, nip19, UnsignedEvent } from 'nostr-tools'
import { QRCodeSVG } from 'qrcode.react' import { QRCodeSVG } from 'qrcode.react'
import { useState } from 'react' import { useState } from 'react'
@ -9,16 +10,21 @@ import {
UserRelaysType UserRelaysType
} from '../controllers' } from '../controllers'
import { useAppSelector, useDidMount } from '../hooks' import { useAppSelector, useDidMount } from '../hooks'
import { getProfilePageRoute } from '../routes' import { appRoutes, getProfilePageRoute } from '../routes'
import '../styles/author.css' import '../styles/author.css'
import '../styles/innerPage.css' import '../styles/innerPage.css'
import '../styles/socialPosts.css' import '../styles/socialPosts.css'
import { UserProfile } from '../types' import { UserProfile } from '../types'
import { copyTextToClipboard, log, LogType, now, npubToHex } from '../utils' import {
copyTextToClipboard,
hexToNpub,
log,
LogType,
now,
npubToHex
} from '../utils'
import { LoadingSpinner } from './LoadingSpinner' import { LoadingSpinner } from './LoadingSpinner'
import { ZapPopUp } from './Zap' import { ZapPopUp } from './Zap'
import { NDKUserProfile } from '@nostr-dev-kit/ndk'
import _ from 'lodash'
type Props = { type Props = {
pubkey: string pubkey: string
@ -34,13 +40,22 @@ export const ProfileSection = ({ pubkey }: Props) => {
}) })
}) })
if (!profile) return null const displayName =
profile?.displayName || profile?.name || '[name not set up]'
const about = profile?.bio || profile?.about || '[bio not set up]'
return ( return (
<div className='IBMSMSplitMainSmallSide'> <div className='IBMSMSplitMainSmallSide'>
<div className='IBMSMSplitMainSmallSideSecWrapper'> <div className='IBMSMSplitMainSmallSideSecWrapper'>
<div className='IBMSMSplitMainSmallSideSec'> <div className='IBMSMSplitMainSmallSideSec'>
<Profile profile={profile} /> <Profile
pubkey={pubkey}
displayName={displayName}
about={about}
image={profile?.image}
nip05={profile?.nip05}
lud16={profile?.lud16}
/>
</div> </div>
<div className='IBMSMSplitMainSmallSideSec'> <div className='IBMSMSplitMainSmallSideSec'>
<div className='IBMSMSMSSS_ShortPosts'> <div className='IBMSMSMSSS_ShortPosts'>
@ -91,12 +106,26 @@ export const ProfileSection = ({ pubkey }: Props) => {
} }
type ProfileProps = { type ProfileProps = {
profile: NDKUserProfile pubkey: string
displayName: string
about: string
image?: string
nip05?: string
lud16?: string
} }
export const Profile = ({ profile }: ProfileProps) => { export const Profile = ({
pubkey,
displayName,
about,
image,
nip05,
lud16
}: ProfileProps) => {
const npub = hexToNpub(pubkey)
const handleCopy = async () => { const handleCopy = async () => {
copyTextToClipboard(profile.npub as string).then((isCopied) => { copyTextToClipboard(npub).then((isCopied) => {
if (isCopied) { if (isCopied) {
toast.success('Npub copied to clipboard!') toast.success('Npub copied to clipboard!')
} else { } else {
@ -107,25 +136,15 @@ export const Profile = ({ profile }: ProfileProps) => {
}) })
} }
const hexPubkey = npubToHex(profile.pubkey as string) let profileRoute = appRoutes.home
const hexPubkey = npubToHex(pubkey)
if (!hexPubkey) return null if (hexPubkey) {
profileRoute = getProfilePageRoute(
const profileRoute = getProfilePageRoute(
nip19.nprofileEncode({ nip19.nprofileEncode({
pubkey: hexPubkey pubkey: hexPubkey
}) })
) )
}
const npub = (profile.npub as string) || ''
const displayName =
profile.displayName ||
profile.name ||
_.truncate(npub, {
length: 16
})
const nip05 = profile.nip05 || ''
const about = profile.bio || profile.about || ''
return ( return (
<div className='IBMSMSMSSS_Author'> <div className='IBMSMSMSSS_Author'>
@ -142,7 +161,7 @@ export const Profile = ({ profile }: ProfileProps) => {
className='IBMSMSMSSS_Author_Top_PP' className='IBMSMSMSSS_Author_Top_PP'
style={{ style={{
background: `url('${ background: `url('${
profile.image || '' image || FALLBACK_PROFILE_IMAGE
}') center / cover no-repeat` }') center / cover no-repeat`
}} }}
></div> ></div>
@ -151,7 +170,9 @@ export const Profile = ({ profile }: ProfileProps) => {
<div className='IBMSMSMSSS_Author_Top_Left_InsideDetails'> <div className='IBMSMSMSSS_Author_Top_Left_InsideDetails'>
<div className='IBMSMSMSSS_Author_TopWrapper'> <div className='IBMSMSMSSS_Author_TopWrapper'>
<p className='IBMSMSMSSS_Author_Top_Name'>{displayName}</p> <p className='IBMSMSMSSS_Author_Top_Name'>{displayName}</p>
{nip05 && (
<p className='IBMSMSMSSS_Author_Top_Handle'>{nip05}</p> <p className='IBMSMSMSSS_Author_Top_Handle'>{nip05}</p>
)}
</div> </div>
</div> </div>
</div> </div>
@ -182,8 +203,8 @@ export const Profile = ({ profile }: ProfileProps) => {
<path d='M384 96L384 0h-112c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48H464c26.51 0 48-21.49 48-48V128h-95.1C398.4 128 384 113.6 384 96zM416 0v96h96L416 0zM192 352V128h-144c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48h192c26.51 0 48-21.49 48-48L288 416h-32C220.7 416 192 387.3 192 352z'></path> <path d='M384 96L384 0h-112c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48H464c26.51 0 48-21.49 48-48V128h-95.1C398.4 128 384 113.6 384 96zM416 0v96h96L416 0zM192 352V128h-144c-26.51 0-48 21.49-48 48v288c0 26.51 21.49 48 48 48h192c26.51 0 48-21.49 48-48L288 416h-32C220.7 416 192 387.3 192 352z'></path>
</svg> </svg>
</div> </div>
<ProfileQRButtonWithPopUp pubkey={hexPubkey} /> <ProfileQRButtonWithPopUp pubkey={pubkey} />
<ZapButtonWithPopUp pubkey={hexPubkey} /> {lud16 && <ZapButtonWithPopUp pubkey={pubkey} />}
</div> </div>
</div> </div>
</div> </div>
@ -196,7 +217,7 @@ export const Profile = ({ profile }: ProfileProps) => {
></div> ></div>
</div> </div>
</div> </div>
<FollowButton pubkey={hexPubkey} /> <FollowButton pubkey={pubkey} />
</div> </div>
) )
} }

View File

@ -15,7 +15,7 @@ export const LANDING_PAGE_DATA = {
], ],
featuredGames: [ featuredGames: [
'Persona 3 Reload', 'Persona 3 Reload',
'Baldur\'s Gate 3', "Baldur's Gate 3",
'Cyberpunk 2077', 'Cyberpunk 2077',
'ELDEN RING', 'ELDEN RING',
'FINAL FANTASY VII REMAKE INTERGRADE' 'FINAL FANTASY VII REMAKE INTERGRADE'
@ -119,3 +119,7 @@ export const GAME_FILES = [
export const MAX_MODS_PER_PAGE = 10 export const MAX_MODS_PER_PAGE = 10
export const MAX_GAMES_PER_PAGE = 10 export const MAX_GAMES_PER_PAGE = 10
// todo: add game and mod fallback image here
export const FALLBACK_PROFILE_IMAGE =
'https://image.nostr.build/a305f4b43f74af3c6dcda42e6a798105a56ac1e3e7b74d7bef171896b3ba7520.png'

View File

@ -141,6 +141,9 @@ export class MetadataController {
}) })
} }
public getNDKRelayList = async (hexKey: string) =>
getRelayListForUser(hexKey, this.ndk)
public getMuteLists = async ( public getMuteLists = async (
pubkey?: string pubkey?: string
): Promise<{ ): Promise<{

View File

@ -18,7 +18,7 @@ export const GamesPage = () => {
const [currentPage, setCurrentPage] = useState(1) const [currentPage, setCurrentPage] = useState(1)
useDidMount(() => { useDidMount(() => {
fetchMods({ limit: 100 }).then((mods) => { fetchMods({ limit: 100, source: window.location.host }).then((mods) => {
mods.sort((a, b) => b.published_at - a.published_at) mods.sort((a, b) => b.published_at - a.published_at)
const gameNames = new Set<string>() const gameNames = new Set<string>()

View File

@ -5,7 +5,7 @@ import { formatDate } from 'date-fns'
import FsLightbox from 'fslightbox-react' import FsLightbox from 'fslightbox-react'
import { Filter, kinds, nip19, UnsignedEvent } from 'nostr-tools' import { Filter, kinds, nip19, UnsignedEvent } from 'nostr-tools'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
import { useNavigate, 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'
@ -16,7 +16,7 @@ import {
UserRelaysType UserRelaysType
} from '../../controllers' } from '../../controllers'
import { useAppSelector, useDidMount } from '../../hooks' import { useAppSelector, useDidMount } from '../../hooks'
import { getModsEditPageRoute } from '../../routes' import { getGamePageRoute, getModsEditPageRoute } from '../../routes'
import '../../styles/comments.css' import '../../styles/comments.css'
import '../../styles/downloads.css' import '../../styles/downloads.css'
import '../../styles/innerPage.css' import '../../styles/innerPage.css'
@ -41,9 +41,9 @@ import {
sendDMUsingRandomKey, sendDMUsingRandomKey,
signAndPublish signAndPublish
} from '../../utils' } from '../../utils'
import { Comments } from './internal/comment'
import { Reactions } from './internal/reactions' import { Reactions } from './internal/reactions'
import { Zap } from './internal/zap' import { Zap } from './internal/zap'
import { Comments } from './internal/comment'
export const ModPage = () => { export const ModPage = () => {
const { naddr } = useParams() const { naddr } = useParams()
@ -214,7 +214,6 @@ type GameProps = {
} }
const Game = ({ naddr, game, author, aTag }: GameProps) => { const Game = ({ naddr, game, author, aTag }: GameProps) => {
const navigate = useNavigate()
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('')
@ -510,15 +509,18 @@ 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 gameRoute = getGamePageRoute(game)
const editRoute = getModsEditPageRoute(naddr)
return ( return (
<> <>
{isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />} {isLoading && <LoadingSpinner desc={loadingSpinnerDesc} />}
<div className='IBMSMSMBSSModFor'> <div className='IBMSMSMBSSModFor'>
<p className='IBMSMSMBSSModForPara'> <p className='IBMSMSMBSSModForPara'>
Mod for:&nbsp; Mod for:&nbsp;
<a className='IBMSMSMBSSModForLink' href='search.html'> <ReactRouterLink className='IBMSMSMBSSModForLink' to={gameRoute}>
{game} {game}
</a> </ReactRouterLink>
</p> </p>
<div className='dropdown dropdownMain' style={{ flexGrow: 'unset' }}> <div className='dropdown dropdownMain' style={{ flexGrow: 'unset' }}>
<button <button
@ -544,9 +546,9 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => {
</button> </button>
<div className={`dropdown-menu dropdown-menu-end dropdownMainMenu`}> <div className={`dropdown-menu dropdown-menu-end dropdownMainMenu`}>
{userState.auth && userState.user?.pubkey === author && ( {userState.auth && userState.user?.pubkey === author && (
<a <ReactRouterLink
className='dropdown-item dropdownMainMenuItem' className='dropdown-item dropdownMainMenuItem'
onClick={() => navigate(getModsEditPageRoute(naddr))} to={editRoute}
> >
<svg <svg
xmlns='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg'
@ -559,7 +561,7 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => {
<path d='M362.7 19.32C387.7-5.678 428.3-5.678 453.3 19.32L492.7 58.75C517.7 83.74 517.7 124.3 492.7 149.3L444.3 197.7L314.3 67.72L362.7 19.32zM421.7 220.3L188.5 453.4C178.1 463.8 165.2 471.5 151.1 475.6L30.77 511C22.35 513.5 13.24 511.2 7.03 504.1C.8198 498.8-1.502 489.7 .976 481.2L36.37 360.9C40.53 346.8 48.16 333.9 58.57 323.5L291.7 90.34L421.7 220.3z'></path> <path d='M362.7 19.32C387.7-5.678 428.3-5.678 453.3 19.32L492.7 58.75C517.7 83.74 517.7 124.3 492.7 149.3L444.3 197.7L314.3 67.72L362.7 19.32zM421.7 220.3L188.5 453.4C178.1 463.8 165.2 471.5 151.1 475.6L30.77 511C22.35 513.5 13.24 511.2 7.03 504.1C.8198 498.8-1.502 489.7 .976 481.2L36.37 360.9C40.53 346.8 48.16 333.9 58.57 323.5L291.7 90.34L421.7 220.3z'></path>
</svg> </svg>
Edit Edit
</a> </ReactRouterLink>
)} )}
<a <a

View File

@ -552,9 +552,20 @@ const UsersResult = ({
<div className='IBMSMList'> <div className='IBMSMList'>
{filteredProfiles.map((profile) => { {filteredProfiles.map((profile) => {
if (profile.pubkey) { if (profile.pubkey) {
const displayName =
profile?.displayName || profile?.name || '[name not set up]'
const about = profile?.bio || profile?.about || '[bio not set up]'
return ( return (
<ErrorBoundary key={profile.pubkey}> <ErrorBoundary key={profile.pubkey}>
<Profile profile={profile} /> <Profile
pubkey={profile.pubkey as string}
displayName={displayName}
about={about}
image={profile?.image}
nip05={profile?.nip05}
lud16={profile?.lud16}
/>
</ErrorBoundary> </ErrorBoundary>
) )
} }

View File

@ -14,6 +14,13 @@ import { PreferencesSetting } from './preference'
import { AdminSetting } from './admin' import { AdminSetting } from './admin'
import { ProfileSection } from 'components/ProfileSection' import { ProfileSection } from 'components/ProfileSection'
import 'styles/feed.css'
import 'styles/innerPage.css'
import 'styles/popup.css'
import 'styles/settings.css'
import 'styles/styles.css'
import 'styles/write.css'
export const SettingsPage = () => { export const SettingsPage = () => {
const location = useLocation() const location = useLocation()
const userState = useAppSelector((state) => state.user) const userState = useAppSelector((state) => state.user)
@ -67,7 +74,11 @@ const SettingTabs = () => {
const navLinks = [ const navLinks = [
{ path: appRoutes.settingsProfile, label: 'Profile', icon: <ProfileSVG /> }, { path: appRoutes.settingsProfile, label: 'Profile', icon: <ProfileSVG /> },
{ path: appRoutes.settingsRelays, label: 'Relays (WIP)', icon: <RelaySVG /> }, {
path: appRoutes.settingsRelays,
label: 'Relays',
icon: <RelaySVG />
},
{ {
path: appRoutes.settingsPreferences, path: appRoutes.settingsPreferences,
label: 'Preferences (WIP)', label: 'Preferences (WIP)',

View File

@ -1,15 +1,286 @@
import { NDKRelayList } from '@nostr-dev-kit/ndk'
import { InputField } from 'components/Inputs' import { InputField } from 'components/Inputs'
import { LoadingSpinner } from 'components/LoadingSpinner'
import {
MetadataController,
RelayController,
UserRelaysType
} from 'controllers'
import { useAppSelector, useDidMount } from 'hooks'
import { Event, kinds, UnsignedEvent } from 'nostr-tools'
import { useEffect, useState } from 'react'
import { toast } from 'react-toastify'
import { log, LogType, normalizeWebSocketURL, now } from 'utils'
const READ_MARKER = 'read'
const WRITE_MARKER = 'write'
export const RelaySettings = () => { export const RelaySettings = () => {
const userState = useAppSelector((state) => state.user)
const [ndkRelayList, setNDKRelayList] = useState<NDKRelayList | null>(null)
const [isPublishing, setIsPublishing] = useState(false)
const [inputValue, setInputValue] = useState('')
useEffect(() => {
const fetchRelayList = async (pubkey: string) => {
const metadataController = await MetadataController.getInstance()
metadataController
.getNDKRelayList(pubkey)
.then((res) => {
setNDKRelayList(res)
})
.catch((err) => {
toast.error(
`An error occurred in fetching user relay list: ${
err.message || err
}`
)
setNDKRelayList(null)
})
}
if (userState.auth && userState.user?.pubkey) {
fetchRelayList(userState.user.pubkey as string)
} else {
setNDKRelayList(null)
}
}, [userState])
const handleAdd = async (relayUrl: string) => {
if (!ndkRelayList) return
const normalizedUrl = normalizeWebSocketURL(relayUrl)
const rawEvent = ndkRelayList.rawEvent()
const unsignedEvent: UnsignedEvent = {
pubkey: rawEvent.pubkey,
kind: kinds.RelayList,
tags: [...rawEvent.tags, ['r', normalizedUrl]],
content: rawEvent.content,
created_at: now()
}
setIsPublishing(true)
const signedEvent = await window.nostr
?.signEvent(unsignedEvent)
.then((event) => event as Event)
.catch((err) => {
toast.error('Failed to sign the event!')
log(true, LogType.Error, 'Failed to sign the event!', err)
return null
})
if (!signedEvent) {
setIsPublishing(false)
return
}
const publishedOnRelays =
await RelayController.getInstance().publishOnRelays(
signedEvent,
ndkRelayList.writeRelayUrls
)
// Handle cases where publishing failed or succeeded
if (publishedOnRelays.length === 0) {
toast.error('Failed to publish relay list event on any relay')
} else {
toast.success(
`Event published successfully to the following relays\n\n${publishedOnRelays.join(
'\n'
)}`
)
const newNDKRelayList = new NDKRelayList(ndkRelayList.ndk, signedEvent)
setNDKRelayList(newNDKRelayList)
}
setIsPublishing(false)
}
const handleRemove = async (relayUrl: string) => {
if (!ndkRelayList) return
const normalizedUrl = normalizeWebSocketURL(relayUrl)
const rawEvent = ndkRelayList.rawEvent()
const nonRelayTags = rawEvent.tags.filter(
(tag) => tag[0] !== 'r' && tag[0] !== 'relay'
)
const relayTags = rawEvent.tags
.filter((tag) => tag[0] === 'r' || tag[0] === 'relay')
.filter((tag) => normalizeWebSocketURL(tag[1]) !== normalizedUrl)
const unsignedEvent: UnsignedEvent = {
pubkey: rawEvent.pubkey,
kind: kinds.RelayList,
tags: [...nonRelayTags, ...relayTags],
content: rawEvent.content,
created_at: now()
}
setIsPublishing(true)
const signedEvent = await window.nostr
?.signEvent(unsignedEvent)
.then((event) => event as Event)
.catch((err) => {
toast.error('Failed to sign the event!')
log(true, LogType.Error, 'Failed to sign the event!', err)
return null
})
if (!signedEvent) {
setIsPublishing(false)
return
}
const publishedOnRelays =
await RelayController.getInstance().publishOnRelays(
signedEvent,
ndkRelayList.writeRelayUrls
)
// Handle cases where publishing failed or succeeded
if (publishedOnRelays.length === 0) {
toast.error('Failed to publish relay list event on any relay')
} else {
toast.success(
`Event published successfully to the following relays\n\n${publishedOnRelays.join(
'\n'
)}`
)
const newNDKRelayList = new NDKRelayList(ndkRelayList.ndk, signedEvent)
setNDKRelayList(newNDKRelayList)
}
setIsPublishing(false)
}
const changeRelayType = async (
relayUrl: string,
relayType: UserRelaysType
) => {
if (!ndkRelayList) return
const normalizedUrl = normalizeWebSocketURL(relayUrl)
const rawEvent = ndkRelayList.rawEvent()
const nonRelayTags = rawEvent.tags.filter(
(tag) => tag[0] !== 'r' && tag[0] !== 'relay'
)
// all relay tags except the changing one
const relayTags = rawEvent.tags
.filter((tag) => tag[0] === 'r' || tag[0] === 'relay')
.filter((tag) => normalizeWebSocketURL(tag[1]) !== normalizedUrl)
// create a new relay tag
const tag = ['r', normalizedUrl]
// set the relay marker
if (relayType !== UserRelaysType.Both) {
tag.push(relayType === UserRelaysType.Read ? READ_MARKER : WRITE_MARKER)
}
const unsignedEvent: UnsignedEvent = {
pubkey: rawEvent.pubkey,
kind: kinds.RelayList,
tags: [...nonRelayTags, ...relayTags, tag],
content: rawEvent.content,
created_at: now()
}
setIsPublishing(true)
const signedEvent = await window.nostr
?.signEvent(unsignedEvent)
.then((event) => event as Event)
.catch((err) => {
toast.error('Failed to sign the event!')
log(true, LogType.Error, 'Failed to sign the event!', err)
return null
})
if (!signedEvent) {
setIsPublishing(false)
return
}
const publishedOnRelays =
await RelayController.getInstance().publishOnRelays(
signedEvent,
ndkRelayList.writeRelayUrls
)
// Handle cases where publishing failed or succeeded
if (publishedOnRelays.length === 0) {
toast.error('Failed to publish relay list event on any relay')
} else {
toast.success(
`Event published successfully to the following relays\n\n${publishedOnRelays.join(
'\n'
)}`
)
const newNDKRelayList = new NDKRelayList(ndkRelayList.ndk, signedEvent)
setNDKRelayList(newNDKRelayList)
}
setIsPublishing(false)
}
if (!ndkRelayList)
return <div>Could not fetch user relay list or user is not logged in </div>
const relayMap = new Map<string, UserRelaysType>()
ndkRelayList.readRelayUrls.forEach((relayUrl) => {
const normalizedUrl = normalizeWebSocketURL(relayUrl)
if (!relayMap.has(normalizedUrl)) {
relayMap.set(normalizedUrl, UserRelaysType.Read)
}
})
ndkRelayList.writeRelayUrls.forEach((relayUrl) => {
const normalizedUrl = normalizeWebSocketURL(relayUrl)
if (relayMap.has(normalizedUrl)) {
relayMap.set(normalizedUrl, UserRelaysType.Both)
} else {
relayMap.set(normalizedUrl, UserRelaysType.Write)
}
})
const relayEntries = Array.from(relayMap.entries())
return ( return (
<>
{isPublishing && <LoadingSpinner desc='Publishing relay list event' />}
<div className='IBMSMSplitMainFullSideFWMid'> <div className='IBMSMSplitMainFullSideFWMid'>
<div className='IBMSMSplitMainFullSideSec'> <div className='IBMSMSplitMainFullSideSec'>
<div className='relayList'> <div className='relayList'>
<div className='inputLabelWrapperMain'> <div className='inputLabelWrapperMain'>
<label className='form-label labelMain'>Your relays</label> <label className='form-label labelMain'>Your relays</label>
</div> </div>
{usersRelays.map((relay, index) => ( {relayEntries.map(([relayUrl, relayType]) => (
<RelayListItem key={index} item={relay} isOwnRelay /> <RelayListItem
key={relayUrl}
relayUrl={relayUrl}
relayType={relayType}
isOwnRelay
handleAdd={handleAdd}
handleRemove={handleRemove}
changeRelayType={changeRelayType}
/>
))} ))}
</div> </div>
</div> </div>
@ -20,11 +291,15 @@ export const RelaySettings = () => {
placeholder='wss://some-relay.com' placeholder='wss://some-relay.com'
type='text' type='text'
name='relay' name='relay'
value='' value={inputValue}
onChange={() => {}} onChange={(_, value) => setInputValue(value)}
/> />
<button className='btn btnMain' type='button'> <button
className='btn btnMain'
type='button'
onClick={() => handleAdd(inputValue).then(() => setInputValue(''))}
>
Add Add
</button> </button>
</div> </div>
@ -38,9 +313,21 @@ export const RelaySettings = () => {
</p> </p>
</div> </div>
<div className='relayList'> <div className='relayList'>
{degmodsRelays.map((relay, index) => ( {degmodRelays.map((relayUrl) => {
<RelayListItem key={index} item={relay} /> const normalizedUrl = normalizeWebSocketURL(relayUrl)
))} const alreadyAdded = relayMap.has(normalizedUrl)
return (
<RelayListItem
key={relayUrl}
relayUrl={normalizedUrl}
alreadyAdded={alreadyAdded}
handleAdd={handleAdd}
handleRemove={handleRemove}
changeRelayType={changeRelayType}
/>
)
})}
</div> </div>
</div> </div>
@ -53,35 +340,76 @@ export const RelaySettings = () => {
</p> </p>
</div> </div>
<div className='relayList'> <div className='relayList'>
{recommendRelays.map((relay, index) => ( {recommendRelays.map((relayUrl) => {
<RelayListItem key={index} item={relay} /> const normalizedUrl = normalizeWebSocketURL(relayUrl)
))} const alreadyAdded = relayMap.has(normalizedUrl)
return (
<RelayListItem
key={relayUrl}
relayUrl={normalizedUrl}
alreadyAdded={alreadyAdded}
handleAdd={handleAdd}
handleRemove={handleRemove}
changeRelayType={changeRelayType}
/>
)
})}
</div> </div>
</div> </div>
</div> </div>
</>
) )
} }
const RelayListItem = ({ type RelayItemProps = {
item, relayUrl: string
isOwnRelay relayType?: UserRelaysType
}: {
item: RelayItem
isOwnRelay?: boolean isOwnRelay?: boolean
}) => { alreadyAdded?: boolean
handleAdd: (relayUrl: string) => void
handleRemove: (relayUrl: string) => void
changeRelayType: (relayUrl: string, relayType: UserRelaysType) => void
}
const RelayListItem = ({
relayUrl,
relayType,
isOwnRelay,
alreadyAdded,
handleAdd,
handleRemove,
changeRelayType
}: RelayItemProps) => {
const [isConnected, setIsConnected] = useState(false)
useDidMount(() => {
RelayController.getInstance()
.connectRelay(relayUrl)
.then((relay) => {
if (relay && relay.connected) {
setIsConnected(true)
} else {
setIsConnected(false)
}
})
})
return ( return (
<div className='relayListItem'> <div className='relayListItem'>
<div className='relayListItemSec relayListItemSecPic'> <div className='relayListItemSec relayListItemSecPic'>
<div <div
className='relayListItemSecPicImg' className='relayListItemSecPicImg'
style={{ style={{
background: item.backgroundColor background: isConnected ? '#60ae60' : '#cd4d45'
}} }}
></div> ></div>
</div> </div>
<div className='relayListItemSec relayListItemSecDetails'> <div className='relayListItemSec relayListItemSecDetails'>
<p className='relayListItemSecDetailsText'>{item.url}</p> <p className='relayListItemSecDetailsText'>{relayUrl}</p>
<div className='relayListItemSecDetailsExtra'> <div className='relayListItemSecDetailsExtra'>
{(relayType === UserRelaysType.Read ||
relayType === UserRelaysType.Both) && (
<svg <svg
xmlns='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg'
viewBox='-64 0 512 512' viewBox='-64 0 512 512'
@ -90,10 +418,14 @@ const RelayListItem = ({
fill='currentColor' fill='currentColor'
data-bs-toggle='tooltip' data-bs-toggle='tooltip'
data-bss-tooltip data-bss-tooltip
aria-label={item.readTitle} aria-label='Read'
> >
<path d='M0 64C0 28.65 28.65 0 64 0H224V128C224 145.7 238.3 160 256 160H384V448C384 483.3 355.3 512 320 512H64C28.65 512 0 483.3 0 448V64zM256 128V0L384 128H256z'></path> <path d='M0 64C0 28.65 28.65 0 64 0H224V128C224 145.7 238.3 160 256 160H384V448C384 483.3 355.3 512 320 512H64C28.65 512 0 483.3 0 448V64zM256 128V0L384 128H256z'></path>
</svg> </svg>
)}
{(relayType === UserRelaysType.Write ||
relayType === UserRelaysType.Both) && (
<svg <svg
xmlns='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg'
viewBox='0 -32 576 576' viewBox='0 -32 576 576'
@ -102,23 +434,10 @@ const RelayListItem = ({
fill='currentColor' fill='currentColor'
data-bs-toggle='tooltip' data-bs-toggle='tooltip'
data-bss-tooltip data-bss-tooltip
aria-label={item.writeTitle} aria-label='Write'
> >
<path d='M0 64C0 28.65 28.65 0 64 0H224V128C224 145.7 238.3 160 256 160H384V299.6L289.3 394.3C281.1 402.5 275.3 412.8 272.5 424.1L257.4 484.2C255.1 493.6 255.7 503.2 258.8 512H64C28.65 512 0 483.3 0 448V64zM256 128V0L384 128H256zM564.1 250.1C579.8 265.7 579.8 291 564.1 306.7L534.7 336.1L463.8 265.1L493.2 235.7C508.8 220.1 534.1 220.1 549.8 235.7L564.1 250.1zM311.9 416.1L441.1 287.8L512.1 358.7L382.9 487.9C378.8 492 373.6 494.9 368 496.3L307.9 511.4C302.4 512.7 296.7 511.1 292.7 507.2C288.7 503.2 287.1 497.4 288.5 491.1L303.5 431.8C304.9 426.2 307.8 421.1 311.9 416.1V416.1z'></path> <path d='M0 64C0 28.65 28.65 0 64 0H224V128C224 145.7 238.3 160 256 160H384V299.6L289.3 394.3C281.1 402.5 275.3 412.8 272.5 424.1L257.4 484.2C255.1 493.6 255.7 503.2 258.8 512H64C28.65 512 0 483.3 0 448V64zM256 128V0L384 128H256zM564.1 250.1C579.8 265.7 579.8 291 564.1 306.7L534.7 336.1L463.8 265.1L493.2 235.7C508.8 220.1 534.1 220.1 549.8 235.7L564.1 250.1zM311.9 416.1L441.1 287.8L512.1 358.7L382.9 487.9C378.8 492 373.6 494.9 368 496.3L307.9 511.4C302.4 512.7 296.7 511.1 292.7 507.2C288.7 503.2 287.1 497.4 288.5 491.1L303.5 431.8C304.9 426.2 307.8 421.1 311.9 416.1V416.1z'></path>
</svg> </svg>
{item.subscribedTitle && (
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
data-bs-toggle='tooltip'
data-bss-tooltip
aria-label={item.subscribedTitle}
>
<path d='M504 256c0 136.967-111.033 248-248 248S8 392.967 8 256 119.033 8 256 8s248 111.033 248 248zm-141.651-35.33c4.937-32.999-20.191-50.739-54.55-62.573l11.146-44.702-27.213-6.781-10.851 43.524c-7.154-1.783-14.502-3.464-21.803-5.13l10.929-43.81-27.198-6.781-11.153 44.686c-5.922-1.349-11.735-2.682-17.377-4.084l.031-.14-37.53-9.37-7.239 29.062s20.191 4.627 19.765 4.913c11.022 2.751 13.014 10.044 12.68 15.825l-12.696 50.925c.76.194 1.744.473 2.829.907-.907-.225-1.876-.473-2.876-.713l-17.796 71.338c-1.349 3.348-4.767 8.37-12.471 6.464.271.395-19.78-4.937-19.78-4.937l-13.51 31.147 35.414 8.827c6.588 1.651 13.045 3.379 19.4 5.006l-11.262 45.213 27.182 6.781 11.153-44.733a1038.209 1038.209 0 0 0 21.687 5.627l-11.115 44.523 27.213 6.781 11.262-45.128c46.404 8.781 81.299 5.239 95.986-36.727 11.836-33.79-.589-53.281-25.004-65.991 17.78-4.098 31.174-15.792 34.747-39.949zm-62.177 87.179c-8.41 33.79-65.308 15.523-83.755 10.943l14.944-59.899c18.446 4.603 77.6 13.717 68.811 48.956zm8.417-87.667c-7.673 30.736-55.031 15.12-70.393 11.292l13.548-54.327c15.363 3.828 64.836 10.973 56.845 43.035z'></path>
</svg>
)} )}
</div> </div>
</div> </div>
@ -148,17 +467,72 @@ const RelayListItem = ({
className='dropdown-menu dropdownMainMenu' className='dropdown-menu dropdownMainMenu'
style={{ position: 'absolute' }} style={{ position: 'absolute' }}
> >
<a className='dropdown-item dropdownMainMenuItem' href='#'> {(relayType === UserRelaysType.Read ||
relayType === UserRelaysType.Write) && (
<div
className='dropdown-item dropdownMainMenuItem'
onClick={() => changeRelayType(relayUrl, UserRelaysType.Both)}
>
Read & Write
</div>
)}
{relayType === UserRelaysType.Both && (
<>
<div
className='dropdown-item dropdownMainMenuItem'
onClick={() =>
changeRelayType(relayUrl, UserRelaysType.Read)
}
>
Read Only
</div>
<div
className='dropdown-item dropdownMainMenuItem'
onClick={() =>
changeRelayType(relayUrl, UserRelaysType.Write)
}
>
Write Only
</div>
</>
)}
{relayType === UserRelaysType.Read && (
<div
className='dropdown-item dropdownMainMenuItem'
onClick={() =>
changeRelayType(relayUrl, UserRelaysType.Write)
}
>
Write Only
</div>
)}
{relayType === UserRelaysType.Write && (
<div
className='dropdown-item dropdownMainMenuItem'
onClick={() => changeRelayType(relayUrl, UserRelaysType.Read)}
>
Read Only
</div>
)}
<div
className='dropdown-item dropdownMainMenuItem'
onClick={() => handleRemove(relayUrl)}
>
Remove Remove
</a> </div>
<a className='dropdown-item dropdownMainMenuItem' href='#'>
Details
</a>
</div> </div>
</div> </div>
)} )}
{!isOwnRelay && ( {!isOwnRelay && !alreadyAdded && (
<button className='btn btnMain' type='button'> <button
className='btn btnMain'
type='button'
onClick={() => handleAdd(relayUrl)}
>
<svg <svg
xmlns='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg'
viewBox='-32 0 512 512' viewBox='-32 0 512 512'
@ -175,68 +549,6 @@ const RelayListItem = ({
) )
} }
interface RelayItem { const degmodRelays = ['wss://relay.degmods.com']
url: string
backgroundColor: string
readTitle: string
writeTitle: string
subscribedTitle: string
}
const usersRelays: RelayItem[] = [ const recommendRelays = ['wss://relay.degmods.com']
{
url: 'wss://relay.wibblywobbly.com',
backgroundColor: '#cd4d45',
readTitle: 'Read',
writeTitle: 'Write',
subscribedTitle: ''
},
{
url: 'wss://relay.wibblywobbly.com',
backgroundColor: '#60ae60',
readTitle: 'Read',
writeTitle: 'Write',
subscribedTitle: 'Paid (Subscribed)'
},
{
url: 'wss://relay.degmods.com',
backgroundColor: '#60ae60',
readTitle: 'Read',
writeTitle: 'Write',
subscribedTitle: ''
}
]
const degmodsRelays: RelayItem[] = [
{
url: 'wss://relay1.degmods.com',
backgroundColor: '#60ae60',
readTitle: 'Read',
writeTitle: 'Write',
subscribedTitle: ''
},
{
url: 'wss://relay2.degmods.com',
backgroundColor: '#60ae60',
readTitle: 'Read',
writeTitle: 'Write',
subscribedTitle: ''
}
]
const recommendRelays: RelayItem[] = [
{
url: 'wss://relay1.degmods.com',
backgroundColor: '#60ae60',
readTitle: 'Read',
writeTitle: 'Write',
subscribedTitle: ''
},
{
url: 'wss://relay2.degmods.com',
backgroundColor: '#60ae60',
readTitle: 'Read',
writeTitle: 'Write',
subscribedTitle: ''
}
]