feat: style upgrades
This commit is contained in:
@ -82,11 +82,13 @@ export default function Category() {
|
||||
|
||||
const results = useRequestBuilder(sub);
|
||||
return (
|
||||
<div>
|
||||
<div className="flex gap-4 overflow-x-scroll scrollbar-hidden">
|
||||
{AllCategories.map(a => (
|
||||
<CategoryLink key={a.id} id={a.id} name={a.name} icon={a.icon} />
|
||||
))}
|
||||
<div className="px-2 p-4">
|
||||
<div className="px-2 min-w-0">
|
||||
<div className="flex gap-4 overflow-x-scroll scrollbar-hidden">
|
||||
{AllCategories.map(a => (
|
||||
<CategoryLink key={a.id} id={a.id} name={a.name} icon={a.icon} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{id && (
|
||||
<div className="flex gap-4 py-8">
|
||||
|
@ -2,7 +2,7 @@ import { useParams } from "react-router-dom";
|
||||
import { NostrPrefix, encodeTLV, parseNostrLink } from "@snort/system";
|
||||
import { unwrap } from "@snort/shared";
|
||||
|
||||
import { LiveChat } from "@/element/live-chat";
|
||||
import { LiveChat } from "@/element/chat/live-chat";
|
||||
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
|
||||
import { findTag } from "@/utils";
|
||||
import { useZapGoal } from "@/hooks/goals";
|
||||
@ -21,7 +21,6 @@ export function ChatPopout() {
|
||||
ev={ev}
|
||||
link={lnk}
|
||||
canWrite={chat}
|
||||
showHeader={false}
|
||||
showScrollbar={false}
|
||||
goal={goal}
|
||||
className="h-inherit"
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { ChatZap } from "@/element/live-chat";
|
||||
import { ChatZap } from "@/element/chat/live-chat";
|
||||
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||
import { useEventReactions } from "@snort/system-react";
|
||||
import { useMemo } from "react";
|
||||
@ -20,7 +20,7 @@ export function DashboardZapColumn({
|
||||
const reactions = useEventReactions(link, feed);
|
||||
const sortedZaps = useMemo(
|
||||
() => reactions.zaps.sort((a, b) => (b.created_at > a.created_at ? 1 : -1)),
|
||||
[reactions.zaps]
|
||||
[reactions.zaps],
|
||||
);
|
||||
const latestZap = sortedZaps.at(0);
|
||||
const zapSum = sortedZaps.reduce((acc, v) => acc + v.amount, 0);
|
||||
|
@ -1,12 +1,12 @@
|
||||
import { LiveChat } from "@/element/live-chat";
|
||||
import LiveVideoPlayer from "@/element/live-video-player";
|
||||
import { LiveChat } from "@/element/chat/live-chat";
|
||||
import LiveVideoPlayer from "@/element/stream/live-video-player";
|
||||
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
|
||||
import { extractStreamInfo } from "@/utils";
|
||||
import { EventExt, NostrEvent, NostrLink } from "@snort/system";
|
||||
import { SnortContext, useReactions } from "@snort/system-react";
|
||||
import { Suspense, lazy, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { FormattedMessage, FormattedNumber } from "react-intl";
|
||||
import { StreamTimer } from "@/element/stream-time";
|
||||
import { StreamTimer } from "@/element/stream/stream-time";
|
||||
import { LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP, StreamState } from "@/const";
|
||||
import { DashboardRaidButton } from "./button-raid";
|
||||
import { DashboardZapColumn } from "./column-zaps";
|
||||
@ -71,7 +71,7 @@ export function DashboardForLink({ link }: { link: NostrLink }) {
|
||||
rb.withFilter().kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP]).replyToLink([streamLink]);
|
||||
}
|
||||
},
|
||||
true
|
||||
true,
|
||||
);
|
||||
|
||||
if (!streamLink && !location.search.includes("setupComplete=true")) return <DashboardIntro />;
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { StreamState } from "@/const";
|
||||
import LiveVideoPlayer from "@/element/live-video-player";
|
||||
import LiveVideoPlayer from "@/element/stream/live-video-player";
|
||||
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
|
||||
import { useStreamLink } from "@/hooks/stream-link";
|
||||
import { extractStreamInfo, trackEvent } from "@/utils";
|
||||
|
@ -1,16 +1,20 @@
|
||||
import { ReactNode, createContext, useState } from "react";
|
||||
import { ReactNode, createContext, useContext, useState } from "react";
|
||||
|
||||
interface LayoutContextType {
|
||||
leftNav: boolean;
|
||||
leftNavExpand: boolean;
|
||||
showHeader: boolean;
|
||||
theme: string;
|
||||
update: (fn: (c: LayoutContextType) => LayoutContextType) => void;
|
||||
}
|
||||
const defaultLayoutContext: LayoutContextType = {
|
||||
leftNav: true,
|
||||
leftNavExpand: false,
|
||||
showHeader: true,
|
||||
theme: "",
|
||||
update: c => c,
|
||||
};
|
||||
export const LayoutContext = createContext<LayoutContextType>(defaultLayoutContext);
|
||||
const LayoutContext = createContext<LayoutContextType>(defaultLayoutContext);
|
||||
|
||||
export function LayoutContextProvider({ children }: { children: ReactNode }) {
|
||||
const [value, setValue] = useState<LayoutContextType>(defaultLayoutContext);
|
||||
@ -26,3 +30,7 @@ export function LayoutContextProvider({ children }: { children: ReactNode }) {
|
||||
</LayoutContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function useLayout() {
|
||||
return useContext(LayoutContext);
|
||||
}
|
||||
|
@ -11,11 +11,11 @@ import { FormattedMessage } from "react-intl";
|
||||
import { Link, useNavigate } from "react-router-dom";
|
||||
import { useLang } from "@/hooks/lang";
|
||||
import { useLogin } from "@/hooks/login";
|
||||
import { useContext, useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { Profile } from "@/element/profile";
|
||||
import { SearchBar } from "./search";
|
||||
import { NavLinkIcon } from "./nav-icon";
|
||||
import { LayoutContext } from "./context";
|
||||
import { useLayout } from "./context";
|
||||
|
||||
export function HeaderNav() {
|
||||
const navigate = useNavigate();
|
||||
@ -23,7 +23,7 @@ export function HeaderNav() {
|
||||
const [showLogin, setShowLogin] = useState(false);
|
||||
const { lang, setLang } = useLang();
|
||||
const country = lang.split(/[-_]/i)[1]?.toLowerCase();
|
||||
const layoutState = useContext(LayoutContext);
|
||||
const layoutState = useLayout();
|
||||
|
||||
function langSelector() {
|
||||
return (
|
||||
@ -54,16 +54,24 @@ export function HeaderNav() {
|
||||
if (!login) return;
|
||||
|
||||
return (
|
||||
<div className="flex gap-3 items-center pr-4 py-1">
|
||||
<div className="flex gap-2 items-center pr-4 py-1">
|
||||
{(!import.meta.env.VITE_SINGLE_PUBLISHER || import.meta.env.VITE_SINGLE_PUBLISHER === login.pubkey) && (
|
||||
<>
|
||||
<Link to="/upload">
|
||||
<IconButton iconName="upload" iconSize={20} className="px-3 py-2 hover:bg-layer-1 rounded-xl" />
|
||||
</Link>
|
||||
<Link to="/dashboard">
|
||||
<IconButton iconName="signal" iconSize={20} className="px-3 py-2 hover:bg-layer-1 rounded-xl" />
|
||||
</Link>
|
||||
</>
|
||||
<Menu
|
||||
menuClassName="ctx-menu"
|
||||
menuButton={
|
||||
<IconButton iconName="plus-circle" iconSize={20} className="px-3 py-2 hover:bg-layer-1 rounded-xl" />
|
||||
}
|
||||
align="end"
|
||||
gap={5}>
|
||||
<MenuItem onClick={() => navigate("/upload")}>
|
||||
<Icon name="upload" size={24} />
|
||||
<FormattedMessage defaultMessage="Upload" />
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => navigate("/dashboard")}>
|
||||
<Icon name="signal" size={24} />
|
||||
<FormattedMessage defaultMessage="Dashboard" />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
)}
|
||||
<Menu
|
||||
menuClassName="ctx-menu"
|
||||
@ -83,19 +91,23 @@ export function HeaderNav() {
|
||||
gap={5}>
|
||||
<MenuItem onClick={() => navigate(profileLink(undefined, login.pubkey))}>
|
||||
<Icon name="user" size={24} />
|
||||
<FormattedMessage defaultMessage="Profile" id="itPgxd" />
|
||||
<FormattedMessage defaultMessage="Profile" />
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => navigate("/settings")}>
|
||||
<Icon name="settings" size={24} />
|
||||
<FormattedMessage defaultMessage="Settings" id="D3idYv" />
|
||||
<FormattedMessage defaultMessage="Settings" />
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => navigate("/widgets")}>
|
||||
<Icon name="widget" size={24} />
|
||||
<FormattedMessage defaultMessage="Widgets" id="jgOqxt" />
|
||||
<FormattedMessage defaultMessage="Widgets" />
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => window.open("https://discord.gg/Wtg6NVDdbT")}>
|
||||
<Icon name="link" size={24} />
|
||||
Discord
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => Login.logout()}>
|
||||
<Icon name="logout" size={24} />
|
||||
<FormattedMessage defaultMessage="Logout" id="C81/uG" />
|
||||
<FormattedMessage defaultMessage="Logout" />
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
</div>
|
||||
@ -123,32 +135,28 @@ export function HeaderNav() {
|
||||
);
|
||||
}
|
||||
|
||||
if (!layoutState.showHeader) return;
|
||||
return (
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex justify-between items-center gap-4">
|
||||
<div className="flex gap-4 items-center m-2">
|
||||
<NavLinkIcon
|
||||
name="hamburger"
|
||||
className="!opacity-100 max-xl:hidden"
|
||||
onClick={() => {
|
||||
layoutState.update(c => {
|
||||
c.leftNav = !c.leftNav;
|
||||
return { ...c };
|
||||
});
|
||||
}}
|
||||
/>
|
||||
{layoutState.leftNav && (
|
||||
<NavLinkIcon
|
||||
name="hamburger"
|
||||
className="!opacity-100 max-xl:hidden"
|
||||
onClick={() => {
|
||||
layoutState.update(c => {
|
||||
c.leftNavExpand = !c.leftNavExpand;
|
||||
return { ...c };
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<Link to="/">
|
||||
<Logo width={33} />
|
||||
</Link>
|
||||
</div>
|
||||
<SearchBar />
|
||||
<div className="flex items-center gap-3">
|
||||
<Link
|
||||
to="https://discord.gg/Wtg6NVDdbT"
|
||||
target="_blank"
|
||||
className="flex items-center max-md:hidden gap-1 bg-layer-1 hover:bg-layer-2 font-bold p-2 rounded-xl">
|
||||
<Icon name="link" />
|
||||
Discord
|
||||
</Link>
|
||||
{langSelector()}
|
||||
{loggedIn()}
|
||||
{loggedOut()}
|
||||
|
@ -1,29 +1,29 @@
|
||||
import { useContext } from "react";
|
||||
import { LayoutContext } from "./context";
|
||||
import { useLayout } from "./context";
|
||||
import { NavLinkIcon } from "./nav-icon";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
export function LeftNav() {
|
||||
const layout = useContext(LayoutContext);
|
||||
const layout = useLayout();
|
||||
|
||||
if (layout.leftNav === false) return;
|
||||
return (
|
||||
<div className="flex flex-col gap-4 p-2 max-xl:hidden">
|
||||
<NavLinkIcon name="signal" route="/streams" className="flex gap-2 items-center">
|
||||
{layout.leftNav && (
|
||||
{layout.leftNavExpand && (
|
||||
<span className="pr-3">
|
||||
<FormattedMessage defaultMessage="Streams" />
|
||||
</span>
|
||||
)}
|
||||
</NavLinkIcon>
|
||||
<NavLinkIcon name="play-circle" route="/videos" className="flex gap-2 items-center">
|
||||
{layout.leftNav && (
|
||||
{layout.leftNavExpand && (
|
||||
<span className="pr-3">
|
||||
<FormattedMessage defaultMessage="Videos" />
|
||||
</span>
|
||||
)}
|
||||
</NavLinkIcon>
|
||||
<NavLinkIcon name="grid" route="/category" className="flex gap-2 items-center">
|
||||
{layout.leftNav && (
|
||||
{layout.leftNavExpand && (
|
||||
<span className="pr-3">
|
||||
<FormattedMessage defaultMessage="Categories" />
|
||||
</span>
|
||||
|
@ -10,13 +10,12 @@ export function SearchBar() {
|
||||
const [search, setSearch] = useState(term ?? "");
|
||||
|
||||
return (
|
||||
<div className="pr-4 h-fit flex items-center rounded-full px-3 py-1 lg:border lg:border-layer-2">
|
||||
<div className="pr-4 h-fit flex items-center rounded-full px-3 py-1 border border-layer-2 max-xl:min-w-0">
|
||||
<input
|
||||
type="text"
|
||||
className="reset max-lg:hidden bg-transparent"
|
||||
className="reset bg-transparent"
|
||||
placeholder={formatMessage({
|
||||
defaultMessage: "Search",
|
||||
id: "xmcVZ0",
|
||||
})}
|
||||
value={search}
|
||||
onChange={e => setSearch(e.target.value)}
|
||||
|
@ -7,11 +7,14 @@ import { StreamPage } from "./stream-page";
|
||||
import { VideoPage } from "./video";
|
||||
import { EventEmbed as NostrEventElement } from "@/element/event-embed";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useLayout } from "./layout/context";
|
||||
import classNames from "classnames";
|
||||
|
||||
export function LinkHandler() {
|
||||
const location = useLocation();
|
||||
const evPreload = getEventFromLocationState(location.state);
|
||||
const link = useStreamLink();
|
||||
const layoutContext = useLayout();
|
||||
|
||||
if (!link) return;
|
||||
|
||||
@ -23,7 +26,7 @@ export function LinkHandler() {
|
||||
);
|
||||
} else if (link.kind === EventKind.LiveEvent) {
|
||||
return (
|
||||
<div className="h-[calc(100dvh-52px)] w-full">
|
||||
<div className={classNames(layoutContext.showHeader ? "h-[calc(100dvh-44px)]" : "h-[calc(100dvh)]", "w-full")}>
|
||||
<StreamPage link={link} evPreload={evPreload} />
|
||||
</div>
|
||||
);
|
||||
|
@ -1,27 +0,0 @@
|
||||
import { LIVE_STREAM } from "@/const";
|
||||
import { LiveChat } from "@/element/live-chat";
|
||||
import { SendZapsDialog } from "@/element/send-zap";
|
||||
import { EventBuilder, NostrLink } from "@snort/system";
|
||||
|
||||
export default function MockPage() {
|
||||
const pubkey = "cf45a6ba1363ad7ed213a078e710d24115ae721c9b47bd1ebf4458eaefb4c2a5";
|
||||
const fakeStream = new EventBuilder()
|
||||
.kind(LIVE_STREAM)
|
||||
.pubKey(pubkey)
|
||||
.tag(["d", "mock"])
|
||||
.tag(["title", "Example Stream"])
|
||||
.tag(["summary", "An example mock stream for debugging"])
|
||||
.tag(["streaming", "https://example.com/live.m3u8"])
|
||||
.tag(["t", "nostr"])
|
||||
.tag(["t", "mock"])
|
||||
.processContent()
|
||||
.build();
|
||||
const fakeStreamLink = NostrLink.fromEvent(fakeStream);
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<LiveChat link={fakeStreamLink} ev={fakeStream} height={600} />
|
||||
<SendZapsDialog lnurl="donate@snort.social" aTag={fakeStreamLink.toEventTag()![1]} pubkey={pubkey} />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -23,7 +23,7 @@ import { Goal } from "@/element/goal";
|
||||
import { TopZappers } from "@/element/top-zappers";
|
||||
import { useProfileClips } from "@/hooks/clips";
|
||||
import VideoGrid from "@/element/video-grid";
|
||||
import { ClipTile } from "@/element/clip-tile";
|
||||
import { ClipTile } from "@/element/stream/clip-tile";
|
||||
|
||||
const defaultBanner = "https://void.cat/d/Hn1AdN5UKmceuDkgDW847q.webp";
|
||||
|
||||
|
@ -15,7 +15,7 @@ export function RootPage() {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<VideoGridSorted evs={streams} showEnded={false} showPopular={true} showRecentClips={true} />
|
||||
<VideoGridSorted evs={streams} showEnded={false} showPopular={true} showRecentClips={false} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,17 +1,19 @@
|
||||
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||
import { Helmet } from "react-helmet";
|
||||
import { Suspense, lazy } from "react";
|
||||
import { Suspense, lazy, useEffect } from "react";
|
||||
import { useMediaQuery } from "usehooks-ts";
|
||||
|
||||
const LiveVideoPlayer = lazy(() => import("@/element/live-video-player"));
|
||||
const LiveVideoPlayer = lazy(() => import("@/element/stream/live-video-player"));
|
||||
import { extractStreamInfo, getHost } from "@/utils";
|
||||
import { LiveChat } from "@/element/live-chat";
|
||||
import { LiveChat } from "@/element/chat/live-chat";
|
||||
import { useZapGoal } from "@/hooks/goals";
|
||||
import { StreamCards } from "@/element/stream-cards";
|
||||
import { ContentWarningOverlay, useContentWarning } from "@/element/nsfw";
|
||||
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
|
||||
import { StreamState } from "@/const";
|
||||
import { StreamInfo } from "@/element/stream-info";
|
||||
import { StreamInfo } from "@/element/stream/stream-info";
|
||||
import { useLayout } from "./layout/context";
|
||||
import { StreamContextProvider } from "@/element/stream/stream-state";
|
||||
|
||||
export function StreamPage({ link, evPreload }: { evPreload?: TaggedNostrEvent; link: NostrLink }) {
|
||||
const ev = useCurrentStreamFeed(link, true, evPreload);
|
||||
@ -31,6 +33,25 @@ export function StreamPage({ link, evPreload }: { evPreload?: TaggedNostrEvent;
|
||||
const goal = useZapGoal(goalTag);
|
||||
const isDesktop = useMediaQuery("(min-width: 1280px)");
|
||||
const isGrownUp = useContentWarning();
|
||||
const layout = useLayout();
|
||||
|
||||
useEffect(() => {
|
||||
if (layout.leftNav) {
|
||||
layout.update(c => {
|
||||
c.leftNav = false;
|
||||
return { ...c };
|
||||
});
|
||||
}
|
||||
}, [layout]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
layout.update(c => {
|
||||
c.leftNav = true;
|
||||
return { ...c };
|
||||
});
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (contentWarning && !isGrownUp) {
|
||||
return <ContentWarningOverlay />;
|
||||
@ -42,38 +63,42 @@ export function StreamPage({ link, evPreload }: { evPreload?: TaggedNostrEvent;
|
||||
...(tags ?? []),
|
||||
].join(", ");
|
||||
return (
|
||||
<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 max-xl:gap-1 h-full">
|
||||
<Helmet>
|
||||
<title>{`${title} - zap.stream`}</title>
|
||||
<meta name="description" content={descriptionContent} />
|
||||
<meta property="og:url" content={`https://${window.location.host}/${link.encode()}`} />
|
||||
<meta property="og:type" content="video" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={descriptionContent} />
|
||||
<meta property="og:image" content={image ?? ""} />
|
||||
</Helmet>
|
||||
<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>
|
||||
<StreamInfo ev={ev as TaggedNostrEvent} goal={goal} />
|
||||
{isDesktop && <StreamCards host={host} />}
|
||||
<StreamContextProvider link={link}>
|
||||
<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 max-xl:gap-1 h-full">
|
||||
<Helmet>
|
||||
<title>{`${title} - zap.stream`}</title>
|
||||
<meta name="description" content={descriptionContent} />
|
||||
<meta property="og:url" content={`https://${window.location.host}/${link.encode()}`} />
|
||||
<meta property="og:type" content="video" />
|
||||
<meta property="og:title" content={title} />
|
||||
<meta property="og:description" content={descriptionContent} />
|
||||
<meta property="og:image" content={image ?? ""} />
|
||||
</Helmet>
|
||||
<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>
|
||||
<div className="lg:px-5 max-lg:px-2">
|
||||
<StreamInfo ev={ev as TaggedNostrEvent} goal={goal} />
|
||||
{isDesktop && <StreamCards host={host} />}
|
||||
</div>
|
||||
</div>
|
||||
<LiveChat
|
||||
link={evLink ?? link}
|
||||
ev={ev}
|
||||
goal={goal}
|
||||
canWrite={status === StreamState.Live}
|
||||
adjustLayout={!isDesktop}
|
||||
showGoal={true}
|
||||
className="min-h-0 xl:border xl:border-layer-2 xl:rounded-xl xl:p-3 max-xl:px-2 h-inherit"
|
||||
/>
|
||||
</div>
|
||||
<LiveChat
|
||||
link={evLink ?? link}
|
||||
ev={ev}
|
||||
goal={goal}
|
||||
canWrite={status === StreamState.Live}
|
||||
showTopZappers={isDesktop}
|
||||
showGoal={true}
|
||||
className="min-h-0 xl:border xl:border-layer-2 xl:rounded-xl xl:p-3 max-xl:px-2 h-inherit"
|
||||
/>
|
||||
</div>
|
||||
</StreamContextProvider>
|
||||
);
|
||||
}
|
||||
|
@ -1 +1,8 @@
|
||||
export function UploadPage() {}
|
||||
export function UploadPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1>Upload</h1>
|
||||
<b>Coming Soon..</b>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,28 +1,66 @@
|
||||
import { StreamInfo } from "@/element/stream-info";
|
||||
import { Textarea } from "@/element/chat/textarea";
|
||||
import { WriteMessage } from "@/element/chat/write-message";
|
||||
import { FollowButton } from "@/element/follow-button";
|
||||
import { Profile, getName } from "@/element/profile";
|
||||
import { SendZapsDialog } from "@/element/send-zap";
|
||||
import { ShareMenu } from "@/element/share-menu";
|
||||
import { StreamSummary } from "@/element/stream/summary";
|
||||
import VideoComments from "@/element/video/comments";
|
||||
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
|
||||
import { getHost, extractStreamInfo } from "@/utils";
|
||||
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
import { useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
export function VideoPage({ link, evPreload }: { link: NostrLink; evPreload?: TaggedNostrEvent }) {
|
||||
const { formatMessage } = useIntl();
|
||||
const ev = useCurrentStreamFeed(link, true, evPreload);
|
||||
const [newComment, setNewComment] = useState("");
|
||||
const host = getHost(ev);
|
||||
const evLink = ev ? NostrLink.fromEvent(ev) : undefined;
|
||||
const {
|
||||
title,
|
||||
summary,
|
||||
image,
|
||||
status,
|
||||
tags,
|
||||
contentWarning,
|
||||
stream,
|
||||
recording,
|
||||
goal: goalTag,
|
||||
} = extractStreamInfo(ev);
|
||||
const { title, summary, image, contentWarning, recording } = extractStreamInfo(ev);
|
||||
const profile = useUserProfile(host);
|
||||
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
||||
|
||||
return (
|
||||
<div className="p-4 w-[80dvw] mx-auto">
|
||||
<video src={recording} controls className="w-full aspect-video" />
|
||||
<StreamInfo ev={ev as TaggedNostrEvent} />
|
||||
<video src={recording} controls className="w-full aspect-video" poster={image} />
|
||||
<div className="grid grid-cols-[auto_450px]">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="font-medium text-xl">{title}</div>
|
||||
<div className="flex justify-between">
|
||||
{/* PROFILE SECTION */}
|
||||
<div className="flex gap-2 items-center">
|
||||
<Profile pubkey={host} />
|
||||
<FollowButton pubkey={host} />
|
||||
</div>
|
||||
{/* ACTIONS */}
|
||||
<div className="flex gap-2">
|
||||
{ev && (
|
||||
<>
|
||||
<ShareMenu ev={ev} />
|
||||
{zapTarget && (
|
||||
<SendZapsDialog
|
||||
lnurl={zapTarget}
|
||||
pubkey={host}
|
||||
aTag={link.tagKey}
|
||||
targetName={getName(ev.pubkey, profile)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{summary && <StreamSummary text={summary} />}
|
||||
<h3>
|
||||
<FormattedMessage defaultMessage="Comments" />
|
||||
</h3>
|
||||
<div>
|
||||
<WriteMessage link={link} emojiPacks={[]} kind={1} />
|
||||
</div>
|
||||
<VideoComments link={link} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
Reference in New Issue
Block a user