diff --git a/src/components/GameCard.tsx b/src/components/GameCard.tsx index 1b34432..0d76af9 100644 --- a/src/components/GameCard.tsx +++ b/src/components/GameCard.tsx @@ -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 { handleGameImageError } from '../utils' -import { getGamePageRoute } from 'routes' type GameCardProps = { title: string @@ -9,13 +9,10 @@ type GameCardProps = { } export const GameCard = ({ title, imageUrl }: GameCardProps) => { - const navigate = useNavigate() + const route = getGamePageRoute(title) return ( -
navigate(getGamePageRoute(title))} - > +
{

{title}

-
+ ) } diff --git a/src/components/ProfileSection.tsx b/src/components/ProfileSection.tsx index 58a82c5..fa4d6e8 100644 --- a/src/components/ProfileSection.tsx +++ b/src/components/ProfileSection.tsx @@ -1,3 +1,4 @@ +import { FALLBACK_PROFILE_IMAGE } from 'constants.ts' import { Event, Filter, kinds, nip19, UnsignedEvent } from 'nostr-tools' import { QRCodeSVG } from 'qrcode.react' import { useState } from 'react' @@ -9,16 +10,21 @@ import { UserRelaysType } from '../controllers' import { useAppSelector, useDidMount } from '../hooks' -import { getProfilePageRoute } from '../routes' +import { appRoutes, getProfilePageRoute } from '../routes' import '../styles/author.css' import '../styles/innerPage.css' import '../styles/socialPosts.css' 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 { ZapPopUp } from './Zap' -import { NDKUserProfile } from '@nostr-dev-kit/ndk' -import _ from 'lodash' type Props = { 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 (
- +
@@ -91,12 +106,26 @@ export const ProfileSection = ({ pubkey }: Props) => { } 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 () => { - copyTextToClipboard(profile.npub as string).then((isCopied) => { + copyTextToClipboard(npub).then((isCopied) => { if (isCopied) { toast.success('Npub copied to clipboard!') } else { @@ -107,25 +136,15 @@ export const Profile = ({ profile }: ProfileProps) => { }) } - const hexPubkey = npubToHex(profile.pubkey as string) - - if (!hexPubkey) return null - - const profileRoute = getProfilePageRoute( - nip19.nprofileEncode({ - 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 || '' + let profileRoute = appRoutes.home + const hexPubkey = npubToHex(pubkey) + if (hexPubkey) { + profileRoute = getProfilePageRoute( + nip19.nprofileEncode({ + pubkey: hexPubkey + }) + ) + } return (
@@ -142,7 +161,7 @@ export const Profile = ({ profile }: ProfileProps) => { className='IBMSMSMSSS_Author_Top_PP' style={{ background: `url('${ - profile.image || '' + image || FALLBACK_PROFILE_IMAGE }') center / cover no-repeat` }} >
@@ -151,7 +170,9 @@ export const Profile = ({ profile }: ProfileProps) => {

{displayName}

-

{nip05}

+ {nip05 && ( +

{nip05}

+ )}
@@ -182,8 +203,8 @@ export const Profile = ({ profile }: ProfileProps) => {
- - + + {lud16 && }
@@ -196,7 +217,7 @@ export const Profile = ({ profile }: ProfileProps) => { > - + ) } diff --git a/src/constants.ts b/src/constants.ts index f4d141b..207bc80 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -15,7 +15,7 @@ export const LANDING_PAGE_DATA = { ], featuredGames: [ 'Persona 3 Reload', - 'Baldur\'s Gate 3', + "Baldur's Gate 3", 'Cyberpunk 2077', 'ELDEN RING', 'FINAL FANTASY VII REMAKE INTERGRADE' @@ -119,3 +119,7 @@ export const GAME_FILES = [ export const MAX_MODS_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' diff --git a/src/controllers/metadata.ts b/src/controllers/metadata.ts index 1eb2dad..6cebdcb 100644 --- a/src/controllers/metadata.ts +++ b/src/controllers/metadata.ts @@ -141,6 +141,9 @@ export class MetadataController { }) } + public getNDKRelayList = async (hexKey: string) => + getRelayListForUser(hexKey, this.ndk) + public getMuteLists = async ( pubkey?: string ): Promise<{ diff --git a/src/pages/games.tsx b/src/pages/games.tsx index d2eb023..37d6759 100644 --- a/src/pages/games.tsx +++ b/src/pages/games.tsx @@ -18,7 +18,7 @@ export const GamesPage = () => { const [currentPage, setCurrentPage] = useState(1) 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) const gameNames = new Set() diff --git a/src/pages/mod/index.tsx b/src/pages/mod/index.tsx index bcce8bd..ac1636f 100644 --- a/src/pages/mod/index.tsx +++ b/src/pages/mod/index.tsx @@ -5,7 +5,7 @@ import { formatDate } from 'date-fns' import FsLightbox from 'fslightbox-react' import { Filter, kinds, nip19, UnsignedEvent } from 'nostr-tools' 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 { BlogCard } from '../../components/BlogCard' import { LoadingSpinner } from '../../components/LoadingSpinner' @@ -16,7 +16,7 @@ import { UserRelaysType } from '../../controllers' import { useAppSelector, useDidMount } from '../../hooks' -import { getModsEditPageRoute } from '../../routes' +import { getGamePageRoute, getModsEditPageRoute } from '../../routes' import '../../styles/comments.css' import '../../styles/downloads.css' import '../../styles/innerPage.css' @@ -41,9 +41,9 @@ import { sendDMUsingRandomKey, signAndPublish } from '../../utils' +import { Comments } from './internal/comment' import { Reactions } from './internal/reactions' import { Zap } from './internal/zap' -import { Comments } from './internal/comment' export const ModPage = () => { const { naddr } = useParams() @@ -214,7 +214,6 @@ type GameProps = { } const Game = ({ naddr, game, author, aTag }: GameProps) => { - const navigate = useNavigate() const userState = useAppSelector((state) => state.user) const [isLoading, setIsLoading] = useState(false) const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('') @@ -510,15 +509,18 @@ const Game = ({ naddr, game, author, aTag }: GameProps) => { userState.user?.npub && userState.user.npub === import.meta.env.VITE_REPORTING_NPUB + const gameRoute = getGamePageRoute(game) + const editRoute = getModsEditPageRoute(naddr) + return ( <> {isLoading && }

Mod for:  - + {game} - +

{userState.auth && userState.user?.pubkey === author && ( - navigate(getModsEditPageRoute(naddr))} + to={editRoute} > { Edit - + )} {filteredProfiles.map((profile) => { if (profile.pubkey) { + const displayName = + profile?.displayName || profile?.name || '[name not set up]' + const about = profile?.bio || profile?.about || '[bio not set up]' + return ( - + ) } diff --git a/src/pages/settings/index.tsx b/src/pages/settings/index.tsx index d846294..60aa2c8 100644 --- a/src/pages/settings/index.tsx +++ b/src/pages/settings/index.tsx @@ -14,6 +14,13 @@ import { PreferencesSetting } from './preference' import { AdminSetting } from './admin' 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 = () => { const location = useLocation() const userState = useAppSelector((state) => state.user) @@ -67,7 +74,11 @@ const SettingTabs = () => { const navLinks = [ { path: appRoutes.settingsProfile, label: 'Profile', icon: }, - { path: appRoutes.settingsRelays, label: 'Relays (WIP)', icon: }, + { + path: appRoutes.settingsRelays, + label: 'Relays', + icon: + }, { path: appRoutes.settingsPreferences, label: 'Preferences (WIP)', diff --git a/src/pages/settings/relay.tsx b/src/pages/settings/relay.tsx index 508b21c..ee79d94 100644 --- a/src/pages/settings/relay.tsx +++ b/src/pages/settings/relay.tsx @@ -1,123 +1,442 @@ +import { NDKRelayList } from '@nostr-dev-kit/ndk' 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 = () => { + const userState = useAppSelector((state) => state.user) + const [ndkRelayList, setNDKRelayList] = useState(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
Could not fetch user relay list or user is not logged in
+ + const relayMap = new Map() + + 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 ( -
-
-
-
- + <> + {isPublishing && } +
+
+
+
+ +
+ {relayEntries.map(([relayUrl, relayType]) => ( + + ))} +
+
+ +
+ setInputValue(value)} + /> + + +
+ +
+
+ +

+ We recommend adding one of our relays if you're planning to + frequently use DEG Mods, for a better experience. +

+
+
+ {degmodRelays.map((relayUrl) => { + const normalizedUrl = normalizeWebSocketURL(relayUrl) + const alreadyAdded = relayMap.has(normalizedUrl) + + return ( + + ) + })} +
+
+ +
+
+ +

+ Relays we recommend using as they support the same functionalities + that our relays provide. +

+
+
+ {recommendRelays.map((relayUrl) => { + const normalizedUrl = normalizeWebSocketURL(relayUrl) + const alreadyAdded = relayMap.has(normalizedUrl) + + return ( + + ) + })}
- {usersRelays.map((relay, index) => ( - - ))}
- -
- {}} - /> - - -
- -
-
- -

- We recommend adding one of our relays if you're planning to - frequently use DEG Mods, for a better experience. -

-
-
- {degmodsRelays.map((relay, index) => ( - - ))} -
-
- -
-
- -

- Relays we recommend using as they support the same functionalities - that our relays provide. -

-
-
- {recommendRelays.map((relay, index) => ( - - ))} -
-
-
+ ) } -const RelayListItem = ({ - item, - isOwnRelay -}: { - item: RelayItem +type RelayItemProps = { + relayUrl: string + relayType?: UserRelaysType 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 (
-

{item.url}

+

{relayUrl}

- - - - - - - {item.subscribedTitle && ( + {(relayType === UserRelaysType.Read || + relayType === UserRelaysType.Both) && ( - + + + )} + + {(relayType === UserRelaysType.Write || + relayType === UserRelaysType.Both) && ( + + )}
@@ -148,17 +467,72 @@ const RelayListItem = ({ className='dropdown-menu dropdownMainMenu' style={{ position: 'absolute' }} > -
+ {(relayType === UserRelaysType.Read || + relayType === UserRelaysType.Write) && ( +
changeRelayType(relayUrl, UserRelaysType.Both)} + > + Read & Write +
+ )} + + {relayType === UserRelaysType.Both && ( + <> +
+ changeRelayType(relayUrl, UserRelaysType.Read) + } + > + Read Only +
+
+ changeRelayType(relayUrl, UserRelaysType.Write) + } + > + Write Only +
+ + )} + + {relayType === UserRelaysType.Read && ( +
+ changeRelayType(relayUrl, UserRelaysType.Write) + } + > + Write Only +
+ )} + + {relayType === UserRelaysType.Write && ( +
changeRelayType(relayUrl, UserRelaysType.Read)} + > + Read Only +
+ )} + +
)} - {!isOwnRelay && ( -