mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-07-27 04:22:21 +02:00
show top zappers on stream
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { Button, ButtonProps, useDisclosure } from "@chakra-ui/react";
|
import { Button, ButtonProps, IconButton, useDisclosure } from "@chakra-ui/react";
|
||||||
import { readablizeSats } from "../../helpers/bolt11";
|
import { readablizeSats } from "../../helpers/bolt11";
|
||||||
import { totalZaps } from "../../helpers/zaps";
|
import { totalZaps } from "../../helpers/zaps";
|
||||||
import { useCurrentAccount } from "../../hooks/use-current-account";
|
import { useCurrentAccount } from "../../hooks/use-current-account";
|
||||||
@@ -31,10 +31,13 @@ export default function NoteZapButton({
|
|||||||
eventZapsService.requestZaps(note.id, clientRelaysService.getReadUrls(), true);
|
eventZapsService.requestZaps(note.id, clientRelaysService.getReadUrls(), true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const total = totalZaps(zaps);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
{total > 0 ? (
|
||||||
<Button
|
<Button
|
||||||
leftIcon={<LightningIcon color="yellow.500" />}
|
leftIcon={<LightningIcon />}
|
||||||
aria-label="Zap Note"
|
aria-label="Zap Note"
|
||||||
title="Zap Note"
|
title="Zap Note"
|
||||||
colorScheme={hasZapped ? "brand" : undefined}
|
colorScheme={hasZapped ? "brand" : undefined}
|
||||||
@@ -42,8 +45,19 @@ export default function NoteZapButton({
|
|||||||
onClick={onOpen}
|
onClick={onOpen}
|
||||||
isDisabled={!metadata?.allowsNostr}
|
isDisabled={!metadata?.allowsNostr}
|
||||||
>
|
>
|
||||||
{readablizeSats(totalZaps(zaps) / 1000)}
|
{readablizeSats(total / 1000)}
|
||||||
</Button>
|
</Button>
|
||||||
|
) : (
|
||||||
|
<IconButton
|
||||||
|
icon={<LightningIcon />}
|
||||||
|
aria-label="Zap Note"
|
||||||
|
title="Zap Note"
|
||||||
|
{...props}
|
||||||
|
onClick={onOpen}
|
||||||
|
isDisabled={!metadata?.allowsNostr}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<ZapModal
|
<ZapModal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
|
@@ -44,12 +44,14 @@ function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode
|
|||||||
return (
|
return (
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
{isMobile && toggleButton}
|
{isMobile && toggleButton}
|
||||||
|
{!isMobile && (
|
||||||
<CopyIconButton
|
<CopyIconButton
|
||||||
text={location.href + "?displayMode=log&colorMode=dark"}
|
text={location.href + "?displayMode=log&colorMode=dark"}
|
||||||
aria-label="Copy chat log URL"
|
aria-label="Copy chat log URL"
|
||||||
title="Copy chat log URL"
|
title="Copy chat log URL"
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
rightIcon={<ExternalLinkIcon />}
|
rightIcon={<ExternalLinkIcon />}
|
||||||
size="sm"
|
size="sm"
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { useRef } from "react";
|
import { useMemo, useRef } from "react";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@@ -36,6 +36,9 @@ import useSubject from "../../../../hooks/use-subject";
|
|||||||
import { useTimelineLoader } from "../../../../hooks/use-timeline-loader";
|
import { useTimelineLoader } from "../../../../hooks/use-timeline-loader";
|
||||||
import { truncatedId } from "../../../../helpers/nostr-event";
|
import { truncatedId } from "../../../../helpers/nostr-event";
|
||||||
import { css } from "@emotion/react";
|
import { css } from "@emotion/react";
|
||||||
|
import TopZappers from "./top-zappers";
|
||||||
|
import { Kind } from "nostr-tools";
|
||||||
|
import { parseZapEvent } from "../../../../helpers/zaps";
|
||||||
|
|
||||||
const hideScrollbar = css`
|
const hideScrollbar = css`
|
||||||
scrollbar-width: 0;
|
scrollbar-width: 0;
|
||||||
@@ -67,6 +70,16 @@ export default function StreamChat({
|
|||||||
|
|
||||||
const events = useSubject(timeline.timeline).sort((a, b) => b.created_at - a.created_at);
|
const events = useSubject(timeline.timeline).sort((a, b) => b.created_at - a.created_at);
|
||||||
|
|
||||||
|
const zaps = useMemo(() => {
|
||||||
|
const parsed = [];
|
||||||
|
for (const event of events) {
|
||||||
|
try {
|
||||||
|
parsed.push(parseZapEvent(event));
|
||||||
|
} catch (e) {}
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}, [events]);
|
||||||
|
|
||||||
const scrollBox = useRef<HTMLDivElement | null>(null);
|
const scrollBox = useRef<HTMLDivElement | null>(null);
|
||||||
const callback = useTimelineCurserIntersectionCallback(timeline);
|
const callback = useTimelineCurserIntersectionCallback(timeline);
|
||||||
|
|
||||||
@@ -104,7 +117,8 @@ export default function StreamChat({
|
|||||||
{actions}
|
{actions}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
)}
|
)}
|
||||||
<CardBody display="flex" flexDirection="column" gap="2" overflow="hidden" p={0}>
|
<CardBody display="flex" flexDirection="column" overflow="hidden" p={0}>
|
||||||
|
<TopZappers zaps={zaps} pt={!isPopup ? 0 : undefined} />
|
||||||
<Flex
|
<Flex
|
||||||
overflowY="scroll"
|
overflowY="scroll"
|
||||||
overflowX="hidden"
|
overflowX="hidden"
|
||||||
@@ -113,6 +127,7 @@ export default function StreamChat({
|
|||||||
flex={1}
|
flex={1}
|
||||||
px="4"
|
px="4"
|
||||||
py="2"
|
py="2"
|
||||||
|
mb="2"
|
||||||
gap="2"
|
gap="2"
|
||||||
css={isChatLog && hideScrollbar}
|
css={isChatLog && hideScrollbar}
|
||||||
>
|
>
|
||||||
|
35
src/views/streams/stream/stream-chat/top-zappers.tsx
Normal file
35
src/views/streams/stream/stream-chat/top-zappers.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { Box, Flex, FlexProps, Text } from "@chakra-ui/react";
|
||||||
|
import { ParsedZap } from "../../../../helpers/zaps";
|
||||||
|
import { UserAvatar } from "../../../../components/user-avatar";
|
||||||
|
import { UserLink } from "../../../../components/user-link";
|
||||||
|
import { LightningIcon } from "../../../../components/icons";
|
||||||
|
import { readablizeSats } from "../../../../helpers/bolt11";
|
||||||
|
|
||||||
|
export default function TopZappers({ zaps, ...props }: FlexProps & { zaps: ParsedZap[] }) {
|
||||||
|
const totals: Record<string, number> = {};
|
||||||
|
for (const zap of zaps) {
|
||||||
|
const p = zap.request.pubkey;
|
||||||
|
if (zap.payment.amount) {
|
||||||
|
totals[p] = (totals[p] || 0) + zap.payment.amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortedTotals = Array.from(Object.entries(totals)).sort((a, b) => b[1] - a[1]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex overflowX="auto" overflowY="hidden" gap="4" py="2" px="4" {...props}>
|
||||||
|
{sortedTotals.map(([pubkey, total]) => (
|
||||||
|
<Flex key={pubkey} gap="2" alignItems="center" maxW="sm">
|
||||||
|
<UserAvatar pubkey={pubkey} size="sm" noProxy />
|
||||||
|
<Box>
|
||||||
|
<UserLink pubkey={pubkey} isTruncated fontWeight="bold" />
|
||||||
|
<Text whiteSpace="nowrap">
|
||||||
|
<LightningIcon />
|
||||||
|
{readablizeSats(total / 1000)}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
))}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
}
|
Reference in New Issue
Block a user