chore: formatting

This commit is contained in:
2023-12-05 12:58:50 +00:00
parent 13edd58987
commit 1c6ff7f729
6 changed files with 235 additions and 218 deletions

View File

@ -1,5 +1,5 @@
import { Button as AlbyZapsButton } from "@getalby/bitcoin-connect-react"; import { Button as AlbyZapsButton } from "@getalby/bitcoin-connect-react";
export default function AlbyButton() { export default function AlbyButton() {
return <AlbyZapsButton /> return <AlbyZapsButton />;
} }

View File

@ -176,15 +176,15 @@ export function ChatMessage({
style={ style={
isTablet isTablet
? { ? {
display: showZapDialog || isHovering ? "flex" : "none", display: showZapDialog || isHovering ? "flex" : "none",
} }
: { : {
position: "fixed", position: "fixed",
top: topOffset ? topOffset - 12 : 0, top: topOffset ? topOffset - 12 : 0,
left: leftOffset ? leftOffset - 32 : 0, left: leftOffset ? leftOffset - 32 : 0,
opacity: showZapDialog || isHovering ? 1 : 0, opacity: showZapDialog || isHovering ? 1 : 0,
pointerEvents: showZapDialog || isHovering ? "auto" : "none", pointerEvents: showZapDialog || isHovering ? "auto" : "none",
} }
}> }>
{zapTarget && ( {zapTarget && (
<SendZapsDialog <SendZapsDialog

View File

@ -14,214 +14,214 @@ import { Profile } from "./profile";
import { StatePill } from "./state-pill"; import { StatePill } from "./state-pill";
interface StatSlot { interface StatSlot {
time: number; time: number;
zaps: number; zaps: number;
messages: number; messages: number;
reactions: number; reactions: number;
} }
export default function StreamSummary({ link, preload }: { link: NostrLink; preload?: NostrEvent }) { export default function StreamSummary({ link, preload }: { link: NostrLink; preload?: NostrEvent }) {
const ev = useCurrentStreamFeed(link, true, preload); const ev = useCurrentStreamFeed(link, true, preload);
const thisLink = ev ? NostrLink.fromEvent(ev) : undefined; const thisLink = ev ? NostrLink.fromEvent(ev) : undefined;
const data = useLiveChatFeed(thisLink, undefined, 5_000); const data = useLiveChatFeed(thisLink, undefined, 5_000);
const reactions = useEventReactions(thisLink ?? link, data.reactions); const reactions = useEventReactions(thisLink ?? link, data.reactions);
const chatSummary = useMemo(() => { const chatSummary = useMemo(() => {
return Object.entries( return Object.entries(
data.messages.reduce((acc, v) => { data.messages.reduce((acc, v) => {
acc[v.pubkey] ??= []; acc[v.pubkey] ??= [];
acc[v.pubkey].push(v); acc[v.pubkey].push(v);
return acc; return acc;
}, {} as Record<string, Array<NostrEvent>>) }, {} as Record<string, Array<NostrEvent>>)
) )
.map(([k, v]) => ({ .map(([k, v]) => ({
pubkey: k, pubkey: k,
messages: v, messages: v,
})) }))
.sort((a, b) => (a.messages.length > b.messages.length ? -1 : 1)); .sort((a, b) => (a.messages.length > b.messages.length ? -1 : 1));
}, [data.messages]); }, [data.messages]);
const zapsSummary = useMemo(() => { const zapsSummary = useMemo(() => {
return Object.entries( return Object.entries(
reactions.zaps.reduce((acc, v) => { reactions.zaps.reduce((acc, v) => {
if (!v.sender) return acc; if (!v.sender) return acc;
acc[v.sender] ??= []; acc[v.sender] ??= [];
acc[v.sender].push(v); acc[v.sender].push(v);
return acc; return acc;
}, {} as Record<string, Array<ParsedZap>>) }, {} as Record<string, Array<ParsedZap>>)
) )
.map(([k, v]) => ({ .map(([k, v]) => ({
pubkey: k, pubkey: k,
zaps: v, zaps: v,
total: v.reduce((acc, vv) => acc + vv.amount, 0), total: v.reduce((acc, vv) => acc + vv.amount, 0),
})) }))
.sort((a, b) => (a.total > b.total ? -1 : 1)); .sort((a, b) => (a.total > b.total ? -1 : 1));
}, [reactions.zaps]); }, [reactions.zaps]);
const title = findTag(ev, "title"); const title = findTag(ev, "title");
const summary = findTag(ev, "summary"); const summary = findTag(ev, "summary");
const status = findTag(ev, "status"); const status = findTag(ev, "status");
const starts = findTag(ev, "starts"); const starts = findTag(ev, "starts");
const Day = 60 * 60 * 24; const Day = 60 * 60 * 24;
const startTime = starts ? Number(starts) : ev?.created_at ?? unixNow(); const startTime = starts ? Number(starts) : ev?.created_at ?? unixNow();
const endTime = status === StreamState.Live ? unixNow() : ev?.created_at ?? unixNow(); const endTime = status === StreamState.Live ? unixNow() : ev?.created_at ?? unixNow();
const streamLength = endTime - startTime; const streamLength = endTime - startTime;
const windowSize = streamLength > Day ? Day : 60 * 10; const windowSize = streamLength > Day ? Day : 60 * 10;
const stats = useMemo(() => { const stats = useMemo(() => {
let min = unixNow(); let min = unixNow();
let max = 0; let max = 0;
const ret = [...data.messages, ...data.reactions] const ret = [...data.messages, ...data.reactions]
.sort((a, b) => (a.created_at > b.created_at ? -1 : 1)) .sort((a, b) => (a.created_at > b.created_at ? -1 : 1))
.reduce((acc, v) => { .reduce((acc, v) => {
const time = Math.floor(v.created_at - (v.created_at % windowSize)); const time = Math.floor(v.created_at - (v.created_at % windowSize));
if (time < min) { if (time < min) {
min = time; 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,
};
} }
return ret; if (time > max) {
}, [data]); max = time;
}
const key = time.toString();
acc[key] ??= {
time,
zaps: 0,
messages: 0,
reactions: 0,
};
return ( if (v.kind === LIVE_STREAM_CHAT) {
<div className="stream-summary"> acc[key].messages++;
<h1>{title}</h1> } else if (v.kind === EventKind.ZapReceipt) {
<p>{summary}</p> acc[key].zaps++;
<div className="flex gap-1"> } else if (v.kind === EventKind.Reaction) {
<StatePill state={status as StreamState} /> acc[key].reactions++;
{streamLength > 0 && ( } else {
<FormattedMessage console.debug("Uncounted stat", v);
defaultMessage="Stream Duration {duration} mins" }
id="J/+m9y" return acc;
values={{ }, {} as Record<string, StatSlot>);
duration: <FormattedNumber value={streamLength / 60} maximumFractionDigits={2} />,
}}
/>
)}
</div>
<h2>
<FormattedMessage defaultMessage="Summary" id="RrCui3" />
</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 flex-col gap-2">
<div>
<FormattedDate value={data.time * 1000} timeStyle="short" dateStyle="short" />
</div>
<div className="flex justify-between">
<div>
<FormattedMessage defaultMessage="Messages" id="hMzcSq" />
</div>
<div>{data.messages}</div>
</div>
<div className="flex justify-between">
<div>
<FormattedMessage defaultMessage="Reactions" id="XgWvGA" />
</div>
<div>{data.reactions}</div>
</div>
<div className="flex justify-between">
<div>
<FormattedMessage defaultMessage="Zaps" id="OEW7yJ" />
</div>
<div>{data.zaps}</div>
</div>
</div>
);
}
return null;
}}
/>
</BarChart>
</ResponsiveContainer>
<div className="flex gap-1"> // fill empty time slots
<div className="plain-paper flex-1"> for (let x = min; x < max; x += windowSize) {
<h3> ret[x.toString()] ??= {
<FormattedMessage defaultMessage="Top Chatters" id="GGaJMU" /> time: x,
</h3> zaps: 0,
<div className="flex flex-col gap-2"> messages: 0,
{chatSummary.slice(0, 5).map(a => ( reactions: 0,
<div className="flex justify-between items-center" key={a.pubkey}> };
<Profile pubkey={a.pubkey} /> }
<div> return ret;
<FormattedMessage }, [data]);
defaultMessage="{n} messages"
id="gzsn7k" return (
values={{ <div className="stream-summary">
n: <FormattedNumber value={a.messages.length} />, <h1>{title}</h1>
}} <p>{summary}</p>
/> <div className="flex gap-1">
</div> <StatePill state={status as StreamState} />
</div> {streamLength > 0 && (
))} <FormattedMessage
defaultMessage="Stream Duration {duration} mins"
id="J/+m9y"
values={{
duration: <FormattedNumber value={streamLength / 60} maximumFractionDigits={2} />,
}}
/>
)}
</div>
<h2>
<FormattedMessage defaultMessage="Summary" id="RrCui3" />
</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 flex-col gap-2">
<div>
<FormattedDate value={data.time * 1000} timeStyle="short" dateStyle="short" />
</div> </div>
</div> <div className="flex justify-between">
<div className="plain-paper flex-1"> <div>
<h3> <FormattedMessage defaultMessage="Messages" id="hMzcSq" />
<FormattedMessage defaultMessage="Top Zappers" id="dVD/AR" /> </div>
</h3> <div>{data.messages}</div>
<div className="flex flex-col gap-2">
{zapsSummary.slice(0, 5).map(a => (
<div className="flex justify-between items-center" key={a.pubkey}>
<Profile pubkey={a.pubkey} />
<div>
<FormattedMessage
defaultMessage="{n} sats"
id="CsCUYo"
values={{
n: formatSats(a.total),
}}
/>
</div>
</div>
))}
</div> </div>
<div className="flex justify-between">
<div>
<FormattedMessage defaultMessage="Reactions" id="XgWvGA" />
</div>
<div>{data.reactions}</div>
</div>
<div className="flex justify-between">
<div>
<FormattedMessage defaultMessage="Zaps" id="OEW7yJ" />
</div>
<div>{data.zaps}</div>
</div>
</div>
);
}
return null;
}}
/>
</BarChart>
</ResponsiveContainer>
<div className="flex gap-1">
<div className="plain-paper flex-1">
<h3>
<FormattedMessage defaultMessage="Top Chatters" id="GGaJMU" />
</h3>
<div className="flex flex-col gap-2">
{chatSummary.slice(0, 5).map(a => (
<div className="flex justify-between items-center" key={a.pubkey}>
<Profile pubkey={a.pubkey} />
<div>
<FormattedMessage
defaultMessage="{n} messages"
id="gzsn7k"
values={{
n: <FormattedNumber value={a.messages.length} />,
}}
/>
</div> </div>
</div> </div>
))}
</div>
</div> </div>
); <div className="plain-paper flex-1">
<h3>
<FormattedMessage defaultMessage="Top Zappers" id="dVD/AR" />
</h3>
<div className="flex flex-col gap-2">
{zapsSummary.slice(0, 5).map(a => (
<div className="flex justify-between items-center" key={a.pubkey}>
<Profile pubkey={a.pubkey} />
<div>
<FormattedMessage
defaultMessage="{n} sats"
id="CsCUYo"
values={{
n: formatSats(a.total),
}}
/>
</div>
</div>
))}
</div>
</div>
</div>
</div>
);
} }

