snort/packages/app/src/Pages/ProfilePage.tsx

385 lines
11 KiB
TypeScript
Raw Normal View History

2022-12-27 23:46:13 +00:00
import "./ProfilePage.css";
2023-02-27 13:17:13 +00:00
import { useEffect, useState } from "react";
2023-02-08 21:10:26 +00:00
import { useIntl, FormattedMessage } from "react-intl";
2023-01-09 11:00:23 +00:00
import { useNavigate, useParams } from "react-router-dom";
2023-03-28 14:34:01 +00:00
import { encodeTLV, EventKind, HexKey, NostrPrefix } from "@snort/nostr";
2022-12-29 22:23:41 +00:00
2023-03-28 14:34:01 +00:00
import { parseNostrLink, getReactions, unwrap } from "Util";
2023-02-03 21:38:14 +00:00
import { formatShort } from "Number";
import Note from "Element/Note";
import Bookmarks from "Element/Bookmarks";
2023-02-10 19:23:52 +00:00
import RelaysMetadata from "Element/RelaysMetadata";
2023-02-06 21:42:47 +00:00
import { Tab, TabElement } from "Element/Tabs";
2023-03-02 17:47:02 +00:00
import Icon from "Icons/Icon";
2023-02-10 11:12:11 +00:00
import useMutedFeed from "Feed/MuteList";
2023-02-10 19:23:52 +00:00
import useRelaysFeed from "Feed/RelaysFeed";
import usePinnedFeed from "Feed/PinnedFeed";
import useBookmarkFeed from "Feed/BookmarkFeed";
2023-02-10 11:12:11 +00:00
import useFollowersFeed from "Feed/FollowersFeed";
import useFollowsFeed from "Feed/FollowsFeed";
2023-03-09 10:13:10 +00:00
import useProfileBadges from "Feed/BadgesFeed";
2023-03-03 14:30:31 +00:00
import { useUserProfile } from "Hooks/useUserProfile";
2023-02-10 11:12:11 +00:00
import useModeration from "Hooks/useModeration";
2023-02-03 21:38:14 +00:00
import useZapsFeed from "Feed/ZapsFeed";
2023-02-10 11:12:11 +00:00
import { default as ZapElement } from "Element/Zap";
2023-01-20 11:11:50 +00:00
import FollowButton from "Element/FollowButton";
2023-04-05 10:58:26 +00:00
import { parseId, hexToBech32 } from "Util";
2023-01-20 11:11:50 +00:00
import Avatar from "Element/Avatar";
import Timeline from "Element/Timeline";
import Text from "Element/Text";
2023-02-07 13:32:32 +00:00
import SendSats from "Element/SendSats";
2023-01-20 11:11:50 +00:00
import Nip05 from "Element/Nip05";
import Copy from "Element/Copy";
2023-02-06 21:42:47 +00:00
import ProfileImage from "Element/ProfileImage";
import BlockList from "Element/BlockList";
2023-01-26 11:34:18 +00:00
import MutedList from "Element/MutedList";
2023-02-10 11:12:11 +00:00
import FollowsList from "Element/FollowListBase";
2023-01-29 22:11:04 +00:00
import IconButton from "Element/IconButton";
import FollowsYou from "Element/FollowsYou";
2023-01-29 22:11:04 +00:00
import QrCode from "Element/QrCode";
import Modal from "Element/Modal";
2023-03-09 10:13:10 +00:00
import BadgeList from "Element/BadgeList";
import { ProxyImg } from "Element/ProxyImg";
2023-02-07 13:32:32 +00:00
import useHorizontalScroll from "Hooks/useHorizontalScroll";
2023-02-27 13:17:13 +00:00
import { EmailRegex } from "Const";
2023-04-05 10:58:26 +00:00
import { getNip05PubKey } from "Pages/Login";
import { LNURL } from "LNURL";
2023-04-14 11:33:19 +00:00
import useLogin from "Hooks/useLogin";
2023-04-05 10:58:26 +00:00
import messages from "./messages";
2023-02-08 21:10:26 +00:00
const NOTES = 0;
const REACTIONS = 1;
const FOLLOWERS = 2;
const FOLLOWS = 3;
const ZAPS = 4;
const MUTED = 5;
const BLOCKED = 6;
2023-02-10 19:23:52 +00:00
const RELAYS = 7;
const BOOKMARKS = 8;
2022-12-18 14:51:32 +00:00
2023-03-28 14:34:01 +00:00
function ZapsProfileTab({ id }: { id: HexKey }) {
const zaps = useZapsFeed(id);
const zapsTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
return (
<div className="main-content">
<div className="zaps-total">
<FormattedMessage {...messages.Sats} values={{ n: formatShort(zapsTotal) }} />
</div>
{zaps.map(z => (
<ZapElement showZapped={false} zap={z} />
))}
</div>
);
}
function FollowersTab({ id }: { id: HexKey }) {
const followers = useFollowersFeed(id);
return <FollowsList pubkeys={followers} showAbout={true} />;
}
function FollowsTab({ id }: { id: HexKey }) {
const follows = useFollowsFeed(id);
return <FollowsList pubkeys={follows} showAbout={true} />;
}
function RelaysTab({ id }: { id: HexKey }) {
const relays = useRelaysFeed(id);
return <RelaysMetadata relays={relays} />;
}
function BookMarksTab({ id }: { id: HexKey }) {
const bookmarks = useBookmarkFeed(id);
2023-04-08 09:35:34 +00:00
return (
<Bookmarks
pubkey={id}
bookmarks={bookmarks.filter(e => e.kind === EventKind.TextNote)}
related={bookmarks.filter(e => e.kind !== EventKind.TextNote)}
/>
);
2023-03-28 14:34:01 +00:00
}
2022-12-18 14:51:32 +00:00
export default function ProfilePage() {
2023-02-08 21:10:26 +00:00
const { formatMessage } = useIntl();
2023-01-31 16:07:46 +00:00
const params = useParams();
const navigate = useNavigate();
2023-02-27 13:17:13 +00:00
const [id, setId] = useState<string>();
2023-01-31 16:07:46 +00:00
const user = useUserProfile(id);
2023-04-14 11:33:19 +00:00
const loginPubKey = useLogin().publicKey;
2023-01-31 16:07:46 +00:00
const isMe = loginPubKey === id;
const [showLnQr, setShowLnQr] = useState<boolean>(false);
const [showProfileQr, setShowProfileQr] = useState<boolean>(false);
const aboutText = user?.about || "";
const about = Text({
content: aboutText,
tags: [],
creator: "",
2023-04-04 18:25:11 +00:00
disableMedia: true,
});
2023-03-25 22:55:34 +00:00
const npub = !id?.startsWith(NostrPrefix.PublicKey) ? hexToBech32(NostrPrefix.PublicKey, id || undefined) : id;
2023-04-05 10:58:26 +00:00
const lnurl = (() => {
try {
return new LNURL(user?.lud16 || user?.lud06 || "");
} catch {
// ignored
}
})();
const website_url =
2023-02-09 12:26:54 +00:00
user?.website && !user.website.startsWith("http") ? "https://" + user.website : user?.website || "";
2023-02-10 11:12:11 +00:00
// feeds
const { blocked } = useModeration();
2023-03-28 14:34:01 +00:00
const pinned = usePinnedFeed(id);
2023-02-10 11:12:11 +00:00
const muted = useMutedFeed(id);
2023-03-09 10:13:10 +00:00
const badges = useProfileBadges(id);
2023-04-04 18:20:14 +00:00
const follows = useFollowsFeed(id);
2023-02-10 11:12:11 +00:00
// tabs
2023-02-08 21:10:26 +00:00
const ProfileTab = {
Notes: { text: formatMessage(messages.Notes), value: NOTES },
Reactions: { text: formatMessage(messages.Reactions), value: REACTIONS },
2023-03-28 14:34:01 +00:00
Followers: { text: formatMessage(messages.Followers), value: FOLLOWERS },
Follows: { text: formatMessage(messages.Follows), value: FOLLOWS },
Zaps: { text: formatMessage(messages.Zaps), value: ZAPS },
Muted: { text: formatMessage(messages.Muted), value: MUTED },
2023-02-10 11:12:11 +00:00
Blocked: { text: formatMessage(messages.BlockedCount, { n: blocked.length }), value: BLOCKED },
2023-03-28 14:34:01 +00:00
Relays: { text: formatMessage(messages.Relays), value: RELAYS },
Bookmarks: { text: formatMessage(messages.Bookmarks), value: BOOKMARKS },
2023-02-08 21:10:26 +00:00
};
const [tab, setTab] = useState<Tab>(ProfileTab.Notes);
2023-03-28 14:34:01 +00:00
const optionalTabs = [ProfileTab.Zaps, ProfileTab.Relays, ProfileTab.Bookmarks, ProfileTab.Muted].filter(a =>
unwrap(a)
) as Tab[];
2023-02-10 11:12:11 +00:00
const horizontalScroll = useHorizontalScroll();
2023-01-10 10:30:33 +00:00
2023-01-31 16:07:46 +00:00
useEffect(() => {
2023-02-27 13:17:13 +00:00
if (params.id?.match(EmailRegex)) {
getNip05PubKey(params.id).then(a => {
setId(a);
});
} else {
2023-03-25 22:55:34 +00:00
const nav = parseNostrLink(params.id ?? "");
if (nav?.type === NostrPrefix.PublicKey || nav?.type === NostrPrefix.Profile) {
// todo: use relays if any for nprofile
setId(nav.id);
} else {
setId(parseId(params.id ?? ""));
}
2023-02-27 13:17:13 +00:00
}
2023-01-31 16:07:46 +00:00
setTab(ProfileTab.Notes);
}, [params]);
2022-12-28 14:51:33 +00:00
2023-01-31 16:07:46 +00:00
function username() {
return (
<div className="name">
<h2>
{user?.display_name || user?.name || "Nostrich"}
2023-02-24 19:28:29 +00:00
<FollowsYou followsMe={follows.includes(loginPubKey ?? "")} />
2023-01-31 16:07:46 +00:00
</h2>
{user?.nip05 && <Nip05 nip05={user.nip05} pubkey={user.pubkey} />}
2023-03-09 10:13:10 +00:00
<BadgeList badges={badges} />
<Copy text={npub} />
2023-01-31 16:07:46 +00:00
{links()}
</div>
);
2023-01-31 16:07:46 +00:00
}
2023-01-31 16:07:46 +00:00
function links() {
return (
<div className="links">
{user?.website && (
<div className="website f-ellipsis">
2023-03-02 18:39:29 +00:00
<Icon name="link" />
<a href={website_url} target="_blank" rel="noreferrer">
{user.website}
</a>
2023-01-31 16:07:46 +00:00
</div>
)}
2023-01-10 07:09:09 +00:00
2023-02-06 21:42:47 +00:00
{lnurl && (
<div className="lnurl f-ellipsis" onClick={() => setShowLnQr(true)}>
2023-03-02 18:39:29 +00:00
<Icon name="zap" />
2023-04-05 10:58:26 +00:00
{lnurl.name}
2023-02-06 21:42:47 +00:00
</div>
)}
2023-02-07 13:32:32 +00:00
<SendSats
2023-04-05 10:58:26 +00:00
lnurl={lnurl?.lnurl}
2023-02-07 13:32:32 +00:00
show={showLnQr}
onClose={() => setShowLnQr(false)}
author={id}
target={user?.display_name || user?.name}
/>
2023-01-31 16:07:46 +00:00
</div>
);
2023-01-31 16:07:46 +00:00
}
2023-01-01 20:31:09 +00:00
2023-01-31 16:07:46 +00:00
function bio() {
return (
aboutText.length > 0 && (
<div dir="auto" className="details">
{about}
</div>
)
);
2023-01-31 16:07:46 +00:00
}
2022-12-28 23:28:28 +00:00
2023-01-31 16:07:46 +00:00
function tabContent() {
2023-02-27 13:17:13 +00:00
if (!id) return null;
2023-02-08 21:10:26 +00:00
switch (tab.value) {
case NOTES:
return (
<>
<div className="main-content">
2023-03-28 14:34:01 +00:00
{pinned
.filter(a => a.kind === EventKind.TextNote)
.map(n => {
return (
<Note
key={`pinned-${n.id}`}
data={n}
related={getReactions(pinned, n.id)}
options={{ showTime: false, showPinned: true, canUnpin: id === loginPubKey }}
/>
);
})}
</div>
<Timeline
key={id}
subject={{
type: "pubkey",
items: [id],
discriminator: id.slice(0, 12),
}}
postsOnly={false}
method={"TIME_RANGE"}
ignoreModeration={true}
window={60 * 60 * 6}
/>
</>
);
2023-02-08 21:10:26 +00:00
case ZAPS: {
2023-03-28 14:34:01 +00:00
return <ZapsProfileTab id={id} />;
2023-02-04 10:16:08 +00:00
}
2023-02-08 21:10:26 +00:00
case FOLLOWS: {
2023-03-28 14:34:01 +00:00
if (isMe) {
return (
<>
<button onClick={() => navigate("/new/import")} className="mb10">
<FormattedMessage defaultMessage="Find Twitter follows" />
</button>
<FollowsList pubkeys={follows} showFollowAll={!isMe} showAbout={false} />;
</>
);
2023-03-28 14:34:01 +00:00
} else {
return <FollowsTab id={id} />;
}
2023-01-31 16:07:46 +00:00
}
2023-02-08 21:10:26 +00:00
case FOLLOWERS: {
2023-03-28 14:34:01 +00:00
return <FollowersTab id={id} />;
2023-01-31 16:07:46 +00:00
}
2023-02-08 21:10:26 +00:00
case MUTED: {
2023-02-10 11:12:11 +00:00
return <MutedList pubkeys={muted} />;
2023-01-31 16:07:46 +00:00
}
2023-02-08 21:10:26 +00:00
case BLOCKED: {
2023-02-10 11:12:11 +00:00
return <BlockList />;
2023-01-31 16:07:46 +00:00
}
2023-02-10 19:23:52 +00:00
case RELAYS: {
2023-03-28 14:34:01 +00:00
return <RelaysTab id={id} />;
2023-02-10 19:23:52 +00:00
}
case BOOKMARKS: {
2023-03-28 14:34:01 +00:00
return <BookMarksTab id={id} />;
}
2023-01-10 10:30:33 +00:00
}
2023-01-31 16:07:46 +00:00
}
2023-01-10 10:30:33 +00:00
2023-01-31 16:07:46 +00:00
function avatar() {
return (
<div className="avatar-wrapper">
<Avatar user={user} />
</div>
);
2023-01-31 16:07:46 +00:00
}
2023-01-29 22:11:04 +00:00
function renderIcons() {
2023-03-25 22:55:34 +00:00
if (!id) return;
2023-03-28 14:34:01 +00:00
const link = encodeTLV(id, NostrPrefix.Profile);
2023-01-29 22:11:04 +00:00
return (
<div className="icon-actions">
<IconButton onClick={() => setShowProfileQr(true)}>
2023-03-02 17:47:02 +00:00
<Icon name="qr" size={16} />
2023-01-29 22:11:04 +00:00
</IconButton>
{showProfileQr && (
<Modal className="qr-modal" onClose={() => setShowProfileQr(false)}>
2023-03-25 22:55:34 +00:00
<ProfileImage pubkey={id} />
<QrCode data={link} className="m10 align-center" />
<Copy text={link} className="align-center" />
2023-01-29 22:11:04 +00:00
</Modal>
)}
{isMe ? (
<>
<button type="button" onClick={() => navigate("/settings")}>
2023-02-08 21:10:26 +00:00
<FormattedMessage {...messages.Settings} />
2023-01-29 22:11:04 +00:00
</button>
</>
) : (
<>
{lnurl && (
<IconButton onClick={() => setShowLnQr(true)}>
2023-03-02 17:47:02 +00:00
<Icon name="zap" size={16} />
</IconButton>
)}
2023-02-27 13:17:13 +00:00
{loginPubKey && (
2023-01-29 22:11:04 +00:00
<>
2023-01-29 19:44:53 +00:00
<IconButton onClick={() => navigate(`/messages/${hexToBech32(NostrPrefix.PublicKey, id)}`)}>
2023-03-02 17:47:02 +00:00
<Icon name="envelope" size={16} />
2023-01-29 22:11:04 +00:00
</IconButton>
</>
)}
</>
)}
</div>
);
2023-01-29 22:11:04 +00:00
}
2023-01-31 16:07:46 +00:00
function userDetails() {
2023-03-25 22:55:34 +00:00
if (!id) return;
2022-12-18 14:51:32 +00:00
return (
2023-01-31 16:07:46 +00:00
<div className="details-wrapper">
{username()}
<div className="profile-actions">
{renderIcons()}
2023-03-25 22:55:34 +00:00
{!isMe && <FollowButton pubkey={id} />}
2023-01-31 16:07:46 +00:00
</div>
{bio()}
</div>
);
2023-01-31 16:07:46 +00:00
}
2023-02-06 21:42:47 +00:00
function renderTab(v: Tab) {
2023-02-14 10:13:19 +00:00
return <TabElement key={v.value} t={v} tab={tab} setTab={setTab} />;
2023-01-31 16:07:46 +00:00
}
const w = window.document.querySelector(".page")?.clientWidth;
return (
<>
<div className="profile flex">
2023-02-09 12:26:54 +00:00
{user?.banner && <ProxyImg alt="banner" className="banner" src={user.banner} size={w} />}
2023-01-31 16:07:46 +00:00
<div className="profile-wrapper flex">
{avatar()}
{userDetails()}
</div>
</div>
2023-02-10 11:12:11 +00:00
<div className="main-content">
<div className="tabs" ref={horizontalScroll}>
{[ProfileTab.Notes, ProfileTab.Followers, ProfileTab.Follows].map(renderTab)}
{optionalTabs.map(renderTab)}
{isMe && blocked.length > 0 && renderTab(ProfileTab.Blocked)}
</div>
2023-01-31 16:07:46 +00:00
</div>
{tabContent()}
</>
);
}