mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-09 20:29:17 +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 { Link as RouterLink } from "react-router-dom";
|
||||||
|
|
||||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||||
@ -8,8 +8,15 @@ import { UserAvatarLink } from "../../user-avatar-link";
|
|||||||
import { UserLink } from "../../user-link";
|
import { UserLink } from "../../user-link";
|
||||||
import GoalProgress from "../../../views/goals/components/goal-progress";
|
import GoalProgress from "../../../views/goals/components/goal-progress";
|
||||||
import GoalZapButton from "../../../views/goals/components/goal-zap-button";
|
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);
|
const nevent = getSharableEventAddress(goal);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -26,7 +33,10 @@ export default function EmbeddedGoal({ goal, ...props }: Omit<CardProps, "childr
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardBody p="2">
|
<CardBody p="2">
|
||||||
<GoalProgress goal={goal} />
|
<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>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
@ -13,17 +13,21 @@ import { safeDecode } from "../../helpers/nip19";
|
|||||||
import EmbeddedStream from "./event-types/embedded-stream";
|
import EmbeddedStream from "./event-types/embedded-stream";
|
||||||
import { EMOJI_PACK_KIND } from "../../helpers/nostr/emoji-packs";
|
import { EMOJI_PACK_KIND } from "../../helpers/nostr/emoji-packs";
|
||||||
import EmbeddedEmojiPack from "./event-types/embedded-emoji-pack";
|
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";
|
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) {
|
switch (event.kind) {
|
||||||
case Kind.Text:
|
case Kind.Text:
|
||||||
return <EmbeddedNote event={event} />;
|
return <EmbeddedNote event={event} />;
|
||||||
case STREAM_KIND:
|
case STREAM_KIND:
|
||||||
return <EmbeddedStream event={event} />;
|
return <EmbeddedStream event={event} />;
|
||||||
case GOAL_KIND:
|
case GOAL_KIND:
|
||||||
return <EmbeddedGoal goal={event} />;
|
return <EmbeddedGoal goal={event} {...goalProps} />;
|
||||||
case EMOJI_PACK_KIND:
|
case EMOJI_PACK_KIND:
|
||||||
return <EmbeddedEmojiPack pack={event} />;
|
return <EmbeddedEmojiPack pack={event} />;
|
||||||
}
|
}
|
||||||
@ -31,22 +35,22 @@ export function EmbedEvent({ event }: { event: NostrEvent }) {
|
|||||||
return <EmbeddedUnknown event={event} />;
|
return <EmbeddedUnknown event={event} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EmbedEventPointer({ pointer }: { pointer: DecodeResult }) {
|
export function EmbedEventPointer({ pointer, ...props }: { pointer: DecodeResult } & EmbedProps) {
|
||||||
switch (pointer.type) {
|
switch (pointer.type) {
|
||||||
case "note": {
|
case "note": {
|
||||||
const { event } = useSingleEvent(pointer.data);
|
const { event } = useSingleEvent(pointer.data);
|
||||||
if (event === undefined) return <NoteLink noteId={pointer.data} />;
|
if (event === undefined) return <NoteLink noteId={pointer.data} />;
|
||||||
return <EmbedEvent event={event} />;
|
return <EmbedEvent event={event} {...props} />;
|
||||||
}
|
}
|
||||||
case "nevent": {
|
case "nevent": {
|
||||||
const { event } = useSingleEvent(pointer.data.id, pointer.data.relays);
|
const { event } = useSingleEvent(pointer.data.id, pointer.data.relays);
|
||||||
if (event === undefined) return <NoteLink noteId={pointer.data.id} />;
|
if (event === undefined) return <NoteLink noteId={pointer.data.id} />;
|
||||||
return <EmbedEvent event={event} />;
|
return <EmbedEvent event={event} {...props} />;
|
||||||
}
|
}
|
||||||
case "naddr": {
|
case "naddr": {
|
||||||
const event = useReplaceableEvent(pointer.data);
|
const event = useReplaceableEvent(pointer.data);
|
||||||
if (!event) return <span>{nip19.naddrEncode(pointer.data)}</span>;
|
if (!event) return <span>{nip19.naddrEncode(pointer.data)}</span>;
|
||||||
return <EmbedEvent event={event} />;
|
return <EmbedEvent event={event} {...props} />;
|
||||||
}
|
}
|
||||||
case "nrelay":
|
case "nrelay":
|
||||||
return <RelayCard url={pointer.data} />;
|
return <RelayCard url={pointer.data} />;
|
||||||
@ -54,8 +58,8 @@ export function EmbedEventPointer({ pointer }: { pointer: DecodeResult }) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EmbedEventNostrLink({ link }: { link: string }) {
|
export function EmbedEventNostrLink({ link, ...props }: { link: string } & EmbedProps) {
|
||||||
const pointer = safeDecode(link);
|
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"
|
fontSize="1.5rem"
|
||||||
colorScheme="brand"
|
colorScheme="brand"
|
||||||
onClick={() => openModal()}
|
onClick={() => openModal()}
|
||||||
|
flexShrink={0}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<AccountSwitcher />
|
<AccountSwitcher />
|
||||||
|
@ -69,7 +69,7 @@ export default function NoteZapButton({ event, allowComment, showEventPreview, .
|
|||||||
onInvoice={handleInvoice}
|
onInvoice={handleInvoice}
|
||||||
pubkey={event.pubkey}
|
pubkey={event.pubkey}
|
||||||
allowComment={allowComment}
|
allowComment={allowComment}
|
||||||
showEventPreview={showEventPreview}
|
showEmbed={showEventPreview}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -1,10 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
ButtonGroup,
|
|
||||||
Flex,
|
Flex,
|
||||||
Heading,
|
|
||||||
Image,
|
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
ModalBody,
|
ModalBody,
|
||||||
@ -31,15 +28,13 @@ import appSettings from "../services/settings/app-settings";
|
|||||||
import useSubject from "../hooks/use-subject";
|
import useSubject from "../hooks/use-subject";
|
||||||
import useUserLNURLMetadata from "../hooks/use-user-lnurl-metadata";
|
import useUserLNURLMetadata from "../hooks/use-user-lnurl-metadata";
|
||||||
import { requestZapInvoice } from "../helpers/zaps";
|
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 { unique } from "../helpers/array";
|
||||||
import { useUserRelays } from "../hooks/use-user-relays";
|
import { useUserRelays } from "../hooks/use-user-relays";
|
||||||
import { RelayMode } from "../classes/relay";
|
import { RelayMode } from "../classes/relay";
|
||||||
import relayScoreboardService from "../services/relay-scoreboard";
|
import relayScoreboardService from "../services/relay-scoreboard";
|
||||||
import { useAdditionalRelayContext } from "../providers/additional-relay-context";
|
import { useAdditionalRelayContext } from "../providers/additional-relay-context";
|
||||||
import { getEventCoordinate, isReplaceable } from "../helpers/nostr/events";
|
import { getEventCoordinate, isReplaceable } from "../helpers/nostr/events";
|
||||||
import { EmbedEvent } from "./embed-event";
|
import { EmbedEvent, EmbedProps } from "./embed-event";
|
||||||
|
|
||||||
type FormValues = {
|
type FormValues = {
|
||||||
amount: number;
|
amount: number;
|
||||||
@ -54,7 +49,8 @@ export type ZapModalProps = Omit<ModalProps, "children"> & {
|
|||||||
initialAmount?: number;
|
initialAmount?: number;
|
||||||
onInvoice: (invoice: string) => void;
|
onInvoice: (invoice: string) => void;
|
||||||
allowComment?: boolean;
|
allowComment?: boolean;
|
||||||
showEventPreview?: boolean;
|
showEmbed?: boolean;
|
||||||
|
embedProps?: EmbedProps;
|
||||||
additionalRelays?: string[];
|
additionalRelays?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -67,7 +63,8 @@ export default function ZapModal({
|
|||||||
initialAmount,
|
initialAmount,
|
||||||
onInvoice,
|
onInvoice,
|
||||||
allowComment = true,
|
allowComment = true,
|
||||||
showEventPreview = true,
|
showEmbed = true,
|
||||||
|
embedProps,
|
||||||
additionalRelays = [],
|
additionalRelays = [],
|
||||||
...props
|
...props
|
||||||
}: ZapModalProps) {
|
}: ZapModalProps) {
|
||||||
@ -180,7 +177,7 @@ export default function ZapModal({
|
|||||||
</Box>
|
</Box>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
{showEventPreview && event && <EmbedEvent event={event} />}
|
{showEmbed && event && <EmbedEvent event={event} {...embedProps} />}
|
||||||
|
|
||||||
{allowComment && (canZap || lnurlMetadata?.commentAllowed) && (
|
{allowComment && (canZap || lnurlMetadata?.commentAllowed) && (
|
||||||
<Input
|
<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 createDefer, { Deferred } from "../classes/deferred";
|
||||||
import { NostrRequest } from "../classes/nostr-request";
|
import { NostrRequest } from "../classes/nostr-request";
|
||||||
import { safeRelayUrl, safeRelayUrls } from "../helpers/url";
|
import { safeRelayUrls } from "../helpers/url";
|
||||||
import { NostrEvent } from "../types/nostr-event";
|
import { NostrEvent } from "../types/nostr-event";
|
||||||
|
|
||||||
class SingleEventService {
|
class SingleEventService {
|
||||||
|
@ -37,7 +37,7 @@ export default function GoalZapButton({
|
|||||||
pubkey={goal.pubkey}
|
pubkey={goal.pubkey}
|
||||||
relays={getGoalRelays(goal)}
|
relays={getGoalRelays(goal)}
|
||||||
allowComment
|
allowComment
|
||||||
showEventPreview={false}
|
showEmbed={false}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
@ -1,38 +1,20 @@
|
|||||||
import { useEffect, useState } from "react";
|
|
||||||
import { Link as RouterLink } from "react-router-dom";
|
import { Link as RouterLink } from "react-router-dom";
|
||||||
import { Card, CardBody, CardHeader, CardProps, Flex, Heading, Link } from "@chakra-ui/react";
|
import { Card, CardBody, CardHeader, CardProps, Flex, Heading, Link } from "@chakra-ui/react";
|
||||||
|
|
||||||
import { ParsedStream, getATag } from "../../../helpers/nostr/stream";
|
import { ParsedStream } from "../../../helpers/nostr/stream";
|
||||||
import { NostrEvent } from "../../../types/nostr-event";
|
import { getGoalName } from "../../../helpers/nostr/goal";
|
||||||
import { NostrRequest } from "../../../classes/nostr-request";
|
|
||||||
import { useReadRelayUrls } from "../../../hooks/use-client-relays";
|
|
||||||
import { GOAL_KIND, getGoalName } from "../../../helpers/nostr/goal";
|
|
||||||
import GoalProgress from "../../goals/components/goal-progress";
|
import GoalProgress from "../../goals/components/goal-progress";
|
||||||
import { getSharableEventAddress } from "../../../helpers/nip19";
|
import { getSharableEventAddress } from "../../../helpers/nip19";
|
||||||
import GoalTopZappers from "../../goals/components/goal-top-zappers";
|
import GoalTopZappers from "../../goals/components/goal-top-zappers";
|
||||||
import GoalZapButton from "../../goals/components/goal-zap-button";
|
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 }) {
|
export default function StreamGoal({ stream, ...props }: Omit<CardProps, "children"> & { stream: ParsedStream }) {
|
||||||
const [goal, setGoal] = useState<NostrEvent>();
|
const goal = useStreamGoal(stream);
|
||||||
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("|")]);
|
|
||||||
|
|
||||||
if (!goal) return null;
|
if (!goal) return null;
|
||||||
const nevent = getSharableEventAddress(goal);
|
const nevent = getSharableEventAddress(goal);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card direction="column" gap="1" {...props}>
|
<Card direction="column" gap="1" {...props}>
|
||||||
<CardHeader px="2" pt="2" pb="0">
|
<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 useUserLNURLMetadata from "../../../hooks/use-user-lnurl-metadata";
|
||||||
import ZapModal from "../../../components/zap-modal";
|
import ZapModal from "../../../components/zap-modal";
|
||||||
import { useRelaySelectionRelays } from "../../../providers/relay-selection-provider";
|
import { useRelaySelectionRelays } from "../../../providers/relay-selection-provider";
|
||||||
|
import useStreamGoal from "../../../hooks/use-stream-goal";
|
||||||
|
|
||||||
export default function StreamZapButton({
|
export default function StreamZapButton({
|
||||||
stream,
|
stream,
|
||||||
@ -21,6 +22,7 @@ export default function StreamZapButton({
|
|||||||
const { requestPay } = useInvoiceModalContext();
|
const { requestPay } = useInvoiceModalContext();
|
||||||
const zapMetadata = useUserLNURLMetadata(stream.host);
|
const zapMetadata = useUserLNURLMetadata(stream.host);
|
||||||
const relays = useRelaySelectionRelays();
|
const relays = useRelaySelectionRelays();
|
||||||
|
const goal = useStreamGoal(stream);
|
||||||
|
|
||||||
const commonProps = {
|
const commonProps = {
|
||||||
"aria-label": "Zap stream",
|
"aria-label": "Zap stream",
|
||||||
@ -43,7 +45,7 @@ export default function StreamZapButton({
|
|||||||
{zapModal.isOpen && (
|
{zapModal.isOpen && (
|
||||||
<ZapModal
|
<ZapModal
|
||||||
isOpen
|
isOpen
|
||||||
event={stream.event}
|
event={goal || stream.event}
|
||||||
pubkey={stream.host}
|
pubkey={stream.host}
|
||||||
onInvoice={async (invoice) => {
|
onInvoice={async (invoice) => {
|
||||||
if (onZap) onZap();
|
if (onZap) onZap();
|
||||||
@ -53,6 +55,8 @@ export default function StreamZapButton({
|
|||||||
onClose={zapModal.onClose}
|
onClose={zapModal.onClose}
|
||||||
initialComment={initComment}
|
initialComment={initComment}
|
||||||
additionalRelays={relays}
|
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 useUserMuteList from "../../../../hooks/use-user-mute-list";
|
||||||
import { useRelaySelectionRelays } from "../../../../providers/relay-selection-provider";
|
import { useRelaySelectionRelays } from "../../../../providers/relay-selection-provider";
|
||||||
import { useCurrentAccount } from "../../../../hooks/use-current-account";
|
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) {
|
export default function useStreamChatTimeline(stream: ParsedStream) {
|
||||||
const account = useCurrentAccount();
|
const account = useCurrentAccount();
|
||||||
@ -20,14 +22,23 @@ export default function useStreamChatTimeline(stream: ParsedStream) {
|
|||||||
[hostMuteList, muteList],
|
[hostMuteList, muteList],
|
||||||
);
|
);
|
||||||
|
|
||||||
const eventFilter = useCallback((event: NostrEvent) => !mutedPubkeys.includes(event.pubkey), [mutedPubkeys]);
|
const goal = useStreamGoal(stream);
|
||||||
return useTimelineLoader(
|
const query = useMemo(() => {
|
||||||
`${getEventUID(stream.event)}-chat`,
|
const streamQuery: NostrQuery = {
|
||||||
streamRelays,
|
|
||||||
{
|
|
||||||
"#a": [getATag(stream)],
|
"#a": [getATag(stream)],
|
||||||
kinds: [STREAM_CHAT_MESSAGE_KIND, Kind.Zap],
|
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