diff --git a/.changeset/yellow-sheep-pay.md b/.changeset/yellow-sheep-pay.md new file mode 100644 index 000000000..e9717775e --- /dev/null +++ b/.changeset/yellow-sheep-pay.md @@ -0,0 +1,5 @@ +--- +"nostrudel": minor +--- + +Show stream goal zaps in stream chat diff --git a/src/components/embed-event/event-types/embedded-goal.tsx b/src/components/embed-event/event-types/embedded-goal.tsx index e3723465d..df72e7568 100644 --- a/src/components/embed-event/event-types/embedded-goal.tsx +++ b/src/components/embed-event/event-types/embedded-goal.tsx @@ -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 & { goal: NostrEvent }) { +export type EmbeddedGoalOptions = { + showActions?: boolean; +}; + +export type EmbeddedGoalProps = Omit & { 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 - + + + {showActions && } + ); diff --git a/src/components/embed-event/index.tsx b/src/components/embed-event/index.tsx index 4fcc31f71..b9cdded49 100644 --- a/src/components/embed-event/index.tsx +++ b/src/components/embed-event/index.tsx @@ -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 ; case STREAM_KIND: return ; case GOAL_KIND: - return ; + return ; case EMOJI_PACK_KIND: return ; } @@ -31,22 +35,22 @@ export function EmbedEvent({ event }: { event: NostrEvent }) { return ; } -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 ; - return ; + return ; } case "nevent": { const { event } = useSingleEvent(pointer.data.id, pointer.data.relays); if (event === undefined) return ; - return ; + return ; } case "naddr": { const event = useReplaceableEvent(pointer.data); if (!event) return {nip19.naddrEncode(pointer.data)}; - return ; + return ; } case "nrelay": return ; @@ -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 ? : <>{link}; + return pointer ? : <>{link}; } diff --git a/src/components/layout/desktop-side-nav.tsx b/src/components/layout/desktop-side-nav.tsx index bc875bedf..45ce03644 100644 --- a/src/components/layout/desktop-side-nav.tsx +++ b/src/components/layout/desktop-side-nav.tsx @@ -45,6 +45,7 @@ export default function DesktopSideNav(props: Omit) { fontSize="1.5rem" colorScheme="brand" onClick={() => openModal()} + flexShrink={0} /> diff --git a/src/components/note/note-zap-button.tsx b/src/components/note/note-zap-button.tsx index 4d50498ba..1d674caf8 100644 --- a/src/components/note/note-zap-button.tsx +++ b/src/components/note/note-zap-button.tsx @@ -69,7 +69,7 @@ export default function NoteZapButton({ event, allowComment, showEventPreview, . onInvoice={handleInvoice} pubkey={event.pubkey} allowComment={allowComment} - showEventPreview={showEventPreview} + showEmbed={showEventPreview} /> )} diff --git a/src/components/zap-modal.tsx b/src/components/zap-modal.tsx index 1ede0cbdc..cb429623f 100644 --- a/src/components/zap-modal.tsx +++ b/src/components/zap-modal.tsx @@ -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 & { 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({ - {showEventPreview && event && } + {showEmbed && event && } {allowComment && (canZap || lnurlMetadata?.commentAllowed) && ( (); + 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; +} diff --git a/src/services/single-event.ts b/src/services/single-event.ts index ea281bf74..5beab3188 100644 --- a/src/services/single-event.ts +++ b/src/services/single-event.ts @@ -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 { diff --git a/src/views/goals/components/goal-zap-button.tsx b/src/views/goals/components/goal-zap-button.tsx index 8dccefbf6..a45370597 100644 --- a/src/views/goals/components/goal-zap-button.tsx +++ b/src/views/goals/components/goal-zap-button.tsx @@ -37,7 +37,7 @@ export default function GoalZapButton({ pubkey={goal.pubkey} relays={getGoalRelays(goal)} allowComment - showEventPreview={false} + showEmbed={false} /> )} diff --git a/src/views/streams/components/stream-goal.tsx b/src/views/streams/components/stream-goal.tsx index ddbf6461b..059cbe06d 100644 --- a/src/views/streams/components/stream-goal.tsx +++ b/src/views/streams/components/stream-goal.tsx @@ -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 & { stream: ParsedStream }) { - const [goal, setGoal] = useState(); - 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 ( diff --git a/src/views/streams/components/stream-zap-button.tsx b/src/views/streams/components/stream-zap-button.tsx index 189556a71..7f366b579 100644 --- a/src/views/streams/components/stream-zap-button.tsx +++ b/src/views/streams/components/stream-zap-button.tsx @@ -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 && ( { if (onZap) onZap(); @@ -53,6 +55,8 @@ export default function StreamZapButton({ onClose={zapModal.onClose} initialComment={initComment} additionalRelays={relays} + showEmbed + embedProps={{ goalProps: { showActions: false } }} /> )} diff --git a/src/views/streams/stream/stream-chat/use-stream-chat-timeline.ts b/src/views/streams/stream/stream-chat/use-stream-chat-timeline.ts index 490dd67d4..b54729b13 100644 --- a/src/views/streams/stream/stream-chat/use-stream-chat-timeline.ts +++ b/src/views/streams/stream/stream-chat/use-stream-chat-timeline.ts @@ -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 }); }