daniyal 1a889213fa
All checks were successful
Release to Staging / build_and_release (push) Successful in 44s
feat: implement profile edit
2024-09-24 21:53:34 +05:00

345 lines
11 KiB
TypeScript

import { InputField } from 'components/Inputs'
import { ProfileQRButtonWithPopUp } from 'components/ProfileSection'
import { useAppDispatch, useAppSelector } from 'hooks'
import { kinds, nip19, UnsignedEvent, Event } from 'nostr-tools'
import { useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { toast } from 'react-toastify'
import { appRoutes, getProfilePageRoute } from 'routes'
import { copyTextToClipboard, log, LogType, now, npubToHex } from 'utils'
import 'styles/profile.css'
import {
NDKEvent,
NDKUserProfile,
profileFromEvent,
serializeProfile
} from '@nostr-dev-kit/ndk'
import { RelayController } from 'controllers'
import { LoadingSpinner } from 'components/LoadingSpinner'
import { setUser } from 'store/reducers/user'
type FormState = {
name: string
displayName: string
bio: string
picture: string
banner: string
nip05: string
lud16: string
}
const defaultFormState: FormState = {
name: '',
displayName: '',
bio: '',
picture: '',
banner: '',
nip05: '',
lud16: ''
}
export const ProfileSettings = () => {
const dispatch = useAppDispatch()
const userState = useAppSelector((state) => state.user)
const [isPublishing, setIsPublishing] = useState(false)
const [formState, setFormState] = useState<FormState>(defaultFormState)
useEffect(() => {
if (userState.auth && userState.user) {
const {
name,
displayName,
about,
bio,
image,
picture,
banner,
nip05,
lud16
} = userState.user
setFormState({
name: name || '',
displayName: displayName || '',
bio: bio || about || '',
picture: typeof picture === 'string' ? picture : image || '',
banner: banner || '',
nip05: nip05 || '',
lud16: lud16 || ''
})
} else {
setFormState(defaultFormState)
}
}, [userState])
const handleInputChange = (field: string, value: string) => {
setFormState((prev) => ({
...prev,
[field]: value
}))
}
const banner =
formState.banner || '/assets/img/DEGMods%20Placeholder%20Img.png'
const picture =
formState.picture || '/assets/img/DEG%20Mods%20Default%20PP.png'
const name = formState.displayName || formState.name || 'User name'
const nip05 = formState.nip05 || 'nip5handle@domain.com'
const npub = (userState.user?.npub as string) || 'npub1address'
const bio =
formState.bio ||
'user bio, this is a long string of temporary text that would be replaced with the user bio from their metadata address'
// In case user is not logged in clicking on profile link will navigate to homepage
let profileRoute = appRoutes.home
if (userState.auth && userState.user) {
const hexPubkey = npubToHex(userState.user.npub as string)
if (hexPubkey) {
profileRoute = getProfilePageRoute(
nip19.nprofileEncode({
pubkey: hexPubkey
})
)
}
}
const handleCopy = async () => {
copyTextToClipboard(npub).then((isCopied) => {
if (isCopied) {
toast.success('Npub copied to clipboard!')
} else {
toast.error(
'Failed to copy, look into console for more details on error!'
)
}
})
}
const handlePublish = async () => {
if (!userState.auth && !userState.user?.pubkey) return
setIsPublishing(true)
const prevProfile = userState.user as NDKUserProfile
const updatedProfile = {
...prevProfile,
name: formState.name,
displayName: formState.displayName,
bio: formState.bio,
picture: formState.picture,
banner: formState.banner,
nip05: formState.nip05,
lud16: formState.lud16
}
const serializedProfile = serializeProfile(updatedProfile)
const unsignedEvent: UnsignedEvent = {
kind: kinds.Metadata,
tags: [],
content: serializedProfile,
created_at: now(),
pubkey: userState.user?.pubkey as string
}
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().publish(
signedEvent as Event
)
// 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'
)}`
)
const ndkEvent = new NDKEvent(undefined, signedEvent)
const userProfile = profileFromEvent(ndkEvent)
dispatch(setUser(userProfile))
}
setIsPublishing(false)
}
return (
<>
{isPublishing && <LoadingSpinner desc='Publishing event' />}
<div className='IBMSMSplitMainFullSideFWMid'>
<div className='IBMSMSplitMainFullSideSec'>
<div className='IBMSMSMBSS_Profile'>
<div className='IBMSMSMBSS_ProfilePreview'>
<div className='IBMSMSMSSS_Author_Top_Left'>
<div
className='IBMSMSMSSS_Author_Top_Banner'
style={{
background: `url(${banner}) center / cover no-repeat`
}}
></div>
<Link
className='IBMSMSMSSS_Author_Top_Left_InsideLinkWrapper'
to={profileRoute}
>
<div className='IBMSMSMSSS_Author_Top_Left_Inside'>
<div className='IBMSMSMSSS_Author_Top_Left_InsidePic'>
<div className='IBMSMSMSSS_Author_Top_PPWrapper'>
<div
className='IBMSMSMSSS_Author_Top_PP'
style={{
background: `url(${picture}) center / cover no-repeat`
}}
></div>
</div>
</div>
<div className='IBMSMSMSSS_Author_Top_Left_InsideDetails'>
<div className='IBMSMSMSSS_Author_TopWrapper'>
<p className='IBMSMSMSSS_Author_Top_Name'>{name}</p>
<p className='IBMSMSMSSS_Author_Top_Handle'>{nip05}</p>
</div>
</div>
</div>
</Link>
<div className='IBMSMSMSSS_Author_Top_AddressWrapper'>
<div className='IBMSMSMSSS_Author_Top_AddressWrapped'>
<p
id='SiteOwnerAddress-1'
className='IBMSMSMSSS_Author_Top_Address'
>
{npub}
</p>
</div>
<div className='IBMSMSMSSS_Author_Top_IconWrapper'>
<div
id='copySiteOwnerAddress'
className='IBMSMSMSSS_Author_Top_IconWrapped'
onClick={handleCopy}
>
<svg
xmlns='http://www.w3.org/2000/svg'
viewBox='0 0 512 512'
width='1em'
height='1em'
fill='currentColor'
className='IBMSMSMSSS_Author_Top_Icon'
>
<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>
</div>
{typeof userState.user?.pubkey === 'string' && (
<ProfileQRButtonWithPopUp
pubkey={userState.user.pubkey}
/>
)}
</div>
</div>
<div className='IBMSMSMSSS_Author_Top_Details'>
<p className='IBMSMSMSSS_Author_Top_Bio'>{bio}</p>
<div
id='OwnerFollowLogin-1'
className='IBMSMSMSSS_Author_Top_NostrLinks'
style={{ display: 'flex' }}
></div>
</div>
</div>
</div>
<div className='IBMSMSMBSS_ProfileEdit'>
<InputField
label='Name'
placeholder=''
name='name'
value={formState.name}
onChange={handleInputChange}
/>
<InputField
label='Display name'
placeholder=''
name='displayName'
value={formState.displayName}
onChange={handleInputChange}
/>
<InputField
label='Bio'
placeholder=''
name='bio'
type='textarea'
value={formState.bio}
onChange={handleInputChange}
/>
<InputField
label='Profile picture URL'
placeholder=''
name='picture'
inputMode='url'
value={formState.picture}
onChange={handleInputChange}
/>
<InputField
label='Banner picture URL'
placeholder=''
name='banner'
inputMode='url'
value={formState.banner}
onChange={handleInputChange}
/>
<InputField
label='Nip-05 address'
placeholder=''
name='nip05'
value={formState.nip05}
onChange={handleInputChange}
/>
<InputField
label='Lightning Address (lud16)'
placeholder=''
name='lud16'
value={formState.lud16}
onChange={handleInputChange}
/>
</div>
<div
className='IBMSMSMBSS_ProfileActions'
style={{
padding: 'unset',
border: 'unset',
justifyContent: 'end'
}}
>
<button
className='btn btnMain'
type='button'
disabled={isPublishing}
onClick={handlePublish}
>
Publish Changes
</button>
</div>
</div>
</div>
</div>
</>
)
}