Compare commits

..

No commits in common. "f18bd8822721b16f0b26c69865aca7925141b666" and "cd43bb3cf9743b6f2041de0f0b17e91ab95cc3e6" have entirely different histories.

45 changed files with 186 additions and 423 deletions

View File

@ -8,11 +8,11 @@
"@noble/hashes": "^1.4.0", "@noble/hashes": "^1.4.0",
"@scure/base": "^1.1.6", "@scure/base": "^1.1.6",
"@snort/shared": "^1.0.17", "@snort/shared": "^1.0.17",
"@snort/system": "^1.6.1", "@snort/system": "^1.5.6",
"@snort/system-react": "^1.6.1", "@snort/system-react": "^1.5.6",
"@snort/system-wasm": "^1.0.5", "@snort/system-wasm": "^1.0.5",
"@snort/wallet": "^0.2.4", "@snort/wallet": "^0.2.1",
"@snort/worker-relay": "^1.3.1", "@snort/worker-relay": "^1.3.0",
"@szhsin/react-menu": "^4.1.0", "@szhsin/react-menu": "^4.1.0",
"@types/webscopeio__react-textarea-autocomplete": "^4.7.5", "@types/webscopeio__react-textarea-autocomplete": "^4.7.5",
"@webscopeio/react-textarea-autocomplete": "^4.9.2", "@webscopeio/react-textarea-autocomplete": "^4.9.2",

View File

@ -8,10 +8,8 @@ export const GOAL = 9041 as EventKind;
export const USER_CARDS = 17_777 as EventKind; export const USER_CARDS = 17_777 as EventKind;
export const CARD = 37_777 as EventKind; export const CARD = 37_777 as EventKind;
export const VIDEO_KIND = 21 as EventKind; export const VIDEO_KIND = 34_235 as EventKind;
export const SHORTS_KIND = 22 as EventKind; export const SHORTS_KIND = 34_236 as EventKind;
export const OLD_VIDEO_KIND = 34_235 as EventKind;
export const OLD_SHORTS_KIND = 34_236 as EventKind;
export const MINUTE = 60; export const MINUTE = 60;
export const HOUR = 60 * MINUTE; export const HOUR = 60 * MINUTE;

View File

