feat: shorts
This commit is contained in:
@ -12,6 +12,7 @@ export const CARD = 37_777 as EventKind;
|
|||||||
export const MUTED = 10_000 as EventKind;
|
export const MUTED = 10_000 as EventKind;
|
||||||
|
|
||||||
export const VIDEO_KIND = 34_235 as EventKind;
|
export const VIDEO_KIND = 34_235 as EventKind;
|
||||||
|
export const SHORTS_KIND = 34_236 as EventKind;
|
||||||
|
|
||||||
export const MINUTE = 60;
|
export const MINUTE = 60;
|
||||||
export const HOUR = 60 * MINUTE;
|
export const HOUR = 60 * MINUTE;
|
||||||
|
@ -11,7 +11,7 @@ import { Emoji as EmojiComponent } from "../emoji";
|
|||||||
import { Profile } from "../profile";
|
import { Profile } from "../profile";
|
||||||
import { Text } from "../text";
|
import { Text } from "../text";
|
||||||
import { useMute } from "../mute-button";
|
import { useMute } from "../mute-button";
|
||||||
import { SendZaps, SendZapsDialog } from "../send-zap";
|
import { SendZaps } from "../send-zap";
|
||||||
import { CollapsibleEvent } from "../collapsible";
|
import { CollapsibleEvent } from "../collapsible";
|
||||||
|
|
||||||
import { useLogin } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
@ -174,12 +174,14 @@ export function ChatMessage({
|
|||||||
opacity: showZapDialog || isHovering ? 1 : 0,
|
opacity: showZapDialog || isHovering ? 1 : 0,
|
||||||
pointerEvents: showZapDialog || isHovering ? "auto" : "none",
|
pointerEvents: showZapDialog || isHovering ? "auto" : "none",
|
||||||
}}>
|
}}>
|
||||||
{zapTarget && <IconButton
|
{zapTarget && (
|
||||||
iconName="zap"
|
<IconButton
|
||||||
iconSize={14}
|
iconName="zap"
|
||||||
className="p-2 rounded-full bg-layer-2 aspect-square"
|
iconSize={14}
|
||||||
onClick={() => setZapping(true)}
|
className="p-2 rounded-full bg-layer-2 aspect-square"
|
||||||
/>}
|
onClick={() => setZapping(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={pickEmoji}
|
onClick={pickEmoji}
|
||||||
iconName="face"
|
iconName="face"
|
||||||
@ -196,15 +198,17 @@ export function ChatMessage({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{zapping && zapTarget && <Modal id="send-zaps" onClose={() => setZapping(false)}>
|
{zapping && zapTarget && (
|
||||||
<SendZaps
|
<Modal id="send-zaps" onClose={() => setZapping(false)}>
|
||||||
lnurl={zapTarget}
|
<SendZaps
|
||||||
eTag={ev.id}
|
lnurl={zapTarget}
|
||||||
pubkey={ev.pubkey}
|
eTag={ev.id}
|
||||||
targetName={profile?.name || ev.pubkey}
|
pubkey={ev.pubkey}
|
||||||
onFinish={() => setZapping(false)}
|
targetName={profile?.name || ev.pubkey}
|
||||||
/>
|
onFinish={() => setZapping(false)}
|
||||||
</Modal>}
|
/>
|
||||||
|
</Modal>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{showEmojiPicker && (
|
{showEmojiPicker && (
|
||||||
<Suspense>
|
<Suspense>
|
||||||
|
@ -6,8 +6,19 @@ import { useLogin } from "@/hooks/login";
|
|||||||
import AsyncButton from "./async-button";
|
import AsyncButton from "./async-button";
|
||||||
import { useContext } from "react";
|
import { useContext } from "react";
|
||||||
import { ZapEvent } from "./send-zap";
|
import { ZapEvent } from "./send-zap";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
export default function EventReactions({ ev, replyKind }: { ev: TaggedNostrEvent; replyKind?: EventKind }) {
|
export default function EventReactions({
|
||||||
|
ev,
|
||||||
|
replyKind,
|
||||||
|
vertical,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
ev: TaggedNostrEvent;
|
||||||
|
replyKind?: EventKind;
|
||||||
|
vertical?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
const link = NostrLink.fromEvent(ev)!;
|
const link = NostrLink.fromEvent(ev)!;
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const system = useContext(SnortContext);
|
const system = useContext(SnortContext);
|
||||||
@ -36,49 +47,61 @@ export default function EventReactions({ ev, replyKind }: { ev: TaggedNostrEvent
|
|||||||
|
|
||||||
const pub = login?.publisher();
|
const pub = login?.publisher();
|
||||||
const totalZaps = grouped.zaps.reduce((acc, v) => acc + v.amount, 0);
|
const totalZaps = grouped.zaps.reduce((acc, v) => acc + v.amount, 0);
|
||||||
const iconClass = "flex gap-2 items-center tabular-nums cursor-pointer select-none hover:text-primary transition";
|
const iconClass = classNames(
|
||||||
|
"flex gap-1 items-center tabular-nums cursor-pointer select-none hover:text-primary transition",
|
||||||
|
{
|
||||||
|
"flex-col": vertical,
|
||||||
|
},
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap gap-4 mt-2 items-center text-neutral-500">
|
<div
|
||||||
|
className={classNames(className, "grid items-center text-neutral-500", {
|
||||||
|
"flex-col gap-8": vertical,
|
||||||
|
"gap-4": !vertical,
|
||||||
|
})}>
|
||||||
{replyKind && (
|
{replyKind && (
|
||||||
<AsyncButton
|
<AsyncButton
|
||||||
className={iconClass}
|
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (pub) {
|
if (pub) {
|
||||||
const evReact = await pub.react(ev);
|
const evReact = await pub.react(ev);
|
||||||
await system.BroadcastEvent(evReact);
|
await system.BroadcastEvent(evReact);
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<Icon name="message-circle" />
|
<div className={iconClass}>
|
||||||
{grouped.replies.length > 0 ? grouped.replies.length : undefined}
|
<Icon name="message-circle" />
|
||||||
|
{grouped.replies.length > 0 ? grouped.replies.length : <> </>}
|
||||||
|
</div>
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
)}
|
)}
|
||||||
<ZapEvent ev={ev}>
|
<ZapEvent ev={ev}>
|
||||||
<div className={iconClass}>
|
<div className={iconClass}>
|
||||||
{reactedIcon("zap", "zap-filled", "text-zap", grouped.zaps, EventKind.ZapReceipt)}
|
{reactedIcon("zap", "zap-filled", "text-zap", grouped.zaps, EventKind.ZapReceipt)}
|
||||||
{totalZaps > 0 ? formatSats(totalZaps) : undefined}
|
{totalZaps > 0 ? formatSats(totalZaps) : <> </>}
|
||||||
</div>
|
</div>
|
||||||
</ZapEvent>
|
</ZapEvent>
|
||||||
<AsyncButton
|
<AsyncButton
|
||||||
className={iconClass}
|
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (pub) {
|
if (pub) {
|
||||||
const evReact = await pub.react(ev);
|
const evReact = await pub.react(ev);
|
||||||
await system.BroadcastEvent(evReact);
|
await system.BroadcastEvent(evReact);
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
{reactedIcon("heart", "heart-solid", "text-red-500", grouped.reactions.positive, EventKind.Reaction)}
|
<div className={iconClass}>
|
||||||
{grouped.reactions.positive.length > 0 ? grouped.reactions.positive.length : undefined}
|
{reactedIcon("heart", "heart-solid", "text-red-500", grouped.reactions.positive, EventKind.Reaction)}
|
||||||
|
{grouped.reactions.positive.length > 0 ? grouped.reactions.positive.length : <> </>}
|
||||||
|
</div>
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
<AsyncButton
|
<AsyncButton
|
||||||
className={iconClass}
|
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
if (pub) {
|
if (pub) {
|
||||||
const evReact = await pub.repost(ev);
|
const evReact = await pub.repost(ev);
|
||||||
await system.BroadcastEvent(evReact);
|
await system.BroadcastEvent(evReact);
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<Icon name="repost" />
|
<div className={iconClass}>
|
||||||
{grouped.reposts.length > 0 ? grouped.reposts.length : undefined}
|
<Icon name="repost" />
|
||||||
|
{grouped.reposts.length > 0 ? grouped.reposts.length : <> </>}
|
||||||
|
</div>
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
74
src/element/video-info.tsx
Normal file
74
src/element/video-info.tsx
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import { FollowButton } from "./follow-button";
|
||||||
|
import { Profile, getName } from "./profile";
|
||||||
|
import { SendZapsDialog } from "./send-zap";
|
||||||
|
import { ShareMenu } from "./share-menu";
|
||||||
|
import { useVideoPlayerContext } from "./video/context";
|
||||||
|
import { getHost } from "@/utils";
|
||||||
|
import { useUserProfile } from "@snort/system-react";
|
||||||
|
import { NostrLink } from "@snort/system";
|
||||||
|
import { StreamSummary } from "./stream/summary";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import { WriteMessage } from "./chat/write-message";
|
||||||
|
import VideoComments from "./video/comments";
|
||||||
|
|
||||||
|
export function VideoInfo({
|
||||||
|
showComments,
|
||||||
|
showShare,
|
||||||
|
showZap,
|
||||||
|
}: {
|
||||||
|
showComments?: boolean;
|
||||||
|
showShare?: boolean;
|
||||||
|
showZap?: boolean;
|
||||||
|
}) {
|
||||||
|
const ctx = useVideoPlayerContext();
|
||||||
|
const ev = ctx.event;
|
||||||
|
const link = NostrLink.fromEvent(ev);
|
||||||
|
const host = getHost(ctx.event);
|
||||||
|
const profile = useUserProfile(host);
|
||||||
|
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={classNames("row-start-2 col-start-1 max-xl:px-4 flex flex-col gap-4", {
|
||||||
|
"mx-auto w-[40dvw]": ctx.widePlayer,
|
||||||
|
})}>
|
||||||
|
<div className="font-medium text-xl">{ctx.video?.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 && (
|
||||||
|
<>
|
||||||
|
{(showShare ?? true) && <ShareMenu ev={ev} />}
|
||||||
|
{(showZap ?? true) && zapTarget && (
|
||||||
|
<SendZapsDialog
|
||||||
|
lnurl={zapTarget}
|
||||||
|
pubkey={host}
|
||||||
|
aTag={link.tagKey}
|
||||||
|
targetName={getName(ev.pubkey, profile)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{ctx.video?.summary && <StreamSummary text={ctx.video.summary} />}
|
||||||
|
{(showComments ?? true) && (
|
||||||
|
<>
|
||||||
|
<h3>
|
||||||
|
<FormattedMessage defaultMessage="Comments" />
|
||||||
|
</h3>
|
||||||
|
<div>
|
||||||
|
<WriteMessage link={link} emojiPacks={[]} kind={1} />
|
||||||
|
</div>
|
||||||
|
<VideoComments link={link} />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,8 +1,10 @@
|
|||||||
import { VideoInfo } from "@/service/video/info";
|
import { VideoInfo } from "@/service/video/info";
|
||||||
|
import { TaggedNostrEvent } from "@snort/system";
|
||||||
import { ReactNode, createContext, useContext, useEffect, useState } from "react";
|
import { ReactNode, createContext, useContext, useEffect, useState } from "react";
|
||||||
|
|
||||||
interface VideoPlayerContext {
|
interface VideoPlayerContext {
|
||||||
video?: VideoInfo;
|
video: VideoInfo;
|
||||||
|
event: TaggedNostrEvent;
|
||||||
widePlayer: boolean;
|
widePlayer: boolean;
|
||||||
update: (fn: (c: VideoPlayerContext) => VideoPlayerContext) => void;
|
update: (fn: (c: VideoPlayerContext) => VideoPlayerContext) => void;
|
||||||
}
|
}
|
||||||
@ -10,15 +12,17 @@ interface VideoPlayerContext {
|
|||||||
const VPContext = createContext<VideoPlayerContext>({
|
const VPContext = createContext<VideoPlayerContext>({
|
||||||
widePlayer: false,
|
widePlayer: false,
|
||||||
update: () => {},
|
update: () => {},
|
||||||
});
|
} as unknown as VideoPlayerContext);
|
||||||
|
|
||||||
export function useVideoPlayerContext() {
|
export function useVideoPlayerContext() {
|
||||||
return useContext(VPContext);
|
return useContext(VPContext);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function VideoPlayerContextProvider({ info, children }: { info: VideoInfo; children?: ReactNode }) {
|
export function VideoPlayerContextProvider({ event, children }: { event: TaggedNostrEvent; children?: ReactNode }) {
|
||||||
|
const info = VideoInfo.parse(event);
|
||||||
const [state, setState] = useState<VideoPlayerContext>({
|
const [state, setState] = useState<VideoPlayerContext>({
|
||||||
video: info,
|
video: info,
|
||||||
|
event,
|
||||||
widePlayer: localStorage.getItem("wide-player") === "true",
|
widePlayer: localStorage.getItem("wide-player") === "true",
|
||||||
update: (fn: (c: VideoPlayerContext) => VideoPlayerContext) => {
|
update: (fn: (c: VideoPlayerContext) => VideoPlayerContext) => {
|
||||||
setState(fn);
|
setState(fn);
|
||||||
|
@ -15,7 +15,15 @@ import useImgProxy from "@/hooks/img-proxy";
|
|||||||
import { useMediaQuery } from "usehooks-ts";
|
import { useMediaQuery } from "usehooks-ts";
|
||||||
import { useVideoPlayerContext } from "./context";
|
import { useVideoPlayerContext } from "./context";
|
||||||
|
|
||||||
export default function VideoPlayer() {
|
export default function VideoPlayer({
|
||||||
|
showPip,
|
||||||
|
showWideMode,
|
||||||
|
loop,
|
||||||
|
}: {
|
||||||
|
showPip?: boolean;
|
||||||
|
showWideMode?: boolean;
|
||||||
|
loop?: boolean;
|
||||||
|
}) {
|
||||||
const isDesktop = useMediaQuery("(min-width: 1280px)");
|
const isDesktop = useMediaQuery("(min-width: 1280px)");
|
||||||
const ctx = useVideoPlayerContext();
|
const ctx = useVideoPlayerContext();
|
||||||
|
|
||||||
@ -23,10 +31,11 @@ export default function VideoPlayer() {
|
|||||||
return (
|
return (
|
||||||
<MediaController className="min-w-0 w-full" mediaStreamType="on-demand">
|
<MediaController className="min-w-0 w-full" mediaStreamType="on-demand">
|
||||||
<video
|
<video
|
||||||
className="max-h-[80dvh] aspect-video"
|
className="max-h-[80dvh]"
|
||||||
slot="media"
|
slot="media"
|
||||||
autoPlay={true}
|
autoPlay={true}
|
||||||
controls={false}
|
controls={false}
|
||||||
|
loop={loop}
|
||||||
poster={proxy(ctx.video?.bestPoster()?.url ?? "")}>
|
poster={proxy(ctx.video?.bestPoster()?.url ?? "")}>
|
||||||
{ctx.video?.sources().map(a => <source src={a.url} type={a.mimeType} />)}
|
{ctx.video?.sources().map(a => <source src={a.url} type={a.mimeType} />)}
|
||||||
</video>
|
</video>
|
||||||
@ -37,9 +46,9 @@ export default function VideoPlayer() {
|
|||||||
<MediaTimeDisplay showDuration></MediaTimeDisplay>
|
<MediaTimeDisplay showDuration></MediaTimeDisplay>
|
||||||
<MediaMuteButton />
|
<MediaMuteButton />
|
||||||
<MediaVolumeRange />
|
<MediaVolumeRange />
|
||||||
<MediaPipButton />
|
{(showPip ?? true) && <MediaPipButton />}
|
||||||
<MediaFullscreenButton />
|
<MediaFullscreenButton />
|
||||||
{isDesktop && (
|
{isDesktop && (showWideMode ?? true) && (
|
||||||
<MediaPlayerSizeButtonReact
|
<MediaPlayerSizeButtonReact
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
ctx.update(c => ({
|
ctx.update(c => ({
|
||||||
|
@ -736,6 +736,9 @@
|
|||||||
"kAEQyV": {
|
"kAEQyV": {
|
||||||
"defaultMessage": "OK"
|
"defaultMessage": "OK"
|
||||||
},
|
},
|
||||||
|
"kGjqZ4": {
|
||||||
|
"defaultMessage": "Latest Shorts"
|
||||||
|
},
|
||||||
"kc5EOy": {
|
"kc5EOy": {
|
||||||
"defaultMessage": "Username is too long"
|
"defaultMessage": "Username is too long"
|
||||||
},
|
},
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { VIDEO_KIND } from "@/const";
|
import { SHORTS_KIND, VIDEO_KIND } from "@/const";
|
||||||
import { useStreamLink } from "@/hooks/stream-link";
|
import { useStreamLink } from "@/hooks/stream-link";
|
||||||
import { getEventFromLocationState } from "@/utils";
|
import { getEventFromLocationState } from "@/utils";
|
||||||
import { NostrPrefix, EventKind } from "@snort/system";
|
import { NostrPrefix, EventKind } from "@snort/system";
|
||||||
@ -9,6 +9,7 @@ import { EventEmbed as NostrEventElement } from "@/element/event-embed";
|
|||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { useLayout } from "./layout/context";
|
import { useLayout } from "./layout/context";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { ShortPage } from "./short";
|
||||||
|
|
||||||
export function LinkHandler() {
|
export function LinkHandler() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
@ -32,6 +33,8 @@ export function LinkHandler() {
|
|||||||
);
|
);
|
||||||
} else if (link.kind === VIDEO_KIND) {
|
} else if (link.kind === VIDEO_KIND) {
|
||||||
return <VideoPage link={link} evPreload={evPreload} />;
|
return <VideoPage link={link} evPreload={evPreload} />;
|
||||||
|
} else if (link.kind === SHORTS_KIND) {
|
||||||
|
return <ShortPage link={link} evPreload={evPreload} />;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
25
src/pages/short.tsx
Normal file
25
src/pages/short.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import EventReactions from "@/element/event-reactions";
|
||||||
|
import { VideoInfo } from "@/element/video-info";
|
||||||
|
import { VideoPlayerContextProvider } from "@/element/video/context";
|
||||||
|
import VideoPlayer from "@/element/video/player";
|
||||||
|
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
|
||||||
|
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||||
|
|
||||||
|
export function ShortPage({ link, evPreload }: { link: NostrLink; evPreload?: TaggedNostrEvent }) {
|
||||||
|
const ev = useCurrentStreamFeed(link, true, evPreload);
|
||||||
|
|
||||||
|
if (!ev) return;
|
||||||
|
return (
|
||||||
|
<VideoPlayerContextProvider event={ev}>
|
||||||
|
<div className="max-xl:py-2 max-xl:w-full xl:w-[550px] mx-auto">
|
||||||
|
<div className="relative">
|
||||||
|
<VideoPlayer showPip={false} showWideMode={false} loop={true} />
|
||||||
|
<div className="absolute bottom-0 -right-14">
|
||||||
|
<EventReactions ev={ev} vertical={true} replyKind={1} className="text-white" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<VideoInfo showComments={false} showZap={false} />
|
||||||
|
</div>
|
||||||
|
</VideoPlayerContextProvider>
|
||||||
|
);
|
||||||
|
}
|
@ -1,3 +1,34 @@
|
|||||||
|
import { SHORTS_KIND } from "@/const";
|
||||||
|
import VideoGrid from "@/element/video-grid";
|
||||||
|
import { VideoTile } from "@/element/video/video-tile";
|
||||||
|
import { findTag } from "@/utils";
|
||||||
|
import { RequestBuilder } from "@snort/system";
|
||||||
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
export function ShortsPage() {
|
export function ShortsPage() {
|
||||||
return <>Coming soon...</>;
|
const rb = new RequestBuilder("shorts");
|
||||||
|
rb.withFilter().kinds([SHORTS_KIND]);
|
||||||
|
|
||||||
|
const videos = useRequestBuilder(rb);
|
||||||
|
|
||||||
|
const sorted = videos.sort((a, b) => {
|
||||||
|
const pubA = findTag(a, "published_at");
|
||||||
|
const pubB = findTag(b, "published_at");
|
||||||
|
return Number(pubA) > Number(pubB) ? -1 : 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4">
|
||||||
|
<h2>
|
||||||
|
<FormattedMessage defaultMessage="Latest Shorts" />
|
||||||
|
</h2>
|
||||||
|
<br />
|
||||||
|
<VideoGrid>
|
||||||
|
{sorted.map(a => (
|
||||||
|
<VideoTile ev={a} key={a.id} style="grid" />
|
||||||
|
))}
|
||||||
|
</VideoGrid>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,32 +1,23 @@
|
|||||||
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 { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
|
||||||
import { getHost, findTag } from "@/utils";
|
import { getHost, findTag } from "@/utils";
|
||||||
import { NostrLink, RequestBuilder, TaggedNostrEvent } from "@snort/system";
|
import { NostrLink, RequestBuilder, TaggedNostrEvent } from "@snort/system";
|
||||||
import { useRequestBuilder, useUserProfile } from "@snort/system-react";
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { StreamTile } from "@/element/stream/stream-tile";
|
import { StreamTile } from "@/element/stream/stream-tile";
|
||||||
import { VIDEO_KIND } from "@/const";
|
import { VIDEO_KIND } from "@/const";
|
||||||
import { VideoInfo } from "@/service/video/info";
|
|
||||||
import { VideoPlayerContextProvider, useVideoPlayerContext } from "@/element/video/context";
|
import { VideoPlayerContextProvider, useVideoPlayerContext } from "@/element/video/context";
|
||||||
import VideoPlayer from "@/element/video/player";
|
import VideoPlayer from "@/element/video/player";
|
||||||
|
import { VideoInfo } from "@/element/video-info";
|
||||||
|
|
||||||
export function VideoPage({ link, evPreload }: { link: NostrLink; evPreload?: TaggedNostrEvent }) {
|
export function VideoPage({ link, evPreload }: { link: NostrLink; evPreload?: TaggedNostrEvent }) {
|
||||||
const ev = useCurrentStreamFeed(link, true, evPreload);
|
const ev = useCurrentStreamFeed(link, true, evPreload);
|
||||||
|
|
||||||
if (!ev) return;
|
if (!ev) return;
|
||||||
const video = VideoInfo.parse(ev);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VideoPlayerContextProvider info={video}>
|
<VideoPlayerContextProvider event={ev}>
|
||||||
<VideoPageInner ev={ev} />
|
<VideoPageInner ev={ev} />
|
||||||
</VideoPlayerContextProvider>
|
</VideoPlayerContextProvider>
|
||||||
);
|
);
|
||||||
@ -37,9 +28,6 @@ function VideoPageInner({ ev }: { ev: TaggedNostrEvent }) {
|
|||||||
const ctx = useVideoPlayerContext();
|
const ctx = useVideoPlayerContext();
|
||||||
const link = NostrLink.fromEvent(ev);
|
const link = NostrLink.fromEvent(ev);
|
||||||
|
|
||||||
const profile = useUserProfile(host);
|
|
||||||
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={classNames("xl:p-4 grow xl:grid xl:gap-2 xl:grid-cols-[auto_450px]", {
|
className={classNames("xl:p-4 grow xl:grid xl:gap-2 xl:grid-cols-[auto_450px]", {
|
||||||
@ -52,43 +40,7 @@ function VideoPageInner({ ev }: { ev: TaggedNostrEvent }) {
|
|||||||
<VideoPlayer />
|
<VideoPlayer />
|
||||||
</div>
|
</div>
|
||||||
{/* VIDEO INFO & COMMENTS */}
|
{/* VIDEO INFO & COMMENTS */}
|
||||||
<div
|
<VideoInfo showComments={true} />
|
||||||
className={classNames("row-start-2 col-start-1 max-xl:px-4 flex flex-col gap-4", {
|
|
||||||
"mx-auto w-[40dvw]": ctx.widePlayer,
|
|
||||||
})}>
|
|
||||||
<div className="font-medium text-xl">{ctx.video?.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>
|
|
||||||
{ctx.video?.summary && <StreamSummary text={ctx.video.summary} />}
|
|
||||||
<h3>
|
|
||||||
<FormattedMessage defaultMessage="Comments" />
|
|
||||||
</h3>
|
|
||||||
<div>
|
|
||||||
<WriteMessage link={link} emojiPacks={[]} kind={1} />
|
|
||||||
</div>
|
|
||||||
<VideoComments link={link} />
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
className={classNames("p-2 col-start-2", {
|
className={classNames("p-2 col-start-2", {
|
||||||
"row-start-1 row-span-3": !ctx.widePlayer,
|
"row-start-1 row-span-3": !ctx.widePlayer,
|
||||||
|
@ -242,6 +242,7 @@
|
|||||||
"jvo0vs": "Save",
|
"jvo0vs": "Save",
|
||||||
"k21gTS": "e.g. about me",
|
"k21gTS": "e.g. about me",
|
||||||
"kAEQyV": "OK",
|
"kAEQyV": "OK",
|
||||||
|
"kGjqZ4": "Latest Shorts",
|
||||||
"kc5EOy": "Username is too long",
|
"kc5EOy": "Username is too long",
|
||||||
"khJ51Q": "Stream Earnings",
|
"khJ51Q": "Stream Earnings",
|
||||||
"kp0NPF": "Planned",
|
"kp0NPF": "Planned",
|
||||||
|
Reference in New Issue
Block a user