fix: short links

This commit is contained in:
Kieran 2023-11-15 10:58:31 +00:00
parent ee2cd4bb4d
commit a6777d0441
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
3 changed files with 205 additions and 207 deletions

View File

@ -4,9 +4,10 @@ import { unixNow } from "@snort/shared";
import { useMemo } from "react";
import { LIVE_STREAM_CHAT, WEEK } from "const";
export function useLiveChatFeed(link: NostrLink, eZaps?: Array<string>, limit = 100) {
const since = useMemo(() => unixNow() - WEEK, [link.id]);
export function useLiveChatFeed(link?: NostrLink, eZaps?: Array<string>, limit = 100) {
const since = useMemo(() => unixNow() - WEEK, [link?.id]);
const sub = useMemo(() => {
if (!link) return null;
const rb = new RequestBuilder(`live:${link.id}:${link.author}`);
rb.withOptions({
leaveOpen: true,
@ -14,7 +15,7 @@ export function useLiveChatFeed(link: NostrLink, eZaps?: Array<string>, limit =
const aTag = `${link.kind}:${link.author}:${link.id}`;
rb.withFilter().kinds([LIVE_STREAM_CHAT]).tag("a", [aTag]).limit(limit);
return rb;
}, [link.id, since, eZaps]);
}, [link?.id, since, eZaps]);
const feed = useRequestBuilder(NoteCollection, sub);
@ -23,8 +24,8 @@ export function useLiveChatFeed(link: NostrLink, eZaps?: Array<string>, limit =
}, [feed.data]);
const reactions = useReactions(
`live:${link.id}:${link.author}:reactions`,
messages.map(a => NostrLink.fromEvent(a)).concat(link),
`live:${link?.id}:${link?.author}:reactions`,
messages.map(a => NostrLink.fromEvent(a)).concat(link ? [link] : []),
undefined,
true
);

View File

@ -1,4 +1,4 @@
import { fetchNip05Pubkey, hexToBech32 } from "@snort/shared";
import { fetchNip05Pubkey } from "@snort/shared";
import { NostrLink, tryParseNostrLink, NostrPrefix } from "@snort/system";
import { useState, useEffect } from "react";
import { useParams } from "react-router-dom";
@ -16,11 +16,7 @@ export function useStreamLink() {
const [handle, domain] = (params.id.includes("@") ? params.id : `${params.id}@zap.stream`).split("@");
fetchNip05Pubkey(handle, domain).then(d => {
if (d) {
setLink({
id: d,
type: NostrPrefix.PublicKey,
encode: () => hexToBech32(NostrPrefix.PublicKey, d),
} as NostrLink);
setLink(new NostrLink(NostrPrefix.PublicKey, d));
}
});
}

View File

@ -17,219 +17,220 @@ import { formatSats } from "number";
import { findTag, getEventFromLocationState } from "utils";
export function StreamSummaryPage() {
const location = useLocation();
const evPreload = getEventFromLocationState(location.state);
const link = useStreamLink();
if (link) {
return <StreamSummary link={link} preload={evPreload} />;
}
const location = useLocation();
const evPreload = getEventFromLocationState(location.state);
const link = useStreamLink();
if (link) {
return <StreamSummary link={link} preload={evPreload} />;
}
}
interface StatSlot {
time: number;
zaps: number;
messages: number;
reactions: number;
time: number;
zaps: number;
messages: number;
reactions: number;
}
export function StreamSummary({ link, preload }: { link: NostrLink; preload?: NostrEvent }) {
const ev = useCurrentStreamFeed(link, true, preload);
const data = useLiveChatFeed(link, undefined, 5_000);
const reactions = useEventReactions(link, data.reactions);
const ev = useCurrentStreamFeed(link, true, preload);
const thisLink = ev ? NostrLink.fromEvent(ev) : undefined;
const data = useLiveChatFeed(thisLink, undefined, 5_000);
const reactions = useEventReactions(thisLink ?? link, data.reactions);
const chatSummary = useMemo(() => {
return Object.entries(
data.messages.reduce((acc, v) => {
acc[v.pubkey] ??= [];
acc[v.pubkey].push(v);
return acc;
}, {} as Record<string, Array<NostrEvent>>)
)
.map(([k, v]) => ({
pubkey: k,
messages: v,
}))
.sort((a, b) => (a.messages.length > b.messages.length ? -1 : 1));
}, [data.messages]);
const chatSummary = useMemo(() => {
return Object.entries(
data.messages.reduce((acc, v) => {
acc[v.pubkey] ??= [];
acc[v.pubkey].push(v);
return acc;
}, {} as Record<string, Array<NostrEvent>>)
)
.map(([k, v]) => ({
pubkey: k,
messages: v,
}))
.sort((a, b) => (a.messages.length > b.messages.length ? -1 : 1));
}, [data.messages]);
const zapsSummary = useMemo(() => {
return Object.entries(
reactions.zaps.reduce((acc, v) => {
if (!v.sender) return acc;
acc[v.sender] ??= [];
acc[v.sender].push(v);
return acc;
}, {} as Record<string, Array<ParsedZap>>)
)
.map(([k, v]) => ({
pubkey: k,
zaps: v,
total: v.reduce((acc, vv) => acc + vv.amount, 0),
}))
.sort((a, b) => (a.total > b.total ? -1 : 1));
}, [reactions.zaps]);
const zapsSummary = useMemo(() => {
return Object.entries(
reactions.zaps.reduce((acc, v) => {
if (!v.sender) return acc;
acc[v.sender] ??= [];
acc[v.sender].push(v);
return acc;
}, {} as Record<string, Array<ParsedZap>>)
)
.map(([k, v]) => ({
pubkey: k,
zaps: v,
total: v.reduce((acc, vv) => acc + vv.amount, 0),
}))
.sort((a, b) => (a.total > b.total ? -1 : 1));
}, [reactions.zaps]);
const title = findTag(ev, "title");
const summary = findTag(ev, "summary");
const status = findTag(ev, "status");
const starts = findTag(ev, "starts");
const title = findTag(ev, "title");
const summary = findTag(ev, "summary");
const status = findTag(ev, "status");
const starts = findTag(ev, "starts");
const Day = 60 * 60 * 24;
const startTime = starts ? Number(starts) : ev?.created_at ?? unixNow();
const endTime = status === StreamState.Live ? unixNow() : ev?.created_at ?? unixNow();
const Day = 60 * 60 * 24;
const startTime = starts ? Number(starts) : ev?.created_at ?? unixNow();
const endTime = status === StreamState.Live ? unixNow() : ev?.created_at ?? unixNow();
const streamLength = endTime - startTime;
const windowSize = streamLength > Day ? Day : 60 * 10;
const streamLength = endTime - startTime;
const windowSize = streamLength > Day ? Day : 60 * 10;
const stats = useMemo(() => {
let min = unixNow();
let max = 0;
const ret = [...data.messages, ...data.reactions]
.sort((a, b) => (a.created_at > b.created_at ? -1 : 1))
.reduce((acc, v) => {
const time = Math.floor(v.created_at - (v.created_at % windowSize));
if (time < min) {
min = time;
const stats = useMemo(() => {
let min = unixNow();
let max = 0;
const ret = [...data.messages, ...data.reactions]
.sort((a, b) => (a.created_at > b.created_at ? -1 : 1))
.reduce((acc, v) => {
const time = Math.floor(v.created_at - (v.created_at % windowSize));
if (time < min) {
min = time;
}
if (time > max) {
max = time;
}
const key = time.toString();
acc[key] ??= {
time,
zaps: 0,
messages: 0,
reactions: 0,
};
if (v.kind === LIVE_STREAM_CHAT) {
acc[key].messages++;
} else if (v.kind === EventKind.ZapReceipt) {
acc[key].zaps++;
} else if (v.kind === EventKind.Reaction) {
acc[key].reactions++;
} else {
console.debug("Uncounted stat", v);
}
return acc;
}, {} as Record<string, StatSlot>);
// fill empty time slots
for (let x = min; x < max; x += windowSize) {
ret[x.toString()] ??= {
time: x,
zaps: 0,
messages: 0,
reactions: 0,
};
}
if (time > max) {
max = time;
}
const key = time.toString();
acc[key] ??= {
time,
zaps: 0,
messages: 0,
reactions: 0,
};
return ret;
}, [data]);
if (v.kind === LIVE_STREAM_CHAT) {
acc[key].messages++;
} else if (v.kind === EventKind.ZapReceipt) {
acc[key].zaps++;
} else if (v.kind === EventKind.Reaction) {
acc[key].reactions++;
} else {
console.debug("Uncounted stat", v);
}
return acc;
}, {} as Record<string, StatSlot>);
return (
<div className="stream-summary">
<h1>{title}</h1>
<p>{summary}</p>
<div className="flex g8">
<StatePill state={status as StreamState} />
{streamLength > 0 && (
<FormattedMessage
defaultMessage="Stream Duration {duration} mins"
values={{
duration: <FormattedNumber value={streamLength / 60} maximumFractionDigits={2} />,
}}
/>
)}
</div>
<h2>
<FormattedMessage defaultMessage="Summary" />
</h2>
<ResponsiveContainer height={200}>
<BarChart data={Object.values(stats)} margin={{ left: 0, right: 0 }} style={{ userSelect: "none" }}>
<XAxis tick={false} />
<YAxis />
<Bar dataKey="messages" fill="green" stackId="" />
<Bar dataKey="zaps" fill="yellow" stackId="" />
<Bar dataKey="reactions" fill="red" stackId="" />
<Tooltip
cursor={{ fill: "rgba(255,255,255,0.2)" }}
content={({ active, payload }) => {
if (active && payload && payload.length) {
const data = payload[0].payload as StatSlot;
return (
<div className="plain-paper flex f-col g8">
<div>
<FormattedDate value={data.time * 1000} timeStyle="short" dateStyle="short" />
</div>
<div className="flex f-space">
<div>
<FormattedMessage defaultMessage="Messages" />
</div>
<div>{data.messages}</div>
</div>
<div className="flex f-space">
<div>
<FormattedMessage defaultMessage="Reactions" />
</div>
<div>{data.reactions}</div>
</div>
<div className="flex f-space">
<div>
<FormattedMessage defaultMessage="Zaps" />
</div>
<div>{data.zaps}</div>
</div>
</div>
);
}
return null;
}}
/>
</BarChart>
</ResponsiveContainer>
// fill empty time slots
for (let x = min; x < max; x += windowSize) {
ret[x.toString()] ??= {
time: x,
zaps: 0,
messages: 0,
reactions: 0,
};
}
return ret;
}, [data]);
return (
<div className="stream-summary">
<h1>{title}</h1>
<p>{summary}</p>
<div className="flex g8">
<StatePill state={status as StreamState} />
{streamLength > 0 && (
<FormattedMessage
defaultMessage="Stream Duration {duration} mins"
values={{
duration: <FormattedNumber value={streamLength / 60} maximumFractionDigits={2} />,
}}
/>
)}
</div>
<h2>
<FormattedMessage defaultMessage="Summary" />
</h2>
<ResponsiveContainer height={200}>
<BarChart data={Object.values(stats)} margin={{ left: 0, right: 0 }} style={{ userSelect: "none" }}>
<XAxis tick={false} />
<YAxis />
<Bar dataKey="messages" fill="green" stackId="" />
<Bar dataKey="zaps" fill="yellow" stackId="" />
<Bar dataKey="reactions" fill="red" stackId="" />
<Tooltip
cursor={{ fill: "rgba(255,255,255,0.2)" }}
content={({ active, payload }) => {
if (active && payload && payload.length) {
const data = payload[0].payload as StatSlot;
return (
<div className="plain-paper flex f-col g8">
<div>
<FormattedDate value={data.time * 1000} timeStyle="short" dateStyle="short" />
<div className="flex g8">
<div className="plain-paper f-1">
<h3>
<FormattedMessage defaultMessage="Top Chatters" />
</h3>
<div className="flex f-col g8">
{chatSummary.slice(0, 5).map(a => (
<div className="flex f-space f-center" key={a.pubkey}>
<Profile pubkey={a.pubkey} />
<div>
<FormattedMessage
defaultMessage="{n} messages"
values={{
n: <FormattedNumber value={a.messages.length} />,
}}
/>
</div>
</div>
))}
</div>
<div className="flex f-space">
<div>
<FormattedMessage defaultMessage="Messages" />
</div>
<div>{data.messages}</div>
</div>
<div className="flex f-space">
<div>
<FormattedMessage defaultMessage="Reactions" />
</div>
<div>{data.reactions}</div>
</div>
<div className="flex f-space">
<div>
<FormattedMessage defaultMessage="Zaps" />
</div>
<div>{data.zaps}</div>
</div>
</div>
);
}
return null;
}}
/>
</BarChart>
</ResponsiveContainer>
<div className="flex g8">
<div className="plain-paper f-1">
<h3>
<FormattedMessage defaultMessage="Top Chatters" />
</h3>
<div className="flex f-col g8">
{chatSummary.slice(0, 5).map(a => (
<div className="flex f-space f-center" key={a.pubkey}>
<Profile pubkey={a.pubkey} />
<div>
<FormattedMessage
defaultMessage="{n} messages"
values={{
n: <FormattedNumber value={a.messages.length} />,
}}
/>
</div>
</div>
))}
</div>
</div>
<div className="plain-paper f-1">
<h3>
<FormattedMessage defaultMessage="Top Zappers" />
</h3>
<div className="flex f-col g8">
{zapsSummary.slice(0, 5).map(a => (
<div className="flex f-space f-center" key={a.pubkey}>
<Profile pubkey={a.pubkey} />
<div>
<FormattedMessage
defaultMessage="{n} sats"
values={{
n: formatSats(a.total),
}}
/>
<div className="plain-paper f-1">
<h3>
<FormattedMessage defaultMessage="Top Zappers" />
</h3>
<div className="flex f-col g8">
{zapsSummary.slice(0, 5).map(a => (
<div className="flex f-space f-center" key={a.pubkey}>
<Profile pubkey={a.pubkey} />
<div>
<FormattedMessage
defaultMessage="{n} sats"
values={{
n: formatSats(a.total),
}}
/>
</div>
</div>
))}
</div>
</div>
</div>
))}
</div>
</div>
</div>
</div>
</div>
);
);
}