feat: link to nests from live streams header

This commit is contained in:
2025-05-07 14:15:07 +01:00
parent fb844a5969
commit ba62f0ef74
4 changed files with 80 additions and 25 deletions

View File

@ -10,6 +10,7 @@ import useLiveStreams from "@/Hooks/useLiveStreams";
import { findTag } from "@/Utils"; import { findTag } from "@/Utils";
import Avatar from "../User/Avatar"; import Avatar from "../User/Avatar";
import { NestsParticipants } from "./nests-participants";
export function LiveStreams() { export function LiveStreams() {
const streams = useLiveStreams(); const streams = useLiveStreams();
@ -17,9 +18,18 @@ export function LiveStreams() {
return ( return (
<div className="flex mx-2 gap-4 overflow-x-auto sm-hide-scrollbar"> <div className="flex mx-2 gap-4 overflow-x-auto sm-hide-scrollbar">
{streams.map(v => ( {streams.map(v => {
<LiveStreamEvent ev={v} key={`${v.kind}:${v.pubkey}:${findTag(v, "d")}`} className="h-[80px]" /> const k = `${v.kind}:${v.pubkey}:${findTag(v, "d")}`;
))} const isVideoStream = v.tags.some(a => a[0] === "streaming" && a[1].includes(".m3u8"));
if (isVideoStream) {
return <LiveStreamEvent ev={v} key={k} className="h-[80px]" />;
}
const isNests = v.tags.some(a => a[0] === "streaming" && a[1].startsWith("wss+livekit://"));
if (isNests) {
return <AudioRoom ev={v} key={k} className="h-[80px]" />;
}
})}
</div> </div>
); );
} }
@ -66,3 +76,37 @@ export function LiveStreamEvent({ ev, className }: { ev: NostrEvent; className?:
</Link> </Link>
); );
} }
export function AudioRoom({ ev, className }: { ev: NostrEvent; className?: string }) {
const { proxy } = useImgProxy();
const title = findTag(ev, "title");
const image = findTag(ev, "image");
const link = NostrLink.fromEvent(ev).encode();
const imageProxy = proxy(image ?? "");
return (
<Link className={classNames("flex gap-2", className)} to={`/${link}`}>
<div className="relative aspect-video">
<div
className="absolute h-full w-full bg-center bg-cover bg-gray-ultradark rounded-lg flex items-end justify-center"
style={
{
backgroundImage: `url(${imageProxy})`,
} as CSSProperties
}>
<div className="flex items-center gap-1">
<NestsParticipants ev={ev} />
</div>
</div>
<div className="absolute left-0 top-0 w-full overflow-hidden">
<div
className="whitespace-nowrap px-1 text-ellipsis overflow-hidden text-xs font-medium bg-background opacity-70 text-center"
title={title}>
{title}
</div>
</div>
</div>
</Link>
);
}

View File

