diff --git a/src/app.tsx b/src/app.tsx
index 675eda6cd..73281c4fd 100644
--- a/src/app.tsx
+++ b/src/app.tsx
@@ -1,5 +1,5 @@
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 { ErrorBoundary } from "./components/error-boundary";
import { Page } from "./components/page";
@@ -35,19 +35,24 @@ import ToolsHomeView from "./views/tools";
import Nip19ToolsView from "./views/tools/nip19";
import UserAboutTab from "./views/user/about";
import UserLikesTab from "./views/user/likes";
+import useSetColorMode from "./hooks/use-set-color-mode";
const LiveStreamsTab = React.lazy(() => import("./views/streams"));
const StreamView = React.lazy(() => import("./views/streams/stream"));
const SearchView = React.lazy(() => import("./views/search"));
-const RootPage = () => (
-
-
- }>
-
-
-
-);
+const RootPage = () => {
+ useSetColorMode();
+
+ return (
+
+
+ }>
+
+
+
+ );
+};
const router = createHashRouter([
{
@@ -118,19 +123,10 @@ const router = createHashRouter([
},
]);
-export const App = () => {
- const { setColorMode } = useColorMode();
- const { colorMode } = useSubject(appSettings);
-
- useEffect(() => {
- setColorMode(colorMode);
- }, [colorMode]);
-
- return (
-
- }>
-
-
-
- );
-};
+export const App = () => (
+
+ }>
+
+
+
+);
diff --git a/src/components/zap-modal.tsx b/src/components/zap-modal.tsx
index 1b1630dae..0b80da729 100644
--- a/src/components/zap-modal.tsx
+++ b/src/components/zap-modal.tsx
@@ -35,6 +35,7 @@ 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";
type FormValues = {
amount: number;
@@ -50,6 +51,7 @@ export type ZapModalProps = Omit & {
onInvoice: (invoice: string) => void;
allowComment?: boolean;
showEventPreview?: boolean;
+ additionalRelays?: string[];
};
export default function ZapModal({
@@ -62,9 +64,11 @@ export default function ZapModal({
onInvoice,
allowComment = true,
showEventPreview = true,
+ additionalRelays = [],
...props
}: ZapModalProps) {
const toast = useToast();
+ const contextRelays = useAdditionalRelayContext();
const { requestSignature } = useSigningContext();
const { customZapAmounts } = useSubject(appSettings);
const userReadRelays = useUserRelays(pubkey)
@@ -104,6 +108,7 @@ export default function ZapModal({
const writeRelays = clientRelaysService.getWriteUrls();
const writeRelaysRanked = relayScoreboardService.getRankedRelays(writeRelays).slice(0, 4);
const userReadRelaysRanked = relayScoreboardService.getRankedRelays(userReadRelays).slice(0, 4);
+ const contextRelaysRanked = relayScoreboardService.getRankedRelays(contextRelays).slice(0, 4);
const zapRequest: DraftNostrEvent = {
kind: Kind.ZapRequest,
@@ -111,10 +116,22 @@ export default function ZapModal({
content: values.comment,
tags: [
["p", pubkey],
- ["relays", ...unique([...writeRelaysRanked, ...userReadRelaysRanked, ...eventRelaysRanked])],
+ [
+ "relays",
+ ...unique([
+ ...contextRelaysRanked,
+ ...writeRelaysRanked,
+ ...userReadRelaysRanked,
+ ...eventRelaysRanked,
+ ...additionalRelays,
+ ]),
+ ],
["amount", String(amountInMilisat)],
],
};
+
+ console.log(zapRequest);
+
if (event) zapRequest.tags.push(["e", event.id]);
if (stream) zapRequest.tags.push(["a", getATag(stream)]);
diff --git a/src/helpers/nostr/stream.ts b/src/helpers/nostr/stream.ts
index 9ddd261bc..74fc38fea 100644
--- a/src/helpers/nostr/stream.ts
+++ b/src/helpers/nostr/stream.ts
@@ -1,10 +1,11 @@
import dayjs from "dayjs";
-import { DraftNostrEvent, NostrEvent } from "../../types/nostr-event";
+import { DraftNostrEvent, NostrEvent, isPTag } from "../../types/nostr-event";
import { unique } from "../array";
export type ParsedStream = {
event: NostrEvent;
author: string;
+ host: string;
title: string;
summary?: string;
image?: string;
@@ -42,10 +43,12 @@ export function parseStreamEvent(stream: NostrEvent): ParsedStream {
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));
return {
author: stream.pubkey,
+ host,
event: stream,
updated: stream.created_at,
streaming,
diff --git a/src/hooks/use-set-color-mode.ts b/src/hooks/use-set-color-mode.ts
new file mode 100644
index 000000000..72c0d7bb4
--- /dev/null
+++ b/src/hooks/use-set-color-mode.ts
@@ -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")]);
+}
diff --git a/src/views/streams/components/stream-card.tsx b/src/views/streams/components/stream-card.tsx
index 8bc6b5390..bc500fcf9 100644
--- a/src/views/streams/components/stream-card.tsx
+++ b/src/views/streams/components/stream-card.tsx
@@ -43,6 +43,7 @@ export default function StreamCard({ stream, ...props }: CardProps & { stream: P
const relays = getEventRelays(stream.event.id).value;
const ranked = relayScoreboardService.getRankedRelays(relays);
const onlyTwo = ranked.slice(0, 2);
+
return nip19.naddrEncode({
identifier,
relays: onlyTwo,
@@ -57,9 +58,9 @@ export default function StreamCard({ stream, ...props }: CardProps & { stream: P
{image && }
-
+
-
+
diff --git a/src/views/streams/stream/index.tsx b/src/views/streams/stream/index.tsx
index fc6316b9b..eedfbb9c6 100644
--- a/src/views/streams/stream/index.tsx
+++ b/src/views/streams/stream/index.tsx
@@ -1,41 +1,71 @@
import { useEffect, useRef, useState } from "react";
import { useScroll } from "react-use";
-import { Box, Button, Flex, Heading, Spacer, Spinner, Text } from "@chakra-ui/react";
-import { Link as RouterLink, useParams, Navigate } from "react-router-dom";
+import { Box, Button, ButtonGroup, Flex, Heading, Spacer, Spinner, Text } from "@chakra-ui/react";
+import { Link as RouterLink, useParams, Navigate, useSearchParams } from "react-router-dom";
import { nip19 } from "nostr-tools";
+import { Global, css } from "@emotion/react";
import { ParsedStream, parseStreamEvent } from "../../../helpers/nostr/stream";
import { NostrRequest } from "../../../classes/nostr-request";
import { useReadRelayUrls } from "../../../hooks/use-client-relays";
import { unique } from "../../../helpers/array";
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 { UserLink } from "../../../components/user-link";
import { useIsMobile } from "../../../hooks/use-is-mobile";
import { AdditionalRelayProvider } from "../../../providers/additional-relay-context";
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 scrollBox = useRef(null);
const scrollState = useScroll(scrollBox);
- const action =
- scrollState.y === 0 ? (
-
- ) : (
-
+ const renderActions = () => {
+ const toggleButton =
+ scrollState.y === 0 ? (
+
+ ) : (
+
+ );
+
+ return (
+
+ {isMobile && toggleButton}
+
+ }
+ 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
+
+
);
+ };
return (
-
-
-
-
-
-
-
-
- {stream.title}
-
-
-
+ {displayMode && (
+
+ )}
+ {!displayMode && (
+
+
+
+
+
+
+
+
+ {stream.title}
+
+
+
+
+
-
-
+ )}
);
@@ -79,6 +121,9 @@ function StreamPage({ stream }: { stream: ParsedStream }) {
export default function StreamView() {
const { naddr } = useParams();
+ const [params] = useSearchParams();
+ useSetColorMode();
+
if (!naddr) return ;
const readRelays = useReadRelayUrls();
@@ -104,8 +149,11 @@ export default function StreamView() {
if (!stream) return ;
return (
-
-
+ // add snort and damus relays so zap.stream will always see zaps
+
+
);
}
diff --git a/src/views/streams/stream/stream-chat/chat-message.tsx b/src/views/streams/stream/stream-chat/chat-message.tsx
index 491d78a4f..0b959de88 100644
--- a/src/views/streams/stream/stream-chat/chat-message.tsx
+++ b/src/views/streams/stream/stream-chat/chat-message.tsx
@@ -19,7 +19,7 @@ export default function ChatMessage({ event, stream }: { event: NostrEvent; stre
-
+
{": "}
diff --git a/src/views/streams/stream/stream-chat/index.tsx b/src/views/streams/stream/stream-chat/index.tsx
index 2ab874883..13898d4ed 100644
--- a/src/views/streams/stream/stream-chat/index.tsx
+++ b/src/views/streams/stream/stream-chat/index.tsx
@@ -35,16 +35,28 @@ import { useTimelineCurserIntersectionCallback } from "../../../../hooks/use-tim
import useSubject from "../../../../hooks/use-subject";
import { useTimelineLoader } from "../../../../hooks/use-timeline-loader";
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({
stream,
actions,
+ displayMode,
...props
-}: CardProps & { stream: ParsedStream; actions?: React.ReactNode }) {
+}: CardProps & { stream: ParsedStream; actions?: React.ReactNode; displayMode?: ChatDisplayMode }) {
const toast = useToast();
const contextRelays = useAdditionalRelayContext();
const readRelays = useReadRelayUrls(contextRelays);
- const userReadRelays = useUserRelays(stream.author)
+ const hostReadRelays = useUserRelays(stream.host)
.filter((r) => r.mode & RelayMode.READ)
.map((r) => r.url);
@@ -67,7 +79,7 @@ export default function StreamChat({
const draft = buildChatMessage(stream, values.content);
const signed = await requestSignature(draft);
if (!signed) throw new Error("Failed to sign");
- nostrPostAction(unique([...contextRelays, ...userReadRelays]), signed);
+ nostrPostAction(unique([...contextRelays, ...hostReadRelays]), signed);
reset();
} catch (e) {
if (e instanceof Error) toast({ description: e.message, status: "error" });
@@ -76,17 +88,22 @@ export default function StreamChat({
const zapModal = useDisclosure();
const { requestPay } = useInvoiceModalContext();
- const zapMetadata = useUserLNURLMetadata(stream.author);
+ const zapMetadata = useUserLNURLMetadata(stream.host);
+
+ const isPopup = !!displayMode;
+ const isChatLog = displayMode === "log";
return (
<>
-
-
- Stream Chat
- {actions}
-
+
+ {!isPopup && (
+
+ Stream Chat
+ {actions}
+
+ )}
{events.map((event) =>
event.kind === 1311 ? (
@@ -106,30 +124,32 @@ export default function StreamChat({
)
)}
-
-
-
- {zapMetadata.metadata?.allowsNostr && (
- }
- aria-label="Zap stream"
- borderColor="yellow.400"
- variant="outline"
- onClick={zapModal.onOpen}
- />
- )}
-
+ {!isChatLog && (
+
+
+
+ {zapMetadata.metadata?.allowsNostr && (
+ }
+ aria-label="Zap stream"
+ borderColor="yellow.400"
+ variant="outline"
+ onClick={zapModal.onOpen}
+ />
+ )}
+
+ )}
@@ -138,7 +158,7 @@ export default function StreamChat({
{
reset();
zapModal.onClose();
@@ -146,6 +166,7 @@ export default function StreamChat({
}}
onClose={zapModal.onClose}
initialComment={getValues().content}
+ additionalRelays={contextRelays}
/>
)}
>