Files
zap.stream/src/element/video-grid-sorted.tsx

204 lines
6.1 KiB
TypeScript

import { useLogin } from "@/hooks/login";
import { useSortedStreams } from "@/hooks/useLiveStreams";
import { getTagValues, getHost, extractStreamInfo } from "@/utils";
import { NostrEvent, NostrLink, TaggedNostrEvent } from "@snort/system";
import { ReactNode, useMemo } from "react";
import { FormattedMessage } from "react-intl";
import VideoGrid from "./video-grid";
import { StreamTile } from "./stream/stream-tile";
import { CategoryTile } from "./category/category-tile";
import { Link } from "react-router-dom";
import Pill from "./pill";
import { CategoryZaps } from "./category/zaps";
import { StreamState, VIDEO_KIND } from "@/const";
import { useRecentClips } from "@/hooks/clips";
import { ClipTile } from "./stream/clip-tile";
interface VideoGridSortedProps {
evs: Array<TaggedNostrEvent>;
showAll?: boolean;
showEnded?: boolean;
showPlanned?: boolean;
showPopular?: boolean;
showRecentClips?: boolean;
showVideos?: boolean;
}
export default function VideoGridSorted({
evs,
showAll,
showEnded,
showPlanned,
showPopular,
showRecentClips,
showVideos,
}: VideoGridSortedProps) {
const login = useLogin();
const mutedHosts = login?.state?.muted ?? [];
const follows = login?.state?.follows ?? [];
const followsHost = (ev: NostrEvent) => follows?.includes(getHost(ev));
const filteredStreams = evs.filter(a => !mutedHosts.includes(NostrLink.publicKey(getHost(a))));
const { live, planned, ended } = useSortedStreams(filteredStreams, showAll ? 0 : undefined);
const hashtags: Array<string> = [];
const following = live.filter(followsHost);
const liveNow = live.filter(e => !following.includes(e));
const hasFollowingLive = following.length > 0;
const plannedEvents = planned.filter(followsHost);
const liveByHashtag = useMemo(() => {
return hashtags
.map(t => ({
tag: t,
live: live.filter(e => {
const evTags = getTagValues(e.tags, "t");
return evTags.includes(t);
}),
}))
.filter(t => t.live.length > 0);
}, [live, hashtags]);
return (
<div className="flex flex-col gap-6">
{hasFollowingLive && (
<GridSection header={<FormattedMessage defaultMessage="Following" id="cPIKU2" />} items={following} />
)}
{!hasFollowingLive && (
<VideoGrid>
{live.map(e => (
<StreamTile ev={e} key={e.id} style="grid" />
))}
</VideoGrid>
)}
{liveByHashtag.map(t => (
<GridSection header={`#${t.tag}`} items={t.live} />
))}
{showVideos && (
<GridSection
header={<FormattedMessage defaultMessage="Videos" />}
items={evs.filter(a => a.kind === VIDEO_KIND)}
/>
)}
{showRecentClips && <RecentClips />}
{hasFollowingLive && liveNow.length > 0 && (
<GridSection header={<FormattedMessage defaultMessage="Live" id="Dn82AL" />} items={liveNow} />
)}
{showPopular && <PopularCategories items={evs} />}
{plannedEvents.length > 0 && (showPlanned ?? true) && (
<GridSection header={<FormattedMessage defaultMessage="Planned" id="kp0NPF" />} items={plannedEvents} />
)}
{ended.length > 0 && (showEnded ?? true) && (
<GridSection header={<FormattedMessage defaultMessage="Ended" id="TP/cMX" />} items={ended} />
)}
</div>
);
}
function GridSection({ header, items }: { header: ReactNode; items: Array<TaggedNostrEvent> }) {
return (
<>
<div className="flex items-center gap-4">
<h3 className="whitespace-nowrap">{header}</h3>
<span className="h-[1px] bg-layer-1 w-full" />
</div>
<VideoGrid>
{items.map(e => (
<StreamTile ev={e} key={e.id} style="grid" />
))}
</VideoGrid>
</>
);
}
function PopularCategories({ items }: { items: Array<TaggedNostrEvent> }) {
const categories = useMemo(() => {
const grouped = items.reduce(
(acc, v) => {
const { gameId, participants, status } = extractStreamInfo(v);
if (gameId) {
acc[gameId] ??= {
gameId,
viewers: 0,
zaps: 0,
streams: 0,
};
if (participants && status === StreamState.Live) {
acc[gameId].viewers += Number(participants);
}
acc[gameId].streams++;
}
return acc;
},
{} as Record<
string,
{
gameId: string;
viewers: number;
zaps: number;
streams: number;
}
>,
);
return Object.values(grouped)
.sort((a, b) => (a.streams > b.streams ? -1 : 1))
.slice(0, 8);
}, [items]);
return (
<>
<div className="flex items-center gap-4">
<h3 className="whitespace-nowrap">
<FormattedMessage defaultMessage="Popular" />
</h3>
<span className="h-[1px] bg-layer-1 w-full" />
</div>
<div className="flex flex-wrap gap-4">
{categories.map(a => (
<Link
key={a.gameId}
to={`/category/${a.gameId}`}
className="xl:w-[180px] lg:w-[170px] max-lg:w-[calc(33.3%-0.75rem)]">
<CategoryTile gameId={a.gameId} showFooterTitle={true}>
<div className="flex gap-2 flex-wrap">
<CategoryZaps gameId={a.gameId} />
{a.viewers > 0 && (
<Pill>
<FormattedMessage
defaultMessage="{n} viewers"
values={{
n: a.viewers,
}}
/>
</Pill>
)}
</div>
</CategoryTile>
</Link>
))}
</div>
</>
);
}
function RecentClips() {
const clips = useRecentClips();
return (
<>
<div className="flex items-center gap-4">
<h3 className="whitespace-nowrap">
<FormattedMessage defaultMessage="Recent Clips" />
</h3>
<span className="h-[1px] bg-layer-1 w-full" />
</div>
<VideoGrid>
{clips.slice(0, 5).map(a => (
<ClipTile ev={a} key={a.id} />
))}
</VideoGrid>
</>
);
}