Create stream modal

This commit is contained in:
Kieran 2023-06-22 11:40:25 +01:00
parent 1ec1c9f636
commit c301d4b9c4
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
12 changed files with 254 additions and 30 deletions

View File

@ -30,12 +30,6 @@
}
.live-chat>.write-message>div:nth-child(1) {
background: #171717;
border-radius: 16px;
padding: 8px 16px;
display: flex;
gap: 10px;
align-items: center;
height: 32px;
flex-grow: 1;
}

View File

@ -40,7 +40,7 @@ export function LiveChat({ link, options }: { link: NostrLink, options?: LiveCha
function writeMessage() {
return <>
<div>
<div className="input">
<input
type="text"
autoFocus={false}
@ -110,7 +110,10 @@ function ChatZap({ ev }: { ev: TaggedRawEvent }) {
return (
<div className="zap pill">
<Icon name="zap" />
<Profile pubkey={parsed.sender ?? ""} />
<Profile pubkey={parsed.sender ?? ""} options={{
showAvatar: !parsed.anonZap,
overrideName: parsed.anonZap ? "Anonymous" : undefined
}}/>
zapped
&nbsp;
{parsed.amount}

22
src/element/modal.css Normal file
View File

@ -0,0 +1,22 @@
.modal {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
background-color: rgba(0, 0, 0, 0.8);
display: flex;
justify-content: center;
z-index: 42;
overflow-y: auto;
}
.modal-body {
display: flex;
width: 430px;
padding: 32px;
margin-top: auto;
margin-bottom: auto;
border-radius: 32px;
background: #171717;
}

26
src/element/modal.tsx Normal file
View File

@ -0,0 +1,26 @@
import "./modal.css";
import { useEffect, MouseEventHandler, ReactNode } from "react";
export interface ModalProps {
className?: string;
onClose?: MouseEventHandler;
children: ReactNode;
}
export default function Modal(props: ModalProps) {
const onClose = props.onClose || (() => undefined);
const className = props.className || "";
useEffect(() => {
document.body.classList.add("scroll-lock");
return () => document.body.classList.remove("scroll-lock");
}, []);
return (
<div className={`modal ${className}`} onClick={onClose}>
<div className="modal-body" onClick={e => e.stopPropagation()}>
{props.children}
</div>
</div>
);
}

View File

@ -0,0 +1,39 @@
.new-stream {
display: flex;
flex-direction: column;
gap: 24px;
width: inherit;
}
.new-stream h3 {
font-size: 24px;
margin: 0;
}
.new-stream div.input {
background: #262626;
height: 32px;
}
.new-stream p {
margin: 0 0 8px 0;
}
.new-stream small {
display: block;
margin: 8px 0 0 0;
}
.new-stream .btn {
padding: 12px 16px;
border-radius: 16px;
width: 100%;
}
.new-stream .btn>span {
justify-content: center;
}
.new-stream .btn:disabled {
opacity: 0.3;
}

View File

@ -0,0 +1,96 @@
import { useEffect, useState } from "react";
import { EventPublisher } from "@snort/system";
import { unixNow } from "@snort/shared";
import "./new-stream.css";
import AsyncButton from "./async-button";
import { System } from "index";
export function NewStream() {
const [title, setTitle] = useState("");
const [summary, setSummary] = useState("");
const [image, setImage] = useState("");
const [stream, setStream] = useState("");
const [isValid, setIsValid] = useState(false);
function validate() {
if (title.length < 2) {
return false;
}
if (stream.length < 5 || !stream.match(/^https?:\/\/.*\.m3u8?$/i)) {
return false;
}
if (image.length > 0 && !image.match(/^https?:\/\//i)) {
return false;
}
return true;
}
useEffect(() => {
setIsValid(validate());
}, [title, summary, image, stream]);
async function publishStream() {
const pub = await EventPublisher.nip7();
if (pub) {
const ev = await pub.generic(eb => {
const now = unixNow();
return eb.kind(30_311)
.tag(["d", now.toString()])
.tag(["title", title])
.tag(["summary", summary])
.tag(["image", image])
.tag(["streaming", stream])
.tag(["status", "live"])
});
console.debug(ev);
System.BroadcastEvent(ev);
}
}
return <div className="new-stream">
<h3>
New Stream
</h3>
<div>
<p>
Title
</p>
<div className="input">
<input type="text" placeholder="What are we steaming today?" value={title} onChange={e => setTitle(e.target.value)} />
</div>
</div>
<div>
<p>
Summary
</p>
<div className="input">
<input type="text" placeholder="A short description of the content" value={summary} onChange={e => setSummary(e.target.value)} />
</div>
</div>
<div>
<p>
Cover image
</p>
<div className="input">
<input type="text" placeholder="https://" value={image} onChange={e => setImage(e.target.value)} />
</div>
</div>
<div>
<p>
Stream Url
</p>
<div className="input">
<input type="text" placeholder="https://" value={stream} onChange={e => setStream(e.target.value)} />
</div>
<small>
Stream type should be HLS
</small>
</div>
<div>
<AsyncButton type="button" className="btn btn-primary" disabled={!isValid} onClick={publishStream}>
Start Stream
</AsyncButton>
</div>
</div>
}

View File

@ -5,8 +5,10 @@ import { hexToBech32 } from "@snort/shared";
import { System } from "index";
export interface ProfileOptions {
showName?: boolean,
showName?: boolean
showAvatar?: boolean
suffix?: string
overrideName?: string
}
export function getName(pk: string, user?: UserMetadata) {
@ -18,7 +20,7 @@ export function Profile({ pubkey, options }: { pubkey: string, options?: Profile
const profile = useUserProfile(System, pubkey);
return <div className="profile">
<img src={profile?.picture} alt="Profile"/>
{(options?.showName ?? true) && getName(pubkey, profile)}
{(options?.showAvatar ?? true) && <img src={profile?.picture ?? ""} />}
{(options?.showName ?? true) && (options?.overrideName ?? getName(pubkey, profile))}
</div>
}

View File

@ -70,4 +70,29 @@ a {
display: flex;
align-items: center;
gap: 8px;
}
input[type="text"] {
font-family: inherit;
border: unset;
background-color: unset;
color: inherit;
width: 100%;
font-size: 16px;
font-weight: 500;
outline: none;
}
div.input {
background: #171717;
border-radius: 16px;
padding: 8px 16px;
display: flex;
gap: 10px;
align-items: center;
}
.scroll-lock {
overflow: hidden;
height: 100vh;
}

View File

@ -20,12 +20,7 @@ header>div:nth-child(1) {
}
header>div:nth-child(2) {
background: #171717;
border-radius: 16px;
padding: 8px 16px;
min-width: 300px;
display: flex;
align-items: center;
height: 32px;
}
@ -35,16 +30,6 @@ header>div:nth-child(3) {
gap: 24px;
}
header input[type="text"] {
border: unset;
background-color: unset;
color: inherit;
width: 100%;
font-size: 16px;
font-weight: 500;
outline: none;
}
header input[type="text"]:active {
border: unset;
}

View File

@ -6,10 +6,14 @@ import AsyncButton from "element/async-button";
import { Login } from "index";
import { useLogin } from "hooks/login";
import { Profile } from "element/profile";
import Modal from "element/modal";
import { NewStream } from "element/new-stream";
import { useState } from "react";
export function LayoutPage() {
const navigate = useNavigate();
const login = useLogin();
const [newStream, setNewStream] = useState(false);
async function doLogin() {
const pub = await EventPublisher.nip7();
@ -22,7 +26,7 @@ export function LayoutPage() {
if (!login) return;
return <>
<button type="button" className="btn btn-primary">
<button type="button" className="btn btn-primary" onClick={() => setNewStream(true)}>
New Stream
<Icon name="signal" />
</button>
@ -48,7 +52,7 @@ export function LayoutPage() {
<div onClick={() => navigate("/")}>
S
</div>
<div>
<div className="input">
<input type="text" placeholder="Search" />
<Icon name="search" size={15} />
</div>
@ -58,5 +62,8 @@ export function LayoutPage() {
</div>
</header>
<Outlet />
{newStream && <Modal onClose={() => setNewStream(false)} >
<NewStream />
</Modal>}
</>
}

View File

@ -50,4 +50,8 @@
.live-page .tags {
display: flex;
gap: 8px;
}
.live-page .actions {
margin: 8px 0 0 0;
}

View File

@ -1,6 +1,6 @@
import "./stream-page.css";
import { parseNostrLink } from "@snort/system";
import { useParams } from "react-router-dom";
import { parseNostrLink, EventPublisher } from "@snort/system";
import { useNavigate, useParams } from "react-router-dom";
import useEventFeed from "hooks/event-feed";
import { LiveVideoPlayer } from "element/live-video-player";
@ -9,15 +9,31 @@ import { Profile } from "element/profile";
import { LiveChat } from "element/live-chat";
import AsyncButton from "element/async-button";
import { Icon } from "element/icon";
import { useLogin } from "hooks/login";
import { System } from "index";
export function StreamPage() {
const params = useParams();
const link = parseNostrLink(params.id!);
const thisEvent = useEventFeed(link);
const login = useLogin();
const navigate = useNavigate();
const stream = findTag(thisEvent.data, "streaming");
const status = findTag(thisEvent.data, "status");
const isLive = status === "live";
const isMine = link.author === login?.pubkey;
async function deleteStream() {
const pub = await EventPublisher.nip7();
if (pub && thisEvent.data) {
const ev = await pub.delete(thisEvent.data.id);
console.debug(ev);
System.BroadcastEvent(ev);
navigate("/");
}
}
return (
<div className="live-page">
<div>
@ -39,6 +55,11 @@ export function StreamPage() {
</span>
))}
</div>
<div className="actions">
{isMine && <AsyncButton type="button" className="btn" onClick={deleteStream}>
Delete
</AsyncButton>}
</div>
</div>
<div>
<div className="flex g24">