From 803d0910af6a7596379579dc20738da11ce1cadd Mon Sep 17 00:00:00 2001 From: Alejandro Gomez Date: Sun, 25 Jun 2023 08:22:50 +0200 Subject: [PATCH] feat: mobile and tablet styles --- package.json | 1 + src/element/emoji.css | 1 - src/element/live-chat.css | 56 +++++++--- src/element/live-chat.tsx | 12 ++- src/element/live-video-player.tsx | 25 +++-- src/element/profile.tsx | 10 +- src/element/textarea.css | 4 + src/hooks/event-feed.ts | 21 ++-- src/pages/layout.css | 105 ++++++++++++++++++- src/pages/layout.tsx | 15 +-- src/pages/root.css | 21 +++- src/pages/stream-page.css | 102 ++++++++++++------ src/pages/stream-page.tsx | 165 ++++++++++++++++++++---------- yarn.lock | 24 +++++ 14 files changed, 430 insertions(+), 132 deletions(-) diff --git a/package.json b/package.json index 82a5d39..54f00a2 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@react-hook/resize-observer": "^1.2.6", "@snort/system-react": "^1.0.8", "@testing-library/jest-dom": "^5.14.1", "@testing-library/react": "^13.0.0", diff --git a/src/element/emoji.css b/src/element/emoji.css index bf193f2..35e29bc 100644 --- a/src/element/emoji.css +++ b/src/element/emoji.css @@ -2,5 +2,4 @@ width: 21px; height: 21px; display: inline-block; - margin-bottom: -5px; } diff --git a/src/element/live-chat.css b/src/element/live-chat.css index 4f4a6a6..ddf9e19 100644 --- a/src/element/live-chat.css +++ b/src/element/live-chat.css @@ -1,13 +1,23 @@ .live-chat { - height: calc(100vh - 72px - 96px); + grid-area: chat; display: flex; flex-direction: column; - padding: 24px 16px 8px 24px; - border: 1px solid #171717; - border-radius: 24px; - gap: 16px; + padding: 8px 16px; + border: none; + height: unset; } +@media (min-width: 1020px) { + .live-chat { + height: calc(100vh - 72px - 96px); + padding: 24px 16px 8px 24px; + border: 1px solid #171717; + border-radius: 24px; + gap: 16px; + } +} + + .live-chat>.header { font-weight: 600; font-size: 24px; @@ -15,12 +25,19 @@ } .live-chat>.messages { - flex-grow: 1; display: flex; gap: 12px; flex-direction: column-reverse; overflow-y: auto; overflow-x: hidden; + padding-bottom: 8px; + border-bottom: 1px solid var(--border, #171717); +} + +@media (min-width: 1020px){ + .live-chat > .messages { + flex-grow: 1; + } } .live-chat>.write-message { @@ -42,6 +59,10 @@ flex-grow: 1; } +.live-chat .message { + word-wrap: break-word; +} + .live-chat .message .profile { gap: 8px; font-weight: 600; @@ -69,6 +90,10 @@ color: white; } +.live-chat .messages .pill:hover { + cursor: default; +} + .live-chat .zap { display: flex; align-items: center; @@ -84,14 +109,21 @@ .top-zappers-container { display: flex; - gap: 8px; - justify-content: space-between; - padding-top: 12px; - padding-bottom: 20px; - border-bottom: 1px solid var(--border, #171717); + padding-top: 8px; + padding-bottom: 8px; overflow-y: scroll; } +@media (min-width: 1020px) { + .top-zappers-container { + display: flex; + gap: 8px; + padding-top: 12px; + padding-bottom: 20px; + border-bottom: 1px solid var(--border, #171717); + } +} + .top-zapper { display: flex; padding: 4px 8px 4px 4px; @@ -114,6 +146,6 @@ margin: 0; } -.top-zapper-icon { +.zap-icon { color: #FFCB44; } diff --git a/src/element/live-chat.tsx b/src/element/live-chat.tsx index c563dbe..7ac1404 100644 --- a/src/element/live-chat.tsx +++ b/src/element/live-chat.tsx @@ -42,7 +42,8 @@ function totalZapped(pubkey: string, zaps: ParsedZap[]) { function TopZappers({ zaps }: { zaps: ParsedZap[] }) { const zappers = zaps .map((z) => (z.anonZap ? "anon" : z.sender)) - .map((p) => p as string); + .map((p) => p as string) + .slice(0, 3); const sortedZappers = useMemo(() => { const sorted = [...new Set([...zappers])]; @@ -63,7 +64,7 @@ function TopZappers({ zaps }: { zaps: ParsedZap[] }) { ) : ( )} - +

