refactor: more css purging
This commit is contained in:
@ -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 {
|
||||
|
@ -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;
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
@ -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} />
|
||||
|
@ -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";
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
52
src/pages/settings/account.tsx
Normal file
52
src/pages/settings/account.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
37
src/pages/settings/index.tsx
Normal file
37
src/pages/settings/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
24
src/pages/settings/stream.tsx
Normal file
24
src/pages/settings/stream.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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;
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import "./tag.css";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { unwrap } from "@snort/shared";
|
||||
|
||||
|
Reference in New Issue
Block a user