import { FALLBACK_PROFILE_IMAGE } from 'constants.ts'
import { Event, Filter, kinds, nip19, UnsignedEvent } from 'nostr-tools'
import { QRCodeSVG } from 'qrcode.react'
import { useEffect, useMemo, useState } from 'react'
import { Link } from 'react-router-dom'
import { toast } from 'react-toastify'
import {
useAppSelector,
useBodyScrollDisable,
useDidMount,
useNDKContext
} from '../hooks'
import { appRoutes, getProfilePageRoute } from '../routes'
import '../styles/author.css'
import '../styles/innerPage.css'
import '../styles/socialPosts.css'
import { UserRelaysType } from '../types'
import {
copyTextToClipboard,
hexToNpub,
log,
LogType,
now,
npubToHex,
truncate
} from '../utils'
import { LoadingSpinner } from './LoadingSpinner'
import { ZapPopUp } from './Zap'
import placeholder from '../assets/img/DEGMods Placeholder Img.png'
import {
NDKEvent,
NDKSubscriptionCacheUsage,
NDKUser
} from '@nostr-dev-kit/ndk'
import { useProfile } from 'hooks/useProfile'
import { createPortal } from 'react-dom'
type Props = {
pubkey: string
}
export const ProfileSection = ({ pubkey }: Props) => {
return (
)
}
type ProfileProps = {
pubkey: string
}
export const Profile = ({ pubkey }: ProfileProps) => {
const profile = useProfile(pubkey, {
cacheUsage: NDKSubscriptionCacheUsage.PARALLEL
})
const displayName =
profile?.displayName || profile?.name || '[name not set up]'
const about = profile?.bio || profile?.about || '[bio not set up]'
const image = profile?.image || FALLBACK_PROFILE_IMAGE
const nip05 = profile?.nip05
const lud16 = profile?.lud16
const npub = hexToNpub(pubkey)
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!'
)
}
})
}
// Try to encode
let profileRoute = appRoutes.home
let nprofile: string | undefined
try {
const hexPubkey = npubToHex(pubkey)
nprofile = hexPubkey
? nip19.nprofileEncode({
pubkey: hexPubkey
})
: undefined
profileRoute = nprofile ? getProfilePageRoute(nprofile) : appRoutes.home
} catch (error) {
// Silently ignore and redirect to home
log(true, LogType.Error, 'Failed to encode profile.', error)
}
return (
{displayName}
{/* Nip05 can sometimes be an empty object '{}' which causes the error */}
{typeof nip05 === 'string' && nip05 !== '' && (
{nip05}
)}
{typeof nprofile !== 'undefined' && (
)}
{typeof lud16 !== 'undefined' && lud16 !== '' && (
)}
)
}
interface Post {
name: string
link: string
content: string
imageUrl?: string
}
const posts: Post[] = [
{
name: 'User name',
link: `feed-note.html`,
content: `user text, this is a long string of temporary text that would be replaced with the user post from their short posts`
},
{
name: 'User name',
link: 'feed-note.html',
content: `user text, this is a long string of temporary text that would be replaced with the user post from their short posts`
},
{
name: 'User name',
link: `feed-note.html`,
content: `user text, this is a long string of temporary text that would be replaced with the user post from their short posts`,
imageUrl: placeholder
}
]
type QRButtonWithPopUpProps = {
nprofile: string
}
export const ProfileQRButtonWithPopUp = ({
nprofile
}: QRButtonWithPopUpProps) => {
const [isOpen, setIsOpen] = useState(false)
useBodyScrollDisable(isOpen)
return (
<>
{isOpen && (
setIsOpen(false)}
/>
)}
>
)
}
interface ProfileQRPopupProps {
nprofile: string
handleClose: () => void
}
const ProfileQRPopup = ({ nprofile, handleClose }: ProfileQRPopupProps) => {
const onQrCodeClicked = async () => {
const href = `https://njump.me/${nprofile}`
const a = document.createElement('a')
a.href = href
a.target = '_blank' // Open in a new tab
a.rel = 'noopener noreferrer' // Recommended for security reasons
a.click()
}
return createPortal(
,
document.body
)
}
type ZapButtonWithPopUpProps = {
pubkey: string
}
const ZapButtonWithPopUp = ({ pubkey }: ZapButtonWithPopUpProps) => {
const [isOpen, setIsOpen] = useState(false)
useBodyScrollDisable(isOpen)
return (
<>
{isOpen && (
setIsOpen(false)}
/>
)}
>
)
}
type FollowButtonProps = {
pubkey: string
}
const FollowButton = ({ pubkey }: FollowButtonProps) => {
const { ndk, fetchEventFromUserRelays, publish } = useNDKContext()
const [isFollowing, setIsFollowing] = useState(false)
const [isLoading, setIsLoading] = useState(false)
const [loadingSpinnerDesc, setLoadingSpinnerDesc] = useState('')
const userState = useAppSelector((state) => state.user)
useDidMount(async () => {
if (userState.auth && userState.user?.pubkey) {
const userHexKey = userState.user.pubkey as string
const { isFollowing: isAlreadyFollowing } = await checkIfFollowing(
userHexKey,
pubkey
)
setIsFollowing(isAlreadyFollowing)
}
})
// Hide follow if own profile
if (
userState.auth &&
userState.user?.pubkey &&
userState.user?.pubkey === pubkey
) {
return null
}
const getUserPubKey = async (): Promise => {
if (userState.auth && userState.user?.pubkey) {
return userState.user.pubkey as string
} else {
try {
return (await window.nostr?.getPublicKey()) as string
} catch (error) {
log(true, LogType.Error, `Could not get pubkey`, error)
return null
}
}
}
const checkIfFollowing = async (
userHexKey: string,
pubkey: string
): Promise<{
isFollowing: boolean
tags: string[][]
}> => {
const filter: Filter = {
kinds: [kinds.Contacts],
authors: [userHexKey]
}
const contactListEvent = await fetchEventFromUserRelays(
filter,
userHexKey,
UserRelaysType.Both
)
if (!contactListEvent)
return {
isFollowing: false,
tags: []
}
return {
isFollowing: contactListEvent.tags.some(
(t) => t[0] === 'p' && t[1] === pubkey
),
tags: contactListEvent.tags
}
}
const signAndPublishEvent = async (
unsignedEvent: UnsignedEvent
): Promise => {
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) return false
const ndkEvent = new NDKEvent(ndk, signedEvent)
const publishedOnRelays = await publish(ndkEvent)
if (publishedOnRelays.length === 0) {
toast.error('Failed to publish event on any relay')
return false
}
toast.success(
`Event published successfully to the following relays\n\n${publishedOnRelays.join(
'\n'
)}`
)
return true
}
const handleFollow = async () => {
setIsLoading(true)
setLoadingSpinnerDesc('Processing follow request')
const userHexKey = await getUserPubKey()
if (!userHexKey) {
setIsLoading(false)
toast.error('Could not get pubkey')
return
}
const { isFollowing: isAlreadyFollowing, tags } = await checkIfFollowing(
userHexKey,
pubkey
)
if (isAlreadyFollowing) {
toast.info('Already following!')
setIsFollowing(true)
setIsLoading(false)
return
}
const unsignedEvent: UnsignedEvent = {
content: '',
created_at: now(),
kind: kinds.Contacts,
pubkey: userHexKey,
tags: [...tags, ['p', pubkey]]
}
setLoadingSpinnerDesc('Signing and publishing follow event')
const success = await signAndPublishEvent(unsignedEvent)
setIsFollowing(success)
setIsLoading(false)
}
const handleUnFollow = async () => {
setIsLoading(true)
setLoadingSpinnerDesc('Processing unfollow request')
const userHexKey = await getUserPubKey()
if (!userHexKey) {
setIsLoading(false)
toast.error('Could not get pubkey')
return
}
const filter: Filter = {
kinds: [kinds.Contacts],
authors: [userHexKey]
}
const contactListEvent = await fetchEventFromUserRelays(
filter,
userHexKey,
UserRelaysType.Both
)
if (
!contactListEvent ||
!contactListEvent.tags.some((t) => t[0] === 'p' && t[1] === pubkey)
) {
// could not found target pubkey in user's follow list
// so, just update the status and return
setIsFollowing(false)
setIsLoading(false)
return
}
const unsignedEvent: UnsignedEvent = {
content: '',
created_at: now(),
kind: kinds.Contacts,
pubkey: userHexKey,
tags: contactListEvent.tags.filter(
(t) => !(t[0] === 'p' && t[1] === pubkey)
)
}
setLoadingSpinnerDesc('Signing and publishing unfollow event')
const success = await signAndPublishEvent(unsignedEvent)
setIsFollowing(!success)
setIsLoading(false)
}
return (
<>
{isFollowing ? 'Un-Follow' : 'Follow'}
{isLoading && }
>
)
}
type ProfileLinkProps = {
pubkey?: string
nip05?: string
}
export const ProfileLink = ({ pubkey, nip05 }: ProfileLinkProps) => {
const { ndk } = useNDKContext()
console.log(`[debug]`, pubkey, nip05)
const [hexPubkey, setHexPubkey] = useState()
const profile = useProfile(hexPubkey, {
cacheUsage: NDKSubscriptionCacheUsage.PARALLEL
})
useEffect(() => {
if (pubkey) {
setHexPubkey(pubkey)
} else if (nip05) {
NDKUser.fromNip05(nip05, ndk).then((user) => {
if (user?.pubkey) {
setHexPubkey(user.pubkey)
}
})
}
}, [pubkey, nip05, ndk])
const profileRoute = useMemo(() => {
let nprofile: string | undefined
try {
if (hexPubkey) {
nprofile = hexPubkey
? nip19.nprofileEncode({
pubkey: hexPubkey
})
: undefined
}
} catch (error) {
// Silently ignore
log(true, LogType.Error, 'Failed to encode profile.', error)
}
return nprofile ? getProfilePageRoute(nprofile) : appRoutes.home
}, [hexPubkey])
const displayName = useMemo(() => {
const npub = hexPubkey ? hexToNpub(hexPubkey) : ''
const displayName = profile?.displayName || profile?.name || truncate(npub)
return displayName
}, [hexPubkey, profile?.displayName, profile?.name])
return @{displayName}
}