View File

@ -140,6 +140,9 @@
"GGaJMU": { "GGaJMU": {
"defaultMessage": "Top Chatters" "defaultMessage": "Top Chatters"
}, },
"Gmiwnd": {
"defaultMessage": "Refresh the page to use the latest version"
},
"Gq6x9o": { "Gq6x9o": {
"defaultMessage": "Cover Image" "defaultMessage": "Cover Image"
}, },
@ -227,6 +230,9 @@
"Qe1MJu": { "Qe1MJu": {
"defaultMessage": "{name} with {amount}" "defaultMessage": "{name} with {amount}"
}, },
"RJ2VxG": {
"defaultMessage": "A new version has been detected"
},
"RJOmzk": { "RJOmzk": {
"defaultMessage": "I have read and agree with {provider}''s {terms}." "defaultMessage": "I have read and agree with {provider}''s {terms}."
}, },
@ -381,6 +387,9 @@
"r2Jjms": { "r2Jjms": {
"defaultMessage": "Log In" "defaultMessage": "Log In"
}, },
"rELDbB": {
"defaultMessage": "Refresh"
},
"rWBFZA": { "rWBFZA": {
"defaultMessage": "Sexually explicit material ahead!" "defaultMessage": "Sexually explicit material ahead!"
}, },

View File

