From ffa4a192f6f6dd69af48bee03d258e8b2b34d066 Mon Sep 17 00:00:00 2001 From: Martti Malmi Date: Fri, 12 Jan 2024 17:02:36 +0200 Subject: [PATCH] split ProfilePage --- packages/app/src/Components/ErrorBoundary.tsx | 4 +- .../app/src/Pages/Profile/AvatarSection.tsx | 118 +++++++++ .../app/src/Pages/Profile/MusicStatus.tsx | 29 +++ .../app/src/Pages/Profile/ProfileDetails.tsx | 113 +++++++++ .../app/src/Pages/Profile/ProfilePage.tsx | 231 ++---------------- 5 files changed, 279 insertions(+), 216 deletions(-) create mode 100644 packages/app/src/Pages/Profile/AvatarSection.tsx create mode 100644 packages/app/src/Pages/Profile/MusicStatus.tsx create mode 100644 packages/app/src/Pages/Profile/ProfileDetails.tsx diff --git a/packages/app/src/Components/ErrorBoundary.tsx b/packages/app/src/Components/ErrorBoundary.tsx index 08b54e19..a2239a0e 100644 --- a/packages/app/src/Components/ErrorBoundary.tsx +++ b/packages/app/src/Components/ErrorBoundary.tsx @@ -5,6 +5,7 @@ import { trackEvent } from "@/Utils"; interface ErrorBoundaryState { hasError: boolean; errorMessage?: string; + stack?: string; } interface ErrorBoundaryProps { @@ -18,7 +19,7 @@ export default class ErrorBoundary extends React.Component

Something went wrong.

Error: {this.state.errorMessage}

+
{this.state.stack}
); } diff --git a/packages/app/src/Pages/Profile/AvatarSection.tsx b/packages/app/src/Pages/Profile/AvatarSection.tsx new file mode 100644 index 00000000..ea6c7411 --- /dev/null +++ b/packages/app/src/Pages/Profile/AvatarSection.tsx @@ -0,0 +1,118 @@ +import { LNURL } from "@snort/shared"; +import { CachedMetadata, encodeTLVEntries, NostrPrefix, TLVEntryType } from "@snort/system"; +import React, { useMemo, useState } from "react"; +import { FormattedMessage } from "react-intl"; +import { Link, useNavigate } from "react-router-dom"; + +import IconButton from "@/Components/Button/IconButton"; +import Copy from "@/Components/Copy/Copy"; +import Modal from "@/Components/Modal/Modal"; +import QrCode from "@/Components/QrCode"; +import { SpotlightMediaModal } from "@/Components/Spotlight/SpotlightMedia"; +import Avatar from "@/Components/User/Avatar"; +import FollowButton from "@/Components/User/FollowButton"; +import ProfileImage from "@/Components/User/ProfileImage"; +import ZapModal from "@/Components/ZapModal/ZapModal"; +import { hexToBech32 } from "@/Utils"; + +const AvatarSection = ({ + user, + id, + loginPubKey, + readonly, + lnurl, +}: { + user?: CachedMetadata; + id?: string; + loginPubKey?: string; + lnurl?: LNURL; + readonly?: boolean; +}) => { + const [showProfileQr, setShowProfileQr] = useState(false); + const [modalImage, setModalImage] = useState(""); + const [showLnQr, setShowLnQr] = useState(false); + const profileId = useMemo(() => hexToBech32(CONFIG.profileLinkPrefix, id), [id]); + const navigate = useNavigate(); + const isMe = loginPubKey === id; + + const renderButtons = () => { + if (!id) return null; + + return ( + <> + setShowProfileQr(true)} icon={{ name: "qr", size: 16 }} /> + {showProfileQr && ( + setShowProfileQr(false)}> + +
+ + +
+
+ )} + {isMe ? ( + <> + + + + + + + + ) : ( + <> + {lnurl && setShowLnQr(true)} icon={{ name: "zap", size: 16 }} />} + {loginPubKey && !readonly && ( + + navigate( + `/messages/${encodeTLVEntries("chat4" as NostrPrefix, { + type: TLVEntryType.Author, + length: 32, + value: id, + })}`, + ) + } + icon={{ name: "envelope", size: 16 }} + /> + )} + + )} + + ); + }; + + return ( +
+ setModalImage(user?.picture || "")} className="pointer" /> +
+ {renderButtons()} + {!isMe && id && } +
+ {modalImage && setModalImage("")} media={[modalImage]} idx={0} />} + setShowLnQr(false)} + /> +
+ ); +}; + +export default AvatarSection; diff --git a/packages/app/src/Pages/Profile/MusicStatus.tsx b/packages/app/src/Pages/Profile/MusicStatus.tsx new file mode 100644 index 00000000..13bf5ede --- /dev/null +++ b/packages/app/src/Pages/Profile/MusicStatus.tsx @@ -0,0 +1,29 @@ +import React from "react"; + +import { ProxyImg } from "@/Components/ProxyImg"; +import { useStatusFeed } from "@/Feed/StatusFeed"; +import { findTag, unwrap } from "@/Utils"; + +export const MusicStatus = ({ id }: { id: string }) => { + const status = useStatusFeed(id, true); + + if (!status.music) return null; + + const link = findTag(status.music, "r"); + const cover = findTag(status.music, "cover"); + + const content = ( +
+ {cover && } + 🎵 {unwrap(status.music).content} +
+ ); + + return link ? ( + + {content} + + ) : ( + content + ); +}; diff --git a/packages/app/src/Pages/Profile/ProfileDetails.tsx b/packages/app/src/Pages/Profile/ProfileDetails.tsx new file mode 100644 index 00000000..73d86b90 --- /dev/null +++ b/packages/app/src/Pages/Profile/ProfileDetails.tsx @@ -0,0 +1,113 @@ +import { LNURL } from "@snort/shared"; +import { CachedMetadata } from "@snort/system"; +import React, { useState } from "react"; + +import Icon from "@/Components/Icons/Icon"; +import Text from "@/Components/Text/Text"; +import BadgeList from "@/Components/User/BadgeList"; +import DisplayName from "@/Components/User/DisplayName"; +import FollowedBy from "@/Components/User/FollowedBy"; +import FollowsYou from "@/Components/User/FollowsYou"; +import Nip05 from "@/Components/User/Nip05"; +import { UserWebsiteLink } from "@/Components/User/UserWebsiteLink"; +import ZapModal from "@/Components/ZapModal/ZapModal"; +import useProfileBadges from "@/Feed/BadgesFeed"; +import useFollowsFeed from "@/Feed/FollowsFeed"; +import useLogin from "@/Hooks/useLogin"; +import { MusicStatus } from "@/Pages/Profile/MusicStatus"; + +const ProfileDetails = ({ + user, + loginPubKey, + id, + aboutText, + lnurl, +}: { + user?: CachedMetadata; + loginPubKey?: string; + showLnQr: boolean; + id?: string; + aboutText: string; + lnurl?: LNURL; +}) => { + const follows = useFollowsFeed(id); + const { showStatus, showBadges } = useLogin(s => ({ + showStatus: s.appData.item.preferences.showStatus ?? false, + showBadges: s.appData.item.preferences.showBadges ?? false, + })); + const [showLnQr, setShowLnQr] = useState(false); + const badges = useProfileBadges(showBadges ? id : undefined); + + if (!user) { + return null; + } + + const username = () => ( + <> +
+

+ + +

+ {user?.nip05 && } +
+ {showBadges && } + {showStatus && } +
{links()}
+ + ); + + const links = () => ( + <> + + {lnurl && ( +
setShowLnQr(true)}> + + {lnurl.name} +
+ )} + setShowLnQr(false)} + /> + + ); + + const bio = () => + aboutText.length > 0 && ( +
+ +
+ ); + + return ( +
+ {username()} + {bio()} + {user?.pubkey && loginPubKey && } +
+ ); +}; + +export default ProfileDetails; diff --git a/packages/app/src/Pages/Profile/ProfilePage.tsx b/packages/app/src/Pages/Profile/ProfilePage.tsx index 464ff6d3..8398dab7 100644 --- a/packages/app/src/Pages/Profile/ProfilePage.tsx +++ b/packages/app/src/Pages/Profile/ProfilePage.tsx @@ -1,56 +1,31 @@ import "./ProfilePage.css"; import { fetchNip05Pubkey, LNURL } from "@snort/shared"; -import { - CachedMetadata, - encodeTLVEntries, - EventKind, - NostrPrefix, - TLVEntryType, - tryParseNostrLink, -} from "@snort/system"; +import { CachedMetadata, EventKind, NostrPrefix, tryParseNostrLink } from "@snort/system"; import { useUserProfile } from "@snort/system-react"; -import { useEffect, useState } from "react"; -import { FormattedMessage } from "react-intl"; -import { Link, useLocation, useNavigate, useParams } from "react-router-dom"; +import { useEffect, useMemo, useState } from "react"; +import { useLocation, useNavigate, useParams } from "react-router-dom"; -import IconButton from "@/Components/Button/IconButton"; -import Copy from "@/Components/Copy/Copy"; import Note from "@/Components/Event/EventComponent"; import Timeline from "@/Components/Feed/Timeline"; -import Icon from "@/Components/Icons/Icon"; -import Modal from "@/Components/Modal/Modal"; import { ProxyImg } from "@/Components/ProxyImg"; -import QrCode from "@/Components/QrCode"; import { SpotlightMediaModal } from "@/Components/Spotlight/SpotlightMedia"; import { Tab, TabElement } from "@/Components/Tabs/Tabs"; -import Text from "@/Components/Text/Text"; -import Avatar from "@/Components/User/Avatar"; -import BadgeList from "@/Components/User/BadgeList"; import BlockList from "@/Components/User/BlockList"; -import DisplayName from "@/Components/User/DisplayName"; -import FollowButton from "@/Components/User/FollowButton"; -import FollowedBy from "@/Components/User/FollowedBy"; import FollowsList from "@/Components/User/FollowListBase"; -import FollowsYou from "@/Components/User/FollowsYou"; import MutedList from "@/Components/User/MutedList"; -import Nip05 from "@/Components/User/Nip05"; -import ProfileImage from "@/Components/User/ProfileImage"; -import { UserWebsiteLink } from "@/Components/User/UserWebsiteLink"; -import ZapModal from "@/Components/ZapModal/ZapModal"; -import useProfileBadges from "@/Feed/BadgesFeed"; import useFollowsFeed from "@/Feed/FollowsFeed"; -import { useStatusFeed } from "@/Feed/StatusFeed"; import useHorizontalScroll from "@/Hooks/useHorizontalScroll"; import { useMuteList, usePinList } from "@/Hooks/useLists"; import useLogin from "@/Hooks/useLogin"; import useModeration from "@/Hooks/useModeration"; +import AvatarSection from "@/Pages/Profile/AvatarSection"; +import ProfileDetails from "@/Pages/Profile/ProfileDetails"; import ProfileTab from "@/Pages/Profile/ProfileTab"; import { BookMarksTab, FollowersTab, FollowsTab, RelaysTab, ZapsProfileTab } from "@/Pages/Profile/ProfileTabs"; import { ProfileTabType } from "@/Pages/Profile/ProfileTabType"; -import { findTag, hexToBech32, parseId, unwrap } from "@/Utils"; +import { parseId, unwrap } from "@/Utils"; import { EmailRegex } from "@/Utils/Const"; -import { ZapTarget } from "@/Utils/Zapper"; interface ProfilePageProps { id?: string; @@ -65,31 +40,27 @@ export default function ProfilePage({ id: propId, state }: ProfilePageProps) { const [id, setId] = useState(profileState?.pubkey); const [relays, setRelays] = useState>(); const user = useUserProfile(profileState ? undefined : id) || profileState; - const login = useLogin(); - const loginPubKey = login.publicKey; + const { loginPubKey, readonly } = useLogin(s => ({ + loginPubKey: s.publicKey, + readonly: s.readonly, + })); const isMe = loginPubKey === id; - const [showLnQr, setShowLnQr] = useState(false); - const [showProfileQr, setShowProfileQr] = useState(false); const [modalImage, setModalImage] = useState(""); const aboutText = user?.about || ""; - const lnurl = (() => { + const lnurl = useMemo(() => { try { return new LNURL(user?.lud16 || user?.lud06 || ""); } catch { // ignored } - })(); - const showBadges = login.appData.item.preferences.showBadges ?? false; - const showStatus = login.appData.item.preferences.showStatus ?? true; + }, [user]); // feeds const { blocked } = useModeration(); const pinned = usePinList(id); const muted = useMuteList(id); - const badges = useProfileBadges(showBadges ? id : undefined); const follows = useFollowsFeed(id); - const status = useStatusFeed(showStatus ? id : undefined, true); // tabs const [tab, setTab] = useState(ProfileTab.Notes); @@ -130,98 +101,6 @@ export default function ProfilePage({ id: propId, state }: ProfilePageProps) { setTab(ProfileTab.Notes); }, [id, propId, params]); - function musicStatus() { - if (!status.music) return; - - const link = findTag(status.music, "r"); - const cover = findTag(status.music, "cover"); - const inner = () => { - return ( -
- {cover && } - 🎵 {unwrap(status.music).content} -
- ); - }; - if (link) { - return ( - - {inner()} - - ); - } - return inner(); - } - - function username() { - return ( - <> -
-

- - -

- {user?.nip05 && } -
- {showBadges && } - {showStatus && <>{musicStatus()}} -
{links()}
- - ); - } - - function links() { - return ( - <> - - {lnurl && ( -
setShowLnQr(true)}> - - {lnurl.name} -
- )} - - setShowLnQr(false)} - /> - - ); - } - - function bio() { - if (!id) return null; - - return ( - aboutText.length > 0 && ( -
- -
- ) - ); - } - function tabContent() { if (!id) return null; @@ -236,7 +115,7 @@ export default function ProfilePage({ id: propId, state }: ProfilePageProps) { ); })} @@ -284,84 +163,6 @@ export default function ProfilePage({ id: propId, state }: ProfilePageProps) { } } - function avatar() { - return ( -
- setModalImage(user?.picture || "")} className="pointer" /> -
- {renderIcons()} - {!isMe && id && } -
-
- ); - } - - function renderIcons() { - if (!id) return; - - const profileId = hexToBech32(CONFIG.profileLinkPrefix, id); - - return ( - <> - setShowProfileQr(true)} icon={{ name: "qr", size: 16 }} /> - {showProfileQr && ( - setShowProfileQr(false)}> - -
- - -
-
- )} - {isMe ? ( - <> - - - - - - - - ) : ( - <> - {lnurl && setShowLnQr(true)} icon={{ name: "zap", size: 16 }} />} - {loginPubKey && !login.readonly && ( - <> - - navigate( - `/messages/${encodeTLVEntries("chat4" as NostrPrefix, { - type: TLVEntryType.Author, - length: 32, - value: id, - })}`, - ) - } - icon={{ name: "envelope", size: 16 }} - /> - - )} - - )} - - ); - } - - function userDetails() { - if (!id) return; - return ( -
- {username()} - {bio()} - {user?.pubkey && loginPubKey && } -
- ); - } - function renderTab(v: Tab) { return ; } @@ -377,13 +178,13 @@ export default function ProfilePage({ id: propId, state }: ProfilePageProps) { className="banner pointer" src={user.banner} size={bannerWidth} - onClick={() => setModalImage(user.banner || "")} + onClick={() => setModalImage(user?.banner || "")} missingImageElement={<>} /> )}
- {avatar()} - {userDetails()} + +