import ContentCopyIcon from '@mui/icons-material/ContentCopy' import { CircularProgress, IconButton, InputProps, List, ListItem, ListSubheader, TextField, Tooltip, Typography, useTheme } from '@mui/material' import { UnsignedEvent, nip19, kinds, VerifiedEvent } from 'nostr-tools' import { useEffect, useMemo, useState } from 'react' import { Link, useParams } from 'react-router-dom' import { toast } from 'react-toastify' import placeholderAvatar from '../../assets/images/nostr-logo.jpg' import { MetadataController, NostrController } from '../../controllers' import { NostrJoiningBlock, ProfileMetadata } from '../../types' import styles from './style.module.scss' import { useDispatch, useSelector } from 'react-redux' import { State } from '../../store/rootReducer' import { LoadingButton } from '@mui/lab' import { Dispatch } from '../../store/store' import { setMetadataEvent } from '../../store/actions' import { LoadingSpinner } from '../../components/LoadingSpinner' import { LoginMethods } from '../../store/auth/types' import { SmartToy } from '@mui/icons-material' export const ProfilePage = () => { const theme = useTheme() const { npub } = useParams() const dispatch: Dispatch = useDispatch() const metadataController = useMemo(() => new MetadataController(), []) const nostrController = NostrController.getInstance() const [pubkey, setPubkey] = useState() const [nostrJoiningBlock, setNostrJoiningBlock] = useState(null) const [profileMetadata, setProfileMetadata] = useState() const [savingProfileMetadata, setSavingProfileMetadata] = useState(false) const [avatarLoading, setAvatarLoading] = useState(false) const metadataState = useSelector((state: State) => state.metadata) const keys = useSelector((state: State) => state.auth?.keyPair) const { usersPubkey, loginMethod } = useSelector((state: State) => state.auth) const [isUsersOwnProfile, setIsUsersOwnProfile] = useState(false) const [isLoading, setIsLoading] = useState(true) const [loadingSpinnerDesc] = useState('Fetching metadata') 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 (pubkey) { metadataController .getNostrJoiningBlockNumber(pubkey) .then((res) => { setNostrJoiningBlock(res) }) .catch((err) => { // todo: handle error console.log('err :>> ', err) }) } if (isUsersOwnProfile && metadataState) { const metadataContent = metadataController.extractProfileMetadataContent( metadataState as VerifiedEvent ) if (metadataContent) { setProfileMetadata(metadataContent) setIsLoading(false) } return } if (pubkey) { const getMetadata = async (pubkey: string) => { const metadataEvent = await metadataController .findMetadata(pubkey) .catch((err) => { toast.error(err) return null }) if (metadataEvent) { const metadataContent = metadataController.extractProfileMetadataContent(metadataEvent) if (metadataContent) { setProfileMetadata(metadataContent) } } setIsLoading(false) } getMetadata(pubkey) } }, [isUsersOwnProfile, metadataState, pubkey, metadataController]) const editItem = ( key: keyof ProfileMetadata, label: string, multiline = false, rows = 1, inputProps?: InputProps ) => ( ) => { const { value } = event.target setProfileMetadata((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 () => { setSavingProfileMetadata(true) const content = JSON.stringify(profileMetadata) // We need to omit cachedAt and create new event // Relay will reject if created_at is too late const updatedMetadataState: UnsignedEvent = { content: content, created_at: Math.round(Date.now() / 1000), kind: kinds.Metadata, pubkey: pubkey!, tags: metadataState?.tags || [] } const signedEvent = await nostrController .signEvent(updatedMetadataState) .catch((error) => { toast.error(`Error saving profile metadata. ${error}`) }) if (signedEvent) { if (!metadataController.validate(signedEvent)) { toast.error(`Metadata is not valid.`) } await metadataController.publishMetadataEvent(signedEvent) dispatch(setMetadataEvent(signedEvent)) } setSavingProfileMetadata(false) } const generateRobotAvatar = () => { setAvatarLoading(true) const robotAvatarLink = `https://robohash.org/${npub}.png?set=set3` setProfileMetadata((prev) => ({ ...prev, picture: '' })) setTimeout(() => { setProfileMetadata((prev) => ({ ...prev, picture: robotAvatarLink })) }) } /** * * @returns robohash generate button, loading spinner or no button */ const robohashButton = () => { if (profileMetadata?.picture?.includes('robohash')) return null return ( {avatarLoading ? ( ) : ( )} ) } return ( <> {isLoading && }
Profile Settings } > {profileMetadata && (
{ event.target.src = placeholderAvatar }} onLoad={() => { setAvatarLoading(false) }} className={styles.img} src={profileMetadata.picture || placeholderAvatar} alt="Profile Image" /> {nostrJoiningBlock && ( On nostr since {nostrJoiningBlock.block.toLocaleString()} )} {editItem('picture', 'Picture URL', undefined, undefined, { endAdornment: robohashButton() })} {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 === LoginMethods.privateKey && keys && keys.private && copyItem( '••••••••••••••••••••••••••••••••••••••••••••••••••', 'Private Key', keys.private )} )}
)}
{isUsersOwnProfile && ( SAVE )}
) }