@ -6,7 +6,7 @@ import {
useParticipantPermissions, useParticipantPermissions,
useParticipants, useParticipants,
} from "@livekit/components-react"; } from "@livekit/components-react";
import { dedupe, unixNow } from "@snort/shared"; import { unixNow } from "@snort/shared";
import { EventKind, EventPublisher, NostrLink, RequestBuilder, SystemInterface, TaggedNostrEvent } from "@snort/system"; import { EventKind, EventPublisher, NostrLink, RequestBuilder, SystemInterface, TaggedNostrEvent } from "@snort/system";
import { useRequestBuilder, useUserProfile } from "@snort/system-react"; import { useRequestBuilder, useUserProfile } from "@snort/system-react";
import classNames from "classnames"; import classNames from "classnames";
@ -22,9 +22,9 @@ import AsyncButton from "../Button/AsyncButton";
import IconButton from "../Button/IconButton"; import IconButton from "../Button/IconButton";
import { ProxyImg } from "../ProxyImg"; import { ProxyImg } from "../ProxyImg";
import Avatar from "../User/Avatar"; import Avatar from "../User/Avatar";
import { AvatarGroup } from "../User/AvatarGroup";
import DisplayName from "../User/DisplayName"; import DisplayName from "../User/DisplayName";
import ProfileImage from "../User/ProfileImage"; import ProfileImage from "../User/ProfileImage";
import { NestsParticipants } from "./nests-participants";
import VuBar from "./VU"; import VuBar from "./VU";
enum RoomTab { enum RoomTab {
@ -116,11 +116,15 @@ function RoomHeader({ ev }: { ev: TaggedNostrEvent }) {
const { image, title } = extractStreamInfo(ev); const { image, title } = extractStreamInfo(ev);
return ( return (
<div className="relative rounded-xl h-[140px] w-full overflow-hidden"> <div className="relative rounded-xl h-[140px] w-full overflow-hidden">
{image ? <ProxyImg src={image} className="w-full" /> : <div className="absolute bg-gray-dark w-full h-full" />} {image ? (
<ProxyImg src={image} className="w-full h-full object-cover object-center" />
) : (
<div className="absolute bg-gray-dark w-full h-full" />
)}
<div className="absolute left-4 top-4 w-full flex justify-between pr-8"> <div className="absolute left-4 top-4 w-full flex justify-between pr-8">
<div className="text-2xl">{title}</div> <div className="text-2xl">{title}</div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<NostrParticipants ev={ev} /> <NestsParticipants ev={ev} />
</div> </div>
</div> </div>
</div> </div>
@ -287,23 +291,6 @@ function WriteChatMessage({ ev }: { ev: TaggedNostrEvent }) {
); );
} }
function NostrParticipants({ ev }: { ev: TaggedNostrEvent }) {
const link = NostrLink.fromEvent(ev);
const sub = useMemo(() => {
const sub = new RequestBuilder(`livekit-participants:${link.tagKey}`);
sub
.withFilter()
.replyToLink([link])
.kinds([10_312 as EventKind])
.since(unixNow() - 600);
return sub;
}, [link.tagKey]);
const presense = useRequestBuilder(sub);
const filteredPresence = presense.filter(ev => ev.created_at > unixNow() - 600);
return <AvatarGroup ids={dedupe(filteredPresence.map(a => a.pubkey))} size={32} />;
}
function LiveKitUser({ p }: { p: RemoteParticipant | LocalParticipant }) { function LiveKitUser({ p }: { p: RemoteParticipant | LocalParticipant }) {
const pubkey = p.identity.startsWith("guest-") ? "anon" : p.identity; const pubkey = p.identity.startsWith("guest-") ? "anon" : p.identity;
const profile = useUserProfile(pubkey); const profile = useUserProfile(pubkey);

View File

@ -0,0 +1,24 @@
import { dedupe, unixNow } from "@snort/shared";
import { EventKind, NostrLink, RequestBuilder, TaggedNostrEvent } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { useMemo } from "react";
import { AvatarGroup } from "../User/AvatarGroup";
export function NestsParticipants({ ev }: { ev: TaggedNostrEvent }) {
const link = NostrLink.fromEvent(ev);
const sub = useMemo(() => {
const sub = new RequestBuilder(`livekit-participants:${link.tagKey}`);
sub.withOptions({ leaveOpen: true });
sub
.withFilter()
.replyToLink([link])
.kinds([10_312 as EventKind])
.since(unixNow() - 600);
return sub;
}, [link.tagKey]);
const presense = useRequestBuilder(sub);
const filteredPresence = presense.filter(ev => ev.created_at > unixNow() - 600);
return <AvatarGroup ids={dedupe(filteredPresence.map(a => a.pubkey)).slice(0, 5)} size={32} />;
}

View File

@ -11,7 +11,7 @@ export default function useLiveStreams() {
const rb = new RequestBuilder("streams"); const rb = new RequestBuilder("streams");
rb.withFilter() rb.withFilter()
.kinds([EventKind.LiveEvent]) .kinds([EventKind.LiveEvent])
.since(unixNow() - Hour); .since(unixNow() - 4 * Hour);
return rb; return rb;
}, []); }, []);