forked from Kieran/zap.stream
Fix chat zap for hosted streams
This commit is contained in:
parent
b89c8db656
commit
672f1dd077
170
src/element/chat-message.tsx
Normal file
170
src/element/chat-message.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
71
src/element/emoji-picker.tsx
Normal file
71
src/element/emoji-picker.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -2,106 +2,26 @@ import "./live-chat.css";
|
|||||||
import {
|
import {
|
||||||
EventKind,
|
EventKind,
|
||||||
NostrLink,
|
NostrLink,
|
||||||
TaggedRawEvent,
|
|
||||||
EventPublisher,
|
|
||||||
ParsedZap,
|
ParsedZap,
|
||||||
parseZap,
|
parseZap,
|
||||||
} from "@snort/system";
|
} from "@snort/system";
|
||||||
import {
|
import {
|
||||||
useState,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
|
||||||
type KeyboardEvent,
|
|
||||||
type ChangeEvent,
|
|
||||||
type RefObject,
|
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useHover, useOnClickOutside, useMediaQuery } from "usehooks-ts";
|
|
||||||
|
|
||||||
import data from "@emoji-mart/data";
|
import { System } from "../index";
|
||||||
import Picker from "@emoji-mart/react";
|
import { useLiveChatFeed } from "../hooks/live-chat";
|
||||||
import useEmoji, { type EmojiPack } from "hooks/emoji";
|
|
||||||
import { System } from "index";
|
|
||||||
import { useLiveChatFeed } from "hooks/live-chat";
|
|
||||||
import AsyncButton from "./async-button";
|
|
||||||
import { Profile } from "./profile";
|
import { Profile } from "./profile";
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
import { Text } from "./text";
|
|
||||||
import { Textarea } from "./textarea";
|
|
||||||
import Spinner from "./spinner";
|
import Spinner from "./spinner";
|
||||||
import { SendZapsDialog } from "./send-zap";
|
import { useLogin } from "../hooks/login";
|
||||||
import { useLogin } from "hooks/login";
|
import { formatSats } from "../number";
|
||||||
import { useUserProfile } from "@snort/system-react";
|
import useTopZappers from "../hooks/top-zappers";
|
||||||
import { formatSats } from "number";
|
import { LIVE_STREAM_CHAT } from "../const";
|
||||||
import useTopZappers from "hooks/top-zappers";
|
import useEventFeed from "../hooks/event-feed";
|
||||||
import { LIVE_STREAM_CHAT } from "const";
|
import { ChatMessage } from "./chat-message";
|
||||||
import { findTag } from "utils";
|
import { WriteMessage } from "./write-message";
|
||||||
|
import { getHost } 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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LiveChatOptions {
|
export interface LiveChatOptions {
|
||||||
canWrite?: boolean;
|
canWrite?: boolean;
|
||||||
@ -133,19 +53,10 @@ function TopZappers({ zaps }: { zaps: ParsedZap[] }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LiveChat({
|
export function LiveChat({ link, options, height, }: { link: NostrLink, options?: LiveChatOptions, height?: number }) {
|
||||||
link,
|
|
||||||
options,
|
|
||||||
height,
|
|
||||||
}: {
|
|
||||||
link: NostrLink;
|
|
||||||
options?: LiveChatOptions;
|
|
||||||
height?: number;
|
|
||||||
}) {
|
|
||||||
const feed = useLiveChatFeed(link);
|
const feed = useLiveChatFeed(link);
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const zaps = feed.zaps
|
const zaps = feed.zaps
|
||||||
.filter((ev) => ev.kind === EventKind.ZapReceipt)
|
|
||||||
.map((ev) => parseZap(ev, System.ProfileLoader.Cache))
|
.map((ev) => parseZap(ev, System.ProfileLoader.Cache))
|
||||||
.filter((z) => z && z.valid);
|
.filter((z) => z && z.valid);
|
||||||
const events = useMemo(() => {
|
const events = useMemo(() => {
|
||||||
@ -153,6 +64,8 @@ export function LiveChat({
|
|||||||
(a, b) => b.created_at - a.created_at
|
(a, b) => b.created_at - a.created_at
|
||||||
);
|
);
|
||||||
}, [feed.messages, feed.zaps]);
|
}, [feed.messages, feed.zaps]);
|
||||||
|
const { data: ev } = useEventFeed(link);
|
||||||
|
const streamer = getHost(ev);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="live-chat" style={height ? { height: `${height}px` } : {}}>
|
<div className="live-chat" style={height ? { height: `${height}px` } : {}}>
|
||||||
@ -170,16 +83,18 @@ export function LiveChat({
|
|||||||
case LIVE_STREAM_CHAT: {
|
case LIVE_STREAM_CHAT: {
|
||||||
return (
|
return (
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
streamer={link.author ?? ""}
|
streamer={streamer}
|
||||||
ev={a}
|
ev={a}
|
||||||
link={link}
|
|
||||||
key={a.id}
|
key={a.id}
|
||||||
reactions={feed.reactions}
|
reactions={feed.reactions}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case EventKind.ZapReceipt: {
|
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;
|
return null;
|
||||||
@ -199,307 +114,25 @@ export function LiveChat({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function emojifyReaction(reaction: string) {
|
function ChatZap({ zap }: { zap: ParsedZap }) {
|
||||||
if (reaction === "+") {
|
if (!zap.valid) {
|
||||||
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) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsed.receiver === streamer ? (
|
return <div className="zap-container">
|
||||||
<div className="zap-container">
|
<div className="zap">
|
||||||
<div className="zap">
|
<Icon name="zap-filled" className="zap-icon" />
|
||||||
<Icon name="zap-filled" className="zap-icon" />
|
<Profile
|
||||||
<Profile
|
pubkey={zap.anonZap ? "anon" : zap.sender ?? "anon"}
|
||||||
pubkey={parsed.anonZap ? "anon" : parsed.sender ?? "anon"}
|
options={{
|
||||||
options={{
|
showAvatar: !zap.anonZap,
|
||||||
showAvatar: !parsed.anonZap,
|
overrideName: zap.anonZap ? "Anon" : undefined,
|
||||||
overrideName: parsed.anonZap ? "Anon" : undefined,
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
zapped
|
||||||
zapped
|
<span className="zap-amount">{formatSats(zap.amount)}</span>
|
||||||
<span className="zap-amount">{formatSats(parsed.amount)}</span>
|
sats
|
||||||
sats
|
|
||||||
</div>
|
|
||||||
{parsed.content && <div className="zap-content">{parsed.content}</div>}
|
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
{zap.content && <div className="zap-content">{zap.content}</div>}
|
||||||
}
|
</div>
|
||||||
|
|
||||||
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>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { TaggedRawEvent } from "@snort/system";
|
import { NostrEvent } from "@snort/system";
|
||||||
import { StreamState } from "index";
|
import { StreamState } from "index";
|
||||||
import { findTag } from "utils";
|
import { findTag } from "utils";
|
||||||
|
|
||||||
@ -9,7 +9,7 @@ export function Tags({
|
|||||||
ev,
|
ev,
|
||||||
}: {
|
}: {
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
ev: TaggedRawEvent;
|
ev: NostrEvent;
|
||||||
}) {
|
}) {
|
||||||
const status = findTag(ev, "status");
|
const status = findTag(ev, "status");
|
||||||
const start = findTag(ev, "starts");
|
const start = findTag(ev, "starts");
|
||||||
|
@ -5,6 +5,7 @@ import { NostrEvent, encodeTLV, NostrPrefix } from "@snort/system";
|
|||||||
import { useInView } from "react-intersection-observer";
|
import { useInView } from "react-intersection-observer";
|
||||||
import { StatePill } from "./state-pill";
|
import { StatePill } from "./state-pill";
|
||||||
import { StreamState } from "index";
|
import { StreamState } from "index";
|
||||||
|
import { getHost } from "utils";
|
||||||
|
|
||||||
export function VideoTile({
|
export function VideoTile({
|
||||||
ev,
|
ev,
|
||||||
@ -20,8 +21,7 @@ export function VideoTile({
|
|||||||
const title = ev.tags.find((a) => a[0] === "title")?.[1];
|
const title = ev.tags.find((a) => a[0] === "title")?.[1];
|
||||||
const image = ev.tags.find((a) => a[0] === "image")?.[1];
|
const image = ev.tags.find((a) => a[0] === "image")?.[1];
|
||||||
const status = ev.tags.find((a) => a[0] === "status")?.[1];
|
const status = ev.tags.find((a) => a[0] === "status")?.[1];
|
||||||
const host =
|
const host = getHost(ev);
|
||||||
ev.tags.find((a) => a[0] === "p" && a[3] === "host")?.[1] ?? ev.pubkey;
|
|
||||||
|
|
||||||
const link = encodeTLV(
|
const link = encodeTLV(
|
||||||
NostrPrefix.Address,
|
NostrPrefix.Address,
|
||||||
|
125
src/element/write-message.tsx
Normal file
125
src/element/write-message.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -15,10 +15,14 @@ export function useLiveChatFeed(link: NostrLink) {
|
|||||||
rb.withOptions({
|
rb.withOptions({
|
||||||
leaveOpen: true,
|
leaveOpen: true,
|
||||||
});
|
});
|
||||||
|
const aTag = `${link.kind}:${link.author}:${link.id}`;
|
||||||
rb.withFilter()
|
rb.withFilter()
|
||||||
.kinds([EventKind.ZapReceipt, LIVE_STREAM_CHAT])
|
.kinds([LIVE_STREAM_CHAT])
|
||||||
.tag("a", [`${link.kind}:${link.author}:${link.id}`])
|
.tag("a", [aTag])
|
||||||
.limit(100);
|
.limit(100);
|
||||||
|
rb.withFilter()
|
||||||
|
.kinds([EventKind.ZapReceipt])
|
||||||
|
.tag("a", [aTag]);
|
||||||
return rb;
|
return rb;
|
||||||
}, [link]);
|
}, [link]);
|
||||||
|
|
||||||
|
@ -4,38 +4,35 @@ import { useNavigate, useParams } from "react-router-dom";
|
|||||||
|
|
||||||
import useEventFeed from "hooks/event-feed";
|
import useEventFeed from "hooks/event-feed";
|
||||||
import { LiveVideoPlayer } from "element/live-video-player";
|
import { LiveVideoPlayer } from "element/live-video-player";
|
||||||
import { findTag } from "utils";
|
import { findTag, getHost } from "utils";
|
||||||
import { Profile, getName } from "element/profile";
|
import { Profile, getName } from "element/profile";
|
||||||
import { LiveChat } from "element/live-chat";
|
import { LiveChat } from "element/live-chat";
|
||||||
import AsyncButton from "element/async-button";
|
import AsyncButton from "element/async-button";
|
||||||
import { useLogin } from "hooks/login";
|
import { useLogin } from "hooks/login";
|
||||||
import { StreamState, System } from "index";
|
import { StreamState, System } from "index";
|
||||||
import { SendZapsDialog } from "element/send-zap";
|
import { SendZapsDialog } from "element/send-zap";
|
||||||
import type { NostrLink } from "@snort/system";
|
import { NostrEvent } from "@snort/system";
|
||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
import { NewStreamDialog } from "element/new-stream";
|
import { NewStreamDialog } from "element/new-stream";
|
||||||
import { Tags } from "element/tags";
|
import { Tags } from "element/tags";
|
||||||
import { StatePill } from "element/state-pill";
|
import { StatePill } from "element/state-pill";
|
||||||
|
|
||||||
function ProfileInfo({ link }: { link: NostrLink }) {
|
function ProfileInfo({ ev }: { ev?: NostrEvent }) {
|
||||||
const thisEvent = useEventFeed(link, true);
|
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const host =
|
const host = getHost(ev);
|
||||||
thisEvent.data?.tags.find((a) => a[0] === "p" && a[3] === "host")?.[1] ??
|
|
||||||
thisEvent.data?.pubkey;
|
|
||||||
const profile = useUserProfile(System, host);
|
const profile = useUserProfile(System, host);
|
||||||
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
||||||
|
|
||||||
const status = thisEvent?.data ? findTag(thisEvent.data, "status") : "";
|
const status = findTag(ev, "status") ?? "";
|
||||||
const isMine = link.author === login?.pubkey;
|
const isMine = ev?.pubkey === login?.pubkey;
|
||||||
|
|
||||||
async function deleteStream() {
|
async function deleteStream() {
|
||||||
const pub = await EventPublisher.nip7();
|
const pub = await EventPublisher.nip7();
|
||||||
if (pub && thisEvent.data) {
|
if (pub && ev) {
|
||||||
const ev = await pub.delete(thisEvent.data.id);
|
const evDelete = await pub.delete(ev.id);
|
||||||
console.debug(ev);
|
console.debug(evDelete);
|
||||||
System.BroadcastEvent(ev);
|
System.BroadcastEvent(evDelete);
|
||||||
navigate("/");
|
navigate("/");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -44,17 +41,17 @@ function ProfileInfo({ link }: { link: NostrLink }) {
|
|||||||
<>
|
<>
|
||||||
<div className="flex info">
|
<div className="flex info">
|
||||||
<div className="f-grow stream-info">
|
<div className="f-grow stream-info">
|
||||||
<h1>{findTag(thisEvent.data, "title")}</h1>
|
<h1>{findTag(ev, "title")}</h1>
|
||||||
<p>{findTag(thisEvent.data, "summary")}</p>
|
<p>{findTag(ev, "summary")}</p>
|
||||||
{thisEvent?.data && (
|
{ev && (
|
||||||
<Tags ev={thisEvent.data}>
|
<Tags ev={ev}>
|
||||||
<StatePill state={status as StreamState} />
|
<StatePill state={status as StreamState} />
|
||||||
</Tags>
|
</Tags>
|
||||||
)}
|
)}
|
||||||
{isMine && (
|
{isMine && (
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
{thisEvent.data && (
|
{ev && (
|
||||||
<NewStreamDialog text="Edit" ev={thisEvent.data} />
|
<NewStreamDialog text="Edit" ev={ev} />
|
||||||
)}
|
)}
|
||||||
<AsyncButton
|
<AsyncButton
|
||||||
type="button"
|
type="button"
|
||||||
@ -68,15 +65,15 @@ function ProfileInfo({ link }: { link: NostrLink }) {
|
|||||||
</div>
|
</div>
|
||||||
<div className="profile-info flex g24">
|
<div className="profile-info flex g24">
|
||||||
<Profile pubkey={host ?? ""} />
|
<Profile pubkey={host ?? ""} />
|
||||||
{zapTarget && thisEvent.data && (
|
{zapTarget && ev && (
|
||||||
<SendZapsDialog
|
<SendZapsDialog
|
||||||
lnurl={zapTarget}
|
lnurl={zapTarget}
|
||||||
pubkey={host}
|
pubkey={host}
|
||||||
aTag={`${thisEvent.data.kind}:${thisEvent.data.pubkey}:${findTag(
|
aTag={`${ev.kind}:${ev.pubkey}:${findTag(
|
||||||
thisEvent.data,
|
ev,
|
||||||
"d"
|
"d"
|
||||||
)}`}
|
)}`}
|
||||||
targetName={getName(thisEvent.data.pubkey, profile)}
|
targetName={getName(ev.pubkey, profile)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -85,10 +82,9 @@ function ProfileInfo({ link }: { link: NostrLink }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function VideoPlayer({ link }: { link: NostrLink }) {
|
function VideoPlayer({ ev }: { ev?: NostrEvent }) {
|
||||||
const thisEvent = useEventFeed(link);
|
const stream = findTag(ev, "streaming");
|
||||||
const stream = findTag(thisEvent.data, "streaming");
|
const image = findTag(ev, "image");
|
||||||
const image = findTag(thisEvent.data, "image");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="video-content">
|
<div className="video-content">
|
||||||
@ -100,11 +96,12 @@ function VideoPlayer({ link }: { link: NostrLink }) {
|
|||||||
export function StreamPage() {
|
export function StreamPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const link = parseNostrLink(params.id!);
|
const link = parseNostrLink(params.id!);
|
||||||
|
const { data: ev } = useEventFeed(link);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<VideoPlayer link={link} />
|
<VideoPlayer ev={ev} />
|
||||||
<ProfileInfo link={link} />
|
<ProfileInfo ev={ev} />
|
||||||
<LiveChat link={link} />
|
<LiveChat link={link} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -52,3 +52,7 @@ export function eventLink(ev: NostrEvent) {
|
|||||||
);
|
);
|
||||||
return `/${naddr}`;
|
return `/${naddr}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getHost(ev?: NostrEvent) {
|
||||||
|
return ev?.tags.find(a => a[0] === "p" && a[3] === "host")?.[1] ?? ev?.pubkey ?? "";
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user