feat: profile tab top zappers

closes #662
This commit is contained in:
2024-09-13 11:21:50 +01:00
parent 267ebe2ed9
commit f335d48e69
8 changed files with 69 additions and 20 deletions

View File

@ -10,7 +10,7 @@ import Icon from "@/Components/Icons/Icon";
import Modal from "@/Components/Modal/Modal"; import Modal from "@/Components/Modal/Modal";
import TabSelectors, { Tab } from "@/Components/TabSelectors/TabSelectors"; import TabSelectors, { Tab } from "@/Components/TabSelectors/TabSelectors";
import ProfileImage from "@/Components/User/ProfileImage"; import ProfileImage from "@/Components/User/ProfileImage";
import { formatShort } from "@/Utils/Number"; import ZapAmount from "@/Components/zap-amount";
import messages from "../../messages"; import messages from "../../messages";
@ -84,10 +84,7 @@ const ReactionsModal = ({ onClose, event, initialTab = 0 }: ReactionsModalProps)
z => z =>
z.sender && ( z.sender && (
<div key={z.id} className="reactions-item"> <div key={z.id} className="reactions-item">
<div className="zap-reaction-icon"> <ZapAmount n={z.amount} />
<Icon name="zap-solid" size={20} className="text-zap" />
<span className="zap-amount">{formatShort(z.amount)}</span>
</div>
<ProfileImage <ProfileImage
showProfileCard={true} showProfileCard={true}
pubkey={z.anonZap ? "" : z.sender} pubkey={z.anonZap ? "" : z.sender}

View File

@ -16,7 +16,7 @@ import { ProfileLink } from "./ProfileLink";
export interface ProfileImageProps { export interface ProfileImageProps {
pubkey: HexKey; pubkey: HexKey;
subHeader?: JSX.Element; subHeader?: ReactNode;
showUsername?: boolean; showUsername?: boolean;
className?: string; className?: string;
link?: string; link?: string;

View File

@ -15,6 +15,7 @@ export interface ProfilePreviewProps {
linkToProfile?: boolean; linkToProfile?: boolean;
profileCards?: boolean; profileCards?: boolean;
}; };
subHeader?: ReactNode;
profile?: UserMetadata; profile?: UserMetadata;
actions?: ReactNode; actions?: ReactNode;
className?: string; className?: string;
@ -50,7 +51,7 @@ export default memo(function ProfilePreview(props: ProfilePreviewProps) {
pubkey={pubkey} pubkey={pubkey}
profile={props.profile} profile={props.profile}
link={options.linkToProfile ?? true ? undefined : ""} link={options.linkToProfile ?? true ? undefined : ""}
subHeader={options.about ? <div className="about">{user?.about}</div> : undefined} subHeader={options.about ? <div className="about">{user?.about}</div> : props.subHeader}
showProfileCard={options.profileCards} showProfileCard={options.profileCards}
/> />
{props.actions ?? ( {props.actions ?? (

View File

@ -0,0 +1,12 @@
import { formatShort } from "@/Utils/Number";
import Icon from "./Icons/Icon";
export default function ZapAmount({ n }: { n: number }) {
return (
<div className="flex gap-2 items-center text-xl font-bold">
<Icon name="zap-solid" size={20} className="text-zap" />
<span>{formatShort(n)}</span>
</div>
);
}

View File

@ -15,9 +15,8 @@ export default function useZapsFeed(link?: NostrLink) {
const zaps = useMemo(() => { const zaps = useMemo(() => {
if (zapsFeed) { if (zapsFeed) {
const profileZaps = zapsFeed.map(a => parseZap(a)).filter(z => z.valid); const parsedZaps = zapsFeed.map(a => parseZap(a)).filter(z => z.valid);
profileZaps.sort((a, b) => b.amount - a.amount); return parsedZaps.sort((a, b) => b.amount - a.amount);
return profileZaps;
} }
return []; return [];
}, [zapsFeed]); }, [zapsFeed]);

View File

@ -1,33 +1,69 @@
import { EventKind, HexKey, NostrLink, NostrPrefix } from "@snort/system"; import { EventKind, HexKey, NostrLink, NostrPrefix, ParsedZap } from "@snort/system";
import { useMemo } from "react"; import { useMemo } from "react";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { Note } from "@/Components/Event/Note/Note"; import { Note } from "@/Components/Event/Note/Note";
import Zap from "@/Components/Event/Zap";
import Timeline from "@/Components/Feed/Timeline"; import Timeline from "@/Components/Feed/Timeline";
import RelaysMetadata from "@/Components/Relay/RelaysMetadata"; import RelaysMetadata from "@/Components/Relay/RelaysMetadata";
import Bookmarks from "@/Components/User/Bookmarks"; import Bookmarks from "@/Components/User/Bookmarks";
import FollowsList from "@/Components/User/FollowListBase"; import FollowsList from "@/Components/User/FollowListBase";
import ProfilePreview from "@/Components/User/ProfilePreview";
import ZapAmount from "@/Components/zap-amount";
import useFollowersFeed from "@/Feed/FollowersFeed"; import useFollowersFeed from "@/Feed/FollowersFeed";
import useFollowsFeed from "@/Feed/FollowsFeed"; import useFollowsFeed from "@/Feed/FollowsFeed";
import useRelaysFeed from "@/Feed/RelaysFeed"; import useRelaysFeed from "@/Feed/RelaysFeed";
import { TimelineSubject } from "@/Feed/TimelineFeed"; import { TimelineSubject } from "@/Feed/TimelineFeed";
import useZapsFeed from "@/Feed/ZapsFeed"; import useZapsFeed from "@/Feed/ZapsFeed";
import { useBookmarkList, usePinList } from "@/Hooks/useLists"; import { useBookmarkList, usePinList } from "@/Hooks/useLists";
import messages from "@/Pages/messages";
import { formatShort } from "@/Utils/Number";
export function ZapsProfileTab({ id }: { id: HexKey }) { export function ZapsProfileTab({ id }: { id: HexKey }) {
const zaps = useZapsFeed(new NostrLink(NostrPrefix.PublicKey, id)); const zaps = useZapsFeed(new NostrLink(NostrPrefix.PublicKey, id));
const zapsTotal = zaps.reduce((acc, z) => acc + z.amount, 0); const zapsTotal = zaps.reduce((acc, z) => acc + z.amount, 0);
const fromGrouped = zaps.reduce(
(acc, v) => {
if (!v.sender) return acc;
acc[v.sender] ??= [];
acc[v.sender].push(v);
return acc;
},
{} as Record<string, Array<ParsedZap>>,
);
return ( return (
<> <>
<h2 className="p"> <div className="p text-2xl font-medium flex justify-between">
<FormattedMessage {...messages.Sats} values={{ n: formatShort(zapsTotal) }} /> <div>
</h2> <FormattedMessage defaultMessage="Profile Zaps" />
{zaps.map(z => ( </div>
<Zap key={z.id} showZapped={false} zap={z} /> <ZapAmount n={zapsTotal} />
))} </div>
{Object.entries(fromGrouped)
.map(a => ({
pubkey: a[0],
total: a[1].reduce((acc, v) => acc + v.amount, 0),
topZap: a[1].reduce((acc, v) => (v.amount > acc.amount ? v : acc), a[1][0]),
zaps: a[1],
}))
.sort((a, b) => {
return b.total > a.total ? 1 : -1;
})
.map(a => (
<div
className="px-4 py-1 hover:bg-gray-dark cursor:pointer rounded-xl flex items-center justify-between"
key={a.pubkey}>
<ProfilePreview
pubkey={a.pubkey}
subHeader={a.topZap.content ? <div className="about">&quot;{a.topZap.content}&quot;</div> : undefined}
options={{
about: false,
}}
actions={<></>}
/>
<div>
<ZapAmount n={a.total} />
</div>
</div>
))}
</> </>
); );
} }

View File

@ -485,6 +485,9 @@
"CHTbO3": { "CHTbO3": {
"defaultMessage": "Failed to load invoice" "defaultMessage": "Failed to load invoice"
}, },
"CJx5Nd": {
"defaultMessage": "Profile Zaps"
},
"CM+Cfj": { "CM+Cfj": {
"defaultMessage": "Follow List" "defaultMessage": "Follow List"
}, },

View File

@ -160,6 +160,7 @@
"C8FsOr": "Popular Servers", "C8FsOr": "Popular Servers",
"C8HhVE": "Suggested Follows", "C8HhVE": "Suggested Follows",
"CHTbO3": "Failed to load invoice", "CHTbO3": "Failed to load invoice",
"CJx5Nd": "Profile Zaps",
"CM+Cfj": "Follow List", "CM+Cfj": "Follow List",
"CM0k0d": "Prune follow list", "CM0k0d": "Prune follow list",
"CVWeJ6": "Trending People", "CVWeJ6": "Trending People",