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:
Kieran 2023-06-25 21:37:41 +00:00
commit 1444749fbb
3 changed files with 115 additions and 6 deletions

View File

@ -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;
}

View File

@ -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 &nbsp;

View File

@ -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>