import ContentCopyIcon from '@mui/icons-material/ContentCopy' import { IconButton, InputProps, List, ListItem, ListSubheader, TextField, Tooltip, Typography, useTheme } from '@mui/material' import { UnsignedEvent, nip19, kinds, VerifiedEvent } from 'nostr-tools' import { useEffect, useMemo, useRef, useState } from 'react' import { Link, useParams } from 'react-router-dom' import { toast } from 'react-toastify' 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' import { getRoboHashPicture } from '../../../utils' export const ProfileSettingsPage = () => { 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 metadataState = useSelector((state: State) => state.metadata) const keys = useSelector((state: State) => state.auth?.keyPair) const { usersPubkey, loginMethod } = useSelector((state: State) => state.auth) const userRobotImage = useSelector((state: State) => state.userRobotImage) 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 (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) } /** * 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) setProfileMetadata((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 = (metadata: ProfileMetadata) => { if (!isUsersOwnProfile) { return metadata.picture || getRoboHashPicture(npub!) } // userRobotImage is used only when visiting own profile // while kind 0 picture is not set return metadata.picture || userRobotImage || getRoboHashPicture(npub!) } return ( <> {isLoading && }
Profile Settings } > {profileMetadata && (
{ event.target.src = getRoboHashPicture(npub!) }} className={styles.img} src={getProfileImage(profileMetadata)} alt="Profile Image" /> {nostrJoiningBlock && ( On nostr since {nostrJoiningBlock.block.toLocaleString()} )} {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 === LoginMethods.privateKey && keys && keys.private && copyItem( '••••••••••••••••••••••••••••••••••••••••••••••••••', 'Private Key', keys.private )} )}
)}
{isUsersOwnProfile && ( SAVE )}
) }