mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-07 03:18:02 +02:00
show stream goal zaps in stream chat
This commit is contained in:
parent
caa538de84
commit
094a6fb9db
5
.changeset/yellow-sheep-pay.md
Normal file
5
.changeset/yellow-sheep-pay.md
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
"nostrudel": minor
|
||||
---
|
||||
|
||||
Show stream goal zaps in stream chat
|
@ -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>
|
||||
);
|
||||
|
@ -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}</>;
|
||||
}
|
||||
|
@ -45,6 +45,7 @@ export default function DesktopSideNav(props: Omit<FlexProps, "children">) {
|
||||
fontSize="1.5rem"
|
||||
colorScheme="brand"
|
||||
onClick={() => openModal()}
|
||||
flexShrink={0}
|
||||
/>
|
||||
</Flex>
|
||||
<AccountSwitcher />
|
||||
|
@ -69,7 +69,7 @@ export default function NoteZapButton({ event, allowComment, showEventPreview, .
|
||||
onInvoice={handleInvoice}
|
||||
pubkey={event.pubkey}
|
||||
allowComment={allowComment}
|
||||
showEventPreview={showEventPreview}
|
||||
showEmbed={showEventPreview}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -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
|
||||
|
29
src/hooks/use-stream-goal.ts
Normal file
29
src/hooks/use-stream-goal.ts
Normal 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;
|
||||
}
|
@ -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 {
|
||||
|
@ -37,7 +37,7 @@ export default function GoalZapButton({
|
||||
pubkey={goal.pubkey}
|
||||
relays={getGoalRelays(goal)}
|
||||
allowComment
|
||||
showEventPreview={false}
|
||||
showEmbed={false}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -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">
|
||||
|
@ -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 } }}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
@ -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 });
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user