feat: clips

This commit is contained in:
Kieran 2023-12-08 15:45:44 +00:00
parent 74c087525c
commit 6d3724fd87
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
12 changed files with 110 additions and 11 deletions

View File

@ -113,5 +113,11 @@
<symbol id="line-chart-up" viewBox="0 0 20 20" fill="none">
<path d="M19 19H2.6C2.03995 19 1.75992 19 1.54601 18.891C1.35785 18.7951 1.20487 18.6422 1.10899 18.454C1 18.2401 1 17.9601 1 17.4V1M19 5L13.5657 10.4343C13.3677 10.6323 13.2687 10.7313 13.1545 10.7684C13.0541 10.8011 12.9459 10.8011 12.8455 10.7684C12.7313 10.7313 12.6323 10.6323 12.4343 10.4343L10.5657 8.56569C10.3677 8.36768 10.2687 8.26867 10.1545 8.23158C10.0541 8.19895 9.94591 8.19895 9.84549 8.23158C9.73133 8.26867 9.63232 8.36768 9.43431 8.56569L5 13M19 5H15M19 5V9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</symbol>
<symbol id="clapperboard" viewBox="0 0 22 20" fill="none">
<path d="M6.78874 0.956901C6.85435 0.628893 6.88715 0.464889 6.84274 0.336538C6.8038 0.223961 6.72594 0.128987 6.62319 0.0687195C6.50604 7.31647e-06 6.33878 7.31647e-06 6.00428 7.31647e-06H5.7587C4.95375 -4.70504e-06 4.28936 -1.46255e-05 3.74818 0.0442022C3.18608 0.0901274 2.66938 0.188692 2.18404 0.435982C1.43139 0.819476 0.819469 1.4314 0.435976 2.18405C0.188685 2.66938 0.0901205 3.18609 0.0441953 3.74818C0.0294183 3.92905 0.0220297 4.01948 0.0341758 4.11998C0.0833235 4.52667 0.425196 4.89762 0.826532 4.97972C0.925715 5.00001 1.0353 5.00001 1.25446 5.00001H5.32428C5.5579 5.00001 5.67471 5.00001 5.77045 4.95817C5.85488 4.92127 5.92747 4.86176 5.98021 4.7862C6.04002 4.70052 6.06293 4.58598 6.10874 4.3569L6.78874 0.956901Z" fill="currentColor"/>
<path d="M8.21111 4.04311C8.14551 4.37112 8.11271 4.53513 8.15711 4.66348C8.19606 4.77605 8.27392 4.87103 8.37667 4.9313C8.49382 5.00001 8.66107 5.00001 8.99557 5.00001H12.3243C12.5579 5.00001 12.6747 5.00001 12.7705 4.95817C12.8549 4.92127 12.9275 4.86176 12.9802 4.7862C13.04 4.70052 13.0629 4.58598 13.1087 4.3569L13.7887 0.956901C13.8543 0.628893 13.8871 0.464889 13.8427 0.336538C13.8038 0.223961 13.7259 0.128987 13.6232 0.0687195C13.506 7.31647e-06 13.3388 7.31647e-06 13.0043 7.31647e-06H9.67557C9.44195 7.31647e-06 9.32514 7.31647e-06 9.2294 0.0418505C9.14497 0.078749 9.07238 0.13826 9.01964 0.213813C8.95983 0.299491 8.93693 0.414032 8.89111 0.643115L8.21111 4.04311Z" fill="currentColor"/>
<path d="M16.6756 -0.00186977C16.4415 -0.00137503 16.3245 -0.00112766 16.229 0.0407906C16.1446 0.0778141 16.0724 0.13713 16.0197 0.212716C15.9601 0.298296 15.9371 0.413236 15.8911 0.643114L15.2111 4.04311C15.1455 4.37112 15.1127 4.53513 15.1571 4.66348C15.1961 4.77605 15.2739 4.87103 15.3767 4.9313C15.4938 5.00001 15.6611 5.00001 15.9956 5.00001H20.7455C20.9647 5.00001 21.0743 5.00001 21.1735 4.97972C21.5748 4.89762 21.9167 4.52668 21.9658 4.11999C21.978 4.01949 21.9706 3.92906 21.9558 3.74822C21.9099 3.18613 21.8113 2.66938 21.564 2.18405C21.1805 1.4314 20.5686 0.819476 19.816 0.435982C19.3306 0.188692 18.8139 0.0901274 18.2518 0.0442022C17.7281 0.00141033 17.2016 -0.00298175 16.6756 -0.00186977Z" fill="currentColor"/>
<path d="M21.891 7.54602C22 7.75993 22 8.03995 22 8.60001V14.2413C22 15.0463 22 15.7106 21.9558 16.2518C21.9099 16.8139 21.8113 17.3306 21.564 17.816C21.1805 18.5686 20.5686 19.1805 19.816 19.564C19.3306 19.8113 18.8139 19.9099 18.2518 19.9558C17.7106 20 17.0463 20 16.2413 20H5.75868C4.95372 20 4.28937 20 3.74818 19.9558C3.18608 19.9099 2.66938 19.8113 2.18404 19.564C1.43139 19.1805 0.819469 18.5686 0.435976 17.816C0.188685 17.3306 0.0901205 16.8139 0.0441953 16.2518C-2.13385e-05 15.7106 -1.15136e-05 15.0463 3.88855e-07 14.2413V8.60001C3.88855e-07 8.03995 3.8743e-07 7.75993 0.108994 7.54602C0.204868 7.35786 0.357848 7.20487 0.54601 7.109C0.759922 7.00001 1.03995 7.00001 1.6 7.00001H20.4C20.9601 7.00001 21.2401 7.00001 21.454 7.109C21.6422 7.20487 21.7951 7.35786 21.891 7.54602Z" fill="currentColor"/>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -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;