{formatSats(total)}

); @@ -76,9 +77,11 @@ function TopZappers({ zaps }: { zaps: ParsedZap[] }) { export function LiveChat({ link, options, + height, }: { link: NostrLink; options?: LiveChatOptions; + height?: number; }) { const messages = useLiveChatFeed(link); const login = useLogin(); @@ -88,7 +91,7 @@ export function LiveChat({ .map((ev) => parseZap(ev, System.ProfileLoader.Cache)) .filter((z) => z && z.valid); return ( -
+
{(options?.showHeader ?? true) && (
Stream Chat
)} @@ -157,7 +160,7 @@ function ChatZap({ ev }: { ev: TaggedRawEvent }) { return (
- + - zapped   {formatSats(parsed.amount)}   sats
diff --git a/src/element/live-video-player.tsx b/src/element/live-video-player.tsx index c3964d7..15da4b5 100644 --- a/src/element/live-video-player.tsx +++ b/src/element/live-video-player.tsx @@ -3,16 +3,23 @@ import { HTMLProps, useEffect, useMemo, useRef, useState } from "react"; export enum VideoStatus { Online = "online", - Offline = "offline" + Offline = "offline", } -export function LiveVideoPlayer(props: HTMLProps & { stream?: string }) { +export function LiveVideoPlayer( + props: HTMLProps & { stream?: string } +) { const video = useRef(null); const streamCached = useMemo(() => props.stream, [props.stream]); const [status, setStatus] = useState(); useEffect(() => { - if (streamCached && video.current && !video.current.src && Hls.isSupported()) { + if ( + streamCached && + video.current && + !video.current.src && + Hls.isSupported() + ) { try { const hls = new Hls(); hls.loadSource(streamCached); @@ -25,10 +32,10 @@ export function LiveVideoPlayer(props: HTMLProps & { stream?: hls.detachMedia(); setStatus(VideoStatus.Offline); } - }) + }); hls.on(Hls.Events.MANIFEST_PARSED, () => { setStatus(VideoStatus.Online); - }) + }); return () => hls.destroy(); } catch (e) { console.error(e); @@ -37,9 +44,11 @@ export function LiveVideoPlayer(props: HTMLProps & { stream?: } }, [video, streamCached]); return ( -
-
{status}
+ <> +
+
{status}
+
+ ); } diff --git a/src/element/profile.tsx b/src/element/profile.tsx index 2ccaf18..8fc9204 100644 --- a/src/element/profile.tsx +++ b/src/element/profile.tsx @@ -24,16 +24,24 @@ export function getName(pk: string, user?: UserMetadata) { export function Profile({ pubkey, + avatarClassname, options, }: { pubkey: string; + avatarClassname?: string; options?: ProfileOptions; }) { const profile = useUserProfile(System, pubkey); return (
- {(options?.showAvatar ?? true) && } + {(options?.showAvatar ?? true) && ( + {profile?.name + )} {(options?.showName ?? true) && (options?.overrideName ?? getName(pubkey, profile))}
diff --git a/src/element/textarea.css b/src/element/textarea.css index df253bf..7614ab5 100644 --- a/src/element/textarea.css +++ b/src/element/textarea.css @@ -1,3 +1,7 @@ +.rta__textarea { + resize: none; +} + .rta__list { border: none; } diff --git a/src/hooks/event-feed.ts b/src/hooks/event-feed.ts index 001b763..d9e05e8 100644 --- a/src/hooks/event-feed.ts +++ b/src/hooks/event-feed.ts @@ -1,5 +1,10 @@ import { useMemo } from "react"; -import { NostrPrefix, RequestBuilder, ReplaceableNoteStore, NostrLink } from "@snort/system"; +import { + NostrPrefix, + RequestBuilder, + ReplaceableNoteStore, + NostrLink, +} from "@snort/system"; import { useRequestBuilder } from "@snort/system-react"; import { System } from "index"; @@ -8,8 +13,8 @@ export default function useEventFeed(link: NostrLink, leaveOpen = false) { const sub = useMemo(() => { const b = new RequestBuilder(`event:${link.id.slice(0, 12)}`); b.withOptions({ - leaveOpen - }) + leaveOpen, + }); if (link.type === NostrPrefix.Address) { const f = b.withFilter().tag("d", [link.id]); if (link.author) { @@ -21,14 +26,18 @@ export default function useEventFeed(link: NostrLink, leaveOpen = false) { } else { const f = b.withFilter().ids([link.id]); if (link.relays) { - link.relays.slice(0, 2).forEach(r => f.relay(r)); + link.relays.slice(0, 2).forEach((r) => f.relay(r)); } if (link.author) { f.authors([link.author]); } } return b; - }, [link]); + }, [link, leaveOpen]); - return useRequestBuilder(System, ReplaceableNoteStore, sub); + return useRequestBuilder( + System, + ReplaceableNoteStore, + sub + ); } diff --git a/src/pages/layout.css b/src/pages/layout.css index d3b6546..1ca015f 100644 --- a/src/pages/layout.css +++ b/src/pages/layout.css @@ -1,4 +1,74 @@ +.page { + display: grid; + grid-template-areas: + "header" + "video-content" + "profile" + "chat"; + grid-template-rows: 64px 230px 56px min-content; + grid-template-columns: 1fr; + height: 100vh; + gap: 0; +} + + +.live-chat { + max-height: calc(100vh - 385px); +} + +@media (min-width: 768px) { + .info { + display: none; + } + + .video-content video { + height: calc(100vh - 64px); + } + + .live-chat { + width: fit-content; + max-height: calc(100vh - 82px); + } + + .page { + display: grid; + grid-template-areas: + "header header" + "video-content chat"; + grid-template-rows: 64px min-content; + grid-template-columns: calc(min(600px, 1fr)) 1fr; + gap: 0; + } +} + +@media (min-width: 1020px) { + .video-content video { + height: unset; + } + + .page { + width: unset; + display: grid; + height: calc(100vh - 72px - 32px - 32px); + padding: 0 40px; + grid-template-columns: auto 376px; + grid-template-rows: unset; + grid-template-areas: + "header header" + "video-content chat" + "profile chat"; + gap: 32px; + } +} + +@media (min-width: 2000px) { + .page { + grid-template-columns: auto 450px; + } +} + header { + grid-area: header; display: grid; grid-template-columns: min-content min-content auto; gap: 24px; @@ -6,7 +76,7 @@ header { padding: 24px 40px 0 40px; } -header>div:nth-child(1) { +header .logo { background: #171717; border-radius: 16px; width: 48px; @@ -19,12 +89,12 @@ header>div:nth-child(1) { cursor: pointer; } -header>div:nth-child(2) { +header .input { min-width: 300px; height: 32px; } -header>div:nth-child(3) { +header .header-right { justify-self: end; display: flex; gap: 24px; @@ -44,4 +114,31 @@ header button { header .profile img { width: 48px; height: 48px; -} \ No newline at end of file +} + +@media (max-width: 1020px) { + header { + padding: 8px 16px 8px 16px; + gap: 8px; + } + + header .header-right { + gap: 8px; + } + + header .input { + min-width: unset; + } + + header .input .search-input { + display: none; + } + + header .new-stream-button-text { + display: none; + } + + header .profile img { + border-radius: 12px; + } +} diff --git a/src/pages/layout.tsx b/src/pages/layout.tsx index 1fa21b0..c23d625 100644 --- a/src/pages/layout.tsx +++ b/src/pages/layout.tsx @@ -37,10 +37,11 @@ export function LayoutPage() { className="btn btn-primary" onClick={() => setNewStream(true)} > - New Stream + New Stream +
-
navigate("/")}>S
+
navigate("/")}> + S +
- +
-
+
{loggedIn()} {loggedOut()}
@@ -95,6 +98,6 @@ export function LayoutPage() { )} - +
); } diff --git a/src/pages/root.css b/src/pages/root.css index 2f1ab52..a8c5b2c 100644 --- a/src/pages/root.css +++ b/src/pages/root.css @@ -5,6 +5,24 @@ padding: 40px; } +@media (max-width: 1020px) { + .video-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; + padding: 16px; + } +} + +@media (max-width: 720px) { + .video-grid { + display: grid; + grid-template-columns: 1fr; + gap: 12px; + padding: 16px; + } +} + @media(min-width: 1600px) { .video-grid { grid-template-columns: repeat(6, 1fr); @@ -18,7 +36,6 @@ } .homepage h2 { - background: #171717; padding: 40px; -} \ No newline at end of file +} diff --git a/src/pages/stream-page.css b/src/pages/stream-page.css index 6c09092..4d4a58e 100644 --- a/src/pages/stream-page.css +++ b/src/pages/stream-page.css @@ -1,27 +1,21 @@ -.live-page { - display: grid; - height: calc(100vh - 72px - 32px - 32px); - padding: 32px 40px; - grid-template-columns: auto 376px; - gap: 32px; +.video-content { + grid-area: video-content; } -@media (min-width: 2000px) { - .live-page { - grid-template-columns: auto 450px; + +.video-content video { + width: 100%; + aspect-ratio: 16/9; + max-height: 230px; +} + +@media (min-width: 768px){ + .video-content video { + max-height: unset; } } -.live-page>div:nth-child(1) { - overflow-y: auto; -} - -.live-page video { - width: 100%; - aspect-ratio: 16/9; -} - -.live-page .pill { +.pill { font-weight: 700; font-size: 14px; line-height: 18px; @@ -29,62 +23,106 @@ text-transform: uppercase; } -.live-page .pill.live { +.pill.live { color: inherit; } -.live-page .info { - margin-top: 32px; +.profile-info { + display: flex; + justify-content: space-between; + padding: 0 16px; + width: 100%; } -.live-page .info h1 { +@media (min-width: 1020px) { + .info { + display: flex; + align-items: flex-start; + justify-content: space-between; + } + + .profile-info { + width: unset; + } +} + +.live-chat .header { + display: none; +} + +.stream-info { + display: none; +} + +@media (min-width: 1020px) { + .live-chat .header { + display: block; + } + + .stream-info { + display: block; + } +} + +.info { + grid-area: profile; + margin-top: 8px +} + +@media (min-width: 1020px) { + .info { + margin-top: 32px; + } +} + +.info h1 { margin: 0 0 8px 0; font-weight: 600; font-size: 28px; line-height: 35px; } -.live-page .info p { +.info p { margin: 0 0 12px 0; } -.live-page .tags { +.tags { display: flex; gap: 8px; } -.live-page .actions { +.actions { margin: 8px 0 0 0; display: flex; gap: 12px; } -.live-page .btn.zap { +.info .btn.zap { padding: 12px 16px; display: flex; align-items: center; gap: 12px; } -.live-page .offline { +.offline { background: rgba(0, 0, 0, 0.5); display: flex; justify-content: center; align-items: center; } -.live-page .online>div { +.online>div { display: none; } -.live-page .offline>div { +.offline>div { position: fixed; text-transform: uppercase; font-size: 30px; font-weight: 700; } -.live-page .offline>video { +.offline>video { z-index: -1; position: relative; -} \ No newline at end of file +} diff --git a/src/pages/stream-page.tsx b/src/pages/stream-page.tsx index 5c243c2..a04a913 100644 --- a/src/pages/stream-page.tsx +++ b/src/pages/stream-page.tsx @@ -1,7 +1,8 @@ import "./stream-page.css"; -import { useState } from "react"; +import { useRef, useState, useLayoutEffect } from "react"; import { parseNostrLink, EventPublisher } from "@snort/system"; import { useNavigate, useParams } from "react-router-dom"; +import useResizeObserver from "@react-hook/resize-observer"; import moment from "moment"; import useEventFeed from "hooks/event-feed"; @@ -15,26 +16,23 @@ import { useLogin } from "hooks/login"; import { StreamState, System } from "index"; import Modal from "element/modal"; import { SendZaps } from "element/send-zap"; +import type { NostrLink } from "@snort/system"; import { useUserProfile } from "@snort/system-react"; import { NewStream } from "element/new-stream"; -export function StreamPage() { - const params = useParams(); - const link = parseNostrLink(params.id!); +function ProfileInfo({ link }: { link: NostrLink }) { const thisEvent = useEventFeed(link, true); const login = useLogin(); const navigate = useNavigate(); const [zap, setZap] = useState(false); const [edit, setEdit] = useState(false); const profile = useUserProfile(System, thisEvent.data?.pubkey); + const zapTarget = profile?.lud16 ?? profile?.lud06; - const stream = findTag(thisEvent.data, "streaming"); const status = findTag(thisEvent.data, "status"); - const image = findTag(thisEvent.data, "image"); const start = findTag(thisEvent.data, "starts"); const isLive = status === "live"; const isMine = link.author === login?.pubkey; - const zapTarget = profile?.lud16 ?? profile?.lud06; async function deleteStream() { const pub = await EventPublisher.nip7(); @@ -47,59 +45,54 @@ export function StreamPage() { } return ( -
-
- -
-
-

{findTag(thisEvent.data, "title")}

-

{findTag(thisEvent.data, "summary")}

-
- {status} - {status === StreamState.Planned && Starts {moment(Number(start) * 1000).fromNow()}} - {thisEvent.data?.tags - .filter((a) => a[0] === "t") - .map((a) => a[1]) - .map((a) => ( - - {a} - - ))} -
- {isMine && ( -
- - - Delete - -
+ <> +
+
+

{findTag(thisEvent.data, "title")}

+

{findTag(thisEvent.data, "summary")}

+
+ {status} + {status === StreamState.Planned && ( + + Starts {moment(Number(start) * 1000).fromNow()} + )} + {thisEvent.data?.tags + .filter((a) => a[0] === "t") + .map((a) => a[1]) + .map((a) => ( + + {a} + + ))}
-
-
- + {isMine && ( +
+ + Delete +
-
+ )} +
+
+ +
- {zap && zapTarget && thisEvent.data && ( setZap(false)}> setEdit(false)}> - setEdit(false)} /> + + )} + + ); +} + +function VideoPlayer({ link }: { link: NostrLink }) { + const thisEvent = useEventFeed(link); + const [zap, setZap] = useState(false); + const [edit, setEdit] = useState(false); + const profile = useUserProfile(System, thisEvent.data?.pubkey); + const zapTarget = profile?.lud16 ?? profile?.lud06; + + const stream = findTag(thisEvent.data, "streaming"); + const image = findTag(thisEvent.data, "image"); + + return ( + <> + {zap && zapTarget && thisEvent.data && ( + setZap(false)}> + setEdit(false)} + targetName={getName(thisEvent.data.pubkey, profile)} + onFinish={() => setZap(false)} /> )} -
+ {edit && thisEvent.data && ( + setEdit(false)}> + setEdit(false)} /> + + )} +
+ +
+ + ); +} + +export function StreamPage() { + const ref = useRef(null); + const params = useParams(); + const link = parseNostrLink(params.id!); + const [height, setHeight] = useState(); + + function setChatHeight() { + const contentHeight = + document.querySelector(".live-page")?.clientHeight || 0; + const videoContentHeight = + document.querySelector(".video-content")?.clientHeight || 0; + if (window.innerWidth <= 480) { + setHeight(contentHeight - videoContentHeight); + } else { + setHeight(undefined); + } + } + + useLayoutEffect(setChatHeight, []); + useResizeObserver(ref, () => setChatHeight()); + + return ( + <> +
+ + + + ); } diff --git a/yarn.lock b/yarn.lock index bb6e718..4608140 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1634,6 +1634,11 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" +"@juggle/resize-observer@^3.3.1": + version "3.4.0" + resolved "https://registry.yarnpkg.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz#08d6c5e20cf7e4cc02fd181c4b0c225cd31dbb60" + integrity sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA== + "@leichtgewicht/ip-codec@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz#b2ac626d6cb9c8718ab459166d4bb405b8ffa78b" @@ -1694,6 +1699,25 @@ schema-utils "^3.0.0" source-map "^0.7.3" +"@react-hook/latest@^1.0.2": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@react-hook/latest/-/latest-1.0.3.tgz#c2d1d0b0af8b69ec6e2b3a2412ba0768ac82db80" + integrity sha512-dy6duzl+JnAZcDbNTfmaP3xHiKtbXYOaz3G51MGVljh548Y8MWzTr+PHLOfvpypEVW9zwvl+VyKjbWKEVbV1Rg== + +"@react-hook/passive-layout-effect@^1.2.0": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@react-hook/passive-layout-effect/-/passive-layout-effect-1.2.1.tgz#c06dac2d011f36d61259aa1c6df4f0d5e28bc55e" + integrity sha512-IwEphTD75liO8g+6taS+4oqz+nnroocNfWVHWz7j+N+ZO2vYrc6PV1q7GQhuahL0IOR7JccFTsFKQ/mb6iZWAg== + +"@react-hook/resize-observer@^1.2.6": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@react-hook/resize-observer/-/resize-observer-1.2.6.tgz#9a8cf4c5abb09becd60d1d65f6bf10eec211e291" + integrity sha512-DlBXtLSW0DqYYTW3Ft1/GQFZlTdKY5VAFIC4+km6IK5NiPPDFchGbEJm1j6pSgMqPRHbUQgHJX7RaR76ic1LWA== + dependencies: + "@juggle/resize-observer" "^3.3.1" + "@react-hook/latest" "^1.0.2" + "@react-hook/passive-layout-effect" "^1.2.0" + "@remix-run/router@1.6.3": version "1.6.3" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.6.3.tgz#8205baf6e17ef93be35bf62c37d2d594e9be0dad"