diff --git a/src/PageManager.tsx b/src/PageManager.tsx index c7a0c23..abb96c4 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -19,6 +19,7 @@ import TooManyRelaysAlertDialog from './components/TooManyRelaysAlertDialog' import ExplorePage from './pages/primary/ExplorePage' import MePage from './pages/primary/MePage' import NotificationListPage from './pages/primary/NotificationListPage' +import ProfilePage from './pages/primary/ProfilePage' import { NotificationProvider } from './providers/NotificationProvider' import { useScreenSize } from './providers/ScreenSizeProvider' import { routes } from './routes' @@ -49,14 +50,16 @@ const PRIMARY_PAGE_REF_MAP = { home: createRef(), explore: createRef(), notifications: createRef(), - me: createRef() + me: createRef(), + profile: createRef() } const PRIMARY_PAGE_MAP = { home: , explore: , notifications: , - me: + me: , + profile: } const PrimaryPageContext = createContext(undefined) diff --git a/src/pages/secondary/ProfilePage/FollowedBy.tsx b/src/components/Profile/FollowedBy.tsx similarity index 100% rename from src/pages/secondary/ProfilePage/FollowedBy.tsx rename to src/components/Profile/FollowedBy.tsx diff --git a/src/pages/secondary/ProfilePage/Followings.tsx b/src/components/Profile/Followings.tsx similarity index 100% rename from src/pages/secondary/ProfilePage/Followings.tsx rename to src/components/Profile/Followings.tsx diff --git a/src/pages/secondary/ProfilePage/ProfileFeed.tsx b/src/components/Profile/ProfileFeed.tsx similarity index 100% rename from src/pages/secondary/ProfilePage/ProfileFeed.tsx rename to src/components/Profile/ProfileFeed.tsx diff --git a/src/pages/secondary/ProfilePage/Relays.tsx b/src/components/Profile/Relays.tsx similarity index 100% rename from src/pages/secondary/ProfilePage/Relays.tsx rename to src/components/Profile/Relays.tsx diff --git a/src/components/Profile/index.tsx b/src/components/Profile/index.tsx new file mode 100644 index 0000000..e2b2020 --- /dev/null +++ b/src/components/Profile/index.tsx @@ -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(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 ( + <> +
+
+ + +
+
+
+ + +
+ + ) + } + if (!profile) return null + + const { banner, username, about, avatar, pubkey, website, lightningAddress } = profile + return ( + <> +
+
+ + + + + + + +
+
+
+ + {isSelf ? ( + + ) : ( + <> + {!!lightningAddress && } + + + )} +
+
+
+
{username}
+ {isFollowingYou && ( +
+ {t('Follows you')} +
+ )} +
+ + {lightningAddress && ( +
+ +
{lightningAddress}
+
+ )} +
+ + +
+ + + + {website && ( + + )} +
+
+ + + {isSelf && ( + + {mutePubkeys.length} +
{t('Muted')}
+
+ )} +
+ {!isSelf && } +
+
+
+
+ + + ) +} diff --git a/src/components/Sidebar/ProfileButton.tsx b/src/components/Sidebar/ProfileButton.tsx new file mode 100644 index 0000000..d707015 --- /dev/null +++ b/src/components/Sidebar/ProfileButton.tsx @@ -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 ( + checkLogin(() => navigate('profile'))} + active={current === 'profile'} + > + + + ) +} diff --git a/src/components/Sidebar/index.tsx b/src/components/Sidebar/index.tsx index 2718af0..df67b3d 100644 --- a/src/components/Sidebar/index.tsx +++ b/src/components/Sidebar/index.tsx @@ -6,6 +6,7 @@ import RelaysButton from './ExploreButton' import HomeButton from './HomeButton' import NotificationsButton from './NotificationButton' import PostButton from './PostButton' +import ProfileButton from './ProfileButton' import SearchButton from './SearchButton' import SettingsButton from './SettingsButton' @@ -24,6 +25,7 @@ export default function PrimaryPageSidebar() { + diff --git a/src/pages/primary/ProfilePage/index.tsx b/src/pages/primary/ProfilePage/index.tsx new file mode 100644 index 0000000..7b4646c --- /dev/null +++ b/src/pages/primary/ProfilePage/index.tsx @@ -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 ( + } + displayScrollToTopButton + ref={ref} + > + + + ) +}) +ProfilePage.displayName = 'ProfilePage' +export default ProfilePage + +function ProfilePageTitlebar() { + const { t } = useTranslation() + + return ( +
+ +
{t('Profile')}
+
+ ) +} diff --git a/src/pages/secondary/ProfilePage/index.tsx b/src/pages/secondary/ProfilePage/index.tsx index 3cd3bd0..98d800f 100644 --- a/src/pages/secondary/ProfilePage/index.tsx +++ b/src/pages/secondary/ProfilePage/index.tsx @@ -1,193 +1,14 @@ -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 Profile from '@/components/Profile' +import { useFetchProfile } from '@/hooks' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' -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 { 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' +import { forwardRef } from 'react' const ProfilePage = forwardRef(({ id, index }: { id?: string; index?: number }, ref) => { - 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(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 ( - -
-
- - -
-
-
- - -
-
- ) - } - if (!profile) return + const { profile } = useFetchProfile(id) - const { banner, username, about, avatar, pubkey, website, lightningAddress } = profile return ( - -
-
- - - - - - - -
-
-
- - {isSelf ? ( - - ) : ( - <> - {!!lightningAddress && } - - - )} -
-
-
-
{username}
- {isFollowingYou && ( -
- {t('Follows you')} -
- )} -
- - {lightningAddress && ( -
- -
{lightningAddress}
-
- )} -
- - -
- - - - {website && ( - - )} -
-
- - - {isSelf && ( - - {mutePubkeys.length} -
{t('Muted')}
-
- )} -
- {!isSelf && } -
-
-
-
- + + ) })