Fix chat zap for hosted streams

This commit is contained in:
Kieran 2023-07-05 13:38:54 +01:00
parent b89c8db656
commit 672f1dd077
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
9 changed files with 442 additions and 438 deletions

View File

@ -0,0 +1,170 @@
import { useUserProfile } from "@snort/system-react";
import { NostrEvent, parseZap, EventPublisher, EventKind } from "@snort/system";
import { useRef, useState, useMemo } from "react";
import { useMediaQuery, useHover, useOnClickOutside } from "usehooks-ts";
import { System } from "../index";
import { formatSats } from "../number";
import { EmojiPicker } from "./emoji-picker";
import { Icon } from "./icon";
import { Profile } from "./profile";
import { Text } from "./text";
import { SendZapsDialog } from "./send-zap";
import { findTag } from "../utils";
interface Emoji {
id: string;
native?: string;
}
function emojifyReaction(reaction: string) {
if (reaction === "+") {
return "💜";
}
if (reaction === "-") {
return "👎";
}
return reaction;
}
export function ChatMessage({
streamer,
ev,
reactions,
}: {
ev: NostrEvent;
streamer: string;
reactions: readonly NostrEvent[];
}) {
const ref = useRef(null);
const emojiRef = useRef(null);
const isTablet = useMediaQuery("(max-width: 1020px)");
const isHovering = useHover(ref);
const [showZapDialog, setShowZapDialog] = useState(false);
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const profile = useUserProfile(System, ev.pubkey);
const zapTarget = profile?.lud16 ?? profile?.lud06;
const zaps = useMemo(() => {
return reactions.filter(a => a.kind === EventKind.ZapReceipt)
.map(a => parseZap(a, System.ProfileLoader.Cache))
.filter(a => a && a.valid);
}, [reactions])
const emojis = useMemo(() => {
const emojified = reactions
.filter((e) => e.kind === EventKind.Reaction && findTag(e, "e") === ev.id)
.map((ev) => emojifyReaction(ev.content));
return [...new Set(emojified)];
}, [ev, reactions]);
const hasReactions = emojis.length > 0;
const totalZaps = useMemo(() => {
const messageZaps = zaps.filter((z) => z.event === ev.id);
return messageZaps.reduce((acc, z) => acc + z.amount, 0);
}, [zaps, ev]);
const hasZaps = totalZaps > 0;
useOnClickOutside(ref, () => {
setShowZapDialog(false);
});
useOnClickOutside(emojiRef, () => {
setShowEmojiPicker(false);
});
async function onEmojiSelect(emoji: Emoji) {
setShowEmojiPicker(false);
setShowZapDialog(false);
try {
const pub = await EventPublisher.nip7();
const reply = await pub?.react(ev, emoji.native || "+1");
if (reply) {
console.debug(reply);
System.BroadcastEvent(reply);
}
} catch (error) { }
}
// @ts-expect-error
const topOffset = ref.current?.getBoundingClientRect().top;
// @ts-expect-error
const leftOffset = ref.current?.getBoundingClientRect().left;
function pickEmoji(ev: any) {
ev.stopPropagation();
setShowEmojiPicker(!showEmojiPicker);
}
return (
<>
<div
className={`message${streamer === ev.pubkey ? " streamer" : ""}`}
ref={ref}
onClick={() => setShowZapDialog(true)}
>
<Profile pubkey={ev.pubkey} />
<Text content={ev.content} tags={ev.tags} />
{(hasReactions || hasZaps) && (
<div className="message-reactions">
{hasZaps && (
<div className="zap-pill">
<Icon name="zap-filled" className="zap-pill-icon" />
<span className="zap-pill-amount">{formatSats(totalZaps)}</span>
</div>
)}
{emojis.map((e) => (
<div className="message-reaction-container">
<span className="message-reaction">{e}</span>
</div>
))}
</div>
)}
{ref.current && (
<div
className="message-zap-container"
style={
isTablet
? {
display: showZapDialog || isHovering ? "flex" : "none",
}
: {
position: "fixed",
top: topOffset - 12,
left: leftOffset - 32,
opacity: showZapDialog || isHovering ? 1 : 0,
pointerEvents:
showZapDialog || isHovering ? "auto" : "none",
}
}
>
{zapTarget && (
<SendZapsDialog
lnurl={zapTarget}
eTag={ev.id}
pubkey={ev.pubkey}
button={
<button className="message-zap-button">
<Icon name="zap" className="message-zap-button-icon" />
</button>
}
targetName={profile?.name || ev.pubkey}
/>
)}
<button className="message-zap-button" onClick={pickEmoji}>
<Icon name="face" className="message-zap-button-icon" />
</button>
</div>
)}
</div>
{showEmojiPicker && (
<EmojiPicker
topOffset={topOffset}
leftOffset={leftOffset}
onEmojiSelect={onEmojiSelect}
onClickOutside={() => setShowEmojiPicker(false)}
ref={emojiRef}
/>
)}
</>
);
}