@ -5,15 +5,13 @@ import { Goal } from "./goal";
import { Note } from "./note"; import { Note } from "./note";
import { EmojiPack } from "./emoji-pack"; import { EmojiPack } from "./emoji-pack";
import { BadgeInfo } from "./badge"; import { BadgeInfo } from "./badge";
import { GOAL, LIVE_STREAM_CLIP, OLD_SHORTS_KIND, OLD_VIDEO_KIND, SHORTS_KIND, StreamState, VIDEO_KIND } from "@/const"; import { GOAL, LIVE_STREAM_CLIP, StreamState } from "@/const";
import { useEventFeed } from "@snort/system-react"; import { useEventFeed } from "@snort/system-react";
import LiveStreamClip from "./stream/clip"; import LiveStreamClip from "./stream/clip";
import { ExternalLink } from "./external-link"; import { ExternalLink } from "./external-link";
import { extractStreamInfo } from "@/utils"; import { extractStreamInfo } from "@/utils";
import LiveVideoPlayer from "./stream/live-video-player"; import LiveVideoPlayer from "./stream/live-video-player";
import { HTMLProps, ReactNode } from "react"; import { HTMLProps } from "react";
import { ShortPage } from "@/pages/short";
import { VideoPage } from "@/pages/video";
interface EventProps { interface EventProps {
link: NostrLink; link: NostrLink;
@ -33,49 +31,36 @@ export function EventIcon({ kind }: { kind?: EventKind }) {
} }
export function NostrEvent({ ev }: { ev: TaggedNostrEvent }) { export function NostrEvent({ ev }: { ev: TaggedNostrEvent }) {
const link = NostrLink.fromEvent(ev);
function modalPage(inner: ReactNode) {
return <div className="rounded-2xl px-4 py-3 md:w-[700px] mx-auto w-full">{inner}</div>;
}
switch (ev.kind) { switch (ev.kind) {
case GOAL: { case GOAL: {
return modalPage(<Goal ev={ev} />); return <Goal ev={ev} />;
} }
case EventKind.EmojiSet: { case EventKind.EmojiSet: {
return modalPage(<EmojiPack ev={ev} />); return <EmojiPack ev={ev} />;
} }
case EventKind.Badge: { case EventKind.Badge: {
return modalPage(<BadgeInfo ev={ev} />); return <BadgeInfo ev={ev} />;
} }
case EventKind.TextNote: { case EventKind.TextNote: {
return modalPage(<Note ev={ev} />); return <Note ev={ev} />;
} }
case LIVE_STREAM_CLIP: { case LIVE_STREAM_CLIP: {
return modalPage(<LiveStreamClip ev={ev} />); return <LiveStreamClip ev={ev} />;
}
case OLD_SHORTS_KIND:
case SHORTS_KIND: {
return <ShortPage link={link} evPreload={ev} />;
}
case OLD_VIDEO_KIND:
case VIDEO_KIND: {
return <VideoPage link={link} evPreload={ev} />;
} }
case EventKind.LiveEvent: { case EventKind.LiveEvent: {
const info = extractStreamInfo(ev); const info = extractStreamInfo(ev);
return modalPage( return (
<LiveVideoPlayer <LiveVideoPlayer
link={link}
title={info.title} title={info.title}
status={info.status} status={info.status}
stream={info.status === StreamState.Live ? info.stream : info.recording} stream={info.status === StreamState.Live ? info.stream : info.recording}
poster={info.image} poster={info.image}
/>, />
); );
} }
default: { default: {
return modalPage(<ExternalLink href={`https://snort.social/${link.encode()}`}>{link.encode()}</ExternalLink>); const link = NostrLink.fromEvent(ev);
return <ExternalLink href={`https://snort.social/${link.encode()}`}>{link.encode()}</ExternalLink>;
} }
} }
} }

View File

@ -46,7 +46,6 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish, onTa
const usdAmounts = [0.05, 0.5, 2, 5, 10, 50, 100, 200]; const usdAmounts = [0.05, 0.5, 2, 5, 10, 50, 100, 200];
const [isFiat, setIsFiat] = useState(false); const [isFiat, setIsFiat] = useState(false);
const [svc, setSvc] = useState<LNURLLike>(); const [svc, setSvc] = useState<LNURLLike>();
const [customAmount, setCustomAmount] = useState(false);
const [amount, setAmount] = useState(satsAmounts[0]); const [amount, setAmount] = useState(satsAmounts[0]);
const [comment, setComment] = useState(""); const [comment, setComment] = useState("");
const [invoice, setInvoice] = useState(""); const [invoice, setInvoice] = useState("");
@ -159,17 +158,12 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish, onTa
)} )}
</small> </small>
<div className="grid grid-cols-5 gap-2 text-center"> <div className="grid grid-cols-5 gap-2 text-center">
{!customAmount && {(isFiat ? usdAmounts : satsAmounts).map(a => (
(isFiat ? usdAmounts : satsAmounts).map(a => ( <Pill key={a} selected={a === amount} onClick={() => setAmount(a)}>
<Pill key={a} selected={a === amount} onClick={() => setAmount(a)}> {isFiat ? `$${a.toLocaleString()}` : formatSats(a)}
{isFiat ? `$${a.toLocaleString()}` : formatSats(a)} </Pill>
</Pill> ))}
))}
<Pill onClick={() => setCustomAmount(s => !s)} selected={customAmount}>
<FormattedMessage defaultMessage="Custom" />
</Pill>
</div> </div>
{customAmount && <input type="number" value={amount} onChange={e => setAmount(e.target.valueAsNumber)} />}
</div> </div>
{svc && (svc.maxCommentLength > 0 || svc.canZap) && ( {svc && (svc.maxCommentLength > 0 || svc.canZap) && (
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">

View File

@ -145,8 +145,7 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
} }
} }
const startsTimestamp = Number(start ?? (new Date().getTime() / 1000)); const startsDate = new Date(parseInt(start ?? "0") * 1000);
const startsDate = new Date(startsTimestamp * 1000);
return ( return (
<> <>
@ -204,9 +203,9 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
<StreamInput label={<FormattedMessage defaultMessage="Start Time" />}> <StreamInput label={<FormattedMessage defaultMessage="Start Time" />}>
<input <input
type="datetime-local" type="datetime-local"
value={`${startsDate.getFullYear().toString().padStart(4, "0")}-${(startsDate.getMonth() + 1).toString().padStart(2, "0")}-${startsDate.getDate().toString().padStart(2, "0")}T${startsDate.getHours().toString().padStart(2, "0")}:${startsDate.getMinutes().toString().padStart(2, "0")}`} value={`${startsDate.getFullYear().toString().padStart(4, "0")}-${startsDate.getMonth().toString().padStart(2, "0")}-${startsDate.getDate().toString().padStart(2, "0")}T${startsDate.getHours().toString().padStart(2, "0")}:${startsDate.getMinutes().toString().padStart(2, "0")}`}
onChange={e => { onChange={e => {
setStart((new Date(e.target.value) / 1000).toString()); setStart((e.target.valueAsNumber / 1000).toString());
}} }}
/> />
</StreamInput> </StreamInput>

View File

@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable @typescript-eslint/ban-ts-comment */
import { CSSProperties, HTMLProps, Suspense, lazy } from "react"; import { CSSProperties, HTMLProps } from "react";
import classNames from "classnames"; import classNames from "classnames";
import { import {
MediaControlBar, MediaControlBar,
@ -20,8 +20,8 @@ import {
} from "media-chrome/react"; } from "media-chrome/react";
import "hls-video-element"; import "hls-video-element";
import { StreamState } from "@/const"; import { StreamState } from "@/const";
import Nip94Player from "./n94-player";
import { NostrLink } from "@snort/system"; import { NostrLink } from "@snort/system";
const Nip94Player = lazy(() => import("./n94-player"));
type VideoPlayerProps = { type VideoPlayerProps = {
title?: string; title?: string;
@ -35,15 +35,12 @@ type VideoPlayerProps = {
export default function LiveVideoPlayer({ title, stream, status, poster, link, ...props }: VideoPlayerProps) { export default function LiveVideoPlayer({ title, stream, status, poster, link, ...props }: VideoPlayerProps) {
function innerPlayer() { function innerPlayer() {
if (stream === "nip94") { if (stream === "nip94") {
return ( return <Nip94Player link={link} />;
<Suspense>
<Nip94Player link={link} />
</Suspense>
);
} else {
/* @ts-ignore Web Componenet */
return <hls-video {...props} slot="media" src={stream} playsInline={true} autoPlay={true} />;
} }
{
/* @ts-ignore Web Componenet */
}
return <hls-video {...props} slot="media" src={stream} playsInline={true} autoPlay={true} />;
} }
return ( return (
<MediaController <MediaController

View File

@ -1,10 +1,13 @@
import { unwrap } from "@snort/shared";
import { NostrLink, NostrPrefix, RequestBuilder, TaggedNostrEvent } from "@snort/system"; import { NostrLink, NostrPrefix, RequestBuilder, TaggedNostrEvent } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react"; import { useRequestBuilder } from "@snort/system-react";
import { useMemo } from "react"; import { useMemo } from "react";
import { LIVE_STREAM } from "@/const"; import { LIVE_STREAM } from "@/const";
import { getHost } from "@/utils";
export function useCurrentStreamFeed(link: NostrLink, leaveOpen = false, evPreload?: TaggedNostrEvent) { export function useCurrentStreamFeed(link: NostrLink, leaveOpen = false, evPreload?: TaggedNostrEvent) {
const author = link.type === NostrPrefix.Address ? unwrap(link.author) : link.id;
const sub = useMemo(() => { const sub = useMemo(() => {
const b = new RequestBuilder(`current-event:${link.id}`); const b = new RequestBuilder(`current-event:${link.id}`);
b.withOptions({ b.withOptions({
@ -13,8 +16,14 @@ export function useCurrentStreamFeed(link: NostrLink, leaveOpen = false, evPrelo
if (link.type === NostrPrefix.PublicKey || link.type === NostrPrefix.Profile) { if (link.type === NostrPrefix.PublicKey || link.type === NostrPrefix.Profile) {
b.withFilter().authors([link.id]).kinds([LIVE_STREAM]); b.withFilter().authors([link.id]).kinds([LIVE_STREAM]);
b.withFilter().tag("p", [link.id]).kinds([LIVE_STREAM]); b.withFilter().tag("p", [link.id]).kinds([LIVE_STREAM]);
} else { } else if (link.type === NostrPrefix.Address) {
b.withFilter().link(link); const f = b.withFilter().tag("d", [link.id]);
if (link.author) {
f.authors([link.author]);
}
if (link.kind) {
f.kinds([link.kind]);
}
} }
return b; return b;
}, [link.id, leaveOpen]); }, [link.id, leaveOpen]);
@ -22,7 +31,9 @@ export function useCurrentStreamFeed(link: NostrLink, leaveOpen = false, evPrelo
const q = useRequestBuilder(sub); const q = useRequestBuilder(sub);
return useMemo(() => { return useMemo(() => {
const hosting = [...q, ...(evPreload ? [evPreload] : [])].sort((a, b) => (b.created_at > a.created_at ? 1 : -1)); const hosting = [...q, ...(evPreload ? [evPreload] : [])]
.filter(a => getHost(a) === author || a.pubkey === author)
.sort((a, b) => (b.created_at > a.created_at ? 1 : -1));
return hosting.at(0); return hosting.at(0);
}, [q]); }, [q]);
} }

View File

@ -1,4 +1,4 @@
import { OLD_SHORTS_KIND, OLD_VIDEO_KIND, SHORTS_KIND, VIDEO_KIND } from "@/const"; import { SHORTS_KIND, VIDEO_KIND } from "@/const";
import { MediaPayload, VideoInfo } from "@/service/video/info"; import { MediaPayload, VideoInfo } from "@/service/video/info";
import { findTag } from "@/utils"; import { findTag } from "@/utils";
import { NostrEvent, TaggedNostrEvent } from "@snort/system"; import { NostrEvent, TaggedNostrEvent } from "@snort/system";
@ -58,7 +58,7 @@ export function useDeadLink(ev: TaggedNostrEvent | NostrEvent) {
useEffect(() => { useEffect(() => {
const links = const links =
ev.kind === VIDEO_KIND || ev.kind === SHORTS_KIND || ev.kind == OLD_SHORTS_KIND || ev.kind == OLD_VIDEO_KIND ev.kind === VIDEO_KIND || ev.kind === SHORTS_KIND
? VideoInfo.parse(ev)?.sources() ? VideoInfo.parse(ev)?.sources()
: [ : [
{ {

View File

@ -4,7 +4,12 @@ import { removeUndefined, sanitizeRelayUrl } from "@snort/shared";
import { Nip96Server } from "@/service/upload/nip96"; import { Nip96Server } from "@/service/upload/nip96";
import { useMemo } from "react"; import { useMemo } from "react";
export const DefaultMediaServers = [new UnknownTag(["server", "https://nostr.download/"])]; export const DefaultMediaServers = [
//"https://media.zap.stream",
new UnknownTag(["server", "https://nostr.build/"]),
new UnknownTag(["server", "https://nostrcheck.me/"]),
new UnknownTag(["server", "https://files.v0l.io/"]),
];
export function useMediaServerList() { export function useMediaServerList() {
const login = useLogin(); const login = useLogin();

View File

@ -221,9 +221,6 @@
"BD0vyn": { "BD0vyn": {
"defaultMessage": "{name} created a clip" "defaultMessage": "{name} created a clip"
}, },
"BHNL+v": {
"defaultMessage": "Raw Data:"
},
"Bd1yEX": { "Bd1yEX": {
"defaultMessage": "New Stream Goal" "defaultMessage": "New Stream Goal"
}, },
@ -536,9 +533,6 @@
"SC2nJT": { "SC2nJT": {
"defaultMessage": "Audio Codec" "defaultMessage": "Audio Codec"
}, },
"Sjo1P4": {
"defaultMessage": "Custom"
},
"TDUfVk": { "TDUfVk": {
"defaultMessage": "Started" "defaultMessage": "Started"
}, },
@ -644,9 +638,6 @@
"ZcgtZo": { "ZcgtZo": {
"defaultMessage": "We've put together a list of easy-to-use exchanges that will allow you to buy a small amount of bitcoin (sats) and to transfer them to your wallet of choice." "defaultMessage": "We've put together a list of easy-to-use exchanges that will allow you to buy a small amount of bitcoin (sats) and to transfer them to your wallet of choice."
}, },
"Zfr//4": {
"defaultMessage": "No duration provided, please try another upload server."
},
"ZmqxZs": { "ZmqxZs": {
"defaultMessage": "You can change this later" "defaultMessage": "You can change this later"
}, },
@ -711,9 +702,6 @@
"dkUMIH": { "dkUMIH": {
"defaultMessage": "Clip by {name}" "defaultMessage": "Clip by {name}"
}, },
"dqGkI+": {
"defaultMessage": "Video durations vary too much, are you sure each variant is the same video?"
},
"e011kf": { "e011kf": {
"defaultMessage": "FAQ", "defaultMessage": "FAQ",
"description": "Title: FAQ page" "description": "Title: FAQ page"
@ -742,9 +730,6 @@
"gzsn7k": { "gzsn7k": {
"defaultMessage": "{n} messages" "defaultMessage": "{n} messages"
}, },
"h6NRY6": {
"defaultMessage": "No Thanks!"
},
"h9mX2/": { "h9mX2/": {
"defaultMessage": "If you use our in-house zap.stream hosting (cheapest and easiest), copy your stream URL and Stream Key to your OBS settings and you should be good to go." "defaultMessage": "If you use our in-house zap.stream hosting (cheapest and easiest), copy your stream URL and Stream Key to your OBS settings and you should be good to go."
}, },
@ -954,9 +939,6 @@
"vP4dFa": { "vP4dFa": {
"defaultMessage": "Visit {link} to get some sweet zap.stream merch!" "defaultMessage": "Visit {link} to get some sweet zap.stream merch!"
}, },
"vaZKTn": {
"defaultMessage": "Add more content"
},
"vrTOHJ": { "vrTOHJ": {
"defaultMessage": "{amount} sats" "defaultMessage": "{amount} sats"
}, },

View File

@ -1,5 +1,5 @@
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { NostrLink, parseNostrLink } from "@snort/system"; import { NostrPrefix, encodeTLV, parseNostrLink } from "@snort/system";
import { unwrap } from "@snort/shared"; import { unwrap } from "@snort/shared";
import { LiveChat } from "@/element/chat/live-chat"; import { LiveChat } from "@/element/chat/live-chat";
@ -13,20 +13,19 @@ export function ChatPopout() {
const ev = useCurrentStreamFeed(link, true); const ev = useCurrentStreamFeed(link, true);
const goal = useZapGoal(findTag(ev, "goal")); const goal = useZapGoal(findTag(ev, "goal"));
const lnk = parseNostrLink(encodeTLV(NostrPrefix.Address, findTag(ev, "d") ?? "", undefined, ev?.kind, ev?.pubkey));
const chat = Boolean(new URL(window.location.href).searchParams.get("chat")); const chat = Boolean(new URL(window.location.href).searchParams.get("chat"));
return ( return (
<div className="h-[calc(100vh-1rem)] w-screen px-2 my-2"> <div className="h-[calc(100vh-1rem)] w-screen px-2 my-2">
{ev && ( <LiveChat
<LiveChat ev={ev}
ev={ev} link={lnk}
link={NostrLink.fromEvent(ev)} canWrite={chat}
canWrite={chat} showScrollbar={false}
showScrollbar={false} goal={goal}
goal={goal} className="h-inherit"
className="h-inherit" autoRaid={false}
autoRaid={false} />
/>
)}
</div> </div>
); );
} }

View File

@ -16,12 +16,12 @@ import { DashboardCard } from "./card";
import { NewStreamDialog } from "@/element/new-stream"; import { NewStreamDialog } from "@/element/new-stream";
import { DashboardSettingsButton } from "./button-settings"; import { DashboardSettingsButton } from "./button-settings";
import DashboardIntro from "./intro"; import DashboardIntro from "./intro";
import { useLocation, useNavigate } from "react-router-dom"; import { useLocation } from "react-router-dom";
import StreamKey from "@/element/provider/nostr/stream-key"; import StreamKey from "@/element/provider/nostr/stream-key";
import { DefaultProvider, NostrStreamProvider, StreamProviderInfo } from "@/providers"; import { DefaultProvider, NostrStreamProvider, StreamProviderInfo } from "@/providers";
import { ExternalLink } from "@/element/external-link"; import { ExternalLink } from "@/element/external-link";
import BalanceTimeEstimate from "@/element/balance-time-estimate"; import BalanceTimeEstimate from "@/element/balance-time-estimate";
import { Layer1Button, Layer2Button, WarningButton } from "@/element/buttons"; import { Layer1Button, WarningButton } from "@/element/buttons";
import { useLogin } from "@/hooks/login"; import { useLogin } from "@/hooks/login";
import AccountTopup from "@/element/provider/nostr/topup"; import AccountTopup from "@/element/provider/nostr/topup";
import classNames from "classnames"; import classNames from "classnames";
@ -30,19 +30,15 @@ import { unixNow } from "@snort/shared";
import { Icon } from "@/element/icon"; import { Icon } from "@/element/icon";
import ForwardingModal from "./forwarding"; import ForwardingModal from "./forwarding";
import BalanceHistoryModal from "./balance-history"; import BalanceHistoryModal from "./balance-history";
import Modal from "@/element/modal";
import { AcceptTos } from "./tos";
const StreamSummary = lazy(() => import("@/element/summary-chart")); const StreamSummary = lazy(() => import("@/element/summary-chart"));
export default function DashboardForLink({ link }: { link: NostrLink }) { export function DashboardForLink({ link }: { link: NostrLink }) {
const navigate = useNavigate();
const streamEvent = useCurrentStreamFeed(link, true); const streamEvent = useCurrentStreamFeed(link, true);
const location = useLocation(); const location = useLocation();
const login = useLogin(); const login = useLogin();
const streamLink = streamEvent ? NostrLink.fromEvent(streamEvent) : undefined; const streamLink = streamEvent ? NostrLink.fromEvent(streamEvent) : undefined;
const { stream, status, image, participants, service } = extractStreamInfo(streamEvent); const { stream, status, image, participants, service } = extractStreamInfo(streamEvent);
const [info, setInfo] = useState<StreamProviderInfo>(); const [info, setInfo] = useState<StreamProviderInfo>();
const [tos, setTos] = useState(info?.tosAccepted ?? false);
const isMyManual = streamEvent?.pubkey === login?.pubkey; const isMyManual = streamEvent?.pubkey === login?.pubkey;
const system = useContext(SnortContext); const system = useContext(SnortContext);
const [recording, setRecording] = useState(Boolean(localStorage.getItem("default-recording") ?? "true")); const [recording, setRecording] = useState(Boolean(localStorage.getItem("default-recording") ?? "true"));
@ -114,7 +110,7 @@ export default function DashboardForLink({ link }: { link: NostrLink }) {
<div <div
className={classNames("grid gap-2 h-[calc(100dvh-52px)] w-full", { className={classNames("grid gap-2 h-[calc(100dvh-52px)] w-full", {
"grid-cols-3": status === StreamState.Live, "grid-cols-3": status === StreamState.Live,
"grid-cols-[20%_80%]": status === StreamState.Ended || status === undefined, "grid-cols-[20%_80%]": status === StreamState.Ended,
"grid-cols-[40%_60%]": status === StreamState.Planned, "grid-cols-[40%_60%]": status === StreamState.Planned,
})}> })}>
<div className="min-h-0 h-full grid grid-rows-[min-content_auto] gap-2"> <div className="min-h-0 h-full grid grid-rows-[min-content_auto] gap-2">
@ -130,14 +126,7 @@ export default function DashboardForLink({ link }: { link: NostrLink }) {
</div> </div>
{streamLink && status === StreamState.Live && !isMyManual && ( {streamLink && status === StreamState.Live && !isMyManual && (
<> <>
<LiveVideoPlayer <LiveVideoPlayer stream={stream} status={status} poster={image} muted={true} className="w-full" />
stream={stream}
link={streamLink}
status={status}
poster={image}
muted={true}
className="w-full"
/>
<div className="flex gap-4"> <div className="flex gap-4">
<DashboardStatsCard <DashboardStatsCard
name={<FormattedMessage defaultMessage="Stream Time" />} name={<FormattedMessage defaultMessage="Stream Time" />}
@ -181,14 +170,7 @@ export default function DashboardForLink({ link }: { link: NostrLink }) {
)} )}
{streamLink && isMyManual && (status === StreamState.Live || status === StreamState.Planned) && ( {streamLink && isMyManual && (status === StreamState.Live || status === StreamState.Planned) && (
<> <>
<LiveVideoPlayer <LiveVideoPlayer stream={stream} status={status} poster={image} muted={true} className="w-full" />
link={streamLink}
stream={stream}
status={status}
poster={image}
muted={true}
className="w-full"
/>
<div className="grid gap-2 grid-cols-3"> <div className="grid gap-2 grid-cols-3">
<DashboardRaidButton link={streamLink} /> <DashboardRaidButton link={streamLink} />
<NewStreamDialog ev={streamEvent} text={<FormattedMessage defaultMessage="Edit Stream Info" />} /> <NewStreamDialog ev={streamEvent} text={<FormattedMessage defaultMessage="Edit Stream Info" />} />
@ -301,29 +283,6 @@ export default function DashboardForLink({ link }: { link: NostrLink }) {
</> </>
)} )}
{streamLink && status === StreamState.Planned && <DashboardCard className="overflow-y-auto"></DashboardCard>} {streamLink && status === StreamState.Planned && <DashboardCard className="overflow-y-auto"></DashboardCard>}
{info && !info.tosAccepted && (
<Modal id="tos-dashboard">
<div className="flex flex-col gap-4">
<h2>Please accept TOS before continuing</h2>
<AcceptTos provider={info?.name} tosLink={info?.tosLink} tos={tos} setTos={setTos} />
<div className="flex items-center justify-between">
<Layer2Button
disabled={!tos}
onClick={async () => {
if (tos) {
await provider.acceptTos();
provider.info().then(setInfo);
}
}}>
<FormattedMessage defaultMessage="Save" />
</Layer2Button>
<WarningButton onClick={() => navigate("/")}>
<FormattedMessage defaultMessage="No Thanks!" />
</WarningButton>
</div>
</div>
</Modal>
)}
</div> </div>
); );
} }

View File

@ -1,19 +1,13 @@
import { useLogin } from "@/hooks/login"; import { useLogin } from "@/hooks/login";
import { NostrLink, NostrPrefix, parseNostrLink } from "@snort/system"; import { NostrLink, NostrPrefix, parseNostrLink } from "@snort/system";
import { Suspense, lazy } from "react"; import { DashboardForLink } from "./dashboard";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
const DashboardForLink = lazy(() => import("./dashboard"));
export default function DashboardPage() { export default function DashboardPage() {
const login = useLogin(); const login = useLogin();
const { id } = useParams(); const { id } = useParams();
if (!login) return; if (!login) return;
const link = id ? parseNostrLink(id) : new NostrLink(NostrPrefix.PublicKey, login.pubkey); const link = id ? parseNostrLink(id) : new NostrLink(NostrPrefix.PublicKey, login.pubkey);
return ( return <DashboardForLink link={link} />;
<Suspense>
<DashboardForLink link={link} />
</Suspense>
);
} }

View File

@ -7,7 +7,6 @@ import { useEffect, useMemo, useState } from "react";
import { FormattedMessage, FormattedNumber } from "react-intl"; import { FormattedMessage, FormattedNumber } from "react-intl";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import ZapGlow from "../zap-glow"; import ZapGlow from "../zap-glow";
import { AcceptTos } from "../tos";
export default function DashboardIntro() { export default function DashboardIntro() {
const navigate = useNavigate(); const navigate = useNavigate();
@ -17,7 +16,7 @@ export default function DashboardIntro() {
const exampleHours = 4; const exampleHours = 4;
const defaultEndpoint = useMemo(() => { const defaultEndpoint = useMemo(() => {
return info?.endpoints?.find(a => a.name == "Best") ?? info?.endpoints?.at(0); return info?.endpoints.find(a => a.name == "Best") ?? info?.endpoints[0];
}, [info]); }, [info]);
const rate = useRates("BTCUSD"); const rate = useRates("BTCUSD");
const exampleCost = rate.ask * (exampleHours * (defaultEndpoint?.rate ?? 0) * 60) * 1e-8; const exampleCost = rate.ask * (exampleHours * (defaultEndpoint?.rate ?? 0) * 60) * 1e-8;
@ -70,7 +69,31 @@ export default function DashboardIntro() {
}} }}
/> />
</p> </p>
{!info?.tosAccepted && <AcceptTos provider={info?.name} tosLink={info?.tosLink} tos={tos} setTos={setTos} />} {!info?.tosAccepted && (
<div>
<div className="flex gap-2 cursor-pointer select-none" onClick={() => setTos(v => !v)}>
<input type="checkbox" checked={tos} onChange={e => setTos(e.target.checked)} />
<p>
<FormattedMessage
defaultMessage="I have read and agree with {provider}'s {terms}."
values={{
provider: info?.name,
terms: (
<span
className="text-primary"
onClick={e => {
e.stopPropagation();
window.open(info?.tosLink, "popup", "width=400,height=800");
}}>
<FormattedMessage defaultMessage="terms and conditions" />
</span>
),
}}
/>
</p>
</div>
</div>
)}
<DefaultButton <DefaultButton
disabled={!tos} disabled={!tos}
onClick={async () => { onClick={async () => {

View File

@ -1,39 +0,0 @@
import { FormattedMessage } from "react-intl";
export function AcceptTos({
provider,
tosLink,
tos,
setTos,
}: {
provider?: string;
tosLink?: string;
tos: boolean;
setTos: (f: (r: boolean) => boolean) => void;
}) {
return (
<div>
<div className="flex gap-2 cursor-pointer select-none" onClick={() => setTos(v => !v)}>
<input type="checkbox" checked={tos} onChange={e => setTos(() => e.target.checked)} />
<p>
<FormattedMessage
defaultMessage="I have read and agree with {provider}'s {terms}."
values={{
provider,
terms: (
<span
className="text-primary"
onClick={e => {
e.stopPropagation();
window.open(tosLink, "popup", "width=400,height=800");
}}>
<FormattedMessage defaultMessage="terms and conditions" />
</span>
),
}}
/>
</p>
</div>
</div>
);
}

View File

@ -1,4 +1,4 @@
import { LIVE_STREAM, OLD_SHORTS_KIND, OLD_VIDEO_KIND, SHORTS_KIND, VIDEO_KIND } from "@/const"; import { LIVE_STREAM, SHORTS_KIND, VIDEO_KIND } from "@/const";
import { useStreamLink } from "@/hooks/stream-link"; import { useStreamLink } from "@/hooks/stream-link";
import { getEventFromLocationState } from "@/utils"; import { getEventFromLocationState } from "@/utils";
import { NostrPrefix } from "@snort/system"; import { NostrPrefix } from "@snort/system";
@ -20,16 +20,20 @@ export function LinkHandler() {
if (!link) return; if (!link) return;
if (link.type === NostrPrefix.Event) { if (link.type === NostrPrefix.Event) {
return <NostrEventElement link={link} />; return (
<div className="rounded-2xl px-4 py-3 md:w-[700px] mx-auto w-full">
<NostrEventElement link={link} />
</div>
);
} else if (link.kind === LIVE_STREAM || link.type === NostrPrefix.PublicKey) { } else if (link.kind === LIVE_STREAM || link.type === NostrPrefix.PublicKey) {
return ( return (
<div className={classNames(layoutContext.showHeader ? "h-[calc(100dvh-44px)]" : "h-[calc(100dvh)]", "w-full")}> <div className={classNames(layoutContext.showHeader ? "h-[calc(100dvh-44px)]" : "h-[calc(100dvh)]", "w-full")}>
<StreamPage link={link} evPreload={evPreload} /> <StreamPage link={link} evPreload={evPreload} />
</div> </div>
); );
} else if (link.kind === VIDEO_KIND || link.kind === OLD_VIDEO_KIND) { } else if (link.kind === VIDEO_KIND) {
return <VideoPage link={link} evPreload={evPreload} />; return <VideoPage link={link} evPreload={evPreload} />;
} else if (link.kind === SHORTS_KIND || link.kind === OLD_SHORTS_KIND) { } else if (link.kind === SHORTS_KIND) {
return <ShortPage link={link} evPreload={evPreload} />; return <ShortPage link={link} evPreload={evPreload} />;
} else { } else {
return ( return (

View File

@ -1,4 +1,4 @@
import { OLD_SHORTS_KIND, SHORTS_KIND } from "@/const"; import { SHORTS_KIND } from "@/const";
import VideoGrid from "@/element/video-grid"; import VideoGrid from "@/element/video-grid";
import { VideoTile } from "@/element/video/video-tile"; import { VideoTile } from "@/element/video/video-tile";
import { findTag } from "@/utils"; import { findTag } from "@/utils";
@ -8,12 +8,13 @@ import { FormattedMessage } from "react-intl";
export function ShortsPage() { export function ShortsPage() {
const rb = new RequestBuilder("shorts"); const rb = new RequestBuilder("shorts");
rb.withFilter().kinds([SHORTS_KIND, OLD_SHORTS_KIND]); rb.withFilter().kinds([SHORTS_KIND]);
const videos = useRequestBuilder(rb); const videos = useRequestBuilder(rb);
const sorted = videos.sort((a, b) => { const sorted = videos.sort((a, b) => {
const pubA = findTag(a, "published_at") ?? a.created_at; const pubA = findTag(a, "published_at");
const pubB = findTag(b, "published_at") ?? b.created_at; const pubB = findTag(b, "published_at");
return Number(pubA) > Number(pubB) ? -1 : 1; return Number(pubA) > Number(pubB) ? -1 : 1;
}); });

View File

@ -1,4 +1,4 @@
import { SHORTS_KIND, VIDEO_KIND } from "@/const"; import { VIDEO_KIND } from "@/const";
import { DefaultButton, IconButton, Layer3Button, PrimaryButton, WarningButton } from "@/element/buttons"; import { DefaultButton, IconButton, Layer3Button, PrimaryButton, WarningButton } from "@/element/buttons";
import { Icon } from "@/element/icon"; import { Icon } from "@/element/icon";
import Modal from "@/element/modal"; import Modal from "@/element/modal";
@ -9,11 +9,11 @@ import { ServerList } from "@/element/upload/server-list";
import useImgProxy from "@/hooks/img-proxy"; import useImgProxy from "@/hooks/img-proxy";
import { useLogin } from "@/hooks/login"; import { useLogin } from "@/hooks/login";
import { useMediaServerList } from "@/hooks/media-servers"; import { useMediaServerList } from "@/hooks/media-servers";
import { Nip94Tags, UploadResult, nip94TagsToIMeta, readNip94Tags } from "@/service/upload"; import { Nip94Tags, UploadResult, nip94TagsToIMeta } from "@/service/upload";
import { Nip96Server } from "@/service/upload/nip96"; import { Nip96Server } from "@/service/upload/nip96";
import { openFile } from "@/utils"; import { openFile } from "@/utils";
import { ExternalStore, removeUndefined, unwrap } from "@snort/shared"; import { ExternalStore, removeUndefined, unixNow, unwrap } from "@snort/shared";
import { EventBuilder, EventPublisher, NostrEvent, NostrLink } from "@snort/system"; import { EventPublisher, NostrLink } from "@snort/system";
import { SnortContext } from "@snort/system-react"; import { SnortContext } from "@snort/system-react";
import { useContext, useEffect, useState, useSyncExternalStore } from "react"; import { useContext, useEffect, useState, useSyncExternalStore } from "react";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
@ -69,23 +69,6 @@ class UploadManager extends ExternalStore<Array<UploadStatus>> {
} }
} }
addUpload(server: string, file: NostrEvent, meta: Nip94Tags, type: UploadStatus["type"]) {
const name = file.content ?? meta.summary ?? meta.alt ?? "";
const uploadKey = `${name}:${server}:${type}`;
this.#uploads.set(uploadKey, {
type,
name,
size: meta.size ?? 0,
server,
result: {
url: meta.url,
header: file,
metadata: meta,
},
});
this.notifyChange();
}
async uploadTo(server: string, file: File, pub: EventPublisher, type: UploadStatus["type"]) { async uploadTo(server: string, file: File, pub: EventPublisher, type: UploadStatus["type"]) {
let uploader = this.#uploaders.get(server); let uploader = this.#uploaders.get(server);
if (!uploader) { if (!uploader) {
@ -147,27 +130,6 @@ class UploadManager extends ExternalStore<Array<UploadStatus>> {
return resGroup; return resGroup;
} }
/**
* Gets the [min, max] duration from all variants
*/
duration() {
const uploads = this.snapshot();
return uploads.reduce(
(acc, v) => {
if (v.result?.metadata?.duration) {
if (acc[1] < v.result.metadata.duration) {
acc[1] = v.result.metadata.duration;
}
if (acc[0] > v.result.metadata.duration) {
acc[0] = v.result.metadata.duration;
}
}
return acc;
},
[1_000_000, 0],
);
}
/** /**
* Create the `imeta` tag for this upload * Create the `imeta` tag for this upload
*/ */
@ -225,24 +187,21 @@ export function UploadPage() {
return error.length == 0 && uploads.length > 0 && uploads.every(a => a.result !== undefined); return error.length == 0 && uploads.length > 0 && uploads.every(a => a.result !== undefined);
} }
function makeEvent() {
const duration = manager.duration();
const eb = new EventBuilder()
.pubKey(login?.pubkey ?? "00".repeat(31))
.kind(duration[1] <= 60 ? SHORTS_KIND : VIDEO_KIND)
.tag(["title", title])
.content(summary);
const imeta = manager.makeIMeta();
imeta.forEach(a => eb.tag(a));
return eb;
}
async function publish() { async function publish() {
const pub = login?.publisher(); const pub = login?.publisher();
if (!pub) return; if (!pub) return;
const ev = await makeEvent().buildAndSign(pub.signer); const ev = await pub.generic(eb => {
eb.kind(VIDEO_KIND);
eb.tag(["d", manager.id]);
eb.tag(["title", title]);
eb.tag(["published_at", unixNow().toString()]);
eb.content(summary);
const imeta = manager.makeIMeta();
imeta.forEach(a => eb.tag(a));
return eb;
});
console.debug(ev); console.debug(ev);
await system.BroadcastEvent(ev); await system.BroadcastEvent(ev);
navigate(`/${NostrLink.fromEvent(ev).encode()}`); navigate(`/${NostrLink.fromEvent(ev).encode()}`);
@ -270,16 +229,7 @@ export function UploadPage() {
const data = await rsp.blob(); const data = await rsp.blob();
const pub = login?.publisher(); const pub = login?.publisher();
if (pub) { if (pub) {
servers.servers.forEach(b => servers.servers.forEach(b => manager.uploadTo(b, new File([data], "thumb.jpg"), pub, "thumb"));
manager.uploadTo(
b,
new File([data], "thumb.jpg", {
type: "image/jpeg",
}),
pub,
"thumb",
),
);
} }
} }
} }
@ -341,17 +291,6 @@ export function UploadPage() {
}), }),
); );
} }
const d = manager.duration();
if (d[0] === 0 || d[1] === 0) {
err.push(formatMessage({ defaultMessage: "No duration provided, please try another upload server." }));
}
if (Math.abs(d[0] - d[1]) >= 0.5) {
err.push(
formatMessage({
defaultMessage: "Video durations vary too much, are you sure each variant is the same video?",
}),
);
}
setError(err); setError(err);
} }
@ -373,7 +312,6 @@ export function UploadPage() {
</> </>
); );
}; };
return ( return (
<div className="max-xl:w-full xl:w-[1200px] xl:mx-auto grid gap-6 xl:grid-cols-[auto_350px] max-xl:px-4"> <div className="max-xl:w-full xl:w-[1200px] xl:mx-auto grid gap-6 xl:grid-cols-[auto_350px] max-xl:px-4">
<div className="flex flex-col gap-6"> <div className="flex flex-col gap-6">
@ -421,11 +359,8 @@ export function UploadPage() {
))} ))}
</div> </div>
{videos > 0 && ( {videos > 0 && (
<div className="flex flex-col gap-2"> <div onClick={() => uploadFile()} className="cursor-pointer">
<div className="text-xl"> {uploadButton()}
<FormattedMessage defaultMessage="Add more content" />
</div>
<div className="flex gap-4 items-center">{uploadButton()}</div>
</div> </div>
)} )}
{uploads.length > 0 && ( {uploads.length > 0 && (
@ -442,7 +377,7 @@ export function UploadPage() {
<FormattedMessage defaultMessage="Thumbnail" /> <FormattedMessage defaultMessage="Thumbnail" />
</div> </div>
<div className="border border-layer-3 border-dashed border-2 rounded-xl aspect-video overflow-hidden"> <div className="border border-layer-3 border-dashed border-2 rounded-xl aspect-video overflow-hidden">
{thumb && <img src={proxy(thumb)} className="w-full h-full object-contain" />} {thumb && <img src={proxy(thumb)} className="w-full h-full" />}
</div> </div>
<div className="flex gap-4"> <div className="flex gap-4">
<DefaultButton onClick={() => uploadThumb()}> <DefaultButton onClick={() => uploadThumb()}>
@ -470,13 +405,9 @@ export function UploadPage() {
</WarningButton> </WarningButton>
</div> </div>
</div> </div>
<pre className="text-xs font-mono overflow-wrap text-pretty">
<div> {JSON.stringify(manager.makeIMeta(), undefined, 2)}
<FormattedMessage defaultMessage="Raw Data:" /> </pre>
<pre className="text-xs font-mono overflow-wrap text-pretty">
{JSON.stringify(makeEvent().build(), undefined, 2)}
</pre>
</div>
{editServers && ( {editServers && (
<Modal id="server-list" onClose={() => setEditServers(false)}> <Modal id="server-list" onClose={() => setEditServers(false)}>
<ServerList /> <ServerList />
@ -485,19 +416,7 @@ export function UploadPage() {
{mediaPicker && ( {mediaPicker && (
<Modal id="media-picker" onClose={() => setMediaPicker(false)} largeModal={true}> <Modal id="media-picker" onClose={() => setMediaPicker(false)} largeModal={true}>
<MediaServerFileList <MediaServerFileList
onPicked={files => { onPicked={() => {
files.forEach(f => {
const meta = readNip94Tags(f.tags);
if (meta.url) {
const url = new URL(meta.url);
manager.addUpload(
`${url.protocol}//${url.host}/`,
f,
meta,
meta.mimeType?.startsWith("image/") ?? false ? "thumb" : "video",
);
}
});
setMediaPicker(false); setMediaPicker(false);
}} }}
/> />

View File

@ -1,4 +1,4 @@
import { OLD_VIDEO_KIND, VIDEO_KIND } from "@/const"; import { VIDEO_KIND } from "@/const";
import VideoGrid from "@/element/video-grid"; import VideoGrid from "@/element/video-grid";
import { findTag, getHost } from "@/utils"; import { findTag, getHost } from "@/utils";
import { NostrLink, RequestBuilder } from "@snort/system"; import { NostrLink, RequestBuilder } from "@snort/system";
@ -11,7 +11,7 @@ export function VideosPage() {
const login = useLogin(); const login = useLogin();
const rb = new RequestBuilder("videos"); const rb = new RequestBuilder("videos");
rb.withFilter().kinds([VIDEO_KIND, OLD_VIDEO_KIND]); rb.withFilter().kinds([VIDEO_KIND]);
const videos = useRequestBuilder(rb); const videos = useRequestBuilder(rb);
@ -22,8 +22,8 @@ export function VideosPage() {
return (login?.state?.muted.length ?? 0) === 0 || !login?.state?.muted.some(a => a.equals(link)); return (login?.state?.muted.length ?? 0) === 0 || !login?.state?.muted.some(a => a.equals(link));
}) })
.sort((a, b) => { .sort((a, b) => {
const pubA = findTag(a, "published_at") ?? a.created_at; const pubA = findTag(a, "published_at");
const pubB = findTag(b, "published_at") ?? b.created_at; const pubB = findTag(b, "published_at");
return Number(pubA) > Number(pubB) ? -1 : 1; return Number(pubA) > Number(pubB) ? -1 : 1;
}); });

View File

@ -16,8 +16,6 @@ export interface Nip94Tags {
summary?: string; summary?: string;
alt?: string; alt?: string;
fallback?: Array<string>; fallback?: Array<string>;
duration?: number;
bitrate?: number;
} }
export interface UploadResult { export interface UploadResult {
@ -105,14 +103,6 @@ export function readNip94Tags(tags: Array<Array<string>>) {
res.fallback.push(v); res.fallback.push(v);
break; break;
} }
case "duration": {
res.duration = Number(v);
break;
}
case "bitrate": {
res.bitrate = Number(v);
break;
}
} }
} }
return res; return res;
@ -136,8 +126,6 @@ export function nip94TagsToIMeta(meta: Nip94Tags) {
ifPush("thumb", meta.thumb); ifPush("thumb", meta.thumb);
ifPush("summary", meta.summary); ifPush("summary", meta.summary);
ifPush("alt", meta.alt); ifPush("alt", meta.alt);
ifPush("duration", meta.duration);
ifPush("bitrate", meta.bitrate);
if (meta.image) { if (meta.image) {
meta.image.forEach(a => ifPush("image", a)); meta.image.forEach(a => ifPush("image", a));
} }

View File

@ -37,7 +37,7 @@ export class Nip96Server {
const fd = new FormData(); const fd = new FormData();
fd.append("size", file.size.toString()); fd.append("size", file.size.toString());
fd.append("caption", filename); fd.append("caption", filename);
fd.append("content_type", file.type); fd.append("media_type", file.type);
fd.append("file", file); fd.append("file", file);
const rsp = await this.#req("", "POST", fd); const rsp = await this.#req("", "POST", fd);

View File

@ -2,6 +2,7 @@ import { NostrEvent } from "@snort/system";
import { GameInfo } from "../game-database"; import { GameInfo } from "../game-database";
import { Nip94Tags, readNip94Tags, readNip94TagsFromIMeta } from "../upload"; import { Nip94Tags, readNip94Tags, readNip94TagsFromIMeta } from "../upload";
import { getHost, sortStreamTags, extractGameTag, findTag } from "@/utils"; import { getHost, sortStreamTags, extractGameTag, findTag } from "@/utils";
import { unwrap } from "@snort/shared";
export interface MediaPayload { export interface MediaPayload {
url: string; url: string;
@ -18,12 +19,9 @@ export class VideoInfo {
goal?: string; goal?: string;
gameId?: string; gameId?: string;
gameInfo?: GameInfo; gameInfo?: GameInfo;
duration?: number;
publishedAt?: number; publishedAt?: number;
get duration() {
return this.media.find(m => m.duration)?.duration;
}
constructor( constructor(
readonly host: string, readonly host: string,
readonly id: string, readonly id: string,
@ -33,13 +31,28 @@ export class VideoInfo {
static parse(ev: NostrEvent) { static parse(ev: NostrEvent) {
const { regularTags, prefixedTags } = sortStreamTags(ev.tags); const { regularTags, prefixedTags } = sortStreamTags(ev.tags);
const ret = new VideoInfo(getHost(ev), findTag(ev, "d") ?? ev.id, regularTags, VideoInfo.#parseMediaTags(ev.tags)); const ret = new VideoInfo(getHost(ev), unwrap(findTag(ev, "d")), regularTags, VideoInfo.#parseMediaTags(ev.tags));
ret.title = findTag(ev, "title"); const matchInto = <K extends keyof VideoInfo>(
ret.summary = findTag(ev, "summary") ?? ev.content; tag: Array<string>,
ret.contentWarning = findTag(ev, "content-warning"); key: string,
ret.goal = findTag(ev, "goal"); into: K,
ret.publishedAt = Number(findTag(ev, "published_at") ?? ev.created_at); fn?: (v: string) => never,
) => {
if (tag[0] === key) {
ret[into] = fn ? fn(tag[1]) : (tag[1] as never);
}
};
for (const t of ev.tags) {
matchInto(t, "d", "id");
matchInto(t, "title", "title");
matchInto(t, "summary", "summary");
matchInto(t, "content-warning", "contentWarning");
matchInto(t, "goal", "goal");
matchInto(t, "duration", "duration");
matchInto(t, "published_at", "publishedAt");
}
const { gameInfo, gameId } = extractGameTag(prefixedTags); const { gameInfo, gameId } = extractGameTag(prefixedTags);
ret.gameId = gameId; ret.gameId = gameId;

View File

@ -730,9 +730,6 @@
"gzsn7k": { "gzsn7k": {
"defaultMessage": "{n} الرسائل" "defaultMessage": "{n} الرسائل"
}, },
"h6NRY6": {
"defaultMessage": "No Thanks!"
},
"h9mX2/": { "h9mX2/": {
"defaultMessage": "إذا كنت تستخدم استضافة zap.stream الداخلية الخاصة بنا (الأرخص والأسهل)، انسخ عنوان URL للبث ومفتاح البث إلى إعدادات OBS الخاصة بك وستكون جاهزًا للعمل." "defaultMessage": "إذا كنت تستخدم استضافة zap.stream الداخلية الخاصة بنا (الأرخص والأسهل)، انسخ عنوان URL للبث ومفتاح البث إلى إعدادات OBS الخاصة بك وستكون جاهزًا للعمل."
}, },

View File

@ -730,9 +730,6 @@
"gzsn7k": { "gzsn7k": {
"defaultMessage": "{n} съобщения" "defaultMessage": "{n} съобщения"
}, },
"h6NRY6": {
"defaultMessage": "No Thanks!"
},
"h9mX2/": { "h9mX2/": {
"defaultMessage": "Ако използвате нашия собствен хостинг zap.stream (най-евтиният и най-лесният), копирайте URL адреса на потока и ключа на потока в настройките на OBS и ще можете да започнете работа." "defaultMessage": "Ако използвате нашия собствен хостинг zap.stream (най-евтиният и най-лесният), копирайте URL адреса на потока и ключа на потока в настройките на OBS и ще можете да започнете работа."
}, },

View File

@ -730,9 +730,6 @@
"gzsn7k": { "gzsn7k": {
"defaultMessage": "{n} messages" "defaultMessage": "{n} messages"
}, },
"h6NRY6": {
"defaultMessage": "No Thanks!"
},
"h9mX2/": { "h9mX2/": {
"defaultMessage": "If you use our in-house zap.stream hosting (cheapest and easiest), copy your stream URL and Stream Key to your OBS settings and you should be good to go." "defaultMessage": "If you use our in-house zap.stream hosting (cheapest and easiest), copy your stream URL and Stream Key to your OBS settings and you should be good to go."
}, },

View File

@ -730,9 +730,6 @@
"gzsn7k": { "gzsn7k": {
"defaultMessage": "{n} meddelelser" "defaultMessage": "{n} meddelelser"
}, },
"h6NRY6": {
"defaultMessage": "No Thanks!"
},
"h9mX2/": { "h9mX2/": {
"defaultMessage": "Hvis du bruger vores egen zap.stream-hosting (billigst og nemmest), skal du kopiere din stream-URL og stream-nøgle til dine OBS-indstillinger, og så skulle du være klar til at gå i gang." "defaultMessage": "Hvis du bruger vores egen zap.stream-hosting (billigst og nemmest), skal du kopiere din stream-URL og stream-nøgle til dine OBS-indstillinger, og så skulle du være klar til at gå i gang."
}, },

View File

@ -730,9 +730,6 @@
"gzsn7k": { "gzsn7k": {
"defaultMessage": "{n} Nachrichten" "defaultMessage": "{n} Nachrichten"
}, },
"h6NRY6": {
"defaultMessage": "No Thanks!"
},
"h9mX2/": { "h9mX2/": {
"defaultMessage": "Wenn du unser hauseigenes zap.stream-Hosting verwendest (am billigsten und einfachsten), kopiere deine Stream-URL und deinen Stream-Schlüssel in deine OBS-Einstellungen und schon solltest du startklar sein." "defaultMessage": "Wenn du unser hauseigenes zap.stream-Hosting verwendest (am billigsten und einfachsten), kopiere deine Stream-URL und deinen Stream-Schlüssel in deine OBS-Einstellungen und schon solltest du startklar sein."
}, },

View File

@ -73,7 +73,6 @@
"Axo/o5": "Science & Technology", "Axo/o5": "Science & Technology",
"AyGauy": "Login", "AyGauy": "Login",
"BD0vyn": "{name} created a clip", "BD0vyn": "{name} created a clip",
"BHNL+v": "Raw Data:",
"Bd1yEX": "New Stream Goal", "Bd1yEX": "New Stream Goal",
"Bep/gA": "Private key", "Bep/gA": "Private key",
"BzQPM+": "Destination", "BzQPM+": "Destination",
@ -177,7 +176,6 @@
"RtYNX5": "Chat Users", "RtYNX5": "Chat Users",
"S39ba6": "What is OBS?", "S39ba6": "What is OBS?",
"SC2nJT": "Audio Codec", "SC2nJT": "Audio Codec",
"Sjo1P4": "Custom",
"TDUfVk": "Started", "TDUfVk": "Started",
"TP/cMX": "Ended", "TP/cMX": "Ended",
"TcDwEB": "Stream Keys", "TcDwEB": "Stream Keys",
@ -212,7 +210,6 @@
"ZXp0z1": "Features", "ZXp0z1": "Features",
"ZaNcK4": "No goals yet", "ZaNcK4": "No goals yet",
"ZcgtZo": "We've put together a list of easy-to-use exchanges that will allow you to buy a small amount of bitcoin (sats) and to transfer them to your wallet of choice.", "ZcgtZo": "We've put together a list of easy-to-use exchanges that will allow you to buy a small amount of bitcoin (sats) and to transfer them to your wallet of choice.",
"Zfr//4": "No duration provided, please try another upload server.",
"ZmqxZs": "You can change this later", "ZmqxZs": "You can change this later",
"ZsYhvh": "Zaps are lightning payments, which are published on nostr as receipts.", "ZsYhvh": "Zaps are lightning payments, which are published on nostr as receipts.",
"Zse7yG": "Raid target", "Zse7yG": "Raid target",
@ -234,7 +231,6 @@
"dOQCL8": "Display name", "dOQCL8": "Display name",
"dVD/AR": "Top Zappers", "dVD/AR": "Top Zappers",
"dkUMIH": "Clip by {name}", "dkUMIH": "Clip by {name}",
"dqGkI+": "Video durations vary too much, are you sure each variant is the same video?",
"e011kf": "FAQ", "e011kf": "FAQ",
"ebmhes": "Nostr Extension", "ebmhes": "Nostr Extension",
"f6biFA": "Oh, and you have {n} sats of free streaming on us! 💜", "f6biFA": "Oh, and you have {n} sats of free streaming on us! 💜",
@ -244,7 +240,6 @@
"gQxxlw": "Goal Name", "gQxxlw": "Goal Name",
"gt65Gg": "Stream goals encourage viewers to support streamers via donations.", "gt65Gg": "Stream goals encourage viewers to support streamers via donations.",
"gzsn7k": "{n} messages", "gzsn7k": "{n} messages",
"h6NRY6": "No Thanks!",
"h9mX2/": "If you use our in-house zap.stream hosting (cheapest and easiest), copy your stream URL and Stream Key to your OBS settings and you should be good to go.", "h9mX2/": "If you use our in-house zap.stream hosting (cheapest and easiest), copy your stream URL and Stream Key to your OBS settings and you should be good to go.",
"hMzcSq": "Messages", "hMzcSq": "Messages",
"heyxZL": "Enable text to speech", "heyxZL": "Enable text to speech",
@ -314,7 +309,6 @@
"ug01Mk": "Time", "ug01Mk": "Time",
"uksRSi": "Latest Videos", "uksRSi": "Latest Videos",
"vP4dFa": "Visit {link} to get some sweet zap.stream merch!", "vP4dFa": "Visit {link} to get some sweet zap.stream merch!",
"vaZKTn": "Add more content",
"vrTOHJ": "{amount} sats", "vrTOHJ": "{amount} sats",
"w+2Vw7": "Shorts", "w+2Vw7": "Shorts",
"w0Xm2F": "Start typing", "w0Xm2F": "Start typing",

View File

@ -730,9 +730,6 @@
"gzsn7k": { "gzsn7k": {
"defaultMessage": "{n} mensajes" "defaultMessage": "{n} mensajes"
}, },
"h6NRY6": {
"defaultMessage": "No Thanks!"
},
"h9mX2/": { "h9mX2/": {
"defaultMessage": "Si utiliza nuestro alojamiento interno zap.stream (el más barato y sencillo), copie la URL y la clave de transmisión en la configuración de OBS y ya puede empezar." "defaultMessage": "Si utiliza nuestro alojamiento interno zap.stream (el más barato y sencillo), copie la URL y la clave de transmisión en la configuración de OBS y ya puede empezar."
}, },

View File

@ -730,9 +730,6 @@
"gzsn7k": { "gzsn7k": {
"defaultMessage": "{n} پیام" "defaultMessage": "{n} پیام"
}, },
"h6NRY6": {
"defaultMessage": "No Thanks!"
},
"h9mX2/": { "h9mX2/": {
"defaultMessage": "If you use our in-house zap.stream hosting (cheapest and easiest), copy your stream URL and Stream Key to your OBS settings and you should be good to go." "defaultMessage": "If you use our in-house zap.stream hosting (cheapest and easiest), copy your stream URL and Stream Key to your OBS settings and you should be good to go."
}, },

View File

@ -730,9 +730,6 @@
"gzsn7k": { "gzsn7k": {
"defaultMessage": "{n} viestiä" "defaultMessage": "{n} viestiä"
}, },
"h6NRY6": {
"defaultMessage": "No Thanks!"
},
"h9mX2/": { "h9mX2/": {
"defaultMessage": "Jos käytät sisäistä zap.stream-hostingia (halvin ja helpoin), kopioi stream URL-osoite ja Stream Key OBS-asetuksiisi, ja sinun pitäisi olla valmis." "defaultMessage": "Jos käytät sisäistä zap.stream-hostingia (halvin ja helpoin), kopioi stream URL-osoite ja Stream Key OBS-asetuksiisi, ja sinun pitäisi olla valmis."
}, },

View File

@ -730,9 +730,6 @@
"gzsn7k": { "gzsn7k": {
"defaultMessage": "{n} messages" "defaultMessage": "{n} messages"
}, },
"h6NRY6": {
"defaultMessage": "No Thanks!"
},
"h9mX2/": { "h9mX2/": {
"defaultMessage": "Si vous utilisez notre hébergement interne zap.stream (le moins cher et le plus simple), copiez l'URL et la clé de votre flux dans vos paramètres OBS et vous devriez être prêt." "defaultMessage": "Si vous utilisez notre hébergement interne zap.stream (le moins cher et le plus simple), copiez l'URL et la clé de votre flux dans vos paramètres OBS et vous devriez être prêt."
}, },

View File

@ -730,9 +730,6 @@
"gzsn7k": { "gzsn7k": {
"defaultMessage": "{n} üzenetek" "defaultMessage": "{n} üzenetek"
}, },
"h6NRY6": {
"defaultMessage": "No Thanks!"
},
"h9mX2/": { "h9mX2/": {
"defaultMessage": "Ha a házon belüli zap.stream tárhelyet használja (ez a legolcsóbb és legegyszerűbb), másolja be a stream URL-jét és a Stream Key-t az OBS beállításaiba, és máris készen áll." "defaultMessage": "Ha a házon belüli zap.stream tárhelyet használja (ez a legolcsóbb és legegyszerűbb), másolja be a stream URL-jét és a Stream Key-t az OBS beállításaiba, és máris készen áll."
}, },

View File

@ -730,9 +730,6 @@
"gzsn7k": { "gzsn7k": {
"defaultMessage": "{n} messaggi" "defaultMessage": "{n} messaggi"
}, },
"h6NRY6": {
"defaultMessage": "No Thanks!"
},
"h9mX2/": { "h9mX2/": {
"defaultMessage": "Se utilizzate il nostro hosting interno zap.stream (il più economico e semplice), copiate l'URL del vostro stream e la Stream Key nelle impostazioni di OBS e sarete pronti a partire." "defaultMessage": "Se utilizzate il nostro hosting interno zap.stream (il più economico e semplice), copiate l'URL del vostro stream e la Stream Key nelle impostazioni di OBS e sarete pronti a partire."
}, },

View File

@ -730,9 +730,6 @@
"gzsn7k": { "gzsn7k": {
"defaultMessage": "{n} メッセージ" "defaultMessage": "{n} メッセージ"
}, },
"h6NRY6": {
"defaultMessage": "No Thanks!"
},
"h9mX2/": { "h9mX2/": {
"defaultMessage": "社内のzap.streamホスティング最も安くて簡単をご利用の場合は、ストリームURLとストリームキーをOBSの設定にコピーしてください。" "defaultMessage": "社内のzap.streamホスティング最も安くて簡単をご利用の場合は、ストリームURLとストリームキーをOBSの設定にコピーしてください。"
}, },

View File

@ -730,9 +730,6 @@
"gzsn7k": { "gzsn7k": {
"defaultMessage": "{n} 메시지" "defaultMessage": "{n} 메시지"
}, },
"h6NRY6": {
"defaultMessage": "No Thanks!"
},
"h9mX2/": { "h9mX2/": {
"defaultMessage": "가장 저렴하고 간편한 자체 zap.stream 호스팅을 사용하는 경우 스트림 URL과 스트림 키를 OBS 설정에 복사하면 바로 사용할 수 있습니다." "defaultMessage": "가장 저렴하고 간편한 자체 zap.stream 호스팅을 사용하는 경우 스트림 URL과 스트림 키를 OBS 설정에 복사하면 바로 사용할 수 있습니다."
}, },

View File

@ -730,9 +730,6 @@
"gzsn7k": { "gzsn7k": {
"defaultMessage": "{n} berichten" "defaultMessage": "{n} berichten"
}, },
"h6NRY6": {
"defaultMessage": "No Thanks!"
},
"h9mX2/": { "h9mX2/": {
"defaultMessage": "Als je onze in-house zap.stream hosting gebruikt (goedkoopste en makkelijkste), kopieer dan je stream URL en Stream Key naar je OBS instellingen en je zou aan de slag moeten kunnen." "defaultMessage": "Als je onze in-house zap.stream hosting gebruikt (goedkoopste en makkelijkste), kopieer dan je stream URL en Stream Key naar je OBS instellingen en je zou aan de slag moeten kunnen."
}, },

View File

@ -730,9 +730,6 @@
"gzsn7k": { "gzsn7k": {
"defaultMessage": "{n} mensagens" "defaultMessage": "{n} mensagens"
}, },
"h6NRY6": {
"defaultMessage": "No Thanks!"
},
"h9mX2/": { "h9mX2/": {
"defaultMessage": "Se você usar nossa hospedagem interna zap.stream (mais barata e mais fácil), copie o URL da transmissão e a chave da transmissão para as configurações do OBS e estará pronto para começar." "defaultMessage": "Se você usar nossa hospedagem interna zap.stream (mais barata e mais fácil), copie o URL da transmissão e a chave da transmissão para as configurações do OBS e estará pronto para começar."
}, },

View File

@ -730,9 +730,6 @@
"gzsn7k": { "gzsn7k": {
"defaultMessage": "{n} сообщения" "defaultMessage": "{n} сообщения"
}, },
"h6NRY6": {
"defaultMessage": "No Thanks!"
},
"h9mX2/": { "h9mX2/": {
"defaultMessage": "Если вы используете наш собственный хостинг zap.stream (самый дешевый и простой), скопируйте URL-адрес вашего потока и ключ потока в настройки OBS, и все будет готово." "defaultMessage": "Если вы используете наш собственный хостинг zap.stream (самый дешевый и простой), скопируйте URL-адрес вашего потока и ключ потока в настройки OBS, и все будет готово."
}, },

View File

@ -730,9 +730,6 @@
"gzsn7k": { "gzsn7k": {
"defaultMessage": "{n} meddelanden" "defaultMessage": "{n} meddelanden"
}, },
"h6NRY6": {
"defaultMessage": "No Thanks!"
},
"h9mX2/": { "h9mX2/": {
"defaultMessage": "Om du använder vår egen zap.stream-hosting (billigast och enklast) kopierar du din stream-URL och Stream Key till dina OBS-inställningar så är du redo att köra." "defaultMessage": "Om du använder vår egen zap.stream-hosting (billigast och enklast) kopierar du din stream-URL och Stream Key till dina OBS-inställningar så är du redo att köra."
}, },

View File

@ -730,9 +730,6 @@
"gzsn7k": { "gzsn7k": {
"defaultMessage": "{n} Ujumbe" "defaultMessage": "{n} Ujumbe"
}, },
"h6NRY6": {
"defaultMessage": "No Thanks!"
},
"h9mX2/": { "h9mX2/": {
"defaultMessage": "Ikiwa unatumia Hosting yetu ya ndani ya zap.stream (bei rahisi na rahisi zaidi), nakili URL yako ya Mtiririko na Ufunguo wako wa Mtiririko kwenye mipangilio yako ya OBS na utakuwa tayari kuanza." "defaultMessage": "Ikiwa unatumia Hosting yetu ya ndani ya zap.stream (bei rahisi na rahisi zaidi), nakili URL yako ya Mtiririko na Ufunguo wako wa Mtiririko kwenye mipangilio yako ya OBS na utakuwa tayari kuanza."
}, },

View File

@ -730,9 +730,6 @@
"gzsn7k": { "gzsn7k": {
"defaultMessage": "{n} ข้อความ" "defaultMessage": "{n} ข้อความ"
}, },
"h6NRY6": {
"defaultMessage": "No Thanks!"
},
"h9mX2/": { "h9mX2/": {
"defaultMessage": "หากคุณใช้โฮสติ้ง zap.stream ของเรา (ถูกที่สุดและง่ายที่สุด) ให้คัดลอก URL สตรีมและคีย์สตรีมของคุณไปที่การตั้งค่า OBS เท่านี้คุณก็พร้อมแล้ว" "defaultMessage": "หากคุณใช้โฮสติ้ง zap.stream ของเรา (ถูกที่สุดและง่ายที่สุด) ให้คัดลอก URL สตรีมและคีย์สตรีมของคุณไปที่การตั้งค่า OBS เท่านี้คุณก็พร้อมแล้ว"
}, },

View File

@ -730,9 +730,6 @@
"gzsn7k": { "gzsn7k": {
"defaultMessage": "{n} 信息" "defaultMessage": "{n} 信息"
}, },
"h6NRY6": {
"defaultMessage": "No Thanks!"
},
"h9mX2/": { "h9mX2/": {
"defaultMessage": "如果您使用我们内部的 zap.stream 托管(最便宜、最简单),请将您的流 URL 和流密钥复制到您的 OBS 设置中,然后就可以使用了。" "defaultMessage": "如果您使用我们内部的 zap.stream 托管(最便宜、最简单),请将您的流 URL 和流密钥复制到您的 OBS 设置中,然后就可以使用了。"
}, },

View File

@ -730,9 +730,6 @@
"gzsn7k": { "gzsn7k": {
"defaultMessage": "{n} 條消息" "defaultMessage": "{n} 條消息"
}, },
"h6NRY6": {
"defaultMessage": "No Thanks!"
},
"h9mX2/": { "h9mX2/": {
"defaultMessage": "If you use our in-house zap.stream hosting (cheapest and easiest), copy your stream URL and Stream Key to your OBS settings and you should be good to go." "defaultMessage": "If you use our in-house zap.stream hosting (cheapest and easiest), copy your stream URL and Stream Key to your OBS settings and you should be good to go."
}, },

View File

@ -2573,14 +2573,14 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@snort/system-react@npm:^1.6.1": "@snort/system-react@npm:^1.5.6":
version: 1.6.1 version: 1.5.6
resolution: "@snort/system-react@npm:1.6.1" resolution: "@snort/system-react@npm:1.5.6"
dependencies: dependencies:
"@snort/shared": "npm:^1.0.17" "@snort/shared": "npm:^1.0.17"
"@snort/system": "npm:^1.6.1" "@snort/system": "npm:^1.5.6"
react: "npm:^18.2.0" react: "npm:^18.2.0"
checksum: 10c0/fe3180c1f4341df4fd585c4031ef632a7d59c7637c6aed16ea254a1c22a66e288516d96939c0080efd0fb73940531855a59a24fae5f82c326f6544362eb0394c checksum: 10c0/b7fb8dbc87328603b202dad9ce57dbff2cb8231829128b48346ba10925ee9fa5103652941b6a0456d6221fce8379a7148dc6507ebe8e1d0d35f7efd92738d08a
languageName: node languageName: node
linkType: hard linkType: hard
@ -2591,9 +2591,9 @@ __metadata:
languageName: node languageName: node
linkType: hard linkType: hard
"@snort/system@npm:^1.6.1": "@snort/system@npm:^1.5.6":
version: 1.6.1 version: 1.5.6
resolution: "@snort/system@npm:1.6.1" resolution: "@snort/system@npm:1.5.6"
dependencies: dependencies:
"@noble/ciphers": "npm:^0.6.0" "@noble/ciphers": "npm:^0.6.0"
"@noble/curves": "npm:^1.4.0" "@noble/curves": "npm:^1.4.0"
@ -2608,33 +2608,33 @@ __metadata:
nostr-social-graph: "npm:^1.0.3" nostr-social-graph: "npm:^1.0.3"
uuid: "npm:^9.0.0" uuid: "npm:^9.0.0"
ws: "npm:^8.14.0" ws: "npm:^8.14.0"
checksum: 10c0/5da01450970f4a51df98369c4cdd2b8886add3480e7962d11ed17498cf7db421bcd5e585abe532ff754efc0bf26b5734b9b24005f958802357ec390ccb74d281 checksum: 10c0/38fee2d55240f91a5e6ea0684a4bd94e6f4c56fab9b6a20d9ebef26dcdd17ed6c9a42bf1804c7bcf135a89c6882659baecede6d885dcead4a167d5e3337c9764
languageName: node languageName: node
linkType: hard linkType: hard
"@snort/wallet@npm:^0.2.4": "@snort/wallet@npm:^0.2.1":
version: 0.2.4 version: 0.2.1
resolution: "@snort/wallet@npm:0.2.4" resolution: "@snort/wallet@npm:0.2.1"
dependencies: dependencies:
"@cashu/cashu-ts": "npm:^1.0.0-rc.3" "@cashu/cashu-ts": "npm:^1.0.0-rc.3"
"@lightninglabs/lnc-web": "npm:^0.3.1-alpha" "@lightninglabs/lnc-web": "npm:^0.3.1-alpha"
"@scure/base": "npm:^1.1.6" "@scure/base": "npm:^1.1.6"
"@snort/shared": "npm:^1.0.17" "@snort/shared": "npm:^1.0.17"
"@snort/system": "npm:^1.6.1" "@snort/system": "npm:^1.5.6"
debug: "npm:^4.3.4" debug: "npm:^4.3.4"
eventemitter3: "npm:^5.0.1" eventemitter3: "npm:^5.0.1"
checksum: 10c0/7d3e23d1d79595ee99e041b816cec5e7bfc6f34bfce6f0864a556de82cfcf29200f6978744b0717b5f6873c5c498e2dd9005571171f1f533f235cfbadf205a44 checksum: 10c0/0dcf4b0336029e336bd6abcd7b79cf60d6fed08b2ab2847a8e791bb2399646e86c95aefc4dcfea08365c3dea417c362cdfa93939d7c36f037762de601936b331
languageName: node languageName: node
linkType: hard linkType: hard
"@snort/worker-relay@npm:^1.3.1": "@snort/worker-relay@npm:^1.3.0":
version: 1.3.1 version: 1.3.0
resolution: "@snort/worker-relay@npm:1.3.1" resolution: "@snort/worker-relay@npm:1.3.0"
dependencies: dependencies:
"@sqlite.org/sqlite-wasm": "npm:^3.46.1-build3" "@sqlite.org/sqlite-wasm": "npm:^3.46.1-build3"
eventemitter3: "npm:^5.0.1" eventemitter3: "npm:^5.0.1"
uuid: "npm:^9.0.1" uuid: "npm:^9.0.1"
checksum: 10c0/19b89e4f96df425d2d73e87fda1f82844bf7f3a1ba114073d0bf4052c9d5fe3eac9e6ca6d88ad8e36b65bae6dfcf69db5cb47828ef1c195419a94bd87ae2ff53 checksum: 10c0/1a0eb175f50787bbcaa585641bf710347b59f3d3426cbf0f83182a5574bf7a63beb3e5d66bb41506e2d50c3ee904d55670c85c7f1542018936dd5a4ce06726e8
languageName: node languageName: node
linkType: hard linkType: hard
@ -7354,11 +7354,11 @@ __metadata:
"@noble/hashes": "npm:^1.4.0" "@noble/hashes": "npm:^1.4.0"
"@scure/base": "npm:^1.1.6" "@scure/base": "npm:^1.1.6"
"@snort/shared": "npm:^1.0.17" "@snort/shared": "npm:^1.0.17"
"@snort/system": "npm:^1.6.1" "@snort/system": "npm:^1.5.6"
"@snort/system-react": "npm:^1.6.1" "@snort/system-react": "npm:^1.5.6"
"@snort/system-wasm": "npm:^1.0.5" "@snort/system-wasm": "npm:^1.0.5"
"@snort/wallet": "npm:^0.2.4" "@snort/wallet": "npm:^0.2.1"
"@snort/worker-relay": "npm:^1.3.1" "@snort/worker-relay": "npm:^1.3.0"
"@szhsin/react-menu": "npm:^4.1.0" "@szhsin/react-menu": "npm:^4.1.0"
"@testing-library/dom": "npm:^9.3.1" "@testing-library/dom": "npm:^9.3.1"
"@types/node": "npm:^20.12.12" "@types/node": "npm:^20.12.12"