View File

@ -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 (
<AsyncButton onClick={makeClip} className="btn btn-primary">
<Icon name="clapperboard" />
<FormattedMessage defaultMessage="Create Clip" id="PA0ej4" />
</AsyncButton>
);
}

View File

@ -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 <ChatRaid ev={a} link={link} />;
}
case LIVE_STREAM_CLIP: {
return <ChatClip ev={a} />;
}
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 }
</div>
);
}
function ChatClip({ ev }: { ev: TaggedNostrEvent }) {
const profile = useUserProfile(ev.pubkey);
const rTag = findTag(ev, "r");
return (
<div className="px-3 py-2 text-center rounded-xl bg-primary uppercase pointer font-bold flex flex-col gap-2">
<div>
<FormattedMessage
defaultMessage="{name} created a clip"
id="BD0vyn"
values={{
name: profile?.name,
}}
/>
</div>
{rTag && <video src={rTag} controls playsInline={true} muted={true} />}
</div>
);
}

View File

@ -50,9 +50,7 @@ export function Text({ content, tags, eventComponent }: TextProps) {
case "mention":
return <Mention pubkey={f.content} />;
case "hashtag":
return <Link to={`/t/${f.content}`}>
#{f.content}
</Link>
return <Link to={`/t/${f.content}`}>#{f.content}</Link>;
default: {
if (f.content.startsWith("lnurlp:")) {
// LUD-17: https://github.com/lnurl/luds/blob/luds/17.md

View File

@ -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<string>, limit = 100) {
const since = useMemo(() => unixNow() - WEEK, [link?.id]);
@ -13,14 +13,16 @@ export function useLiveChatFeed(link?: NostrLink, eZaps?: Array<string>, 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(

View File

@ -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"
},

View File

@ -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);
}

View File

@ -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 })
</div>
{ev && (
<>
<ClipButton ev={ev} />
<ShareMenu ev={ev} />
{zapTarget && (
<SendZapsDialog

View File

@ -15,6 +15,9 @@ export class NostrStreamProvider implements StreamProvider {
#publisher?: EventPublisher;
constructor(readonly name: string, readonly url: string, pub?: EventPublisher) {
if (!url.endsWith("/")) {
this.url = `${url}/`;
}
this.#publisher = pub;
}
@ -88,6 +91,10 @@ export class NostrStreamProvider implements StreamProvider {
await this.#getJson("DELETE", `account/forward/${id}`);
}
async createClip(id: string) {
return await this.#getJson<{ url: string }>("POST", `clip/${id}`);
}
async #getJson<T>(method: "GET" | "POST" | "PATCH" | "DELETE", path: string, body?: unknown): Promise<T> {
const pub = (() => {
if (this.#publisher) {

View File

@ -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?",

View File

@ -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<string>, 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]) ?? [];