feat: add profile menu item to sidebar
This commit is contained in:
@@ -19,6 +19,7 @@ import TooManyRelaysAlertDialog from './components/TooManyRelaysAlertDialog'
|
|||||||
import ExplorePage from './pages/primary/ExplorePage'
|
import ExplorePage from './pages/primary/ExplorePage'
|
||||||
import MePage from './pages/primary/MePage'
|
import MePage from './pages/primary/MePage'
|
||||||
import NotificationListPage from './pages/primary/NotificationListPage'
|
import NotificationListPage from './pages/primary/NotificationListPage'
|
||||||
|
import ProfilePage from './pages/primary/ProfilePage'
|
||||||
import { NotificationProvider } from './providers/NotificationProvider'
|
import { NotificationProvider } from './providers/NotificationProvider'
|
||||||
import { useScreenSize } from './providers/ScreenSizeProvider'
|
import { useScreenSize } from './providers/ScreenSizeProvider'
|
||||||
import { routes } from './routes'
|
import { routes } from './routes'
|
||||||
@@ -49,14 +50,16 @@ const PRIMARY_PAGE_REF_MAP = {
|
|||||||
home: createRef<TPageRef>(),
|
home: createRef<TPageRef>(),
|
||||||
explore: createRef<TPageRef>(),
|
explore: createRef<TPageRef>(),
|
||||||
notifications: createRef<TPageRef>(),
|
notifications: createRef<TPageRef>(),
|
||||||
me: createRef<TPageRef>()
|
me: createRef<TPageRef>(),
|
||||||
|
profile: createRef<TPageRef>()
|
||||||
}
|
}
|
||||||
|
|
||||||
const PRIMARY_PAGE_MAP = {
|
const PRIMARY_PAGE_MAP = {
|
||||||
home: <NoteListPage ref={PRIMARY_PAGE_REF_MAP.home} />,
|
home: <NoteListPage ref={PRIMARY_PAGE_REF_MAP.home} />,
|
||||||
explore: <ExplorePage ref={PRIMARY_PAGE_REF_MAP.explore} />,
|
explore: <ExplorePage ref={PRIMARY_PAGE_REF_MAP.explore} />,
|
||||||
notifications: <NotificationListPage ref={PRIMARY_PAGE_REF_MAP.notifications} />,
|
notifications: <NotificationListPage ref={PRIMARY_PAGE_REF_MAP.notifications} />,
|
||||||
me: <MePage ref={PRIMARY_PAGE_REF_MAP.me} />
|
me: <MePage ref={PRIMARY_PAGE_REF_MAP.me} />,
|
||||||
|
profile: <ProfilePage ref={PRIMARY_PAGE_REF_MAP.profile} />
|
||||||
}
|
}
|
||||||
|
|
||||||
const PrimaryPageContext = createContext<TPrimaryPageContext | undefined>(undefined)
|
const PrimaryPageContext = createContext<TPrimaryPageContext | undefined>(undefined)
|
||||||
|
|||||||
191
src/components/Profile/index.tsx
Normal file
191
src/components/Profile/index.tsx
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
import Collapsible from '@/components/Collapsible'
|
||||||
|
import FollowButton from '@/components/FollowButton'
|
||||||
|
import Nip05 from '@/components/Nip05'
|
||||||
|
import NpubQrCode from '@/components/NpubQrCode'
|
||||||
|
import ProfileAbout from '@/components/ProfileAbout'
|
||||||
|
import ProfileBanner from '@/components/ProfileBanner'
|
||||||
|
import ProfileOptions from '@/components/ProfileOptions'
|
||||||
|
import ProfileZapButton from '@/components/ProfileZapButton'
|
||||||
|
import PubkeyCopy from '@/components/PubkeyCopy'
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
|
import { useFetchFollowings, useFetchProfile } from '@/hooks'
|
||||||
|
import { toMuteList, toProfileEditor } from '@/lib/link'
|
||||||
|
import { generateImageByPubkey } from '@/lib/pubkey'
|
||||||
|
import { SecondaryPageLink, useSecondaryPage } from '@/PageManager'
|
||||||
|
import { useMuteList } from '@/providers/MuteListProvider'
|
||||||
|
import { useNostr } from '@/providers/NostrProvider'
|
||||||
|
import client from '@/services/client.service'
|
||||||
|
import { Link, Zap } from 'lucide-react'
|
||||||
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
import FollowedBy from './FollowedBy'
|
||||||
|
import Followings from './Followings'
|
||||||
|
import ProfileFeed from './ProfileFeed'
|
||||||
|
import Relays from './Relays'
|
||||||
|
|
||||||
|
export default function Profile({ id }: { id?: string }) {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
const { push } = useSecondaryPage()
|
||||||
|
const { profile, isFetching } = useFetchProfile(id)
|
||||||
|
const { pubkey: accountPubkey } = useNostr()
|
||||||
|
const { mutePubkeys } = useMuteList()
|
||||||
|
const { followings } = useFetchFollowings(profile?.pubkey)
|
||||||
|
const isFollowingYou = useMemo(() => {
|
||||||
|
return (
|
||||||
|
!!accountPubkey && accountPubkey !== profile?.pubkey && followings.includes(accountPubkey)
|
||||||
|
)
|
||||||
|
}, [followings, profile, accountPubkey])
|
||||||
|
const defaultImage = useMemo(
|
||||||
|
() => (profile?.pubkey ? generateImageByPubkey(profile?.pubkey) : ''),
|
||||||
|
[profile]
|
||||||
|
)
|
||||||
|
const [topContainerHeight, setTopContainerHeight] = useState(0)
|
||||||
|
const isSelf = accountPubkey === profile?.pubkey
|
||||||
|
const [topContainer, setTopContainer] = useState<HTMLDivElement | null>(null)
|
||||||
|
const topContainerRef = useCallback((node: HTMLDivElement | null) => {
|
||||||
|
if (node) {
|
||||||
|
setTopContainer(node)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!profile?.pubkey) return
|
||||||
|
|
||||||
|
const forceUpdateCache = async () => {
|
||||||
|
await Promise.all([
|
||||||
|
client.forceUpdateRelayListEvent(profile.pubkey),
|
||||||
|
client.fetchProfile(profile.pubkey, true)
|
||||||
|
])
|
||||||
|
}
|
||||||
|
forceUpdateCache()
|
||||||
|
}, [profile?.pubkey])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!topContainer) return
|
||||||
|
|
||||||
|
const checkHeight = () => {
|
||||||
|
setTopContainerHeight(topContainer.scrollHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkHeight()
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(() => {
|
||||||
|
checkHeight()
|
||||||
|
})
|
||||||
|
|
||||||
|
observer.observe(topContainer)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
observer.disconnect()
|
||||||
|
}
|
||||||
|
}, [topContainer])
|
||||||
|
|
||||||
|
if (!profile && isFetching) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<div className="relative bg-cover bg-center mb-2">
|
||||||
|
<Skeleton className="w-full aspect-[3/1] rounded-none" />
|
||||||
|
<Skeleton className="w-24 h-24 absolute bottom-0 left-3 translate-y-1/2 border-4 border-background rounded-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="px-4">
|
||||||
|
<Skeleton className="h-5 w-28 mt-14 mb-1" />
|
||||||
|
<Skeleton className="h-5 w-56 mt-2 my-1 rounded-full" />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!profile) return null
|
||||||
|
|
||||||
|
const { banner, username, about, avatar, pubkey, website, lightningAddress } = profile
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div ref={topContainerRef}>
|
||||||
|
<div className="relative bg-cover bg-center mb-2">
|
||||||
|
<ProfileBanner banner={banner} pubkey={pubkey} className="w-full aspect-[3/1]" />
|
||||||
|
<Avatar className="w-24 h-24 absolute left-3 bottom-0 translate-y-1/2 border-4 border-background">
|
||||||
|
<AvatarImage src={avatar} className="object-cover object-center" />
|
||||||
|
<AvatarFallback>
|
||||||
|
<img src={defaultImage} />
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
</div>
|
||||||
|
<div className="px-4">
|
||||||
|
<div className="flex justify-end h-8 gap-2 items-center">
|
||||||
|
<ProfileOptions pubkey={pubkey} />
|
||||||
|
{isSelf ? (
|
||||||
|
<Button
|
||||||
|
className="w-20 min-w-20 rounded-full"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => push(toProfileEditor())}
|
||||||
|
>
|
||||||
|
{t('Edit')}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{!!lightningAddress && <ProfileZapButton pubkey={pubkey} />}
|
||||||
|
<FollowButton pubkey={pubkey} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="pt-2">
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<div className="text-xl font-semibold truncate select-text">{username}</div>
|
||||||
|
{isFollowingYou && (
|
||||||
|
<div className="text-muted-foreground rounded-full bg-muted text-xs h-fit px-2 shrink-0">
|
||||||
|
{t('Follows you')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Nip05 pubkey={pubkey} />
|
||||||
|
{lightningAddress && (
|
||||||
|
<div className="text-sm text-yellow-400 flex gap-1 items-center select-text">
|
||||||
|
<Zap className="size-4 shrink-0" />
|
||||||
|
<div className="flex-1 max-w-fit w-0 truncate">{lightningAddress}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex gap-1 mt-1">
|
||||||
|
<PubkeyCopy pubkey={pubkey} />
|
||||||
|
<NpubQrCode pubkey={pubkey} />
|
||||||
|
</div>
|
||||||
|
<Collapsible>
|
||||||
|
<ProfileAbout
|
||||||
|
about={about}
|
||||||
|
className="text-wrap break-words whitespace-pre-wrap mt-2 select-text"
|
||||||
|
/>
|
||||||
|
</Collapsible>
|
||||||
|
{website && (
|
||||||
|
<div className="flex gap-1 items-center text-primary mt-2 truncate select-text">
|
||||||
|
<Link size={14} className="shrink-0" />
|
||||||
|
<a
|
||||||
|
href={website}
|
||||||
|
target="_blank"
|
||||||
|
className="hover:underline truncate flex-1 max-w-fit w-0"
|
||||||
|
>
|
||||||
|
{website}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="flex justify-between items-center mt-2 text-sm">
|
||||||
|
<div className="flex gap-4 items-center">
|
||||||
|
<Followings pubkey={pubkey} />
|
||||||
|
<Relays pubkey={pubkey} />
|
||||||
|
{isSelf && (
|
||||||
|
<SecondaryPageLink to={toMuteList()} className="flex gap-1 hover:underline w-fit">
|
||||||
|
{mutePubkeys.length}
|
||||||
|
<div className="text-muted-foreground">{t('Muted')}</div>
|
||||||
|
</SecondaryPageLink>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!isSelf && <FollowedBy pubkey={pubkey} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ProfileFeed pubkey={pubkey} topSpace={topContainerHeight + 100} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
19
src/components/Sidebar/ProfileButton.tsx
Normal file
19
src/components/Sidebar/ProfileButton.tsx
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { usePrimaryPage } from '@/PageManager'
|
||||||
|
import { useNostr } from '@/providers/NostrProvider'
|
||||||
|
import { UserRound } from 'lucide-react'
|
||||||
|
import SidebarItem from './SidebarItem'
|
||||||
|
|
||||||
|
export default function ProfileButton() {
|
||||||
|
const { navigate, current } = usePrimaryPage()
|
||||||
|
const { checkLogin } = useNostr()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SidebarItem
|
||||||
|
title="Profile"
|
||||||
|
onClick={() => checkLogin(() => navigate('profile'))}
|
||||||
|
active={current === 'profile'}
|
||||||
|
>
|
||||||
|
<UserRound strokeWidth={3} />
|
||||||
|
</SidebarItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import RelaysButton from './ExploreButton'
|
|||||||
import HomeButton from './HomeButton'
|
import HomeButton from './HomeButton'
|
||||||
import NotificationsButton from './NotificationButton'
|
import NotificationsButton from './NotificationButton'
|
||||||
import PostButton from './PostButton'
|
import PostButton from './PostButton'
|
||||||
|
import ProfileButton from './ProfileButton'
|
||||||
import SearchButton from './SearchButton'
|
import SearchButton from './SearchButton'
|
||||||
import SettingsButton from './SettingsButton'
|
import SettingsButton from './SettingsButton'
|
||||||
|
|
||||||
@@ -24,6 +25,7 @@ export default function PrimaryPageSidebar() {
|
|||||||
<RelaysButton />
|
<RelaysButton />
|
||||||
<NotificationsButton />
|
<NotificationsButton />
|
||||||
<SearchButton />
|
<SearchButton />
|
||||||
|
<ProfileButton />
|
||||||
<SettingsButton />
|
<SettingsButton />
|
||||||
<PostButton />
|
<PostButton />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
34
src/pages/primary/ProfilePage/index.tsx
Normal file
34
src/pages/primary/ProfilePage/index.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import Profile from '@/components/Profile'
|
||||||
|
import PrimaryPageLayout from '@/layouts/PrimaryPageLayout'
|
||||||
|
import { useNostr } from '@/providers/NostrProvider'
|
||||||
|
import { UserRound } from 'lucide-react'
|
||||||
|
import { forwardRef } from 'react'
|
||||||
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
|
const ProfilePage = forwardRef((_, ref) => {
|
||||||
|
const { pubkey } = useNostr()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PrimaryPageLayout
|
||||||
|
pageName="profile"
|
||||||
|
titlebar={<ProfilePageTitlebar />}
|
||||||
|
displayScrollToTopButton
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<Profile id={pubkey ?? undefined} />
|
||||||
|
</PrimaryPageLayout>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
ProfilePage.displayName = 'ProfilePage'
|
||||||
|
export default ProfilePage
|
||||||
|
|
||||||
|
function ProfilePageTitlebar() {
|
||||||
|
const { t } = useTranslation()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2 items-center h-full pl-3">
|
||||||
|
<UserRound />
|
||||||
|
<div className="text-lg font-semibold">{t('Profile')}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,193 +1,14 @@
|
|||||||
import Collapsible from '@/components/Collapsible'
|
import Profile from '@/components/Profile'
|
||||||
import FollowButton from '@/components/FollowButton'
|
import { useFetchProfile } from '@/hooks'
|
||||||
import Nip05 from '@/components/Nip05'
|
|
||||||
import NpubQrCode from '@/components/NpubQrCode'
|
|
||||||
import ProfileAbout from '@/components/ProfileAbout'
|
|
||||||
import ProfileBanner from '@/components/ProfileBanner'
|
|
||||||
import ProfileOptions from '@/components/ProfileOptions'
|
|
||||||
import ProfileZapButton from '@/components/ProfileZapButton'
|
|
||||||
import PubkeyCopy from '@/components/PubkeyCopy'
|
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
|
||||||
import { Button } from '@/components/ui/button'
|
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
|
||||||
import { useFetchFollowings, useFetchProfile } from '@/hooks'
|
|
||||||
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
|
||||||
import { toMuteList, toProfileEditor } from '@/lib/link'
|
import { forwardRef } from 'react'
|
||||||
import { generateImageByPubkey } from '@/lib/pubkey'
|
|
||||||
import { SecondaryPageLink, useSecondaryPage } from '@/PageManager'
|
|
||||||
import { useMuteList } from '@/providers/MuteListProvider'
|
|
||||||
import { useNostr } from '@/providers/NostrProvider'
|
|
||||||
import client from '@/services/client.service'
|
|
||||||
import { Link, Zap } from 'lucide-react'
|
|
||||||
import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import NotFoundPage from '../NotFoundPage'
|
|
||||||
import FollowedBy from './FollowedBy'
|
|
||||||
import Followings from './Followings'
|
|
||||||
import ProfileFeed from './ProfileFeed'
|
|
||||||
import Relays from './Relays'
|
|
||||||
|
|
||||||
const ProfilePage = forwardRef(({ id, index }: { id?: string; index?: number }, ref) => {
|
const ProfilePage = forwardRef(({ id, index }: { id?: string; index?: number }, ref) => {
|
||||||
const { t } = useTranslation()
|
const { profile } = useFetchProfile(id)
|
||||||
const { push } = useSecondaryPage()
|
|
||||||
const { profile, isFetching } = useFetchProfile(id)
|
|
||||||
const { pubkey: accountPubkey } = useNostr()
|
|
||||||
const { mutePubkeys } = useMuteList()
|
|
||||||
const { followings } = useFetchFollowings(profile?.pubkey)
|
|
||||||
const isFollowingYou = useMemo(() => {
|
|
||||||
return (
|
return (
|
||||||
!!accountPubkey && accountPubkey !== profile?.pubkey && followings.includes(accountPubkey)
|
<SecondaryPageLayout index={index} title={profile?.username} displayScrollToTopButton ref={ref}>
|
||||||
)
|
<Profile id={id} />
|
||||||
}, [followings, profile, accountPubkey])
|
|
||||||
const defaultImage = useMemo(
|
|
||||||
() => (profile?.pubkey ? generateImageByPubkey(profile?.pubkey) : ''),
|
|
||||||
[profile]
|
|
||||||
)
|
|
||||||
const [topContainerHeight, setTopContainerHeight] = useState(0)
|
|
||||||
const isSelf = accountPubkey === profile?.pubkey
|
|
||||||
const [topContainer, setTopContainer] = useState<HTMLDivElement | null>(null)
|
|
||||||
const topContainerRef = useCallback((node: HTMLDivElement | null) => {
|
|
||||||
if (node) {
|
|
||||||
setTopContainer(node)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!profile?.pubkey) return
|
|
||||||
|
|
||||||
const forceUpdateCache = async () => {
|
|
||||||
await Promise.all([
|
|
||||||
client.forceUpdateRelayListEvent(profile.pubkey),
|
|
||||||
client.fetchProfile(profile.pubkey, true)
|
|
||||||
])
|
|
||||||
}
|
|
||||||
forceUpdateCache()
|
|
||||||
}, [profile?.pubkey])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!topContainer) return
|
|
||||||
|
|
||||||
const checkHeight = () => {
|
|
||||||
setTopContainerHeight(topContainer.scrollHeight)
|
|
||||||
}
|
|
||||||
|
|
||||||
checkHeight()
|
|
||||||
|
|
||||||
const observer = new ResizeObserver(() => {
|
|
||||||
checkHeight()
|
|
||||||
})
|
|
||||||
|
|
||||||
observer.observe(topContainer)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
observer.disconnect()
|
|
||||||
}
|
|
||||||
}, [topContainer])
|
|
||||||
|
|
||||||
if (!profile && isFetching) {
|
|
||||||
return (
|
|
||||||
<SecondaryPageLayout index={index} ref={ref}>
|
|
||||||
<div>
|
|
||||||
<div className="relative bg-cover bg-center mb-2">
|
|
||||||
<Skeleton className="w-full aspect-[3/1] rounded-none" />
|
|
||||||
<Skeleton className="w-24 h-24 absolute bottom-0 left-3 translate-y-1/2 border-4 border-background rounded-full" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="px-4">
|
|
||||||
<Skeleton className="h-5 w-28 mt-14 mb-1" />
|
|
||||||
<Skeleton className="h-5 w-56 mt-2 my-1 rounded-full" />
|
|
||||||
</div>
|
|
||||||
</SecondaryPageLayout>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (!profile) return <NotFoundPage />
|
|
||||||
|
|
||||||
const { banner, username, about, avatar, pubkey, website, lightningAddress } = profile
|
|
||||||
return (
|
|
||||||
<SecondaryPageLayout index={index} title={username} displayScrollToTopButton ref={ref}>
|
|
||||||
<div ref={topContainerRef}>
|
|
||||||
<div className="relative bg-cover bg-center mb-2">
|
|
||||||
<ProfileBanner banner={banner} pubkey={pubkey} className="w-full aspect-[3/1]" />
|
|
||||||
<Avatar className="w-24 h-24 absolute left-3 bottom-0 translate-y-1/2 border-4 border-background">
|
|
||||||
<AvatarImage src={avatar} className="object-cover object-center" />
|
|
||||||
<AvatarFallback>
|
|
||||||
<img src={defaultImage} />
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
</div>
|
|
||||||
<div className="px-4">
|
|
||||||
<div className="flex justify-end h-8 gap-2 items-center">
|
|
||||||
<ProfileOptions pubkey={pubkey} />
|
|
||||||
{isSelf ? (
|
|
||||||
<Button
|
|
||||||
className="w-20 min-w-20 rounded-full"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => push(toProfileEditor())}
|
|
||||||
>
|
|
||||||
{t('Edit')}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{!!lightningAddress && <ProfileZapButton pubkey={pubkey} />}
|
|
||||||
<FollowButton pubkey={pubkey} />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="pt-2">
|
|
||||||
<div className="flex gap-2 items-center">
|
|
||||||
<div className="text-xl font-semibold truncate select-text">{username}</div>
|
|
||||||
{isFollowingYou && (
|
|
||||||
<div className="text-muted-foreground rounded-full bg-muted text-xs h-fit px-2 shrink-0">
|
|
||||||
{t('Follows you')}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Nip05 pubkey={pubkey} />
|
|
||||||
{lightningAddress && (
|
|
||||||
<div className="text-sm text-yellow-400 flex gap-1 items-center select-text">
|
|
||||||
<Zap className="size-4 shrink-0" />
|
|
||||||
<div className="flex-1 max-w-fit w-0 truncate">{lightningAddress}</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex gap-1 mt-1">
|
|
||||||
<PubkeyCopy pubkey={pubkey} />
|
|
||||||
<NpubQrCode pubkey={pubkey} />
|
|
||||||
</div>
|
|
||||||
<Collapsible>
|
|
||||||
<ProfileAbout
|
|
||||||
about={about}
|
|
||||||
className="text-wrap break-words whitespace-pre-wrap mt-2 select-text"
|
|
||||||
/>
|
|
||||||
</Collapsible>
|
|
||||||
{website && (
|
|
||||||
<div className="flex gap-1 items-center text-primary mt-2 truncate select-text">
|
|
||||||
<Link size={14} className="shrink-0" />
|
|
||||||
<a
|
|
||||||
href={website}
|
|
||||||
target="_blank"
|
|
||||||
className="hover:underline truncate flex-1 max-w-fit w-0"
|
|
||||||
>
|
|
||||||
{website}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="flex justify-between items-center mt-2 text-sm">
|
|
||||||
<div className="flex gap-4 items-center">
|
|
||||||
<Followings pubkey={pubkey} />
|
|
||||||
<Relays pubkey={pubkey} />
|
|
||||||
{isSelf && (
|
|
||||||
<SecondaryPageLink to={toMuteList()} className="flex gap-1 hover:underline w-fit">
|
|
||||||
{mutePubkeys.length}
|
|
||||||
<div className="text-muted-foreground">{t('Muted')}</div>
|
|
||||||
</SecondaryPageLink>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{!isSelf && <FollowedBy pubkey={pubkey} />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ProfileFeed pubkey={pubkey} topSpace={topContainerHeight + 100} />
|
|
||||||
</SecondaryPageLayout>
|
</SecondaryPageLayout>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user