Create stream modal
This commit is contained in:
parent
1ec1c9f636
commit
c301d4b9c4
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
{parsed.amount}
|
||||
|
22
src/element/modal.css
Normal file
22
src/element/modal.css
Normal 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
26
src/element/modal.tsx
Normal 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>
|
||||
);
|
||||
}
|
39
src/element/new-stream.css
Normal file
39
src/element/new-stream.css
Normal 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;
|
||||
}
|
96
src/element/new-stream.tsx
Normal file
96
src/element/new-stream.tsx
Normal 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>
|
||||
}
|
@ -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>
|
||||
}
|
@ -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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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>}
|
||||
</>
|
||||
}
|
@ -50,4 +50,8 @@
|
||||
.live-page .tags {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.live-page .actions {
|
||||
margin: 8px 0 0 0;
|
||||
}
|
@ -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">
|
||||
|
Loading…
x
Reference in New Issue
Block a user