View File

@ -0,0 +1,71 @@
import data, { Emoji } from "@emoji-mart/data";
import Picker from "@emoji-mart/react";
import { RefObject } from "react";
import { EmojiPack } from "../hooks/emoji";
interface EmojiPickerProps {
topOffset: number;
leftOffset: number;
emojiPacks?: EmojiPack[];
onEmojiSelect: (e: Emoji) => void;
onClickOutside: () => void;
height?: number;
ref: RefObject<HTMLDivElement>;
}
export function EmojiPicker({
topOffset,
leftOffset,
onEmojiSelect,
onClickOutside,
emojiPacks = [],
height = 300,
ref,
}: EmojiPickerProps) {
const customEmojiList = emojiPacks.map((pack) => {
return {
id: pack.address,
name: pack.name,
emojis: pack.emojis.map((e) => {
const [, name, url] = e;
return {
id: name,
name,
skins: [{ src: url }],
};
}),
};
});
return (
<>
<div
style={{
position: "fixed",
top: topOffset - height - 10,
left: leftOffset,
zIndex: 1,
}}
ref={ref}
>
<style>
{`
em-emoji-picker { max-height: ${height}px; }
`}
</style>
<Picker
autoFocus
data={data}
custom={customEmojiList}
perLine={7}
previewPosition="none"
skinTonePosition="search"
theme="dark"
onEmojiSelect={onEmojiSelect}
onClickOutside={onClickOutside}
maxFrequentRows={0}
/>
</div>
</>
);
}

View File