@ -145,20 +145,25 @@ export function LayoutPage() {
} }
function NewVersionBanner() { function NewVersionBanner() {
const newVersion = useSyncExternalStore(c => NewVersion.hook(c), () => NewVersion.snapshot()); const newVersion = useSyncExternalStore(
c => NewVersion.hook(c),
() => NewVersion.snapshot()
);
if (!newVersion) return; if (!newVersion) return;
return <div className="fixed top-0 left-0 w-max flex bg-slate-800 py-2 px-4 opacity-95"> return (
<div className="grow"> <div className="fixed top-0 left-0 w-max flex bg-slate-800 py-2 px-4 opacity-95">
<h1> <div className="grow">
<FormattedMessage defaultMessage="A new version has been detected" id="RJ2VxG" /> <h1>
</h1> <FormattedMessage defaultMessage="A new version has been detected" id="RJ2VxG" />
<p> </h1>
<FormattedMessage defaultMessage="Refresh the page to use the latest version" id="Gmiwnd" /> <p>
</p> <FormattedMessage defaultMessage="Refresh the page to use the latest version" id="Gmiwnd" />
</p>
</div>
<AsyncButton onClick={() => window.location.reload()} className="btn">
<FormattedMessage defaultMessage="Refresh" id="rELDbB" />
</AsyncButton>
</div> </div>
<AsyncButton onClick={() => window.location.reload()} className="btn"> );
<FormattedMessage defaultMessage="Refresh" id="rELDbB" />
</AsyncButton>
</div>
} }

