show stream goal zaps in stream chat

This commit is contained in:
hzrd149 2023-09-08 10:52:54 -05:00
parent caa538de84
commit 094a6fb9db
12 changed files with 99 additions and 56 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Show stream goal zaps in stream chat

View File

@ -1,4 +1,4 @@
import { Card, CardBody, CardHeader, CardProps, Heading, Link, Text } from "@chakra-ui/react";
import { Card, CardBody, CardHeader, CardProps, Flex, Heading, Link, Text } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router-dom";
import { getSharableEventAddress } from "../../../helpers/nip19";
@ -8,8 +8,15 @@ import { UserAvatarLink } from "../../user-avatar-link";
import { UserLink } from "../../user-link";
import GoalProgress from "../../../views/goals/components/goal-progress";
import GoalZapButton from "../../../views/goals/components/goal-zap-button";
import GoalTopZappers from "../../../views/goals/components/goal-top-zappers";
export default function EmbeddedGoal({ goal, ...props }: Omit<CardProps, "children"> & { goal: NostrEvent }) {
export type EmbeddedGoalOptions = {
showActions?: boolean;
};
export type EmbeddedGoalProps = Omit<CardProps, "children"> & { goal: NostrEvent } & EmbeddedGoalOptions;
export default function EmbeddedGoal({ goal, showActions = true, ...props }: EmbeddedGoalProps) {
const nevent = getSharableEventAddress(goal);
return (
@ -26,7 +33,10 @@ export default function EmbeddedGoal({ goal, ...props }: Omit<CardProps, "childr
</CardHeader>
<CardBody p="2">
<GoalProgress goal={goal} />
<GoalZapButton goal={goal} mt="2" />
<Flex gap="2" alignItems="flex-end">
<GoalTopZappers goal={goal} overflow="hidden" />
{showActions && <GoalZapButton goal={goal} flexShrink={0} />}
</Flex>
</CardBody>
</Card>
);

View File

@ -13,17 +13,21 @@ import { safeDecode } from "../../helpers/nip19";
import EmbeddedStream from "./event-types/embedded-stream";
import { EMOJI_PACK_KIND } from "../../helpers/nostr/emoji-packs";
import EmbeddedEmojiPack from "./event-types/embedded-emoji-pack";
import EmbeddedGoal from "./event-types/embedded-goal";
import EmbeddedGoal, { EmbeddedGoalOptions } from "./event-types/embedded-goal";
import EmbeddedUnknown from "./event-types/embedded-unknown";
export function EmbedEvent({ event }: { event: NostrEvent }) {
export type EmbedProps = {
goalProps?: EmbeddedGoalOptions;
};
export function EmbedEvent({ event, goalProps }: { event: NostrEvent } & EmbedProps) {
switch (event.kind) {
case Kind.Text:
return <EmbeddedNote event={event} />;
case STREAM_KIND:
return <EmbeddedStream event={event} />;
case GOAL_KIND:
return <EmbeddedGoal goal={event} />;
return <EmbeddedGoal goal={event} {...goalProps} />;
case EMOJI_PACK_KIND:
return <EmbeddedEmojiPack pack={event} />;
}
@ -31,22 +35,22 @@ export function EmbedEvent({ event }: { event: NostrEvent }) {
return <EmbeddedUnknown event={event} />;
}
export function EmbedEventPointer({ pointer }: { pointer: DecodeResult }) {
export function EmbedEventPointer({ pointer, ...props }: { pointer: DecodeResult } & EmbedProps) {
switch (pointer.type) {
case "note": {
const { event } = useSingleEvent(pointer.data);
if (event === undefined) return <NoteLink noteId={pointer.data} />;
return <EmbedEvent event={event} />;
return <EmbedEvent event={event} {...props} />;
}
case "nevent": {
const { event } = useSingleEvent(pointer.data.id, pointer.data.relays);
if (event === undefined) return <NoteLink noteId={pointer.data.id} />;
return <EmbedEvent event={event} />;
return <EmbedEvent event={event} {...props} />;
}
case "naddr": {
const event = useReplaceableEvent(pointer.data);
if (!event) return <span>{nip19.naddrEncode(pointer.data)}</span>;
return <EmbedEvent event={event} />;
return <EmbedEvent event={event} {...props} />;
}
case "nrelay":
return <RelayCard url={pointer.data} />;
@ -54,8 +58,8 @@ export function EmbedEventPointer({ pointer }: { pointer: DecodeResult }) {
return null;
}
export function EmbedEventNostrLink({ link }: { link: string }) {
export function EmbedEventNostrLink({ link, ...props }: { link: string } & EmbedProps) {
const pointer = safeDecode(link);
return pointer ? <EmbedEventPointer pointer={pointer} /> : <>{link}</>;
return pointer ? <EmbedEventPointer pointer={pointer} {...props} /> : <>{link}</>;
}

View File

@ -45,6 +45,7 @@ export default function DesktopSideNav(props: Omit<FlexProps, "children">) {
fontSize="1.5rem"
colorScheme="brand"
onClick={() => openModal()}
flexShrink={0}
/>
</Flex>
<AccountSwitcher />

View File

@ -69,7 +69,7 @@ export default function NoteZapButton({ event, allowComment, showEventPreview, .
onInvoice={handleInvoice}
pubkey={event.pubkey}
allowComment={allowComment}
showEventPreview={showEventPreview}
showEmbed={showEventPreview}
/>
)}
</>

View File

@ -1,10 +1,7 @@
import {
Box,
Button,
ButtonGroup,
Flex,
Heading,
Image,
Input,
Modal,
ModalBody,
@ -31,15 +28,13 @@ import appSettings from "../services/settings/app-settings";
import useSubject from "../hooks/use-subject";
import useUserLNURLMetadata from "../hooks/use-user-lnurl-metadata";
import { requestZapInvoice } from "../helpers/zaps";
import { ParsedStream, getATag } from "../helpers/nostr/stream";
import EmbeddedNote from "./embed-event/event-types/embedded-note";
import { unique } from "../helpers/array";
import { useUserRelays } from "../hooks/use-user-relays";
import { RelayMode } from "../classes/relay";
import relayScoreboardService from "../services/relay-scoreboard";
import { useAdditionalRelayContext } from "../providers/additional-relay-context";
import { getEventCoordinate, isReplaceable } from "../helpers/nostr/events";
import { EmbedEvent } from "./embed-event";
import { EmbedEvent, EmbedProps } from "./embed-event";
type FormValues = {
amount: number;
@ -54,7 +49,8 @@ export type ZapModalProps = Omit<ModalProps, "children"> & {
initialAmount?: number;
onInvoice: (invoice: string) => void;
allowComment?: boolean;
showEventPreview?: boolean;
showEmbed?: boolean;
embedProps?: EmbedProps;
additionalRelays?: string[];
};
@ -67,7 +63,8 @@ export default function ZapModal({
initialAmount,
onInvoice,
allowComment = true,
showEventPreview = true,
showEmbed = true,
embedProps,
additionalRelays = [],
...props
}: ZapModalProps) {
@ -180,7 +177,7 @@ export default function ZapModal({
</Box>
</Flex>
{showEventPreview && event && <EmbedEvent event={event} />}
{showEmbed && event && <EmbedEvent event={event} {...embedProps} />}
{allowComment && (canZap || lnurlMetadata?.commentAllowed) && (
<Input

View File

@ -0,0 +1,29 @@
import { useEffect, useState } from "react";
import { GOAL_KIND } from "../helpers/nostr/goal";
import { ParsedStream, getATag } from "../helpers/nostr/stream";
import { NostrEvent } from "../types/nostr-event";
import { useReadRelayUrls } from "./use-client-relays";
import singleEventService from "../services/single-event";
import { NostrRequest } from "../classes/nostr-request";
export default function useStreamGoal(stream: ParsedStream) {
const [goal, setGoal] = useState<NostrEvent>();
const relays = useReadRelayUrls(stream.relays);
useEffect(() => {
if (stream.goal) {
singleEventService.requestEvent(stream.goal, relays).then((event) => {
setGoal(event);
});
} else {
const request = new NostrRequest(relays);
request.onEvent.subscribe((event) => {
setGoal(event);
});
request.start({ "#a": [getATag(stream)], kinds: [GOAL_KIND] });
}
}, [stream.identifier, stream.goal, relays.join("|")]);
return goal;
}

View File

@ -1,6 +1,6 @@
import createDefer, { Deferred } from "../classes/deferred";
import { NostrRequest } from "../classes/nostr-request";
import { safeRelayUrl, safeRelayUrls } from "../helpers/url";
import { safeRelayUrls } from "../helpers/url";
import { NostrEvent } from "../types/nostr-event";
class SingleEventService {

View File

@ -37,7 +37,7 @@ export default function GoalZapButton({
pubkey={goal.pubkey}
relays={getGoalRelays(goal)}
allowComment
showEventPreview={false}
showEmbed={false}
/>
)}
</>

View File

@ -1,38 +1,20 @@
import { useEffect, useState } from "react";
import { Link as RouterLink } from "react-router-dom";
import { Card, CardBody, CardHeader, CardProps, Flex, Heading, Link } from "@chakra-ui/react";
import { ParsedStream, getATag } from "../../../helpers/nostr/stream";
import { NostrEvent } from "../../../types/nostr-event";
import { NostrRequest } from "../../../classes/nostr-request";
import { useReadRelayUrls } from "../../../hooks/use-client-relays";
import { GOAL_KIND, getGoalName } from "../../../helpers/nostr/goal";
import { ParsedStream } from "../../../helpers/nostr/stream";
import { getGoalName } from "../../../helpers/nostr/goal";
import GoalProgress from "../../goals/components/goal-progress";
import { getSharableEventAddress } from "../../../helpers/nip19";
import GoalTopZappers from "../../goals/components/goal-top-zappers";
import GoalZapButton from "../../goals/components/goal-zap-button";
import singleEventService from "../../../services/single-event";
import useStreamGoal from "../../../hooks/use-stream-goal";
export default function StreamGoal({ stream, ...props }: Omit<CardProps, "children"> & { stream: ParsedStream }) {
const [goal, setGoal] = useState<NostrEvent>();
const relays = useReadRelayUrls(stream.relays);
useEffect(() => {
if (stream.goal) {
singleEventService.requestEvent(stream.goal, relays).then((event) => {
setGoal(event);
});
} else {
const request = new NostrRequest(relays);
request.onEvent.subscribe((event) => {
setGoal(event);
});
request.start({ "#a": [getATag(stream)], kinds: [GOAL_KIND] });
}
}, [stream.identifier, stream.goal, relays.join("|")]);
const goal = useStreamGoal(stream);
if (!goal) return null;
const nevent = getSharableEventAddress(goal);
return (
<Card direction="column" gap="1" {...props}>
<CardHeader px="2" pt="2" pb="0">

View File

@ -5,6 +5,7 @@ import { useInvoiceModalContext } from "../../../providers/invoice-modal";
import useUserLNURLMetadata from "../../../hooks/use-user-lnurl-metadata";
import ZapModal from "../../../components/zap-modal";
import { useRelaySelectionRelays } from "../../../providers/relay-selection-provider";
import useStreamGoal from "../../../hooks/use-stream-goal";
export default function StreamZapButton({
stream,
@ -21,6 +22,7 @@ export default function StreamZapButton({
const { requestPay } = useInvoiceModalContext();
const zapMetadata = useUserLNURLMetadata(stream.host);
const relays = useRelaySelectionRelays();
const goal = useStreamGoal(stream);
const commonProps = {
"aria-label": "Zap stream",
@ -43,7 +45,7 @@ export default function StreamZapButton({
{zapModal.isOpen && (
<ZapModal
isOpen
event={stream.event}
event={goal || stream.event}
pubkey={stream.host}
onInvoice={async (invoice) => {
if (onZap) onZap();
@ -53,6 +55,8 @@ export default function StreamZapButton({
onClose={zapModal.onClose}
initialComment={initComment}
additionalRelays={relays}
showEmbed
embedProps={{ goalProps: { showActions: false } }}
/>
)}
</>

View File

@ -8,6 +8,8 @@ import { NostrEvent, isPTag } from "../../../../types/nostr-event";
import useUserMuteList from "../../../../hooks/use-user-mute-list";
import { useRelaySelectionRelays } from "../../../../providers/relay-selection-provider";
import { useCurrentAccount } from "../../../../hooks/use-current-account";
import useStreamGoal from "../../../../hooks/use-stream-goal";
import { NostrQuery } from "../../../../types/nostr-query";
export default function useStreamChatTimeline(stream: ParsedStream) {
const account = useCurrentAccount();
@ -20,14 +22,23 @@ export default function useStreamChatTimeline(stream: ParsedStream) {
[hostMuteList, muteList],
);
const eventFilter = useCallback((event: NostrEvent) => !mutedPubkeys.includes(event.pubkey), [mutedPubkeys]);
return useTimelineLoader(
`${getEventUID(stream.event)}-chat`,
streamRelays,
{
const goal = useStreamGoal(stream);
const query = useMemo(() => {
const streamQuery: NostrQuery = {
"#a": [getATag(stream)],
kinds: [STREAM_CHAT_MESSAGE_KIND, Kind.Zap],
},
{ eventFilter },
);
};
if (goal) {
return [
streamQuery,
// also get zaps to goal
{ "#e": [goal.id], kinds: [Kind.Zap] },
];
}
return streamQuery;
}, [stream, goal]);
const eventFilter = useCallback((event: NostrEvent) => !mutedPubkeys.includes(event.pubkey), [mutedPubkeys]);
return useTimelineLoader(`${getEventUID(stream.event)}-chat`, streamRelays, query, { eventFilter });
}