refactor: more css purging

This commit is contained in:
2024-03-04 12:44:17 +00:00
parent ae37f361ce
commit 6dd9730ca6
60 changed files with 728 additions and 1120 deletions

View File

@ -31,7 +31,7 @@
.text-to-speech-settings .labeled-input label {
font-size: 14px;
color: var(--text-muted);
@apply text-layer-4;
}
.text-to-speech-settings textarea {

View File

@ -1,16 +0,0 @@
.popout-chat .live-chat {
padding: 8px 16px;
width: calc(100vw - 32px);
height: calc(100vh - 16px);
margin-left: 0;
border: unset;
border-radius: unset;
}
.popout-chat .live-chat .messages {
padding-right: unset;
}
.popout-chat.embed .live-chat .messages::-webkit-scrollbar {
display: none;
}

View File

@ -1,4 +1,3 @@
import "./chat-popout.css";
import { useParams } from "react-router-dom";
import { NostrPrefix, encodeTLV, parseNostrLink } from "@snort/system";
import { unwrap } from "@snort/shared";
@ -17,15 +16,15 @@ export function ChatPopout() {
const lnk = parseNostrLink(encodeTLV(NostrPrefix.Address, findTag(ev, "d") ?? "", undefined, ev?.kind, ev?.pubkey));
const chat = Boolean(new URL(window.location.href).searchParams.get("chat"));
return (
<div className={`popout-chat${chat ? "" : " embed"}`}>
<div className="h-[calc(100vh-1rem)] w-screen px-2 my-2">
<LiveChat
ev={ev}
link={lnk}
options={{
canWrite: chat,
showHeader: false,
}}
canWrite={chat}
showHeader={false}
showScrollbar={false}
goal={goal}
className="h-inherit"
/>
</div>
);

View File

@ -3,12 +3,11 @@ import LiveVideoPlayer from "@/element/live-video-player";
import { MuteButton } from "@/element/mute-button";
import { Profile } from "@/element/profile";
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
import { useLiveChatFeed } from "@/hooks/live-chat";
import { useLogin } from "@/hooks/login";
import { extractStreamInfo } from "@/utils";
import { dedupe } from "@snort/shared";
import { NostrLink, NostrPrefix, ParsedZap } from "@snort/system";
import { useEventReactions } from "@snort/system-react";
import { NostrLink, NostrPrefix, ParsedZap, TaggedNostrEvent } from "@snort/system";
import { useEventReactions, useReactions } from "@snort/system-react";
import classNames from "classnames";
import { HTMLProps, ReactNode, useEffect, useMemo, useState } from "react";
import { FormattedMessage, FormattedNumber } from "react-intl";
@ -17,6 +16,7 @@ import { StreamTimer } from "@/element/stream-time";
import { DashboardRaidMenu } from "@/element/raid-menu";
import { DefaultButton } from "@/element/buttons";
import Modal from "@/element/modal";
import { LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP } from "@/const";
export default function DashboardPage() {
const login = useLogin();
@ -35,11 +35,22 @@ function DashboardForLink({ link }: { link: NostrLink }) {
setMaxParticipants(v => (v < Number(participants) ? Number(participants) : v));
}
}, [participants]);
const feed = useReactions(
`live:${link?.id}:${streamLink?.author}:reactions`,
streamLink ? [streamLink] : [],
rb => {
if (streamLink) {
rb.withFilter().kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP]).replyToLink([streamLink]);
}
},
true
);
if (!streamLink) return;
return (
<div className="grid grid-cols-3 gap-2 full-page-height">
<div className="h-inhreit flex gap-4 flex-col">
<div className="grid grid-cols-3 gap-2 h-[calc(100%-48px-1rem)]">
<div className="min-h-0 h-full grid grid-rows-[min-content_auto] gap-2">
<DashboardCard className="flex flex-col gap-4">
<h3>
<FormattedMessage defaultMessage="Stream" id="uYw2LD" />
@ -63,14 +74,12 @@ function DashboardForLink({ link }: { link: NostrLink }) {
<FormattedMessage defaultMessage="Chat Users" id="RtYNX5" />
</h3>
<div className="h-[calc(100%-4rem)] overflow-y-scroll">
<DashboardChatList link={streamLink} />
<DashboardChatList feed={feed} />
</div>
</DashboardCard>
</div>
<div className="h-inhreit flex gap-4 flex-col">
<DashboardZapColumn link={streamLink} />
</div>
<LiveChat link={streamLink} ev={streamEvent} />
<DashboardZapColumn link={streamLink} feed={feed} />
<LiveChat link={streamLink} ev={streamEvent} className="min-h-0" />
</div>
);
}
@ -92,17 +101,15 @@ function DashboardStatsCard({
<div
{...props}
className={classNames("flex-1 bg-layer-1 flex flex-col gap-1 px-4 py-2 rounded-xl", props.className)}>
<div className="text-layer-3 font-medium">{name}</div>
<div className="text-layer-4 font-medium">{name}</div>
<div>{value}</div>
</div>
);
}
function DashboardChatList({ link }: { link: NostrLink }) {
const feed = useLiveChatFeed(link);
function DashboardChatList({ feed }: { feed: Array<TaggedNostrEvent> }) {
const pubkeys = useMemo(() => {
return dedupe(feed.messages.map(a => a.pubkey));
return dedupe(feed.map(a => a.pubkey));
}, [feed]);
return pubkeys.map(a => (
@ -118,9 +125,8 @@ function DashboardChatList({ link }: { link: NostrLink }) {
));
}
function DashboardZapColumn({ link }: { link: NostrLink }) {
const feed = useLiveChatFeed(link);
const reactions = useEventReactions(link, feed.reactions);
function DashboardZapColumn({ link, feed }: { link: NostrLink; feed: Array<TaggedNostrEvent> }) {
const reactions = useEventReactions(link, feed);
const sortedZaps = useMemo(
() => reactions.zaps.sort((a, b) => (b.created_at > a.created_at ? 1 : -1)),
@ -128,11 +134,11 @@ function DashboardZapColumn({ link }: { link: NostrLink }) {
);
const latestZap = sortedZaps.at(0);
return (
<DashboardCard className="h-inhreit flex flex-col gap-4">
<DashboardCard className="min-h-0 h-full flex flex-col gap-4">
<h3>
<FormattedMessage defaultMessage="Zaps" id="OEW7yJ" />
</h3>
<div className="h-inhreit flex flex-col gap-2 overflow-y-scroll">
<div className="flex flex-col gap-2 overflow-y-scroll">
{latestZap && <DashboardHighlightZap zap={latestZap} />}
{sortedZaps.slice(1).map(a => (
<ChatZap zap={a} />

View File

@ -1,96 +1,3 @@
.page {
--page-pad-tb: 16px;
--page-pad-lr: 40px;
--header-page-padding: calc(var(--page-pad-tb) + var(--page-pad-tb));
padding: var(--page-pad-tb) var(--page-pad-lr);
display: flex;
flex-direction: column;
gap: var(--gap-s);
}
@media (max-width: 1020px) {
.page {
--page-pad-tb: 8px;
--page-pad-lr: 0;
}
header {
padding: 0 16px;
}
}
header {
display: flex;
gap: 24px;
}
header .btn-header {
height: 32px;
border-bottom: 2px solid transparent;
user-select: none;
cursor: pointer;
font-weight: 700;
font-size: 16px;
line-height: 20px;
padding: 8px 16px;
display: flex;
align-items: center;
}
header .btn-header.active {
border-bottom: 2px solid;
}
header .btn-header:hover {
border-bottom: 2px solid;
}
header .paper {
min-width: 300px;
height: 32px;
}
header .header-right {
justify-self: end;
display: flex;
gap: 24px;
}
header input[type="text"]:active {
border: unset;
}
header button {
height: 48px;
display: flex;
align-items: center;
gap: 8px;
}
@media (max-width: 1020px) {
header .header-right {
gap: 8px;
}
header .paper {
min-width: unset;
}
header .paper .search-input {
display: none;
}
header .new-stream-button-text {
display: none;
}
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.fullscreen-exclusive {
width: 100vw;
height: 100vh;
@ -123,14 +30,6 @@ header button {
opacity: 0.02;
}
.age-check .btn {
padding: 12px 16px;
}
.profile-menu {
cursor: pointer;
}
.tnum {
font-feature-settings: "tnum";
}

View File

@ -12,10 +12,9 @@ import { useLogin, useLoginEvents } from "@/hooks/login";
import { Profile } from "@/element/profile";
import { NewStreamDialog } from "@/element/new-stream";
import { LoginSignup } from "@/element/login-signup";
import { Login } from "@/index";
import { Login } from "@/login";
import { useLang } from "@/hooks/lang";
import { AllLocales } from "@/intl";
import { NewVersion } from "@/serviceWorker";
import { trackEvent } from "@/utils";
import { BorderButton, DefaultButton } from "@/element/buttons";
import Modal from "@/element/modal";
@ -117,8 +116,13 @@ export function LayoutPage() {
<Icon name="login" />
</BorderButton>
{showLogin && (
<Modal id="login">
<LoginSignup close={() => setShowLogin(false)} />
<Modal
id="login"
onClose={() => setShowLogin(false)}
bodyClassName="my-auto bg-layer-1 rounded-xl overflow-hidden">
<div className="w-full">
<LoginSignup close={() => setShowLogin(false)} />
</div>
</Modal>
)}
</>
@ -130,17 +134,16 @@ export function LayoutPage() {
(styles as Record<string, string>)["--primary"] = login.color;
}
return (
<div className="page" style={styles}>
<div className="pt-4 px-2 xl:px-5 h-[calc(100dvh-1rem)]" style={styles}>
<Helmet>
<title>Home - zap.stream</title>
</Helmet>
<header>
<div className="flex justify-between mb-4">
<div
className="bg-white text-black flex items-center cursor-pointer rounded-2xl aspect-square px-1"
onClick={() => navigate("/")}>
<Logo width={40} height={40} />
</div>
<div className="grow flex items-center gap-2"></div>
<div className="flex items-center gap-3">
<Link
to="https://discord.gg/Wtg6NVDdbT"
@ -153,9 +156,8 @@ export function LayoutPage() {
{loggedIn()}
{loggedOut()}
</div>
</header>
</div>
<Outlet />
{NewVersion && <NewVersionBanner />}
</div>
);
}

View File

@ -1,7 +1,7 @@
import "./profile-page.css";
import { useMemo } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { CachedMetadata, NostrEvent, NostrLink, TaggedNostrEvent, parseNostrLink } from "@snort/system";
import { Link, useNavigate, useParams } from "react-router-dom";
import { CachedMetadata, NostrEvent, NostrLink, NostrPrefix, TaggedNostrEvent, parseNostrLink } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { unwrap } from "@snort/shared";
import { FormattedMessage } from "react-intl";
@ -22,6 +22,8 @@ import { useGoals } from "@/hooks/goals";
import { Goal } from "@/element/goal";
import { TopZappers } from "@/element/top-zappers";
import { useClips } from "@/hooks/clips";
import { getName } from "@/element/profile";
import VideoGrid from "@/element/video-grid";
const defaultBanner = "https://void.cat/d/Hn1AdN5UKmceuDkgDW847q.webp";
@ -36,7 +38,7 @@ export function ProfilePage() {
}, [streams]);
return (
<div className="flex flex-col gap-3 px-4">
<div className="flex flex-col gap-3 xl:px-4">
<img
className="rounded-xl object-cover h-[360px]"
alt={profile?.name || link.id}
@ -140,7 +142,7 @@ function ProfileStreamList({ streams }: { streams: Array<TaggedNostrEvent> }) {
return <FormattedMessage defaultMessage="No streams yet" id="0rVLjV" />;
}
return (
<div className="grid gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-8">
<VideoGrid>
{streams.map(ev => (
<div key={ev.id} className="flex flex-col gap-1">
<VideoTile ev={ev} showAuthor={false} showStatus={false} />
@ -155,7 +157,7 @@ function ProfileStreamList({ streams }: { streams: Array<TaggedNostrEvent> }) {
</span>
</div>
))}
</div>
</VideoGrid>
);
}
@ -180,8 +182,32 @@ function ProfileClips({ link }: { link: NostrLink }) {
if (clips.length === 0) {
return <FormattedMessage defaultMessage="No clips yet" id="ObZZEz" />;
}
return clips.map(a => {
const r = findTag(a, "r");
return <video src={r} />;
});
return clips.map(a => <ProfileClip ev={a} key={a.id} />);
}
function ProfileClip({ ev }: { ev: NostrEvent }) {
const profile = useUserProfile(ev.pubkey);
const r = findTag(ev, "r");
const title = findTag(ev, "title");
return (
<div className="w-[300px] flex flex-col gap-4 bg-layer-1 rounded-xl px-3 py-2">
<span>
<FormattedMessage
defaultMessage="Clip by {name}"
id="dkUMIH"
values={{
name: (
<Link
to={`/p/${new NostrLink(NostrPrefix.PublicKey, ev.pubkey).encode()}`}
className="font-medium text-primary">
{getName(ev.pubkey, profile)}
</Link>
),
}}
/>
</span>
{title}
<video src={r} controls />
</div>
);
}

View File

@ -1,12 +1,12 @@
import "./root.css";
import { FormattedMessage } from "react-intl";
import { useCallback, useMemo } from "react";
import type { NostrEvent } from "@snort/system";
import { ReactNode, useCallback, useMemo } from "react";
import type { NostrEvent, TaggedNostrEvent } from "@snort/system";
import { VideoTile } from "@/element/video-tile";
import { useLogin } from "@/hooks/login";
import { getHost, getTagValues } from "@/utils";
import { useStreamsFeed } from "@/hooks/live-streams";
import VideoGrid from "@/element/video-grid";
export function RootPage() {
const login = useLogin();
@ -43,76 +43,47 @@ export function RootPage() {
}, [live, hashtags]);
return (
<div className="homepage">
<div className="flex flex-col gap-6">
{hasFollowingLive && (
<>
<h2 className="divider line one-line">
<FormattedMessage defaultMessage="Following" id="cPIKU2" />
</h2>
<div className="video-grid">
{following.map(e => (
<VideoTile ev={e} key={e.id} />
))}
</div>
</>
<RootSection header={<FormattedMessage defaultMessage="Following" id="cPIKU2" />} items={following} />
)}
{!hasFollowingLive && (
<div className="video-grid">
<VideoGrid>
{live
.filter(e => !mutedHosts.has(getHost(e)))
.map(e => (
<VideoTile ev={e} key={e.id} />
))}
</div>
</VideoGrid>
)}
{liveByHashtag.map(t => (
<>
<h2 className="divider line one-line">#{t.tag}</h2>
<div className="video-grid">
{t.live.map(e => (
<VideoTile ev={e} key={e.id} />
))}
</div>
</>
<RootSection header={`#${t.tag}`} items={t.live} />
))}
{hasFollowingLive && liveNow.length > 0 && (
<>
<h2 className="divider line one-line">
<FormattedMessage defaultMessage="Live" id="Dn82AL" />
</h2>
<div className="video-grid">
{liveNow
.filter(e => !mutedHosts.has(getHost(e)))
.map(e => (
<VideoTile ev={e} key={e.id} />
))}
</div>
</>
<RootSection header={<FormattedMessage defaultMessage="Live" id="Dn82AL" />} items={liveNow} />
)}
{plannedEvents.length > 0 && (
<>
<h2 className="divider line one-line">
<FormattedMessage defaultMessage="Planned" id="kp0NPF" />
</h2>
<div className="video-grid">
{plannedEvents.map(e => (
<VideoTile ev={e} key={e.id} />
))}
</div>
</>
<RootSection header={<FormattedMessage defaultMessage="Planned" id="kp0NPF" />} items={plannedEvents} />
)}
{endedEvents.length > 0 && (
<>
<h2 className="divider line one-line">
<FormattedMessage defaultMessage="Ended" id="TP/cMX" />
</h2>
<div className="video-grid">
{endedEvents.map(e => (
<VideoTile ev={e} key={e.id} />
))}
</div>
</>
<RootSection header={<FormattedMessage defaultMessage="Ended" id="TP/cMX" />} items={endedEvents} />
)}
</div>
);
}
function RootSection({ header, items }: { header: ReactNode; items: Array<TaggedNostrEvent> }) {
return (
<>
<h2 className="flex items-center gap-4">
{header}
<span className="h-[1px] bg-layer-1 w-full" />
</h2>
<VideoGrid>
{items.map(e => (
<VideoTile ev={e} key={e.id} />
))}
</VideoGrid>
</>
);
}

View File

@ -1,125 +0,0 @@
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { FormattedMessage } from "react-intl";
import { hexToBech32, unwrap } from "@snort/shared";
import { useLogin } from "@/hooks/login";
import Copy from "@/element/copy";
import { NostrProviderDialog } from "@/element/nostr-provider-dialog";
import { useStreamProvider } from "@/hooks/stream-provider";
import { Login } from "..";
import { StatePill } from "@/element/state-pill";
import { NostrStreamProvider } from "@/providers";
import { StreamState } from "@/const";
import { Layer1Button } from "@/element/buttons";
const enum Tab {
Account,
Stream,
}
export function SettingsPage() {
const navigate = useNavigate();
const login = useLogin();
const [tab, setTab] = useState(Tab.Account);
const providers = useStreamProvider();
useEffect(() => {
if (!login) {
navigate("/");
}
}, [login]);
function tabContent() {
switch (tab) {
case Tab.Account: {
return (
<>
<h1>
<FormattedMessage defaultMessage="Account" id="TwyMau" />
</h1>
{login?.pubkey && (
<div className="public-key">
<p>
<FormattedMessage defaultMessage="Logged in as" id="DZKuuP" />
</p>
<Copy text={hexToBech32("npub", login.pubkey)} />
</div>
)}
{login?.privateKey && (
<div className="private-key">
<p>
<FormattedMessage defaultMessage="Private key" id="Bep/gA" />
</p>
<Layer1Button>
<FormattedMessage defaultMessage="Copy" id="4l6vz1" />
</Layer1Button>
</div>
)}
<h1>
<FormattedMessage defaultMessage="Theme" id="Pe0ogR" />
</h1>
<div>
<StatePill state={StreamState.Live} />
</div>
<div className="flex gap-2">
{["#7F006A", "#E206BF", "#7406E2", "#3F06E2", "#393939", "#ff563f", "#ff8d2b", "#34d2fe"].map(a => (
<div
className={`w-4 h-4 pointer${login?.color === a ? " border" : ""}`}
title={a}
style={{ backgroundColor: a }}
onClick={() => Login.setColor(a)}></div>
))}
</div>
</>
);
}
case Tab.Stream: {
return (
<>
<h1>
<FormattedMessage defaultMessage="Stream" id="uYw2LD" />
</h1>
<div className="flex flex-col gap-4">
<NostrProviderDialog
provider={unwrap(providers.find(a => a.name === "zap.stream")) as NostrStreamProvider}
showEndpoints={true}
showEditor={false}
showForwards={true}
/>
</div>
</>
);
}
}
}
function tabName(t: Tab) {
switch (t) {
case Tab.Account:
return <FormattedMessage defaultMessage="Account" id="TwyMau" />;
case Tab.Stream:
return <FormattedMessage defaultMessage="Stream" id="uYw2LD" />;
}
}
return (
<div className="rounded-2xl p-3 md:w-[700px] mx-auto w-full">
<div className="flex flex-col gap-2">
<h1>
<FormattedMessage defaultMessage="Settings" id="D3idYv" />
</h1>
<div className="flex flex-col gap-2">
<div className="flex gap-2">
{[Tab.Account, Tab.Stream].map(t => (
<Layer1Button onClick={() => setTab(t)} className={t === tab ? "active" : ""}>
{tabName(t)}
</Layer1Button>
))}
</div>
<div className="p-5 bg-layer-1 rounded-3xl flex flex-col gap-3">{tabContent()}</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,52 @@
import { StreamState } from "@/const";
import { Layer1Button } from "@/element/buttons";
import Copy from "@/element/copy";
import { StatePill } from "@/element/state-pill";
import { useLogin } from "@/hooks/login";
import { Login } from "@/login";
import { hexToBech32 } from "@snort/shared";
import { FormattedMessage } from "react-intl";
export default function AccountSettingsTab() {
const login = useLogin();
return (
<>
<h1>
<FormattedMessage defaultMessage="Account" id="TwyMau" />
</h1>
{login?.pubkey && (
<div className="public-key">
<p>
<FormattedMessage defaultMessage="Logged in as" id="DZKuuP" />
</p>
<Copy text={hexToBech32("npub", login.pubkey)} />
</div>
)}
{login?.privateKey && (
<div className="private-key">
<p>
<FormattedMessage defaultMessage="Private key" id="Bep/gA" />
</p>
<Layer1Button>
<FormattedMessage defaultMessage="Copy" id="4l6vz1" />
</Layer1Button>
</div>
)}
<h1>
<FormattedMessage defaultMessage="Theme" id="Pe0ogR" />
</h1>
<div>
<StatePill state={StreamState.Live} />
</div>
<div className="flex gap-2">
{["#7F006A", "#E206BF", "#7406E2", "#3F06E2", "#393939", "#ff563f", "#ff8d2b", "#34d2fe"].map(a => (
<div
className={`w-4 h-4 pointer${login?.color === a ? " border" : ""}`}
title={a}
style={{ backgroundColor: a }}
onClick={() => Login.setColor(a)}></div>
))}
</div>
</>
);
}

View File

@ -0,0 +1,37 @@
import { Layer1Button } from "@/element/buttons";
import { FormattedMessage } from "react-intl";
import { Outlet, useNavigate } from "react-router-dom";
const Tabs = [
{
name: <FormattedMessage defaultMessage="Account" id="TwyMau" />,
path: "",
} as const,
{
name: <FormattedMessage defaultMessage="Stream" id="uYw2LD" />,
path: "stream",
} as const,
];
export default function SettingsPage() {
const naviage = useNavigate();
return (
<div className="rounded-2xl p-3 md:w-[700px] mx-auto w-full">
<div className="flex flex-col gap-2">
<h1>
<FormattedMessage defaultMessage="Settings" id="D3idYv" />
</h1>
<div className="flex flex-col gap-2">
<div className="flex gap-2">
{Tabs.map(t => (
<Layer1Button onClick={() => naviage(t.path)}>{t.name}</Layer1Button>
))}
</div>
<div className="p-5 bg-layer-1 rounded-3xl flex flex-col gap-3">
<Outlet />
</div>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,24 @@
import { NostrProviderDialog } from "@/element/nostr-provider-dialog";
import { useStreamProvider } from "@/hooks/stream-provider";
import { NostrStreamProvider } from "@/providers";
import { unwrap } from "@snort/shared";
import { FormattedMessage } from "react-intl";
export function StreamSettingsTab() {
const providers = useStreamProvider();
return (
<>
<h1>
<FormattedMessage defaultMessage="Stream" id="uYw2LD" />
</h1>
<div className="flex flex-col gap-4">
<NostrProviderDialog
provider={unwrap(providers.find(a => a.name === "zap.stream")) as NostrStreamProvider}
showEndpoints={true}
showEditor={false}
showForwards={true}
/>
</div>
</>
);
}

View File

@ -1,117 +0,0 @@
.stream-page {
display: grid;
grid-template-columns: auto 450px;
gap: var(--gap-m);
}
.stream-page .video-content {
overflow-y: auto;
gap: var(--gap-s);
display: flex;
flex-direction: column;
-ms-overflow-style: none;
scrollbar-width: none;
}
.stream-page .video-content::-webkit-scrollbar {
display: none;
}
@media (max-width: 1020px) {
.stream-page {
display: flex;
flex-direction: column;
height: calc(100vh - var(--header-page-padding) - var(--header-height) - var(--gap-s));
}
.stream-page .video-content {
overflow-y: visible;
}
.stream-page .live-chat {
border-radius: 0;
border: 0;
padding: 8px 16px;
height: unset;
min-height: 0;
}
.stream-page .live-chat .top-zappers h3,
.stream-page .live-chat .header {
display: none;
}
.stream-page .info {
flex-direction: column;
}
.stream-page .stream-info {
display: none;
}
.stream-page .profile-info {
width: calc(100% - 32px);
}
.stream-page .video-content video {
max-height: 30vh;
}
}
.profile-info {
display: flex;
justify-content: space-between;
gap: var(--gap-m);
}
.profile-info .btn {
padding: 12px 16px;
}
.info h1 {
margin: 0 0 8px 0;
font-weight: 600;
font-size: 28px;
line-height: 35px;
}
.info p {
margin: 0 0 12px 0;
}
.actions {
margin: 8px 0 0 0;
display: flex;
gap: 12px;
}
.offline {
background: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
position: absolute;
width: 100%;
height: 100%;
}
.online > div {
display: none;
}
.offline > div {
text-transform: uppercase;
font-size: 30px;
font-weight: 700;
}
@media (min-width: 768px) {
.offline > div {
top: 10em;
}
}
.offline > video {
z-index: -1;
position: relative;
}

View File

@ -1,4 +1,3 @@
import "./stream-page.css";
import { NostrLink, TaggedNostrEvent } from "@snort/system";
import { useLocation, useNavigate } from "react-router-dom";
import { Helmet } from "react-helmet";
@ -30,8 +29,9 @@ import { StreamState } from "@/const";
import { NotificationsButton } from "@/element/notifications-button";
import { WarningButton } from "@/element/buttons";
import Pill from "@/element/pill";
import { useMediaQuery } from "usehooks-ts";
function ProfileInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedNostrEvent }) {
function StreamInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedNostrEvent }) {
const system = useContext(SnortContext);
const login = useLogin();
const navigate = useNavigate();
@ -55,11 +55,11 @@ function ProfileInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedNostrEv
const viewers = Number(participants ?? "0");
return (
<>
<div className="flex gap-2 max-lg:px-2 max-xl:flex-col">
<div className="flex gap-2 max-xl:flex-col">
<div className="grow flex flex-col gap-2 max-xl:hidden">
<h1>{title}</h1>
<p>{summary}</p>
<div className="tags">
<div className="flex gap-2 flex-wrap">
<StatePill state={status as StreamState} />
<Pill>
<FormattedMessage defaultMessage="{n} viewers" id="3adEeb" values={{ n: formatSats(viewers) }} />
@ -72,7 +72,7 @@ function ProfileInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedNostrEv
{ev && <Tags ev={ev} />}
</div>
{isMine && (
<div className="actions">
<div className="flex gap-4">
{ev && <NewStreamDialog text="Edit" ev={ev} btnClassName="btn" />}
<WarningButton onClick={deleteStream}>
<FormattedMessage defaultMessage="Delete" id="K3r6DQ" />
@ -80,7 +80,7 @@ function ProfileInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedNostrEv
</div>
)}
</div>
<div className="flex justify-between sm:gap-4 max-sm:gap-2 nowrap max-md:flex-col lg:items-center">
<div className="flex justify-between sm:gap-4 max-sm:gap-2 flex-wrap max-md:flex-col lg:items-center">
<Profile pubkey={host ?? ""} />
<div className="flex gap-2">
<FollowButton pubkey={host} hideWhenFollowing={true} />
@ -133,6 +133,7 @@ export function StreamPage({ link, evPreload }: { evPreload?: NostrEvent; link:
goal: goalTag,
} = extractStreamInfo(ev);
const goal = useZapGoal(goalTag);
const isDesktop = useMediaQuery("(min-width: 1280px)");
if (contentWarning && !isContentWarningAccepted()) {
return <ContentWarningOverlay />;
@ -144,7 +145,7 @@ export function StreamPage({ link, evPreload }: { evPreload?: NostrEvent; link:
...(tags ?? []),
].join(", ");
return (
<div className="stream-page full-page-height">
<div className="xl:grid xl:grid-cols-[auto_450px] 2xl:xl:grid-cols-[auto_500px] max-xl:flex max-xl:flex-col xl:gap-4 h-[calc(100%-48px-1rem)]">
<Helmet>
<title>{`${title} - zap.stream`}</title>
<meta name="description" content={descriptionContent} />
@ -154,25 +155,28 @@ export function StreamPage({ link, evPreload }: { evPreload?: NostrEvent; link:
<meta property="og:description" content={descriptionContent} />
<meta property="og:image" content={image ?? ""} />
</Helmet>
<div className="video-content">
<div className="flex flex-col gap-2 xl:overflow-y-auto scrollbar-hidden">
<Suspense>
<LiveVideoPlayer
title={title}
stream={status === StreamState.Live ? stream : recording}
poster={image}
status={status}
className="max-xl:max-h-[30vh] xl:w-full mx-auto"
/>
</Suspense>
<ProfileInfo ev={ev as TaggedNostrEvent} goal={goal} />
<StreamCards host={host} />
<StreamInfo ev={ev as TaggedNostrEvent} goal={goal} />
{isDesktop && <StreamCards host={host} />}
</div>
<LiveChat
link={evLink ?? link}
ev={ev}
goal={goal}
options={{
canWrite: status === StreamState.Live,
}}
canWrite={status === StreamState.Live}
showHeader={isDesktop}
showTopZappers={isDesktop}
showGoal={true}
className="min-h-0 xl:border xl:border-layer-1 xl:rounded-xl xl:p-5"
/>
</div>
);

View File

@ -1,12 +0,0 @@
@import "./root.css";
.tag-page-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 0 12px;
}
.tag-page h1 {
margin: 0;
}

View File

@ -1,4 +1,3 @@
import "./tag.css";
import { useParams } from "react-router-dom";
import { unwrap } from "@snort/shared";