diff --git a/src/element/category/category-tile.tsx b/src/element/category/category-tile.tsx index 5d2ae9b..38f63d3 100644 --- a/src/element/category/category-tile.tsx +++ b/src/element/category/category-tile.tsx @@ -20,13 +20,13 @@ export function CategoryTile({ return (
-
- {game?.cover && ( - - )} - {!game?.cover && game?.className && ( -
- )} +
+
+ {game?.cover && } + {!game?.cover && game?.className && ( +
+ )} +
{showDetail && (

{game?.name}

diff --git a/src/element/category/top-streamers.tsx b/src/element/category/top-streamers.tsx new file mode 100644 index 0000000..bbeba2f --- /dev/null +++ b/src/element/category/top-streamers.tsx @@ -0,0 +1,49 @@ +import { useCategoryZaps } from "@/hooks/category-zaps"; +import { formatSatsCompact } from "@/number"; +import { getName } from "../profile"; +import { Avatar } from "../avatar"; +import { useUserProfile } from "@snort/system-react"; +import { Icon } from "../icon"; +import { FormattedMessage } from "react-intl"; +import { profileLink } from "@/utils"; +import { Link } from "react-router-dom"; + +export function CategoryTopZapsStreamer({ gameId }: { gameId: string }) { + const zaps = useCategoryZaps(gameId); + + return ( +
+
+ +
+ +
+
+
+
+ {Object.entries(zaps.topPubkeys) + .sort(([, a], [, b]) => (a > b ? -1 : 1)) + .slice(0, 4) + .map(([pubkey, amount]) => ( + + ))} +
+
+
+ ); +} + +function TopStreamer({ pubkey, amount }: { pubkey: string; amount: number }) { + const profile = useUserProfile(pubkey); + return ( +
+ + + +
+
{formatSatsCompact(amount)}
+
{getName(pubkey, profile)}
+
+
+ ); +} diff --git a/src/element/category/zaps.tsx b/src/element/category/zaps.tsx index 335da6a..4ad1adb 100644 --- a/src/element/category/zaps.tsx +++ b/src/element/category/zaps.tsx @@ -6,7 +6,7 @@ import { formatSatsCompact } from "@/number"; export function CategoryZaps({ gameId }: { gameId: string }) { const zaps = useCategoryZaps(gameId); - const total = zaps.reduce((acc, v) => (acc += v.amount), 0); + const total = zaps.parsed.reduce((acc, v) => (acc += v.amount), 0); return ( diff --git a/src/element/chat/live-chat.tsx b/src/element/chat/live-chat.tsx index 66bf4bf..ded5960 100644 --- a/src/element/chat/live-chat.tsx +++ b/src/element/chat/live-chat.tsx @@ -159,7 +159,7 @@ export function LiveChat({ return { ...c }; }); layoutContext.update(c => { - c.showHeader = !c.showHeader; + c.showHeader = !streamContext.showDetails; return { ...c }; }); }}> diff --git a/src/element/modal.tsx b/src/element/modal.tsx index 921a734..7173bb5 100644 --- a/src/element/modal.tsx +++ b/src/element/modal.tsx @@ -1,5 +1,5 @@ import classNames from "classnames"; -import React, { ReactNode, useEffect } from "react"; +import React, { ReactNode, useEffect, useState } from "react"; import { createPortal } from "react-dom"; import { IconButton } from "./buttons"; @@ -11,36 +11,9 @@ export interface ModalProps { onClick?: (e: React.MouseEvent) => void; children: ReactNode; showClose?: boolean; + ready?: boolean; } -let scrollbarWidth: number | null = null; - -const getScrollbarWidth = () => { - if (scrollbarWidth !== null) { - return scrollbarWidth; - } - - const outer = document.createElement("div"); - outer.style.visibility = "hidden"; - outer.style.width = "100px"; - - document.body.appendChild(outer); - - const widthNoScroll = outer.offsetWidth; - outer.style.overflow = "scroll"; - - const inner = document.createElement("div"); - inner.style.width = "100%"; - outer.appendChild(inner); - - const widthWithScroll = inner.offsetWidth; - - outer.parentNode?.removeChild(outer); - - scrollbarWidth = widthNoScroll - widthWithScroll; - return scrollbarWidth; -}; - export default function Modal(props: ModalProps) { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape" && props.onClose) { @@ -50,13 +23,10 @@ export default function Modal(props: ModalProps) { useEffect(() => { document.body.classList.add("scroll-lock"); - document.body.style.paddingRight = `${getScrollbarWidth()}px`; - document.addEventListener("keydown", handleKeyDown); return () => { document.body.classList.remove("scroll-lock"); - document.body.style.paddingRight = ""; document.removeEventListener("keydown", handleKeyDown); }; }, []); @@ -76,7 +46,13 @@ export default function Modal(props: ModalProps) { e.stopPropagation(); }}>
e.stopPropagation()} onClick={e => { e.stopPropagation(); diff --git a/src/element/send-zap.tsx b/src/element/send-zap.tsx index cbad465..d02170e 100644 --- a/src/element/send-zap.tsx +++ b/src/element/send-zap.tsx @@ -35,10 +35,11 @@ export interface SendZapsProps { eTag?: string; targetName?: string; onFinish: () => void; + onTargetReady?: () => void; button?: ReactNode; } -export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: SendZapsProps) { +export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish, onTargetReady }: SendZapsProps) { const satsAmounts = [ 21, 69, 121, 420, 1_000, 2_100, 4_200, 10_000, 21_000, 42_000, 69_000, 100_000, 210_000, 500_000, 1_000_000, ]; @@ -63,9 +64,14 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: Se useEffect(() => { if (!svc) { if (typeof lnurl === "string") { - loadService(lnurl).catch(console.warn); + loadService(lnurl) + .then(() => { + onTargetReady?.(); + }) + .catch(console.warn); } else { setSvc(lnurl); + onTargetReady?.(); } } }, [lnurl]); @@ -207,6 +213,7 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: Se export function SendZapsDialog(props: Omit) { const [open, setOpen] = useState(false); + const [ready, setReady] = useState(false); return ( <> {props.button ? ( @@ -218,8 +225,8 @@ export function SendZapsDialog(props: Omit) { )} {open && ( - setOpen(false)}> - setOpen(false)} /> + setOpen(false)} ready={ready}> + setOpen(false)} onTargetReady={() => setReady(true)} /> )} @@ -230,19 +237,21 @@ export function ZapEvent({ ev, children }: { children: ReactNode; ev: TaggedNost const host = getHost(ev); const profile = useUserProfile(host); const [open, setOpen] = useState(false); + const [ready, setReady] = useState(false); const target = profile?.lud16 ?? profile?.lud06; return ( <>
setOpen(true)}>{children}
{open && ( - setOpen(false)}> + setOpen(false)} ready={ready}> setOpen(false)} + onTargetReady={() => setReady(true)} /> )} diff --git a/src/element/video-tile.tsx b/src/element/video-tile.tsx index fa31b02..b49f907 100644 --- a/src/element/video-tile.tsx +++ b/src/element/video-tile.tsx @@ -61,7 +61,7 @@ export function VideoTile({ {hasImg ? ( { setHasImage(false); diff --git a/src/hooks/category-zaps.ts b/src/hooks/category-zaps.ts index f2ce6c2..2723b87 100644 --- a/src/hooks/category-zaps.ts +++ b/src/hooks/category-zaps.ts @@ -18,7 +18,21 @@ export function useCategoryZaps(gameId: string) { const zapEvents = useRequestBuilder(rbZaps); const zaps = useMemo(() => { - return zapEvents.map(a => parseZap(a)); + const parsed = zapEvents.map(a => parseZap(a)); + return { + parsed: parsed, + all: zapEvents, + topPubkeys: parsed.reduce( + (acc, v) => { + if (v.receiver) { + acc[v.receiver] ??= 0; + acc[v.receiver] += v.amount; + } + return acc; + }, + {} as Record, + ), + }; }, [zapEvents]); return zaps; diff --git a/src/lang.json b/src/lang.json index 8e6af29..a97289d 100644 --- a/src/lang.json +++ b/src/lang.json @@ -706,6 +706,9 @@ "kp0NPF": { "defaultMessage": "Planned" }, + "lXbG97": { + "defaultMessage": "Most Zapped Streamers" + }, "lZpRMR": { "defaultMessage": "Check here if this stream contains nudity or pornographic content." }, diff --git a/src/pages/category.tsx b/src/pages/category.tsx index b7ebba2..f4e5175 100644 --- a/src/pages/category.tsx +++ b/src/pages/category.tsx @@ -1,6 +1,6 @@ import CategoryLink from "@/element/category/category-link"; import { CategoryTile } from "@/element/category/category-tile"; -import { CategoryZaps } from "@/element/category/zaps"; +import { CategoryTopZapsStreamer } from "@/element/category/top-streamers"; import VideoGridSorted from "@/element/video-grid-sorted"; import { EventKind, RequestBuilder } from "@snort/system"; import { useRequestBuilder } from "@snort/system-react"; @@ -82,25 +82,17 @@ export default function Category() { const results = useRequestBuilder(sub); return ( -
-
-
+
+
+
{AllCategories.map(a => ( ))}
{id && ( -
- - -
- } - /> +
+ } />
)} diff --git a/src/pages/root.tsx b/src/pages/root.tsx index a99e5d7..4c60935 100644 --- a/src/pages/root.tsx +++ b/src/pages/root.tsx @@ -8,8 +8,8 @@ export function RootPage() { return (
-
-
+
+
{AllCategories.filter(a => a.priority === 0).map(a => ( ))} diff --git a/src/pages/video.tsx b/src/pages/video.tsx index 362c481..8c988e4 100644 --- a/src/pages/video.tsx +++ b/src/pages/video.tsx @@ -34,7 +34,7 @@ export function VideoPage({ link, evPreload }: { link: NostrLink; evPreload?: Ta const ev = useCurrentStreamFeed(link, true, evPreload); const host = getHost(ev); const [widePlayer, setWidePlayer] = useState(localStorage.getItem("wide-player") === "true"); - const { title, summary, image, contentWarning, recording } = extractStreamInfo(ev); + const { title, summary, image, recording } = extractStreamInfo(ev); const profile = useUserProfile(host); const { proxy } = useImgProxy(); const zapTarget = profile?.lud16 ?? profile?.lud06; diff --git a/src/translations/en.json b/src/translations/en.json index 2e7af9a..ad55f06 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -232,6 +232,7 @@ "kc5EOy": "Username is too long", "khJ51Q": "Stream Earnings", "kp0NPF": "Planned", + "lXbG97": "Most Zapped Streamers", "lZpRMR": "Check here if this stream contains nudity or pornographic content.", "ljmS5P": "Endpoint", "miQKuZ": "Stream Time",