mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-09-19 03:51:34 +02:00
add stream chat popup
correctly show host user on streams
This commit is contained in:
46
src/app.tsx
46
src/app.tsx
@@ -1,5 +1,5 @@
|
|||||||
import React, { Suspense, useEffect } from "react";
|
import React, { Suspense, useEffect } from "react";
|
||||||
import { createHashRouter, Outlet, RouterProvider, ScrollRestoration, useLocation } from "react-router-dom";
|
import { createHashRouter, Outlet, RouterProvider, ScrollRestoration, useSearchParams } from "react-router-dom";
|
||||||
import { Spinner, useColorMode } from "@chakra-ui/react";
|
import { Spinner, useColorMode } from "@chakra-ui/react";
|
||||||
import { ErrorBoundary } from "./components/error-boundary";
|
import { ErrorBoundary } from "./components/error-boundary";
|
||||||
import { Page } from "./components/page";
|
import { Page } from "./components/page";
|
||||||
@@ -35,19 +35,24 @@ import ToolsHomeView from "./views/tools";
|
|||||||
import Nip19ToolsView from "./views/tools/nip19";
|
import Nip19ToolsView from "./views/tools/nip19";
|
||||||
import UserAboutTab from "./views/user/about";
|
import UserAboutTab from "./views/user/about";
|
||||||
import UserLikesTab from "./views/user/likes";
|
import UserLikesTab from "./views/user/likes";
|
||||||
|
import useSetColorMode from "./hooks/use-set-color-mode";
|
||||||
|
|
||||||
const LiveStreamsTab = React.lazy(() => import("./views/streams"));
|
const LiveStreamsTab = React.lazy(() => import("./views/streams"));
|
||||||
const StreamView = React.lazy(() => import("./views/streams/stream"));
|
const StreamView = React.lazy(() => import("./views/streams/stream"));
|
||||||
const SearchView = React.lazy(() => import("./views/search"));
|
const SearchView = React.lazy(() => import("./views/search"));
|
||||||
|
|
||||||
const RootPage = () => (
|
const RootPage = () => {
|
||||||
<Page>
|
useSetColorMode();
|
||||||
<ScrollRestoration />
|
|
||||||
<Suspense fallback={<Spinner />}>
|
return (
|
||||||
<Outlet />
|
<Page>
|
||||||
</Suspense>
|
<ScrollRestoration />
|
||||||
</Page>
|
<Suspense fallback={<Spinner />}>
|
||||||
);
|
<Outlet />
|
||||||
|
</Suspense>
|
||||||
|
</Page>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const router = createHashRouter([
|
const router = createHashRouter([
|
||||||
{
|
{
|
||||||
@@ -118,19 +123,10 @@ const router = createHashRouter([
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const App = () => {
|
export const App = () => (
|
||||||
const { setColorMode } = useColorMode();
|
<ErrorBoundary>
|
||||||
const { colorMode } = useSubject(appSettings);
|
<Suspense fallback={<Spinner />}>
|
||||||
|
<RouterProvider router={router} />
|
||||||
useEffect(() => {
|
</Suspense>
|
||||||
setColorMode(colorMode);
|
</ErrorBoundary>
|
||||||
}, [colorMode]);
|
);
|
||||||
|
|
||||||
return (
|
|
||||||
<ErrorBoundary>
|
|
||||||
<Suspense fallback={<Spinner />}>
|
|
||||||
<RouterProvider router={router} />
|
|
||||||
</Suspense>
|
|
||||||
</ErrorBoundary>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
@@ -35,6 +35,7 @@ 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";
|
||||||
|
|
||||||
type FormValues = {
|
type FormValues = {
|
||||||
amount: number;
|
amount: number;
|
||||||
@@ -50,6 +51,7 @@ export type ZapModalProps = Omit<ModalProps, "children"> & {
|
|||||||
onInvoice: (invoice: string) => void;
|
onInvoice: (invoice: string) => void;
|
||||||
allowComment?: boolean;
|
allowComment?: boolean;
|
||||||
showEventPreview?: boolean;
|
showEventPreview?: boolean;
|
||||||
|
additionalRelays?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ZapModal({
|
export default function ZapModal({
|
||||||
@@ -62,9 +64,11 @@ export default function ZapModal({
|
|||||||
onInvoice,
|
onInvoice,
|
||||||
allowComment = true,
|
allowComment = true,
|
||||||
showEventPreview = true,
|
showEventPreview = true,
|
||||||
|
additionalRelays = [],
|
||||||
...props
|
...props
|
||||||
}: ZapModalProps) {
|
}: ZapModalProps) {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
const contextRelays = useAdditionalRelayContext();
|
||||||
const { requestSignature } = useSigningContext();
|
const { requestSignature } = useSigningContext();
|
||||||
const { customZapAmounts } = useSubject(appSettings);
|
const { customZapAmounts } = useSubject(appSettings);
|
||||||
const userReadRelays = useUserRelays(pubkey)
|
const userReadRelays = useUserRelays(pubkey)
|
||||||
@@ -104,6 +108,7 @@ export default function ZapModal({
|
|||||||
const writeRelays = clientRelaysService.getWriteUrls();
|
const writeRelays = clientRelaysService.getWriteUrls();
|
||||||
const writeRelaysRanked = relayScoreboardService.getRankedRelays(writeRelays).slice(0, 4);
|
const writeRelaysRanked = relayScoreboardService.getRankedRelays(writeRelays).slice(0, 4);
|
||||||
const userReadRelaysRanked = relayScoreboardService.getRankedRelays(userReadRelays).slice(0, 4);
|
const userReadRelaysRanked = relayScoreboardService.getRankedRelays(userReadRelays).slice(0, 4);
|
||||||
|
const contextRelaysRanked = relayScoreboardService.getRankedRelays(contextRelays).slice(0, 4);
|
||||||
|
|
||||||
const zapRequest: DraftNostrEvent = {
|
const zapRequest: DraftNostrEvent = {
|
||||||
kind: Kind.ZapRequest,
|
kind: Kind.ZapRequest,
|
||||||
@@ -111,10 +116,22 @@ export default function ZapModal({
|
|||||||
content: values.comment,
|
content: values.comment,
|
||||||
tags: [
|
tags: [
|
||||||
["p", pubkey],
|
["p", pubkey],
|
||||||
["relays", ...unique([...writeRelaysRanked, ...userReadRelaysRanked, ...eventRelaysRanked])],
|
[
|
||||||
|
"relays",
|
||||||
|
...unique([
|
||||||
|
...contextRelaysRanked,
|
||||||
|
...writeRelaysRanked,
|
||||||
|
...userReadRelaysRanked,
|
||||||
|
...eventRelaysRanked,
|
||||||
|
...additionalRelays,
|
||||||
|
]),
|
||||||
|
],
|
||||||
["amount", String(amountInMilisat)],
|
["amount", String(amountInMilisat)],
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
console.log(zapRequest);
|
||||||
|
|
||||||
if (event) zapRequest.tags.push(["e", event.id]);
|
if (event) zapRequest.tags.push(["e", event.id]);
|
||||||
if (stream) zapRequest.tags.push(["a", getATag(stream)]);
|
if (stream) zapRequest.tags.push(["a", getATag(stream)]);
|
||||||
|
|
||||||
|
@@ -1,10 +1,11 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
|
import { DraftNostrEvent, NostrEvent, isPTag } from "../../types/nostr-event";
|
||||||
import { unique } from "../array";
|
import { unique } from "../array";
|
||||||
|
|
||||||
export type ParsedStream = {
|
export type ParsedStream = {
|
||||||
event: NostrEvent;
|
event: NostrEvent;
|
||||||
author: string;
|
author: string;
|
||||||
|
host: string;
|
||||||
title: string;
|
title: string;
|
||||||
summary?: string;
|
summary?: string;
|
||||||
image?: string;
|
image?: string;
|
||||||
@@ -42,10 +43,12 @@ export function parseStreamEvent(stream: NostrEvent): ParsedStream {
|
|||||||
status = "ended";
|
status = "ended";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const host = stream.tags.filter(isPTag)[0]?.[1] ?? stream.pubkey;
|
||||||
const tags = unique(stream.tags.filter((t) => t[0] === "t" && t[1]).map((t) => t[1] as string));
|
const tags = unique(stream.tags.filter((t) => t[0] === "t" && t[1]).map((t) => t[1] as string));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
author: stream.pubkey,
|
author: stream.pubkey,
|
||||||
|
host,
|
||||||
event: stream,
|
event: stream,
|
||||||
updated: stream.created_at,
|
updated: stream.created_at,
|
||||||
streaming,
|
streaming,
|
||||||
|
15
src/hooks/use-set-color-mode.ts
Normal file
15
src/hooks/use-set-color-mode.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import { useColorMode } from "@chakra-ui/react";
|
||||||
|
import useSubject from "./use-subject";
|
||||||
|
import appSettings from "../services/app-settings";
|
||||||
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
export default function useSetColorMode() {
|
||||||
|
const { setColorMode } = useColorMode();
|
||||||
|
const { colorMode } = useSubject(appSettings);
|
||||||
|
const [params] = useSearchParams();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setColorMode(params.get("colorMode") || colorMode);
|
||||||
|
}, [colorMode, params.get("colorMode")]);
|
||||||
|
}
|
@@ -43,6 +43,7 @@ export default function StreamCard({ stream, ...props }: CardProps & { stream: P
|
|||||||
const relays = getEventRelays(stream.event.id).value;
|
const relays = getEventRelays(stream.event.id).value;
|
||||||
const ranked = relayScoreboardService.getRankedRelays(relays);
|
const ranked = relayScoreboardService.getRankedRelays(relays);
|
||||||
const onlyTwo = ranked.slice(0, 2);
|
const onlyTwo = ranked.slice(0, 2);
|
||||||
|
|
||||||
return nip19.naddrEncode({
|
return nip19.naddrEncode({
|
||||||
identifier,
|
identifier,
|
||||||
relays: onlyTwo,
|
relays: onlyTwo,
|
||||||
@@ -57,9 +58,9 @@ export default function StreamCard({ stream, ...props }: CardProps & { stream: P
|
|||||||
<LinkBox as={CardBody} p="2" display="flex" flexDirection="column" gap="2">
|
<LinkBox as={CardBody} p="2" display="flex" flexDirection="column" gap="2">
|
||||||
{image && <Image src={image} alt={title} borderRadius="lg" />}
|
{image && <Image src={image} alt={title} borderRadius="lg" />}
|
||||||
<Flex gap="2" alignItems="center">
|
<Flex gap="2" alignItems="center">
|
||||||
<UserAvatar pubkey={stream.author} size="sm" />
|
<UserAvatar pubkey={stream.host} size="sm" noProxy />
|
||||||
<Heading size="sm">
|
<Heading size="sm">
|
||||||
<UserLink pubkey={stream.author} />
|
<UserLink pubkey={stream.host} />
|
||||||
</Heading>
|
</Heading>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Heading size="md">
|
<Heading size="md">
|
||||||
|
@@ -1,41 +1,71 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import { useScroll } from "react-use";
|
import { useScroll } from "react-use";
|
||||||
import { Box, Button, Flex, Heading, Spacer, Spinner, Text } from "@chakra-ui/react";
|
import { Box, Button, ButtonGroup, Flex, Heading, Spacer, Spinner, Text } from "@chakra-ui/react";
|
||||||
import { Link as RouterLink, useParams, Navigate } from "react-router-dom";
|
import { Link as RouterLink, useParams, Navigate, useSearchParams } from "react-router-dom";
|
||||||
import { nip19 } from "nostr-tools";
|
import { nip19 } from "nostr-tools";
|
||||||
|
import { Global, css } from "@emotion/react";
|
||||||
|
|
||||||
import { ParsedStream, parseStreamEvent } from "../../../helpers/nostr/stream";
|
import { ParsedStream, parseStreamEvent } from "../../../helpers/nostr/stream";
|
||||||
import { NostrRequest } from "../../../classes/nostr-request";
|
import { NostrRequest } from "../../../classes/nostr-request";
|
||||||
import { useReadRelayUrls } from "../../../hooks/use-client-relays";
|
import { useReadRelayUrls } from "../../../hooks/use-client-relays";
|
||||||
import { unique } from "../../../helpers/array";
|
import { unique } from "../../../helpers/array";
|
||||||
import { LiveVideoPlayer } from "../../../components/live-video-player";
|
import { LiveVideoPlayer } from "../../../components/live-video-player";
|
||||||
import StreamChat from "./stream-chat";
|
import StreamChat, { ChatDisplayMode } from "./stream-chat";
|
||||||
import { UserAvatarLink } from "../../../components/user-avatar-link";
|
import { UserAvatarLink } from "../../../components/user-avatar-link";
|
||||||
import { UserLink } from "../../../components/user-link";
|
import { UserLink } from "../../../components/user-link";
|
||||||
import { useIsMobile } from "../../../hooks/use-is-mobile";
|
import { useIsMobile } from "../../../hooks/use-is-mobile";
|
||||||
import { AdditionalRelayProvider } from "../../../providers/additional-relay-context";
|
import { AdditionalRelayProvider } from "../../../providers/additional-relay-context";
|
||||||
import StreamSummaryContent from "../components/stream-summary-content";
|
import StreamSummaryContent from "../components/stream-summary-content";
|
||||||
import { ArrowDownSIcon, ArrowUpSIcon } from "../../../components/icons";
|
import { ArrowDownSIcon, ArrowUpSIcon, ExternalLinkIcon } from "../../../components/icons";
|
||||||
|
import useSetColorMode from "../../../hooks/use-set-color-mode";
|
||||||
|
import { CopyIconButton } from "../../../components/copy-icon-button";
|
||||||
|
|
||||||
function StreamPage({ stream }: { stream: ParsedStream }) {
|
function StreamPage({ stream, displayMode }: { stream: ParsedStream; displayMode?: ChatDisplayMode }) {
|
||||||
const isMobile = useIsMobile();
|
const isMobile = useIsMobile();
|
||||||
const scrollBox = useRef<HTMLDivElement | null>(null);
|
const scrollBox = useRef<HTMLDivElement | null>(null);
|
||||||
const scrollState = useScroll(scrollBox);
|
const scrollState = useScroll(scrollBox);
|
||||||
|
|
||||||
const action =
|
const renderActions = () => {
|
||||||
scrollState.y === 0 ? (
|
const toggleButton =
|
||||||
<Button
|
scrollState.y === 0 ? (
|
||||||
size="sm"
|
<Button
|
||||||
onClick={() => scrollBox.current?.scroll(0, scrollBox.current.scrollHeight)}
|
size="sm"
|
||||||
leftIcon={<ArrowDownSIcon />}
|
onClick={() => scrollBox.current?.scroll(0, scrollBox.current.scrollHeight)}
|
||||||
>
|
leftIcon={<ArrowDownSIcon />}
|
||||||
View Chat
|
>
|
||||||
</Button>
|
View Chat
|
||||||
) : (
|
</Button>
|
||||||
<Button size="sm" onClick={() => scrollBox.current?.scroll(0, 0)} leftIcon={<ArrowUpSIcon />}>
|
) : (
|
||||||
View Stream
|
<Button size="sm" onClick={() => scrollBox.current?.scroll(0, 0)} leftIcon={<ArrowUpSIcon />}>
|
||||||
</Button>
|
View Stream
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ButtonGroup>
|
||||||
|
{isMobile && toggleButton}
|
||||||
|
<CopyIconButton
|
||||||
|
text={location.href + "?displayMode=log&colorMode=dark"}
|
||||||
|
aria-label="Copy chat log URL"
|
||||||
|
title="Copy chat log URL"
|
||||||
|
size="sm"
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
rightIcon={<ExternalLinkIcon />}
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
const w = 512;
|
||||||
|
const h = 910;
|
||||||
|
const y = window.screenTop + window.innerHeight - h;
|
||||||
|
const x = window.screenLeft + window.innerWidth - w;
|
||||||
|
window.open(location.href + "?displayMode=popup", "_blank", `width=${w},height=${h},left=${x},top=${y}`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Open
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
@@ -43,35 +73,47 @@ function StreamPage({ stream }: { stream: ParsedStream }) {
|
|||||||
overflowX="hidden"
|
overflowX="hidden"
|
||||||
overflowY="auto"
|
overflowY="auto"
|
||||||
direction={isMobile ? "column" : "row"}
|
direction={isMobile ? "column" : "row"}
|
||||||
p={isMobile ? 0 : "2"}
|
p={isMobile || !!displayMode ? 0 : "2"}
|
||||||
gap={isMobile ? 0 : "4"}
|
gap={isMobile ? 0 : "4"}
|
||||||
ref={scrollBox}
|
ref={scrollBox}
|
||||||
>
|
>
|
||||||
<Flex gap={isMobile ? "2" : "4"} direction="column" flexGrow={isMobile ? 0 : 1}>
|
{displayMode && (
|
||||||
<LiveVideoPlayer stream={stream.streaming} autoPlay poster={stream.image} maxH="100vh" />
|
<Global
|
||||||
<Flex gap={isMobile ? "2" : "4"} alignItems="center" p={isMobile ? "2" : 0}>
|
styles={css`
|
||||||
<UserAvatarLink pubkey={stream.author} />
|
body {
|
||||||
<Box>
|
background: transparent;
|
||||||
<Heading size="md">
|
}
|
||||||
<UserLink pubkey={stream.author} />
|
`}
|
||||||
</Heading>
|
/>
|
||||||
<Text>{stream.title}</Text>
|
)}
|
||||||
</Box>
|
{!displayMode && (
|
||||||
<Spacer />
|
<Flex gap={isMobile ? "2" : "4"} direction="column" flexGrow={isMobile ? 0 : 1}>
|
||||||
<Button as={RouterLink} to="/streams">
|
<LiveVideoPlayer stream={stream.streaming} autoPlay poster={stream.image} maxH="100vh" />
|
||||||
Back
|
<Flex gap={isMobile ? "2" : "4"} alignItems="center" p={isMobile ? "2" : 0}>
|
||||||
</Button>
|
<UserAvatarLink pubkey={stream.host} noProxy />
|
||||||
|
<Box>
|
||||||
|
<Heading size="md">
|
||||||
|
<UserLink pubkey={stream.host} />
|
||||||
|
</Heading>
|
||||||
|
<Text>{stream.title}</Text>
|
||||||
|
</Box>
|
||||||
|
<Spacer />
|
||||||
|
<Button as={RouterLink} to="/streams">
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
<StreamSummaryContent stream={stream} px={isMobile ? "2" : 0} />
|
||||||
</Flex>
|
</Flex>
|
||||||
<StreamSummaryContent stream={stream} px={isMobile ? "2" : 0} />
|
)}
|
||||||
</Flex>
|
|
||||||
<StreamChat
|
<StreamChat
|
||||||
stream={stream}
|
stream={stream}
|
||||||
flexGrow={1}
|
flexGrow={1}
|
||||||
maxW={isMobile ? undefined : "lg"}
|
maxW={isMobile || !!displayMode ? undefined : "lg"}
|
||||||
maxH="100vh"
|
maxH="100vh"
|
||||||
minH={isMobile ? "100vh" : undefined}
|
minH={isMobile ? "100vh" : undefined}
|
||||||
flexShrink={0}
|
flexShrink={0}
|
||||||
actions={isMobile && action}
|
actions={renderActions()}
|
||||||
|
displayMode={displayMode}
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
@@ -79,6 +121,9 @@ function StreamPage({ stream }: { stream: ParsedStream }) {
|
|||||||
|
|
||||||
export default function StreamView() {
|
export default function StreamView() {
|
||||||
const { naddr } = useParams();
|
const { naddr } = useParams();
|
||||||
|
const [params] = useSearchParams();
|
||||||
|
useSetColorMode();
|
||||||
|
|
||||||
if (!naddr) return <Navigate replace to="/streams" />;
|
if (!naddr) return <Navigate replace to="/streams" />;
|
||||||
|
|
||||||
const readRelays = useReadRelayUrls();
|
const readRelays = useReadRelayUrls();
|
||||||
@@ -104,8 +149,11 @@ export default function StreamView() {
|
|||||||
|
|
||||||
if (!stream) return <Spinner />;
|
if (!stream) return <Spinner />;
|
||||||
return (
|
return (
|
||||||
<AdditionalRelayProvider relays={relays}>
|
// add snort and damus relays so zap.stream will always see zaps
|
||||||
<StreamPage stream={stream} />
|
<AdditionalRelayProvider
|
||||||
|
relays={unique([...relays, "wss://relay.snort.social", "wss://relay.damus.io", "wss://nos.lol"])}
|
||||||
|
>
|
||||||
|
<StreamPage stream={stream} displayMode={(params.get("displayMode") as ChatDisplayMode) ?? undefined} />
|
||||||
</AdditionalRelayProvider>
|
</AdditionalRelayProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -19,7 +19,7 @@ export default function ChatMessage({ event, stream }: { event: NostrEvent; stre
|
|||||||
<NoteZapButton note={event} size="xs" variant="ghost" float="right" ml="2" allowComment={false} />
|
<NoteZapButton note={event} size="xs" variant="ghost" float="right" ml="2" allowComment={false} />
|
||||||
<Text ref={ref}>
|
<Text ref={ref}>
|
||||||
<UserAvatar pubkey={event.pubkey} size="xs" display="inline-block" mr="2" />
|
<UserAvatar pubkey={event.pubkey} size="xs" display="inline-block" mr="2" />
|
||||||
<Text as="span" fontWeight="bold" color={event.pubkey === stream.author ? "rgb(248, 56, 217)" : "cyan"}>
|
<Text as="span" fontWeight="bold" color={event.pubkey === stream.host ? "rgb(248, 56, 217)" : "cyan"}>
|
||||||
<UserLink pubkey={event.pubkey} />
|
<UserLink pubkey={event.pubkey} />
|
||||||
{": "}
|
{": "}
|
||||||
</Text>
|
</Text>
|
||||||
|
@@ -35,16 +35,28 @@ import { useTimelineCurserIntersectionCallback } from "../../../../hooks/use-tim
|
|||||||
import useSubject from "../../../../hooks/use-subject";
|
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";
|
||||||
|
|
||||||
|
const hideScrollbar = css`
|
||||||
|
scrollbar-width: 0;
|
||||||
|
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export type ChatDisplayMode = "log" | "popup";
|
||||||
|
|
||||||
export default function StreamChat({
|
export default function StreamChat({
|
||||||
stream,
|
stream,
|
||||||
actions,
|
actions,
|
||||||
|
displayMode,
|
||||||
...props
|
...props
|
||||||
}: CardProps & { stream: ParsedStream; actions?: React.ReactNode }) {
|
}: CardProps & { stream: ParsedStream; actions?: React.ReactNode; displayMode?: ChatDisplayMode }) {
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const contextRelays = useAdditionalRelayContext();
|
const contextRelays = useAdditionalRelayContext();
|
||||||
const readRelays = useReadRelayUrls(contextRelays);
|
const readRelays = useReadRelayUrls(contextRelays);
|
||||||
const userReadRelays = useUserRelays(stream.author)
|
const hostReadRelays = useUserRelays(stream.host)
|
||||||
.filter((r) => r.mode & RelayMode.READ)
|
.filter((r) => r.mode & RelayMode.READ)
|
||||||
.map((r) => r.url);
|
.map((r) => r.url);
|
||||||
|
|
||||||
@@ -67,7 +79,7 @@ export default function StreamChat({
|
|||||||
const draft = buildChatMessage(stream, values.content);
|
const draft = buildChatMessage(stream, values.content);
|
||||||
const signed = await requestSignature(draft);
|
const signed = await requestSignature(draft);
|
||||||
if (!signed) throw new Error("Failed to sign");
|
if (!signed) throw new Error("Failed to sign");
|
||||||
nostrPostAction(unique([...contextRelays, ...userReadRelays]), signed);
|
nostrPostAction(unique([...contextRelays, ...hostReadRelays]), signed);
|
||||||
reset();
|
reset();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
if (e instanceof Error) toast({ description: e.message, status: "error" });
|
||||||
@@ -76,17 +88,22 @@ export default function StreamChat({
|
|||||||
|
|
||||||
const zapModal = useDisclosure();
|
const zapModal = useDisclosure();
|
||||||
const { requestPay } = useInvoiceModalContext();
|
const { requestPay } = useInvoiceModalContext();
|
||||||
const zapMetadata = useUserLNURLMetadata(stream.author);
|
const zapMetadata = useUserLNURLMetadata(stream.host);
|
||||||
|
|
||||||
|
const isPopup = !!displayMode;
|
||||||
|
const isChatLog = displayMode === "log";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<IntersectionObserverProvider callback={callback} root={scrollBox}>
|
<IntersectionObserverProvider callback={callback} root={scrollBox}>
|
||||||
<ImageGalleryProvider>
|
<ImageGalleryProvider>
|
||||||
<Card {...props} overflow="hidden">
|
<Card {...props} overflow="hidden" background={isChatLog ? "transparent" : undefined}>
|
||||||
<CardHeader py="3" display="flex" justifyContent="space-between" alignItems="center">
|
{!isPopup && (
|
||||||
<Heading size="md">Stream Chat</Heading>
|
<CardHeader py="3" display="flex" justifyContent="space-between" alignItems="center">
|
||||||
{actions}
|
<Heading size="md">Stream Chat</Heading>
|
||||||
</CardHeader>
|
{actions}
|
||||||
|
</CardHeader>
|
||||||
|
)}
|
||||||
<CardBody display="flex" flexDirection="column" gap="2" overflow="hidden" p={0}>
|
<CardBody display="flex" flexDirection="column" gap="2" overflow="hidden" p={0}>
|
||||||
<Flex
|
<Flex
|
||||||
overflowY="scroll"
|
overflowY="scroll"
|
||||||
@@ -97,6 +114,7 @@ export default function StreamChat({
|
|||||||
px="4"
|
px="4"
|
||||||
py="2"
|
py="2"
|
||||||
gap="2"
|
gap="2"
|
||||||
|
css={isChatLog && hideScrollbar}
|
||||||
>
|
>
|
||||||
{events.map((event) =>
|
{events.map((event) =>
|
||||||
event.kind === 1311 ? (
|
event.kind === 1311 ? (
|
||||||
@@ -106,30 +124,32 @@ export default function StreamChat({
|
|||||||
)
|
)
|
||||||
)}
|
)}
|
||||||
</Flex>
|
</Flex>
|
||||||
<Box
|
{!isChatLog && (
|
||||||
as="form"
|
<Box
|
||||||
borderRadius="md"
|
as="form"
|
||||||
flexShrink={0}
|
borderRadius="md"
|
||||||
display="flex"
|
flexShrink={0}
|
||||||
gap="2"
|
display="flex"
|
||||||
px="2"
|
gap="2"
|
||||||
pb="2"
|
px="2"
|
||||||
onSubmit={sendMessage}
|
pb="2"
|
||||||
>
|
onSubmit={sendMessage}
|
||||||
<Input placeholder="Message" {...register("content", { required: true })} autoComplete="off" />
|
>
|
||||||
<Button colorScheme="brand" type="submit" isLoading={formState.isSubmitting}>
|
<Input placeholder="Message" {...register("content", { required: true })} autoComplete="off" />
|
||||||
Send
|
<Button colorScheme="brand" type="submit" isLoading={formState.isSubmitting}>
|
||||||
</Button>
|
Send
|
||||||
{zapMetadata.metadata?.allowsNostr && (
|
</Button>
|
||||||
<IconButton
|
{zapMetadata.metadata?.allowsNostr && (
|
||||||
icon={<LightningIcon color="yellow.400" />}
|
<IconButton
|
||||||
aria-label="Zap stream"
|
icon={<LightningIcon color="yellow.400" />}
|
||||||
borderColor="yellow.400"
|
aria-label="Zap stream"
|
||||||
variant="outline"
|
borderColor="yellow.400"
|
||||||
onClick={zapModal.onOpen}
|
variant="outline"
|
||||||
/>
|
onClick={zapModal.onOpen}
|
||||||
)}
|
/>
|
||||||
</Box>
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
</ImageGalleryProvider>
|
</ImageGalleryProvider>
|
||||||
@@ -138,7 +158,7 @@ export default function StreamChat({
|
|||||||
<ZapModal
|
<ZapModal
|
||||||
isOpen
|
isOpen
|
||||||
stream={stream}
|
stream={stream}
|
||||||
pubkey={stream.author}
|
pubkey={stream.host}
|
||||||
onInvoice={async (invoice) => {
|
onInvoice={async (invoice) => {
|
||||||
reset();
|
reset();
|
||||||
zapModal.onClose();
|
zapModal.onClose();
|
||||||
@@ -146,6 +166,7 @@ export default function StreamChat({
|
|||||||
}}
|
}}
|
||||||
onClose={zapModal.onClose}
|
onClose={zapModal.onClose}
|
||||||
initialComment={getValues().content}
|
initialComment={getValues().content}
|
||||||
|
additionalRelays={contextRelays}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
Reference in New Issue
Block a user