Live event page
This commit is contained in:
parent
adaa8a71e7
commit
769e093663
11
packages/app/src/Element/LiveChat.css
Normal file
11
packages/app/src/Element/LiveChat.css
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
.live-chat {
|
||||||
|
height: calc(100vh - 73px);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-chat > div:nth-child(1) {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
63
packages/app/src/Element/LiveChat.tsx
Normal file
63
packages/app/src/Element/LiveChat.tsx
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import "./LiveChat.css";
|
||||||
|
import { NostrLink, TaggedRawEvent } from "@snort/system";
|
||||||
|
import { useUserProfile } from "@snort/system-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import Textarea from "./Textarea";
|
||||||
|
import { useLiveChatFeed } from "Feed/LiveChatFeed";
|
||||||
|
import useEventPublisher from "Feed/EventPublisher";
|
||||||
|
import { System } from "index";
|
||||||
|
import { getDisplayName } from "Element/ProfileImage";
|
||||||
|
|
||||||
|
export function LiveChat({ ev, link }: { ev: TaggedRawEvent; link: NostrLink }) {
|
||||||
|
const [chat, setChat] = useState("");
|
||||||
|
const messages = useLiveChatFeed(link);
|
||||||
|
const pub = useEventPublisher();
|
||||||
|
|
||||||
|
async function sendChatMessage() {
|
||||||
|
const reply = await pub?.note(chat, eb => {
|
||||||
|
return eb.tag(["a", `${link.kind}:${link.author}:${link.id}`]);
|
||||||
|
});
|
||||||
|
if (reply) {
|
||||||
|
console.debug(reply);
|
||||||
|
System.BroadcastEvent(reply);
|
||||||
|
}
|
||||||
|
setChat("");
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="live-chat">
|
||||||
|
<div>
|
||||||
|
{[...(messages.data ?? [])]
|
||||||
|
.sort((a, b) => b.created_at - a.created_at)
|
||||||
|
.map(a => (
|
||||||
|
<ChatMessage ev={a} key={a.id} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Textarea
|
||||||
|
autoFocus={false}
|
||||||
|
className=""
|
||||||
|
onChange={v => setChat(v.target.value)}
|
||||||
|
value={chat}
|
||||||
|
onFocus={() => {}}
|
||||||
|
placeholder=""
|
||||||
|
onKeyDown={async e => {
|
||||||
|
if (e.code === "Enter") {
|
||||||
|
await sendChatMessage();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChatMessage({ ev }: { ev: TaggedRawEvent }) {
|
||||||
|
const profile = useUserProfile(System, ev.pubkey);
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<b>{getDisplayName(profile, ev.pubkey)}</b>
|
||||||
|
:
|
||||||
|
{ev.content}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,11 +1,25 @@
|
|||||||
import { NostrEvent } from "@snort/system";
|
import { NostrEvent, NostrPrefix, encodeTLV } from "@snort/system";
|
||||||
import { findTag } from "SnortUtils";
|
import { findTag, unwrap } from "SnortUtils";
|
||||||
import { LiveVideoPlayer } from "Element/LiveVideoPlayer";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
export function LiveEvent({ ev }: { ev: NostrEvent }) {
|
export function LiveEvent({ ev }: { ev: NostrEvent }) {
|
||||||
const stream = findTag(ev, "streaming");
|
const title = findTag(ev, "title");
|
||||||
if (stream) {
|
const d = unwrap(findTag(ev, "d"));
|
||||||
return <LiveVideoPlayer src={stream} />;
|
return (
|
||||||
}
|
<div className="text">
|
||||||
return null;
|
<div className="flex card">
|
||||||
|
<div className="f-grow">
|
||||||
|
<h3>{title}</h3>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Link to={`/live/${encodeTLV(NostrPrefix.Address, d, undefined, ev.kind, ev.pubkey)}`}>
|
||||||
|
<button className="primary" type="button">
|
||||||
|
<FormattedMessage defaultMessage="Watch Live!" />
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,19 @@
|
|||||||
import Hls from "hls.js";
|
import Hls from "hls.js";
|
||||||
import { HTMLProps, useEffect, useRef } from "react";
|
import { HTMLProps, useEffect, useRef } from "react";
|
||||||
|
|
||||||
export function LiveVideoPlayer(props: HTMLProps<HTMLVideoElement>) {
|
export function LiveVideoPlayer(props: HTMLProps<HTMLVideoElement> & { stream: string }) {
|
||||||
const video = useRef<HTMLVideoElement>(null);
|
const video = useRef<HTMLVideoElement>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.src && video.current && !video.current.src && Hls.isSupported()) {
|
if (props.stream && video.current && !video.current.src && Hls.isSupported()) {
|
||||||
const hls = new Hls();
|
const hls = new Hls();
|
||||||
hls.loadSource(props.src);
|
hls.loadSource(props.stream);
|
||||||
hls.attachMedia(video.current);
|
hls.attachMedia(video.current);
|
||||||
return () => hls.destroy();
|
return () => hls.destroy();
|
||||||
}
|
}
|
||||||
}, [video, props]);
|
}, [video, props]);
|
||||||
return (
|
return (
|
||||||
<div className="w-max">
|
<div>
|
||||||
<video className="w-max" ref={video} controls={true} />
|
<video ref={video} {...props} controls={true} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -174,7 +174,7 @@ export function MediaElement(props: MediaElementProps) {
|
|||||||
return <audio key={props.url} src={url} controls onError={() => probeFor402()} />;
|
return <audio key={props.url} src={url} controls onError={() => probeFor402()} />;
|
||||||
} else if (props.mime.startsWith("video/")) {
|
} else if (props.mime.startsWith("video/")) {
|
||||||
if (props.url.endsWith(".m3u8")) {
|
if (props.url.endsWith(".m3u8")) {
|
||||||
return <LiveVideoPlayer src={props.url} />;
|
return <LiveVideoPlayer stream={props.url} />;
|
||||||
}
|
}
|
||||||
return <video key={props.url} src={url} controls onError={() => probeFor402()} />;
|
return <video key={props.url} src={url} controls onError={() => probeFor402()} />;
|
||||||
} else {
|
} else {
|
||||||
|
19
packages/app/src/Feed/LiveChatFeed.tsx
Normal file
19
packages/app/src/Feed/LiveChatFeed.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { FlatNoteStore, NostrLink, RequestBuilder } from "@snort/system";
|
||||||
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
|
import { System } from "index";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
export function useLiveChatFeed(link: NostrLink) {
|
||||||
|
const sub = useMemo(() => {
|
||||||
|
const rb = new RequestBuilder(`live:${link.id}:${link.author}`);
|
||||||
|
rb.withOptions({
|
||||||
|
leaveOpen: true,
|
||||||
|
});
|
||||||
|
rb.withFilter()
|
||||||
|
.tag("a", [`${link.kind}:${link.author}:${link.id}`])
|
||||||
|
.limit(100);
|
||||||
|
return rb;
|
||||||
|
}, [link]);
|
||||||
|
|
||||||
|
return useRequestBuilder<FlatNoteStore>(System, FlatNoteStore, sub);
|
||||||
|
}
|
@ -43,7 +43,7 @@ export default function Layout() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const shouldHideNoteCreator = useMemo(() => {
|
const shouldHideNoteCreator = useMemo(() => {
|
||||||
const hideOn = ["/settings", "/messages", "/new", "/login", "/donate", "/p/", "/e", "/subscribe"];
|
const hideOn = ["/settings", "/messages", "/new", "/login", "/donate", "/p/", "/e", "/subscribe", "/live"];
|
||||||
return isReplyNoteCreatorShowing || hideOn.some(a => location.pathname.startsWith(a));
|
return isReplyNoteCreatorShowing || hideOn.some(a => location.pathname.startsWith(a));
|
||||||
}, [location, isReplyNoteCreatorShowing]);
|
}, [location, isReplyNoteCreatorShowing]);
|
||||||
|
|
||||||
@ -53,9 +53,10 @@ export default function Layout() {
|
|||||||
}, [location]);
|
}, [location]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const widePage = ["/login", "/messages"];
|
const widePage = ["/login", "/messages", "/live"];
|
||||||
|
const noScroll = ["/messages", "/live"];
|
||||||
if (widePage.some(a => location.pathname.startsWith(a))) {
|
if (widePage.some(a => location.pathname.startsWith(a))) {
|
||||||
setPageClass("");
|
setPageClass(noScroll.some(a => location.pathname.startsWith(a)) ? "scroll-lock" : "");
|
||||||
} else {
|
} else {
|
||||||
setPageClass("page");
|
setPageClass("page");
|
||||||
}
|
}
|
||||||
|
8
packages/app/src/Pages/LivePage.css
Normal file
8
packages/app/src/Pages/LivePage.css
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
.live-page {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-page video {
|
||||||
|
width: 100%;
|
||||||
|
}
|
29
packages/app/src/Pages/LivePage.tsx
Normal file
29
packages/app/src/Pages/LivePage.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
import "./LivePage.css";
|
||||||
|
import { parseNostrLink } from "@snort/system";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
|
||||||
|
import { LiveVideoPlayer } from "Element/LiveVideoPlayer";
|
||||||
|
import { findTag, unwrap } from "SnortUtils";
|
||||||
|
import PageSpinner from "Element/PageSpinner";
|
||||||
|
import { LiveChat } from "Element/LiveChat";
|
||||||
|
import useEventFeed from "Feed/EventFeed";
|
||||||
|
|
||||||
|
export function LivePage() {
|
||||||
|
const params = useParams();
|
||||||
|
const link = parseNostrLink(unwrap(params.id));
|
||||||
|
const thisEvent = useEventFeed(link);
|
||||||
|
|
||||||
|
if (!thisEvent.data) {
|
||||||
|
return <PageSpinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="live-page main-content">
|
||||||
|
<div>
|
||||||
|
<h3>{findTag(thisEvent.data, "title")}</h3>
|
||||||
|
<LiveVideoPlayer stream={unwrap(findTag(thisEvent.data, "streaming"))} autoPlay={true} />
|
||||||
|
</div>
|
||||||
|
<LiveChat ev={thisEvent.data} link={link} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -445,7 +445,7 @@ div.form-col {
|
|||||||
grid-template-columns: auto;
|
grid-template-columns: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
body.scroll-lock {
|
.scroll-lock {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
@ -36,6 +36,7 @@ import DebugPage from "Pages/Debug";
|
|||||||
import { db } from "Db";
|
import { db } from "Db";
|
||||||
import { preload, RelayMetrics, UserCache, UserRelays } from "Cache";
|
import { preload, RelayMetrics, UserCache, UserRelays } from "Cache";
|
||||||
import { LoginStore } from "Login";
|
import { LoginStore } from "Login";
|
||||||
|
import { LivePage } from "Pages/LivePage";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Singleton nostr system
|
* Singleton nostr system
|
||||||
@ -145,6 +146,10 @@ export const router = createBrowserRouter([
|
|||||||
path: "/zap-pool",
|
path: "/zap-pool",
|
||||||
element: <ZapPoolPage />,
|
element: <ZapPoolPage />,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/live/:id",
|
||||||
|
element: <LivePage />,
|
||||||
|
},
|
||||||
...NewUserRoutes,
|
...NewUserRoutes,
|
||||||
...WalletRoutes,
|
...WalletRoutes,
|
||||||
...SubscribeRoutes,
|
...SubscribeRoutes,
|
||||||
|
Loading…
Reference in New Issue
Block a user