View File

@ -46,6 +46,7 @@
"Fodi9+": "Get paid by viewers", "Fodi9+": "Get paid by viewers",
"G/yZLu": "Remove", "G/yZLu": "Remove",
"GGaJMU": "Top Chatters", "GGaJMU": "Top Chatters",
"Gmiwnd": "Refresh the page to use the latest version",
"Gq6x9o": "Cover Image", "Gq6x9o": "Cover Image",
"H/bNs9": "Save this and keep it safe! If you lose this key, you won't be able to access your account ever again. Yep, it's that serious!", "H/bNs9": "Save this and keep it safe! If you lose this key, you won't be able to access your account ever again. Yep, it's that serious!",
"H5+NAX": "Balance", "H5+NAX": "Balance",
@ -75,6 +76,7 @@
"QRRCp0": "Stream URL", "QRRCp0": "Stream URL",
"QceMQZ": "Goal: {amount}", "QceMQZ": "Goal: {amount}",
"Qe1MJu": "{name} with {amount}", "Qe1MJu": "{name} with {amount}",
"RJ2VxG": "A new version has been detected",
"RJOmzk": "I have read and agree with {provider}''s {terms}.", "RJOmzk": "I have read and agree with {provider}''s {terms}.",
"RXQdxR": "Please login to write messages!", "RXQdxR": "Please login to write messages!",
"RrCui3": "Summary", "RrCui3": "Summary",
@ -126,6 +128,7 @@
"oZrFyI": "Stream type should be HLS", "oZrFyI": "Stream type should be HLS",
"pO/lPX": "Scheduled for {date}", "pO/lPX": "Scheduled for {date}",
"r2Jjms": "Log In", "r2Jjms": "Log In",
"rELDbB": "Refresh",
"rWBFZA": "Sexually explicit material ahead!", "rWBFZA": "Sexually explicit material ahead!",
"rbrahO": "Close", "rbrahO": "Close",
"rfC1Zq": "Save card", "rfC1Zq": "Save card",