forked from Kieran/zap.stream
Merge pull request 'feat: show top zappers' (#3) from verbiricha/stream:top-zappers into main
Reviewed-on: Kieran/stream#3
This commit is contained in:
commit
1444749fbb
@ -12,7 +12,6 @@
|
||||
font-weight: 600;
|
||||
font-size: 24px;
|
||||
line-height: 30px;
|
||||
padding: 0px 0px 16px;
|
||||
}
|
||||
|
||||
.live-chat>.messages {
|
||||
@ -75,3 +74,46 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.top-zappers h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-family: Outfit;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.top-zappers-container {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
padding-top: 12px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid var(--border, #171717);
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
.top-zapper {
|
||||
display: flex;
|
||||
padding: 4px 8px 4px 4px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border-radius: 49px;
|
||||
border: 1px solid var(--border, #171717);
|
||||
}
|
||||
|
||||
.top-zapper .top-zapper-amount {
|
||||
font-size: 15px;
|
||||
font-family: Outfit;
|
||||
font-weight: 700;
|
||||
line-height: 22px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.top-zapper .top-zapper-name {
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.top-zapper-icon {
|
||||
color: #FFCB44;
|
||||
}
|
||||
|
@ -4,9 +4,16 @@ import {
|
||||
NostrLink,
|
||||
TaggedRawEvent,
|
||||
EventPublisher,
|
||||
ParsedZap,
|
||||
parseZap,
|
||||
} from "@snort/system";
|
||||
import { useState, type KeyboardEvent, type ChangeEvent } from "react";
|
||||
import {
|
||||
useState,
|
||||
useMemo,
|
||||
useEffect,
|
||||
type KeyboardEvent,
|
||||
type ChangeEvent,
|
||||
} from "react";
|
||||
|
||||
import useEmoji from "hooks/emoji";
|
||||
import { System } from "index";
|
||||
@ -26,6 +33,46 @@ export interface LiveChatOptions {
|
||||
showHeader?: boolean;
|
||||
}
|
||||
|
||||
function totalZapped(pubkey: string, zaps: ParsedZap[]) {
|
||||
return zaps
|
||||
.filter((z) => (z.anonZap ? pubkey === "anon" : z.sender === pubkey))
|
||||
.reduce((acc, z) => acc + z.amount, 0);
|
||||
}
|
||||
|
||||
function TopZappers({ zaps }: { zaps: ParsedZap[] }) {
|
||||
const zappers = zaps
|
||||
.map((z) => (z.anonZap ? "anon" : z.sender))
|
||||
.map((p) => p as string);
|
||||
|
||||
const sortedZappers = useMemo(() => {
|
||||
const sorted = [...new Set([...zappers])];
|
||||
sorted.sort((a, b) => totalZapped(b, zaps) - totalZapped(a, zaps));
|
||||
return sorted;
|
||||
}, [zaps, zappers]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h3>Top zappers</h3>
|
||||
<div className="top-zappers-container">
|
||||
{sortedZappers.map((pk, idx) => {
|
||||
const total = totalZapped(pk, zaps);
|
||||
return (
|
||||
<div className="top-zapper" key={pk}>
|
||||
{pk === "anon" ? (
|
||||
<p className="top-zapper-name">Anon</p>
|
||||
) : (
|
||||
<Profile pubkey={pk} options={{ showName: false }} />
|
||||
)}
|
||||
<Icon name="zap" className="top-zapper-icon" />
|
||||
<p className="top-zapper-amount">{formatSats(total)}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function LiveChat({
|
||||
link,
|
||||
options,
|
||||
@ -35,11 +82,21 @@ export function LiveChat({
|
||||
}) {
|
||||
const messages = useLiveChatFeed(link);
|
||||
const login = useLogin();
|
||||
const events = messages.data ?? [];
|
||||
const zaps = events
|
||||
.filter((ev) => ev.kind === EventKind.ZapReceipt)
|
||||
.map((ev) => parseZap(ev, System.ProfileLoader.Cache))
|
||||
.filter((z) => z && z.valid);
|
||||
return (
|
||||
<div className="live-chat">
|
||||
{(options?.showHeader ?? true) && (
|
||||
<div className="header">Stream Chat</div>
|
||||
)}
|
||||
{zaps.length > 0 && (
|
||||
<div className="top-zappers">
|
||||
<TopZappers zaps={zaps} />
|
||||
</div>
|
||||
)}
|
||||
<div className="messages">
|
||||
{[...(messages.data ?? [])]
|
||||
.sort((a, b) => b.created_at - a.created_at)
|
||||
@ -82,6 +139,18 @@ function ChatZap({ ev }: { ev: TaggedRawEvent }) {
|
||||
const parsed = parseZap(ev, System.ProfileLoader.Cache);
|
||||
useUserProfile(System, parsed.anonZap ? undefined : parsed.sender);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
!parsed.valid &&
|
||||
parsed.errors.includes("zap service pubkey doesn't match") &&
|
||||
parsed.sender
|
||||
) {
|
||||
System.ProfileLoader.TrackMetadata(parsed.sender);
|
||||
return () =>
|
||||
System.ProfileLoader.UntrackMetadata(parsed.sender as string);
|
||||
}
|
||||
}, [parsed]);
|
||||
|
||||
if (!parsed.valid) {
|
||||
return null;
|
||||
}
|
||||
@ -93,7 +162,7 @@ function ChatZap({ ev }: { ev: TaggedRawEvent }) {
|
||||
pubkey={parsed.anonZap ? "" : parsed.sender ?? ""}
|
||||
options={{
|
||||
showAvatar: !parsed.anonZap,
|
||||
overrideName: parsed.anonZap ? "Anonymous" : undefined,
|
||||
overrideName: parsed.anonZap ? "Anon" : undefined,
|
||||
}}
|
||||
/>
|
||||
zapped
|
||||
|
@ -33,9 +33,7 @@ export function Profile({
|
||||
|
||||
return (
|
||||
<div className="profile">
|
||||
{(options?.showAvatar ?? true) && (
|
||||
<img alt={profile?.name || pubkey} src={profile?.picture ?? ""} />
|
||||
)}
|
||||
{(options?.showAvatar ?? true) && <img src={profile?.picture ?? ""} />}
|
||||
{(options?.showName ?? true) &&
|
||||
(options?.overrideName ?? getName(pubkey, profile))}
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user