@ -2,106 +2,26 @@ import "./live-chat.css";
import {
EventKind,
NostrLink,
TaggedRawEvent,
EventPublisher,
ParsedZap,
parseZap,
} from "@snort/system";
import {
useState,
useEffect,
useMemo,
useRef,
type KeyboardEvent,
type ChangeEvent,
type RefObject,
} from "react";
import { useHover, useOnClickOutside, useMediaQuery } from "usehooks-ts";
import data from "@emoji-mart/data";
import Picker from "@emoji-mart/react";
import useEmoji, { type EmojiPack } from "hooks/emoji";
import { System } from "index";
import { useLiveChatFeed } from "hooks/live-chat";
import AsyncButton from "./async-button";
import { System } from "../index";
import { useLiveChatFeed } from "../hooks/live-chat";
import { Profile } from "./profile";
import { Icon } from "./icon";
import { Text } from "./text";
import { Textarea } from "./textarea";
import Spinner from "./spinner";
import { SendZapsDialog } from "./send-zap";
import { useLogin } from "hooks/login";
import { useUserProfile } from "@snort/system-react";
import { formatSats } from "number";
import useTopZappers from "hooks/top-zappers";
import { LIVE_STREAM_CHAT } from "const";
import { findTag } from "utils";
interface EmojiPickerProps {
topOffset: number;
leftOffset: number;
emojiPacks?: EmojiPack[];
onEmojiSelect: (e: Emoji) => void;
onClickOutside: () => void;
height?: number;
ref: RefObject<HTMLDivElement>;
}
function EmojiPicker({
topOffset,
leftOffset,
onEmojiSelect,
onClickOutside,
emojiPacks = [],
height = 300,
ref,
}: EmojiPickerProps) {
const customEmojiList = emojiPacks.map((pack) => {
return {
id: pack.address,
name: pack.name,
emojis: pack.emojis.map((e) => {
const [, name, url] = e;
return {
id: name,
name,
skins: [{ src: url }],
};
}),
};
});
return (
<>
<div
style={{
position: "fixed",
top: topOffset - height - 10,
left: leftOffset,
zIndex: 1,
}}
ref={ref}
>
<style>
{`
em-emoji-picker { max-height: ${height}px; }
`}
</style>
<Picker
autoFocus
data={data}
custom={customEmojiList}
perLine={7}
previewPosition="none"
skinTonePosition="search"
theme="dark"
onEmojiSelect={onEmojiSelect}
onClickOutside={onClickOutside}
maxFrequentRows={0}
/>
</div>
</>
);
}
import { useLogin } from "../hooks/login";
import { formatSats } from "../number";
import useTopZappers from "../hooks/top-zappers";
import { LIVE_STREAM_CHAT } from "../const";
import useEventFeed from "../hooks/event-feed";
import { ChatMessage } from "./chat-message";
import { WriteMessage } from "./write-message";
import { getHost } from "utils";
export interface LiveChatOptions {
canWrite?: boolean;
@ -133,19 +53,10 @@ function TopZappers({ zaps }: { zaps: ParsedZap[] }) {
);
}
export function LiveChat({
link,
options,
height,
}: {
link: NostrLink;
options?: LiveChatOptions;
height?: number;
}) {
export function LiveChat({ link, options, height, }: { link: NostrLink, options?: LiveChatOptions, height?: number }) {
const feed = useLiveChatFeed(link);
const login = useLogin();
const zaps = feed.zaps
.filter((ev) => ev.kind === EventKind.ZapReceipt)
.map((ev) => parseZap(ev, System.ProfileLoader.Cache))
.filter((z) => z && z.valid);
const events = useMemo(() => {
@ -153,6 +64,8 @@ export function LiveChat({
(a, b) => b.created_at - a.created_at
);
}, [feed.messages, feed.zaps]);
const { data: ev } = useEventFeed(link);
const streamer = getHost(ev);
return (
<div className="live-chat" style={height ? { height: `${height}px` } : {}}>
@ -170,16 +83,18 @@ export function LiveChat({
case LIVE_STREAM_CHAT: {
return (
<ChatMessage
streamer={link.author ?? ""}
streamer={streamer}
ev={a}
link={link}
key={a.id}
reactions={feed.reactions}
/>
);
}
case EventKind.ZapReceipt: {
return <ChatZap streamer={link.author ?? ""} ev={a} key={a.id} />;
const zap = zaps.find(b => b.id === a.id && b.receiver === streamer);
if (zap) {
return <ChatZap zap={zap} key={a.id} />;
}
}
}
return null;
@ -199,307 +114,25 @@ export function LiveChat({
);
}
function emojifyReaction(reaction: string) {
if (reaction === "+") {
return "💜";
}
if (reaction === "-") {
return "👎";
}
return reaction;
}
interface Emoji {
id: string;
native?: string;
}
function ChatMessage({
streamer,
ev,
link,
reactions,
}: {
streamer: string;
ev: TaggedRawEvent;
link: NostrLink;
reactions: readonly TaggedRawEvent[];
}) {
const ref = useRef(null);
const emojiRef = useRef(null);
const isTablet = useMediaQuery("(max-width: 1020px)");
const isHovering = useHover(ref);
const [showZapDialog, setShowZapDialog] = useState(false);
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const profile = useUserProfile(System, ev.pubkey);
const zapTarget = profile?.lud16 ?? profile?.lud06;
const zaps = reactions
.filter((ev) => ev.kind === EventKind.ZapReceipt)
.map((ev) => parseZap(ev, System.ProfileLoader.Cache))
.filter((z) => z && z.valid);
const emojis = useMemo(() => {
const emojified = reactions
.filter((e) => e.kind === EventKind.Reaction && findTag(e, "e") === ev.id)
.map((ev) => emojifyReaction(ev.content));
return [...new Set(emojified)];
}, [ev, reactions]);
const hasReactions = emojis.length > 0;
const totalZaps = useMemo(() => {
const messageZaps = zaps.filter((z) => z.event === ev.id);
return messageZaps.reduce((acc, z) => acc + z.amount, 0);
}, [reactions, ev]);
const hasZaps = totalZaps > 0;
useOnClickOutside(ref, () => {
setShowZapDialog(false);
});
useOnClickOutside(emojiRef, () => {
setShowEmojiPicker(false);
});
async function onEmojiSelect(emoji: Emoji) {
setShowEmojiPicker(false);
setShowZapDialog(false);
try {
const pub = await EventPublisher.nip7();
const reply = await pub?.react(ev, emoji.native || "+1");
if (reply) {
console.debug(reply);
System.BroadcastEvent(reply);
}
} catch (error) {}
}
// @ts-expect-error
const topOffset = ref.current?.getBoundingClientRect().top;
// @ts-expect-error
const leftOffset = ref.current?.getBoundingClientRect().left;
function pickEmoji(ev: any) {
ev.stopPropagation();
setShowEmojiPicker(!showEmojiPicker);
}
return (
<>
<div
className={`message${link.author === ev.pubkey ? " streamer" : ""}`}
ref={ref}
onClick={() => setShowZapDialog(true)}
>
<Profile pubkey={ev.pubkey} />
<Text content={ev.content} tags={ev.tags} />
{(hasReactions || hasZaps) && (
<div className="message-reactions">
{hasZaps && (
<div className="zap-pill">
<Icon name="zap-filled" className="zap-pill-icon" />
<span className="zap-pill-amount">{formatSats(totalZaps)}</span>
</div>
)}
{emojis.map((e) => (
<div className="message-reaction-container">
<span className="message-reaction">{e}</span>
</div>
))}
</div>
)}
{ref.current && (
<div
className="message-zap-container"
style={
isTablet
? {
display: showZapDialog || isHovering ? "flex" : "none",
}
: {
position: "fixed",
top: topOffset - 12,
left: leftOffset - 32,
opacity: showZapDialog || isHovering ? 1 : 0,
pointerEvents:
showZapDialog || isHovering ? "auto" : "none",
}
}
>
{zapTarget && (
<SendZapsDialog
lnurl={zapTarget}
eTag={ev.id}
pubkey={ev.pubkey}
button={
<button className="message-zap-button">
<Icon name="zap" className="message-zap-button-icon" />
</button>
}
targetName={profile?.name || ev.pubkey}
/>
)}
<button className="message-zap-button" onClick={pickEmoji}>
<Icon name="face" className="message-zap-button-icon" />
</button>
</div>
)}
</div>
{showEmojiPicker && (
<EmojiPicker
topOffset={topOffset}
leftOffset={leftOffset}
onEmojiSelect={onEmojiSelect}
onClickOutside={() => setShowEmojiPicker(false)}
ref={emojiRef}
/>
)}
</>
);
}
function ChatZap({ streamer, ev }: { streamer: string; ev: TaggedRawEvent }) {
const parsed = parseZap(ev, System.ProfileLoader.Cache);
useUserProfile(System, parsed.anonZap ? undefined : parsed.sender);
useEffect(() => {
if (
!parsed.valid &&
parsed.errors.includes("zap service pubkey doesn't match") &&
parsed.sender
) {
System.ProfileLoader.TrackMetadata(parsed.sender);
return () =>
System.ProfileLoader.UntrackMetadata(parsed.sender as string);
}
}, [parsed]);
if (!parsed.valid) {
function ChatZap({ zap }: { zap: ParsedZap }) {
if (!zap.valid) {
return null;
}
return parsed.receiver === streamer ? (
<div className="zap-container">
<div className="zap">
<Icon name="zap-filled" className="zap-icon" />
<Profile
pubkey={parsed.anonZap ? "anon" : parsed.sender ?? "anon"}
options={{
showAvatar: !parsed.anonZap,
overrideName: parsed.anonZap ? "Anon" : undefined,
}}
/>
zapped
<span className="zap-amount">{formatSats(parsed.amount)}</span>
sats
</div>
{parsed.content && <div className="zap-content">{parsed.content}</div>}
return <div className="zap-container">
<div className="zap">
<Icon name="zap-filled" className="zap-icon" />
<Profile
pubkey={zap.anonZap ? "anon" : zap.sender ?? "anon"}
options={{
showAvatar: !zap.anonZap,
overrideName: zap.anonZap ? "Anon" : undefined,
}}
/>
zapped
<span className="zap-amount">{formatSats(zap.amount)}</span>
sats
</div>
) : null;
}
function WriteMessage({ link }: { link: NostrLink }) {
const ref = useRef(null);
const emojiRef = useRef(null);
const [chat, setChat] = useState("");
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const login = useLogin();
const userEmojiPacks = useEmoji(login!.pubkey);
const userEmojis = userEmojiPacks.map((pack) => pack.emojis).flat();
const channelEmojiPacks = useEmoji(link.author!);
const channelEmojis = channelEmojiPacks.map((pack) => pack.emojis).flat();
const emojis = userEmojis.concat(channelEmojis);
const names = emojis.map((t) => t.at(1));
const allEmojiPacks = userEmojiPacks.concat(channelEmojiPacks);
// @ts-expect-error
const topOffset = ref.current?.getBoundingClientRect().top;
// @ts-expect-error
const leftOffset = ref.current?.getBoundingClientRect().left;
async function sendChatMessage() {
const pub = await EventPublisher.nip7();
if (chat.length > 1) {
let emojiNames = new Set();
for (const name of names) {
if (chat.includes(`:${name}:`)) {
emojiNames.add(name);
}
}
const reply = await pub?.generic((eb) => {
const emoji = [...emojiNames].map((name) =>
emojis.find((e) => e.at(1) === name)
);
eb.kind(LIVE_STREAM_CHAT as EventKind)
.content(chat)
.tag(["a", `${link.kind}:${link.author}:${link.id}`, "", "root"])
.processContent();
for (const e of emoji) {
if (e) {
eb.tag(e);
}
}
return eb;
});
if (reply) {
console.debug(reply);
System.BroadcastEvent(reply);
}
setChat("");
}
}
function onEmojiSelect(emoji: Emoji) {
if (emoji.native) {
setChat(`${chat}${emoji.native}`);
} else {
setChat(`${chat}:${emoji.id}:`);
}
setShowEmojiPicker(false);
}
async function onKeyDown(e: KeyboardEvent) {
if (e.code === "Enter") {
e.preventDefault();
await sendChatMessage();
}
}
async function onChange(e: ChangeEvent) {
// @ts-expect-error
setChat(e.target.value);
}
function pickEmoji(ev: any) {
ev.stopPropagation();
setShowEmojiPicker(!showEmojiPicker);
}
return (
<>
<div className="paper" ref={ref}>
<Textarea
emojis={emojis}
value={chat}
onKeyDown={onKeyDown}
onChange={onChange}
/>
<div onClick={pickEmoji}>
<Icon name="face" className="write-emoji-button" />
</div>
{showEmojiPicker && (
<EmojiPicker
topOffset={topOffset}
leftOffset={leftOffset}
emojiPacks={allEmojiPacks}
onEmojiSelect={onEmojiSelect}
onClickOutside={() => setShowEmojiPicker(false)}
ref={emojiRef}
/>
)}
</div>
<AsyncButton onClick={sendChatMessage} className="btn btn-border">
Send
</AsyncButton>
</>
);
}
{zap.content && <div className="zap-content">{zap.content}</div>}
</div>
}

View File

@ -1,6 +1,6 @@
import type { ReactNode } from "react";
import moment from "moment";
import { TaggedRawEvent } from "@snort/system";
import { NostrEvent } from "@snort/system";
import { StreamState } from "index";
import { findTag } from "utils";
@ -9,7 +9,7 @@ export function Tags({
ev,
}: {
children?: ReactNode;
ev: TaggedRawEvent;
ev: NostrEvent;
}) {
const status = findTag(ev, "status");
const start = findTag(ev, "starts");

View File

@ -5,6 +5,7 @@ import { NostrEvent, encodeTLV, NostrPrefix } from "@snort/system";
import { useInView } from "react-intersection-observer";
import { StatePill } from "./state-pill";
import { StreamState } from "index";
import { getHost } from "utils";
export function VideoTile({
ev,
@ -20,8 +21,7 @@ export function VideoTile({
const title = ev.tags.find((a) => a[0] === "title")?.[1];
const image = ev.tags.find((a) => a[0] === "image")?.[1];
const status = ev.tags.find((a) => a[0] === "status")?.[1];
const host =
ev.tags.find((a) => a[0] === "p" && a[3] === "host")?.[1] ?? ev.pubkey;
const host = getHost(ev);
const link = encodeTLV(
NostrPrefix.Address,

View File

@ -0,0 +1,125 @@
import EmojiPicker from "@emoji-mart/react";
import {NostrLink, EventPublisher, EventKind} from "@snort/system";
import { useRef, useState, ChangeEvent } from "react";
import { LIVE_STREAM_CHAT } from "../const";
import useEmoji from "../hooks/emoji";
import { useLogin } from "../hooks/login";
import { System } from "../index";
import AsyncButton from "./async-button";
import { Icon } from "./icon";
import { Textarea } from "./textarea";
interface Emoji {
id: string;
native?: string;
}
export function WriteMessage({ link }: { link: NostrLink }) {
const ref = useRef(null);
const emojiRef = useRef(null);
const [chat, setChat] = useState("");
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const login = useLogin();
const userEmojiPacks = useEmoji(login!.pubkey);
const userEmojis = userEmojiPacks.map((pack) => pack.emojis).flat();
const channelEmojiPacks = useEmoji(link.author!);
const channelEmojis = channelEmojiPacks.map((pack) => pack.emojis).flat();
const emojis = userEmojis.concat(channelEmojis);
const names = emojis.map((t) => t.at(1));
const allEmojiPacks = userEmojiPacks.concat(channelEmojiPacks);
// @ts-expect-error
const topOffset = ref.current?.getBoundingClientRect().top;
// @ts-expect-error
const leftOffset = ref.current?.getBoundingClientRect().left;
async function sendChatMessage() {
const pub = await EventPublisher.nip7();
if (chat.length > 1) {
let emojiNames = new Set();
for (const name of names) {
if (chat.includes(`:${name}:`)) {
emojiNames.add(name);
}
}
const reply = await pub?.generic((eb) => {
const emoji = [...emojiNames].map((name) =>
emojis.find((e) => e.at(1) === name)
);
eb.kind(LIVE_STREAM_CHAT as EventKind)
.content(chat)
.tag(["a", `${link.kind}:${link.author}:${link.id}`, "", "root"])
.processContent();
for (const e of emoji) {
if (e) {
eb.tag(e);
}
}
return eb;
});
if (reply) {
console.debug(reply);
System.BroadcastEvent(reply);
}
setChat("");
}
}
function onEmojiSelect(emoji: Emoji) {
if (emoji.native) {
setChat(`${chat}${emoji.native}`);
} else {
setChat(`${chat}:${emoji.id}:`);
}
setShowEmojiPicker(false);
}
async function onKeyDown(e: React.KeyboardEvent) {
if (e.code === "Enter") {
e.preventDefault();
await sendChatMessage();
}
}
async function onChange(e: ChangeEvent) {
// @ts-expect-error
setChat(e.target.value);
}
function pickEmoji(ev: any) {
ev.stopPropagation();
setShowEmojiPicker(!showEmojiPicker);
}
return (
<>
<div className="paper" ref={ref}>
<Textarea
emojis={emojis}
value={chat}
onKeyDown={onKeyDown}
onChange={onChange}
/>
<div onClick={pickEmoji}>
<Icon name="face" className="write-emoji-button" />
</div>
{showEmojiPicker && (
<EmojiPicker
topOffset={topOffset}
leftOffset={leftOffset}
emojiPacks={allEmojiPacks}
onEmojiSelect={onEmojiSelect}
onClickOutside={() => setShowEmojiPicker(false)}
ref={emojiRef}
/>
)}
</div>
<AsyncButton onClick={sendChatMessage} className="btn btn-border">
Send
</AsyncButton>
</>
);
}

View File

@ -15,10 +15,14 @@ export function useLiveChatFeed(link: NostrLink) {
rb.withOptions({
leaveOpen: true,
});
const aTag = `${link.kind}:${link.author}:${link.id}`;
rb.withFilter()
.kinds([EventKind.ZapReceipt, LIVE_STREAM_CHAT])
.tag("a", [`${link.kind}:${link.author}:${link.id}`])
.kinds([LIVE_STREAM_CHAT])
.tag("a", [aTag])
.limit(100);
rb.withFilter()
.kinds([EventKind.ZapReceipt])
.tag("a", [aTag]);
return rb;
}, [link]);

View File

@ -4,38 +4,35 @@ import { useNavigate, useParams } from "react-router-dom";
import useEventFeed from "hooks/event-feed";
import { LiveVideoPlayer } from "element/live-video-player";
import { findTag } from "utils";
import { findTag, getHost } from "utils";
import { Profile, getName } from "element/profile";
import { LiveChat } from "element/live-chat";
import AsyncButton from "element/async-button";
import { useLogin } from "hooks/login";
import { StreamState, System } from "index";
import { SendZapsDialog } from "element/send-zap";
import type { NostrLink } from "@snort/system";
import { NostrEvent } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { NewStreamDialog } from "element/new-stream";
import { Tags } from "element/tags";
import { StatePill } from "element/state-pill";
function ProfileInfo({ link }: { link: NostrLink }) {
const thisEvent = useEventFeed(link, true);
function ProfileInfo({ ev }: { ev?: NostrEvent }) {
const login = useLogin();
const navigate = useNavigate();
const host =
thisEvent.data?.tags.find((a) => a[0] === "p" && a[3] === "host")?.[1] ??
thisEvent.data?.pubkey;
const host = getHost(ev);
const profile = useUserProfile(System, host);
const zapTarget = profile?.lud16 ?? profile?.lud06;
const status = thisEvent?.data ? findTag(thisEvent.data, "status") : "";
const isMine = link.author === login?.pubkey;
const status = findTag(ev, "status") ?? "";
const isMine = ev?.pubkey === login?.pubkey;
async function deleteStream() {
const pub = await EventPublisher.nip7();
if (pub && thisEvent.data) {
const ev = await pub.delete(thisEvent.data.id);
console.debug(ev);
System.BroadcastEvent(ev);
if (pub && ev) {
const evDelete = await pub.delete(ev.id);
console.debug(evDelete);
System.BroadcastEvent(evDelete);
navigate("/");
}
}
@ -44,17 +41,17 @@ function ProfileInfo({ link }: { link: NostrLink }) {
<>
<div className="flex info">
<div className="f-grow stream-info">
<h1>{findTag(thisEvent.data, "title")}</h1>
<p>{findTag(thisEvent.data, "summary")}</p>
{thisEvent?.data && (
<Tags ev={thisEvent.data}>
<h1>{findTag(ev, "title")}</h1>
<p>{findTag(ev, "summary")}</p>
{ev && (
<Tags ev={ev}>
<StatePill state={status as StreamState} />
</Tags>
)}
{isMine && (
<div className="actions">
{thisEvent.data && (
<NewStreamDialog text="Edit" ev={thisEvent.data} />
{ev && (
<NewStreamDialog text="Edit" ev={ev} />
)}
<AsyncButton
type="button"
@ -68,15 +65,15 @@ function ProfileInfo({ link }: { link: NostrLink }) {
</div>
<div className="profile-info flex g24">
<Profile pubkey={host ?? ""} />
{zapTarget && thisEvent.data && (
{zapTarget && ev && (
<SendZapsDialog
lnurl={zapTarget}
pubkey={host}
aTag={`${thisEvent.data.kind}:${thisEvent.data.pubkey}:${findTag(
thisEvent.data,
aTag={`${ev.kind}:${ev.pubkey}:${findTag(
ev,
"d"
)}`}
targetName={getName(thisEvent.data.pubkey, profile)}
targetName={getName(ev.pubkey, profile)}
/>
)}
</div>
@ -85,10 +82,9 @@ function ProfileInfo({ link }: { link: NostrLink }) {
);
}
function VideoPlayer({ link }: { link: NostrLink }) {
const thisEvent = useEventFeed(link);
const stream = findTag(thisEvent.data, "streaming");
const image = findTag(thisEvent.data, "image");
function VideoPlayer({ ev }: { ev?: NostrEvent }) {
const stream = findTag(ev, "streaming");
const image = findTag(ev, "image");
return (
<div className="video-content">
@ -100,11 +96,12 @@ function VideoPlayer({ link }: { link: NostrLink }) {
export function StreamPage() {
const params = useParams();
const link = parseNostrLink(params.id!);
const { data: ev } = useEventFeed(link);
return (
<>
<VideoPlayer link={link} />
<ProfileInfo link={link} />
<VideoPlayer ev={ev} />
<ProfileInfo ev={ev} />
<LiveChat link={link} />
</>
);

View File

@ -51,4 +51,8 @@ export function eventLink(ev: NostrEvent) {
ev.pubkey
);
return `/${naddr}`;
}
export function getHost(ev?: NostrEvent) {
return ev?.tags.find(a => a[0] === "p" && a[3] === "host")?.[1] ?? ev?.pubkey ?? "";
}