diff --git a/public/icons.svg b/public/icons.svg index b49eed7..3c59b87 100644 --- a/public/icons.svg +++ b/public/icons.svg @@ -113,5 +113,11 @@ + + + + + + diff --git a/src/const.ts b/src/const.ts index 3478a9c..30130da 100644 --- a/src/const.ts +++ b/src/const.ts @@ -3,6 +3,7 @@ import { EventKind } from "@snort/system"; export const LIVE_STREAM = 30_311 as EventKind; export const LIVE_STREAM_CHAT = 1_311 as EventKind; export const LIVE_STREAM_RAID = 1_312 as EventKind; +export const LIVE_STREAM_CLIP = 1_313 as EventKind; export const EMOJI_PACK = 30_030 as EventKind; export const USER_EMOJIS = 10_030 as EventKind; export const GOAL = 9041 as EventKind; diff --git a/src/element/clip-button.tsx b/src/element/clip-button.tsx new file mode 100644 index 0000000..0260a5a --- /dev/null +++ b/src/element/clip-button.tsx @@ -0,0 +1,46 @@ +import { useLogin } from "@/hooks/login"; +import { NostrStreamProvider } from "@/providers"; +import { FormattedMessage } from "react-intl"; +import AsyncButton from "./async-button"; +import { LIVE_STREAM_CLIP } from "@/const"; +import { NostrLink, TaggedNostrEvent } from "@snort/system"; +import { extractStreamInfo } from "@/utils"; +import { unwrap } from "@snort/shared"; +import { useContext } from "react"; +import { SnortContext } from "@snort/system-react"; +import { Icon } from "./icon"; + +export function ClipButton({ ev }: { ev: TaggedNostrEvent }) { + const system = useContext(SnortContext); + const { id, service } = extractStreamInfo(ev); + const login = useLogin(); + + if (!service) return; + + async function makeClip() { + if (!service || !id) return; + const publisher = login?.publisher(); + if (!publisher) return; + + const provider = new NostrStreamProvider("", service, publisher); + const clip = await provider.createClip(id); + console.debug(clip); + + const ee = await publisher.generic(eb => { + return eb + .kind(LIVE_STREAM_CLIP) + .tag(unwrap(NostrLink.fromEvent(ev).toEventTag("root"))) + .tag(["r", clip.url]) + .tag(["alt", `Live stream clip created on https://zap.stream\n${clip.url}`]); + }); + console.debug(ee); + await system.BroadcastEvent(ee); + } + + return ( + + + + + ); +} diff --git a/src/element/live-chat.tsx b/src/element/live-chat.tsx index 149c453..ed2d93f 100644 --- a/src/element/live-chat.tsx +++ b/src/element/live-chat.tsx @@ -1,6 +1,6 @@ import "./live-chat.css"; import { FormattedMessage } from "react-intl"; -import { EventKind, NostrEvent, NostrLink, ParsedZap, TaggedNostrEvent, parseNostrLink } from "@snort/system"; +import { EventKind, NostrEvent, NostrLink, ParsedZap, TaggedNostrEvent } from "@snort/system"; import { useEventReactions, useUserProfile } from "@snort/system-react"; import { unixNow, unwrap } from "@snort/shared"; import { useMemo } from "react"; @@ -20,10 +20,9 @@ import { useBadges } from "@/hooks/badges"; import { useLogin } from "@/hooks/login"; import { useAddress, useEvent } from "@/hooks/event"; import { formatSats } from "@/number"; -import { LIVE_STREAM_CHAT, LIVE_STREAM_RAID, WEEK } from "@/const"; +import { LIVE_STREAM_CHAT, LIVE_STREAM_CLIP, LIVE_STREAM_RAID, WEEK } from "@/const"; import { findTag, getHost, getTagValues, uniqBy } from "@/utils"; import { TopZappers } from "./top-zappers"; -import { Mention } from "./mention"; import { Link } from "react-router-dom"; export interface LiveChatOptions { @@ -157,6 +156,9 @@ export function LiveChat({ case LIVE_STREAM_RAID: { return ; } + case LIVE_STREAM_CLIP: { + return ; + } case EventKind.ZapReceipt: { const zap = reactions.zaps.find(b => b.id === a.id && b.receiver === host); if (zap) { @@ -252,3 +254,22 @@ export function ChatRaid({ link, ev }: { link: NostrLink; ev: TaggedNostrEvent } ); } + +function ChatClip({ ev }: { ev: TaggedNostrEvent }) { + const profile = useUserProfile(ev.pubkey); + const rTag = findTag(ev, "r"); + return ( +
+
+ +
+ {rTag &&
+ ); +} diff --git a/src/element/text.tsx b/src/element/text.tsx index 5644816..8432763 100644 --- a/src/element/text.tsx +++ b/src/element/text.tsx @@ -50,9 +50,7 @@ export function Text({ content, tags, eventComponent }: TextProps) { case "mention": return ; case "hashtag": - return - #{f.content} - + return #{f.content}; default: { if (f.content.startsWith("lnurlp:")) { // LUD-17: https://github.com/lnurl/luds/blob/luds/17.md diff --git a/src/hooks/live-chat.tsx b/src/hooks/live-chat.tsx index 7c62ece..8a8e2ad 100644 --- a/src/hooks/live-chat.tsx +++ b/src/hooks/live-chat.tsx @@ -2,7 +2,7 @@ import { NostrLink, NoteCollection, RequestBuilder } from "@snort/system"; import { useReactions, useRequestBuilder } from "@snort/system-react"; import { unixNow } from "@snort/shared"; import { useMemo } from "react"; -import { LIVE_STREAM_CHAT, LIVE_STREAM_RAID, WEEK } from "@/const"; +import { LIVE_STREAM_CHAT, LIVE_STREAM_CLIP, LIVE_STREAM_RAID, WEEK } from "@/const"; export function useLiveChatFeed(link?: NostrLink, eZaps?: Array, limit = 100) { const since = useMemo(() => unixNow() - WEEK, [link?.id]); @@ -13,14 +13,16 @@ export function useLiveChatFeed(link?: NostrLink, eZaps?: Array, limit = leaveOpen: true, }); const aTag = `${link.kind}:${link.author}:${link.id}`; - rb.withFilter().kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID]).tag("a", [aTag]).limit(limit); + rb.withFilter().kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP]).tag("a", [aTag]).limit(limit); return rb; }, [link?.id, since, eZaps]); const feed = useRequestBuilder(NoteCollection, sub); const messages = useMemo(() => { - return (feed.data ?? []).filter(ev => ev.kind === LIVE_STREAM_CHAT || ev.kind === LIVE_STREAM_RAID); + return (feed.data ?? []).filter( + ev => ev.kind === LIVE_STREAM_CHAT || ev.kind === LIVE_STREAM_RAID || ev.kind === LIVE_STREAM_CLIP + ); }, [feed.data]); const reactions = useReactions( diff --git a/src/lang.json b/src/lang.json index 9eb85ff..136897a 100644 --- a/src/lang.json +++ b/src/lang.json @@ -122,6 +122,9 @@ "AyGauy": { "defaultMessage": "Login" }, + "BD0vyn": { + "defaultMessage": "{name} created a clip" + }, "BGxpTN": { "defaultMessage": "Stream Chat" }, @@ -236,6 +239,9 @@ "Oxqtyf": { "defaultMessage": "We hooked you up with a lightning wallet so you can get paid by viewers right away!" }, + "PA0ej4": { + "defaultMessage": "Create Clip" + }, "Pe0ogR": { "defaultMessage": "Theme" }, diff --git a/src/pages/stream-page.css b/src/pages/stream-page.css index 9e04de6..910d337 100644 --- a/src/pages/stream-page.css +++ b/src/pages/stream-page.css @@ -57,12 +57,15 @@ .stream-page .profile-info { width: calc(100% - 32px); } + + .stream-page .video-content video { + max-height: 30vh; + } } .profile-info { display: flex; justify-content: space-between; - padding: 0 16px; gap: var(--gap-m); } diff --git a/src/pages/stream-page.tsx b/src/pages/stream-page.tsx index 472ef00..b4c400e 100644 --- a/src/pages/stream-page.tsx +++ b/src/pages/stream-page.tsx @@ -27,8 +27,9 @@ import { ContentWarningOverlay, isContentWarningAccepted } from "@/element/conte import { useCurrentStreamFeed } from "@/hooks/current-stream-feed"; import { useStreamLink } from "@/hooks/stream-link"; import { FollowButton } from "@/element/follow-button"; +import { ClipButton } from "@/element/clip-button"; -function ProfileInfo({ ev, goal }: { ev?: NostrEvent; goal?: TaggedNostrEvent }) { +function ProfileInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedNostrEvent }) { const system = useContext(SnortContext); const login = useLogin(); const navigate = useNavigate(); @@ -85,6 +86,7 @@ function ProfileInfo({ ev, goal }: { ev?: NostrEvent; goal?: TaggedNostrEvent }) {ev && ( <> + {zapTarget && ( ("POST", `clip/${id}`); + } + async #getJson(method: "GET" | "POST" | "PATCH" | "DELETE", path: string, body?: unknown): Promise { const pub = (() => { if (this.#publisher) { diff --git a/src/translations/en.json b/src/translations/en.json index cb69914..57af62c 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -40,6 +40,7 @@ "Atr2p4": "NSFW Content", "AukrPM": "No viewer data available", "AyGauy": "Login", + "BD0vyn": "{name} created a clip", "BGxpTN": "Stream Chat", "Bep/gA": "Private key", "C81/uG": "Logout", @@ -78,6 +79,7 @@ "OKhRC6": "Share", "OWgHbg": "Edit card", "Oxqtyf": "We hooked you up with a lightning wallet so you can get paid by viewers right away!", + "PA0ej4": "Create Clip", "Pe0ogR": "Theme", "Q3au2v": "About {estimate}", "QRHNuF": "What are we steaming today?", diff --git a/src/utils.ts b/src/utils.ts index 799e1a7..fdea474 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -85,6 +85,7 @@ export function getPlaceholder(id: string) { } interface StreamInfo { + id?: string; title?: string; summary?: string; image?: string; @@ -97,7 +98,9 @@ interface StreamInfo { participants?: string; starts?: string; ends?: string; + service?: string; } + export function extractStreamInfo(ev?: NostrEvent) { const ret = {} as StreamInfo; const matchTag = (tag: Array, k: string, into: (v: string) => void) => { @@ -107,6 +110,7 @@ export function extractStreamInfo(ev?: NostrEvent) { }; for (const t of ev?.tags ?? []) { + matchTag(t, "d", v => (ret.id = v)); matchTag(t, "title", v => (ret.title = v)); matchTag(t, "summary", v => (ret.summary = v)); matchTag(t, "image", v => (ret.image = v)); @@ -118,6 +122,7 @@ export function extractStreamInfo(ev?: NostrEvent) { matchTag(t, "goal", v => (ret.goal = v)); matchTag(t, "starts", v => (ret.starts = v)); matchTag(t, "ends", v => (ret.ends = v)); + matchTag(t, "service", v => (ret.service = v)); } ret.tags = ev?.tags.filter(a => a[0] === "t").map(a => a[1]) ?? [];