import React, { useEffect, useRef, useState } from 'react' import { useParams } from 'react-router-dom' import { toast } from 'react-toastify' import { SmartToy } from '@mui/icons-material' import ContentCopyIcon from '@mui/icons-material/ContentCopy' import LaunchIcon from '@mui/icons-material/Launch' import { LoadingButton } from '@mui/lab' import { Box, IconButton, InputProps, List, ListItem, ListSubheader, TextField, Tooltip } from '@mui/material' import { NDKEvent, NDKUserProfile, profileFromEvent, serializeProfile } from '@nostr-dev-kit/ndk' import { launch as launchNostrLoginDialog } from 'nostr-login' import { kinds, nip19, UnsignedEvent } from 'nostr-tools' import { NostrController } from '../../../controllers' import { useNDKContext } from '../../../hooks' import { useAppDispatch, useAppSelector } from '../../../hooks/store' import { getRoboHashPicture, unixNow } from '../../../utils' import { Container } from '../../../components/Container' import { Footer } from '../../../components/Footer/Footer' import { LoadingSpinner } from '../../../components/LoadingSpinner' import { setMetadataEvent } from '../../../store/actions' import { LoginMethod, NostrLoginAuthMethod } from '../../../store/auth/types' import { Dispatch } from '../../../store/store' import styles from './style.module.scss' export const ProfileSettingsPage = () => { const dispatch: Dispatch = useAppDispatch() const { npub } = useParams() const { ndk, findMetadata, publish } = useNDKContext() const [pubkey, setPubkey] = useState() const [userProfile, setUserProfile] = useState(null) const userRobotImage = useAppSelector((state) => state.userRobotImage) const metadataState = useAppSelector((state) => state.metadata) const keys = useAppSelector((state) => state.auth?.keyPair) const { usersPubkey, loginMethod, nostrLoginAuthMethod } = useAppSelector( (state) => state.auth ) const [savingProfileMetadata, setSavingProfileMetadata] = useState(false) const [isUsersOwnProfile, setIsUsersOwnProfile] = useState(false) const [isLoading, setIsLoading] = useState(true) const [loadingSpinnerDesc] = useState('Fetching metadata') const robotSet = useRef(1) useEffect(() => { if (npub) { try { const hexPubkey = nip19.decode(npub).data as string setPubkey(hexPubkey) if (hexPubkey === usersPubkey) setIsUsersOwnProfile(true) } catch (error) { toast.error('Error occurred in decoding npub' + error) } } }, [npub, usersPubkey]) useEffect(() => { if (isUsersOwnProfile && metadataState) { const ndkEvent = new NDKEvent(ndk, metadataState) const profile = profileFromEvent(ndkEvent) setUserProfile(profile) setIsLoading(false) return } if (pubkey) { findMetadata(pubkey) .then((profile) => { setUserProfile(profile) }) .catch((err) => { toast.error(err) }) .finally(() => { setIsLoading(false) }) } }, [ndk, isUsersOwnProfile, metadataState, pubkey, findMetadata]) const editItem = ( key: keyof NDKUserProfile, label: string, multiline = false, rows = 1, inputProps?: InputProps ) => ( ) => { const { value } = event.target setUserProfile((prev) => ({ ...prev, [key]: value })) }} /> ) const copyItem = ( value: string, label: string, copyValue?: string, isPassword = false ) => ( { navigator.clipboard.writeText(copyValue || value) toast.success('Copied to clipboard', { autoClose: 1000, hideProgressBar: true }) }} > }} /> ) const handleSaveMetadata = async () => { if (!userProfile) return setSavingProfileMetadata(true) const serializedProfile = serializeProfile(userProfile) const unsignedEvent: UnsignedEvent = { content: serializedProfile, created_at: unixNow(), kind: kinds.Metadata, pubkey: pubkey!, tags: [] } const nostrController = NostrController.getInstance() const signedEvent = await nostrController .signEvent(unsignedEvent) .catch((error) => { toast.error(`Error saving profile metadata. ${error}`) return null }) if (!signedEvent) { setSavingProfileMetadata(false) return } const ndkEvent = new NDKEvent(ndk, signedEvent) const publishedOnRelays = await publish(ndkEvent) // Handle cases where publishing failed or succeeded if (publishedOnRelays.length === 0) { toast.error('Failed to publish event on any relay') } else { toast.success( `Event published successfully to the following relays\n\n${publishedOnRelays.join( '\n' )}` ) dispatch(setMetadataEvent(signedEvent)) } setSavingProfileMetadata(false) } /** * Called by clicking on the robot icon inside Picture URL input * On every click, next robohash set will be generated. * There are 5 sets at the moment, after 5th set function will start over from set 1. */ const generateRobotAvatar = () => { robotSet.current++ if (robotSet.current > 5) robotSet.current = 1 const robotAvatarLink = getRoboHashPicture(npub!, robotSet.current) setUserProfile((prev) => ({ ...prev, picture: robotAvatarLink })) } /** * * @returns robohash generate button, loading spinner or no button */ const robohashButton = () => { return ( ) } /** * Handles the logic for Image URL. * If no picture in kind 0 found - use robohash avatar * * @returns robohash image url */ const getProfileImage = (profile: NDKUserProfile) => { if (!isUsersOwnProfile) { return profile.image || getRoboHashPicture(npub!) } // userRobotImage is used only when visiting own profile // while kind 0 picture is not set return profile.image || userRobotImage || getRoboHashPicture(npub!) } return ( <> {isLoading && } Profile Settings } > {userProfile && (
{userProfile.banner ? ( Banner Image ) : ( No banner found )} {editItem('banner', 'Banner URL', undefined, undefined)} ) => { event.currentTarget.src = getRoboHashPicture(npub!) }} className={styles.img} src={getProfileImage(userProfile)} alt="Profile Image" /> {editItem('picture', 'Picture URL', undefined, undefined, { endAdornment: isUsersOwnProfile ? robohashButton() : undefined })} {editItem('name', 'Username')} {editItem('display_name', 'Display Name')} {editItem('nip05', 'Nostr Address (nip05)')} {editItem('lud16', 'Lightning Address (lud16)')} {editItem('about', 'About', true, 4)} {editItem('website', 'Website')} {isUsersOwnProfile && ( <> {usersPubkey && copyItem(nip19.npubEncode(usersPubkey), 'Public Key')} {loginMethod === LoginMethod.privateKey && keys && keys.private && copyItem( '••••••••••••••••••••••••••••••••••••••••••••••••••', 'Private Key', keys.private )} )} {isUsersOwnProfile && ( <> {loginMethod === LoginMethod.nostrLogin && nostrLoginAuthMethod === NostrLoginAuthMethod.Local && ( { launchNostrLoginDialog('import') }} > ) }} /> )} )}
)}
{isUsersOwnProfile && ( SAVE )}