forked from Kieran/snort
Update live stream page
This commit is contained in:
parent
9f00157bdc
commit
fc2304c9fa
@ -1,11 +1,46 @@
|
|||||||
.live-chat {
|
.live-chat {
|
||||||
height: calc(100vh - 73px);
|
height: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.live-chat > div:nth-child(1) {
|
.live-chat > div:nth-child(1) {
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 29px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-chat > div:nth-child(2) {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
flex-direction: column-reverse;
|
flex-direction: column-reverse;
|
||||||
|
margin-block-end: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-chat > div:nth-child(3) {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-chat .message {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-chat .message .name {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--highlight);
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-chat .message .avatar {
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,34 @@
|
|||||||
import "./LiveChat.css";
|
import "./LiveChat.css";
|
||||||
import { NostrLink, TaggedRawEvent } from "@snort/system";
|
import { EventKind, NostrLink, TaggedRawEvent } from "@snort/system";
|
||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Textarea from "./Textarea";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
|
import Textarea from "Element/Textarea";
|
||||||
import { useLiveChatFeed } from "Feed/LiveChatFeed";
|
import { useLiveChatFeed } from "Feed/LiveChatFeed";
|
||||||
import useEventPublisher from "Feed/EventPublisher";
|
import useEventPublisher from "Feed/EventPublisher";
|
||||||
import { System } from "index";
|
|
||||||
import { getDisplayName } from "Element/ProfileImage";
|
import { getDisplayName } from "Element/ProfileImage";
|
||||||
|
import Avatar from "Element/Avatar";
|
||||||
|
import AsyncButton from "Element/AsyncButton";
|
||||||
|
import Text from "Element/Text";
|
||||||
|
import { System } from "index";
|
||||||
|
import { profileLink } from "SnortUtils";
|
||||||
|
|
||||||
export function LiveChat({ ev, link }: { ev: TaggedRawEvent; link: NostrLink }) {
|
export function LiveChat({ ev, link }: { ev: TaggedRawEvent; link: NostrLink }) {
|
||||||
const [chat, setChat] = useState("");
|
const [chat, setChat] = useState("");
|
||||||
const messages = useLiveChatFeed(link);
|
const messages = useLiveChatFeed(link);
|
||||||
const pub = useEventPublisher();
|
const pub = useEventPublisher();
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
|
|
||||||
async function sendChatMessage() {
|
async function sendChatMessage() {
|
||||||
const reply = await pub?.note(chat, eb => {
|
if (chat.length > 1) {
|
||||||
return eb.tag(["a", `${link.kind}:${link.author}:${link.id}`]);
|
const reply = await pub?.generic(eb => {
|
||||||
|
return eb
|
||||||
|
.kind(1311 as EventKind)
|
||||||
|
.content(chat)
|
||||||
|
.tag(["a", `${link.kind}:${link.author}:${link.id}`])
|
||||||
|
.processContent();
|
||||||
});
|
});
|
||||||
if (reply) {
|
if (reply) {
|
||||||
console.debug(reply);
|
console.debug(reply);
|
||||||
@ -23,8 +36,12 @@ export function LiveChat({ ev, link }: { ev: TaggedRawEvent; link: NostrLink })
|
|||||||
}
|
}
|
||||||
setChat("");
|
setChat("");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div className="live-chat">
|
<div className="live-chat">
|
||||||
|
<div>
|
||||||
|
<FormattedMessage defaultMessage="Stream Chat" />
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
{[...(messages.data ?? [])]
|
{[...(messages.data ?? [])]
|
||||||
.sort((a, b) => b.created_at - a.created_at)
|
.sort((a, b) => b.created_at - a.created_at)
|
||||||
@ -39,13 +56,19 @@ export function LiveChat({ ev, link }: { ev: TaggedRawEvent; link: NostrLink })
|
|||||||
onChange={v => setChat(v.target.value)}
|
onChange={v => setChat(v.target.value)}
|
||||||
value={chat}
|
value={chat}
|
||||||
onFocus={() => {}}
|
onFocus={() => {}}
|
||||||
placeholder=""
|
placeholder={formatMessage({
|
||||||
|
defaultMessage: "Message...",
|
||||||
|
})}
|
||||||
onKeyDown={async e => {
|
onKeyDown={async e => {
|
||||||
if (e.code === "Enter") {
|
if (e.code === "Enter") {
|
||||||
|
e.preventDefault();
|
||||||
await sendChatMessage();
|
await sendChatMessage();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<AsyncButton onClick={sendChatMessage}>
|
||||||
|
<FormattedMessage defaultMessage="Send" />
|
||||||
|
</AsyncButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -53,11 +76,17 @@ export function LiveChat({ ev, link }: { ev: TaggedRawEvent; link: NostrLink })
|
|||||||
|
|
||||||
function ChatMessage({ ev }: { ev: TaggedRawEvent }) {
|
function ChatMessage({ ev }: { ev: TaggedRawEvent }) {
|
||||||
const profile = useUserProfile(System, ev.pubkey);
|
const profile = useUserProfile(System, ev.pubkey);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="message">
|
||||||
<b>{getDisplayName(profile, ev.pubkey)}</b>
|
<div className="name" onClick={() => navigate(profileLink(ev.pubkey, ev.relays))}>
|
||||||
:
|
<Avatar user={profile} />
|
||||||
{ev.content}
|
{getDisplayName(profile, ev.pubkey)}:
|
||||||
|
</div>
|
||||||
|
<span>
|
||||||
|
<Text disableMedia={true} content={ev.content} creator={ev.pubkey} tags={ev.tags} />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -50,7 +50,7 @@ export default function Text({ content, tags, creator, disableMedia, depth }: Te
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (validateLink()) {
|
if (validateLink()) {
|
||||||
if (disableMedia ?? false) {
|
if ((disableMedia ?? false) && !a.startsWith("nostr:")) {
|
||||||
return (
|
return (
|
||||||
<a href={a} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
|
<a href={a} onClick={e => e.stopPropagation()} target="_blank" rel="noreferrer" className="ext">
|
||||||
{a}
|
{a}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FlatNoteStore, NostrLink, RequestBuilder } from "@snort/system";
|
import { EventKind, FlatNoteStore, NostrLink, RequestBuilder } from "@snort/system";
|
||||||
import { useRequestBuilder } from "@snort/system-react";
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
import { System } from "index";
|
import { System } from "index";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
@ -10,6 +10,7 @@ export function useLiveChatFeed(link: NostrLink) {
|
|||||||
leaveOpen: true,
|
leaveOpen: true,
|
||||||
});
|
});
|
||||||
rb.withFilter()
|
rb.withFilter()
|
||||||
|
.kinds([EventKind.ZapReceipt, 1311 as EventKind])
|
||||||
.tag("a", [`${link.kind}:${link.author}:${link.id}`])
|
.tag("a", [`${link.kind}:${link.author}:${link.id}`])
|
||||||
.limit(100);
|
.limit(100);
|
||||||
return rb;
|
return rb;
|
||||||
|
@ -1,8 +1,31 @@
|
|||||||
.live-page {
|
.live-page {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto 250px;
|
height: calc(100% - 105px);
|
||||||
|
padding: 24px;
|
||||||
|
grid-template-columns: auto 350px;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 2000px) {
|
||||||
|
.live-page {
|
||||||
|
grid-template-columns: auto 450px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-page > div:nth-child(1) {
|
||||||
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.live-page video {
|
.live-page video {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
aspect-ratio: 16/9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-page .pill {
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--font-secondary-color);
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,10 @@ import { findTag, unwrap } from "SnortUtils";
|
|||||||
import PageSpinner from "Element/PageSpinner";
|
import PageSpinner from "Element/PageSpinner";
|
||||||
import { LiveChat } from "Element/LiveChat";
|
import { LiveChat } from "Element/LiveChat";
|
||||||
import useEventFeed from "Feed/EventFeed";
|
import useEventFeed from "Feed/EventFeed";
|
||||||
|
import ProfilePreview from "Element/ProfilePreview";
|
||||||
|
import AsyncButton from "Element/AsyncButton";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import Icon from "Icons/Icon";
|
||||||
|
|
||||||
export function LivePage() {
|
export function LivePage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@ -20,8 +24,40 @@ export function LivePage() {
|
|||||||
return (
|
return (
|
||||||
<div className="live-page main-content">
|
<div className="live-page main-content">
|
||||||
<div>
|
<div>
|
||||||
<h3>{findTag(thisEvent.data, "title")}</h3>
|
|
||||||
<LiveVideoPlayer stream={unwrap(findTag(thisEvent.data, "streaming"))} autoPlay={true} />
|
<LiveVideoPlayer stream={unwrap(findTag(thisEvent.data, "streaming"))} autoPlay={true} />
|
||||||
|
<div className="flex">
|
||||||
|
<div className="f-grow">
|
||||||
|
<h1>{findTag(thisEvent.data, "title")}</h1>
|
||||||
|
<p>{findTag(thisEvent.data, "summary")}</p>
|
||||||
|
<div>
|
||||||
|
{thisEvent.data?.tags
|
||||||
|
.filter(a => a[0] === "t")
|
||||||
|
.map(a => a[1])
|
||||||
|
.map(a => (
|
||||||
|
<div className="pill" key={a}>
|
||||||
|
{a}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ProfilePreview
|
||||||
|
pubkey={thisEvent.data.pubkey}
|
||||||
|
className="g10"
|
||||||
|
options={{
|
||||||
|
about: false,
|
||||||
|
}}
|
||||||
|
actions={
|
||||||
|
<div className="flex">
|
||||||
|
<AsyncButton onClick={() => {}}>
|
||||||
|
<Icon name="zap" size={16} className="mr5" />
|
||||||
|
<FormattedMessage defaultMessage="Zap" />
|
||||||
|
</AsyncButton>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<LiveChat ev={thisEvent.data} link={link} />
|
<LiveChat ev={thisEvent.data} link={link} />
|
||||||
</div>
|
</div>
|
||||||
|
@ -498,6 +498,14 @@ small.xs {
|
|||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.g5 {
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.g10 {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.error {
|
.error {
|
||||||
color: var(--error);
|
color: var(--error);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user