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 (
+
+ );
+}
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]) ?? [];