Compare commits
10 Commits
cd43bb3cf9
...
f18bd88227
Author | SHA1 | Date | |
---|---|---|---|
f18bd88227 | |||
de88158b3a | |||
![]() |
4bade72dd6 | ||
![]() |
ed8f7a02c1 | ||
![]() |
3c638dd0e3 | ||
f39e0f3d3f | |||
24b9f0ba63 | |||
9cf9199b29 | |||
5540034ade | |||
0a2ba19032 |
@ -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.5.6",
|
"@snort/system": "^1.6.1",
|
||||||
"@snort/system-react": "^1.5.6",
|
"@snort/system-react": "^1.6.1",
|
||||||
"@snort/system-wasm": "^1.0.5",
|
"@snort/system-wasm": "^1.0.5",
|
||||||
"@snort/wallet": "^0.2.1",
|
"@snort/wallet": "^0.2.4",
|
||||||
"@snort/worker-relay": "^1.3.0",
|
"@snort/worker-relay": "^1.3.1",
|
||||||
"@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",
|
||||||
|
@ -8,8 +8,10 @@ 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 = 34_235 as EventKind;
|
export const VIDEO_KIND = 21 as EventKind;
|
||||||
export const SHORTS_KIND = 34_236 as EventKind;
|
export const SHORTS_KIND = 22 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;
|
||||||
|
@ -5,13 +5,15 @@ 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, StreamState } from "@/const";
|
import { GOAL, LIVE_STREAM_CLIP, OLD_SHORTS_KIND, OLD_VIDEO_KIND, SHORTS_KIND, StreamState, VIDEO_KIND } 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 } from "react";
|
import { HTMLProps, ReactNode } from "react";
|
||||||
|
import { ShortPage } from "@/pages/short";
|
||||||
|
import { VideoPage } from "@/pages/video";
|
||||||
|
|
||||||
interface EventProps {
|
interface EventProps {
|
||||||
link: NostrLink;
|
link: NostrLink;
|
||||||
@ -31,36 +33,49 @@ 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 <Goal ev={ev} />;
|
return modalPage(<Goal ev={ev} />);
|
||||||
}
|
}
|
||||||
case EventKind.EmojiSet: {
|
case EventKind.EmojiSet: {
|
||||||
return <EmojiPack ev={ev} />;
|
return modalPage(<EmojiPack ev={ev} />);
|
||||||
}
|
}
|
||||||
case EventKind.Badge: {
|
case EventKind.Badge: {
|
||||||
return <BadgeInfo ev={ev} />;
|
return modalPage(<BadgeInfo ev={ev} />);
|
||||||
}
|
}
|
||||||
case EventKind.TextNote: {
|
case EventKind.TextNote: {
|
||||||
return <Note ev={ev} />;
|
return modalPage(<Note ev={ev} />);
|
||||||
}
|
}
|
||||||
case LIVE_STREAM_CLIP: {
|
case LIVE_STREAM_CLIP: {
|
||||||
return <LiveStreamClip ev={ev} />;
|
return modalPage(<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 (
|
return modalPage(
|
||||||
<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: {
|
||||||
const link = NostrLink.fromEvent(ev);
|
return modalPage(<ExternalLink href={`https://snort.social/${link.encode()}`}>{link.encode()}</ExternalLink>);
|
||||||
return <ExternalLink href={`https://snort.social/${link.encode()}`}>{link.encode()}</ExternalLink>;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -46,6 +46,7 @@ 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("");
|
||||||
@ -158,12 +159,17 @@ 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">
|
||||||
{(isFiat ? usdAmounts : satsAmounts).map(a => (
|
{!customAmount &&
|
||||||
<Pill key={a} selected={a === amount} onClick={() => setAmount(a)}>
|
(isFiat ? usdAmounts : satsAmounts).map(a => (
|
||||||
{isFiat ? `$${a.toLocaleString()}` : formatSats(a)}
|
<Pill key={a} selected={a === amount} onClick={() => setAmount(a)}>
|
||||||
</Pill>
|
{isFiat ? `$${a.toLocaleString()}` : formatSats(a)}
|
||||||
))}
|
</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">
|
||||||
|
@ -145,7 +145,8 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const startsDate = new Date(parseInt(start ?? "0") * 1000);
|
const startsTimestamp = Number(start ?? (new Date().getTime() / 1000));
|
||||||
|
const startsDate = new Date(startsTimestamp * 1000);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -203,9 +204,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().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() + 1).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((e.target.valueAsNumber / 1000).toString());
|
setStart((new Date(e.target.value) / 1000).toString());
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</StreamInput>
|
</StreamInput>
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
/* eslint-disable @typescript-eslint/ban-ts-comment */
|
||||||
import { CSSProperties, HTMLProps } from "react";
|
import { CSSProperties, HTMLProps, Suspense, lazy } 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,12 +35,15 @@ 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 <Nip94Player link={link} />;
|
return (
|
||||||
}
|
<Suspense>
|
||||||
{
|
<Nip94Player link={link} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
/* @ts-ignore Web Componenet */
|
/* @ts-ignore Web Componenet */
|
||||||
|
return <hls-video {...props} slot="media" src={stream} playsInline={true} autoPlay={true} />;
|
||||||
}
|
}
|
||||||
return <hls-video {...props} slot="media" src={stream} playsInline={true} autoPlay={true} />;
|
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<MediaController
|
<MediaController
|
||||||
|
@ -1,13 +1,10 @@
|
|||||||
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({
|
||||||
@ -16,14 +13,8 @@ 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 if (link.type === NostrPrefix.Address) {
|
} else {
|
||||||
const f = b.withFilter().tag("d", [link.id]);
|
b.withFilter().link(link);
|
||||||
if (link.author) {
|
|
||||||
f.authors([link.author]);
|
|
||||||
}
|
|
||||||
if (link.kind) {
|
|
||||||
f.kinds([link.kind]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return b;
|
return b;
|
||||||
}, [link.id, leaveOpen]);
|
}, [link.id, leaveOpen]);
|
||||||
@ -31,9 +22,7 @@ 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] : [])]
|
const hosting = [...q, ...(evPreload ? [evPreload] : [])].sort((a, b) => (b.created_at > a.created_at ? 1 : -1));
|
||||||
.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]);
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { SHORTS_KIND, VIDEO_KIND } from "@/const";
|
import { OLD_SHORTS_KIND, OLD_VIDEO_KIND, 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 === VIDEO_KIND || ev.kind === SHORTS_KIND || ev.kind == OLD_SHORTS_KIND || ev.kind == OLD_VIDEO_KIND
|
||||||
? VideoInfo.parse(ev)?.sources()
|
? VideoInfo.parse(ev)?.sources()
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
|
@ -4,12 +4,7 @@ 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 = [
|
export const DefaultMediaServers = [new UnknownTag(["server", "https://nostr.download/"])];
|
||||||
//"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();
|
||||||
|
@ -221,6 +221,9 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@ -533,6 +536,9 @@
|
|||||||
"SC2nJT": {
|
"SC2nJT": {
|
||||||
"defaultMessage": "Audio Codec"
|
"defaultMessage": "Audio Codec"
|
||||||
},
|
},
|
||||||
|
"Sjo1P4": {
|
||||||
|
"defaultMessage": "Custom"
|
||||||
|
},
|
||||||
"TDUfVk": {
|
"TDUfVk": {
|
||||||
"defaultMessage": "Started"
|
"defaultMessage": "Started"
|
||||||
},
|
},
|
||||||
@ -638,6 +644,9 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@ -702,6 +711,9 @@
|
|||||||
"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"
|
||||||
@ -730,6 +742,9 @@
|
|||||||
"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."
|
||||||
},
|
},
|
||||||
@ -939,6 +954,9 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { useParams } from "react-router-dom";
|
import { useParams } from "react-router-dom";
|
||||||
import { NostrPrefix, encodeTLV, parseNostrLink } from "@snort/system";
|
import { NostrLink, 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,19 +13,20 @@ 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">
|
||||||
<LiveChat
|
{ev && (
|
||||||
ev={ev}
|
<LiveChat
|
||||||
link={lnk}
|
ev={ev}
|
||||||
canWrite={chat}
|
link={NostrLink.fromEvent(ev)}
|
||||||
showScrollbar={false}
|
canWrite={chat}
|
||||||
goal={goal}
|
showScrollbar={false}
|
||||||
className="h-inherit"
|
goal={goal}
|
||||||
autoRaid={false}
|
className="h-inherit"
|
||||||
/>
|
autoRaid={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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 } from "react-router-dom";
|
import { useLocation, useNavigate } 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, WarningButton } from "@/element/buttons";
|
import { Layer1Button, Layer2Button, 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,15 +30,19 @@ 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 function DashboardForLink({ link }: { link: NostrLink }) {
|
export default 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"));
|
||||||
@ -110,7 +114,7 @@ export 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,
|
"grid-cols-[20%_80%]": status === StreamState.Ended || status === undefined,
|
||||||
"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">
|
||||||
@ -126,7 +130,14 @@ export function DashboardForLink({ link }: { link: NostrLink }) {
|
|||||||
</div>
|
</div>
|
||||||
{streamLink && status === StreamState.Live && !isMyManual && (
|
{streamLink && status === StreamState.Live && !isMyManual && (
|
||||||
<>
|
<>
|
||||||
<LiveVideoPlayer stream={stream} status={status} poster={image} muted={true} className="w-full" />
|
<LiveVideoPlayer
|
||||||
|
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" />}
|
||||||
@ -170,7 +181,14 @@ export function DashboardForLink({ link }: { link: NostrLink }) {
|
|||||||
)}
|
)}
|
||||||
{streamLink && isMyManual && (status === StreamState.Live || status === StreamState.Planned) && (
|
{streamLink && isMyManual && (status === StreamState.Live || status === StreamState.Planned) && (
|
||||||
<>
|
<>
|
||||||
<LiveVideoPlayer stream={stream} status={status} poster={image} muted={true} className="w-full" />
|
<LiveVideoPlayer
|
||||||
|
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" />} />
|
||||||
@ -283,6 +301,29 @@ export 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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,19 @@
|
|||||||
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 { DashboardForLink } from "./dashboard";
|
import { Suspense, lazy } from "react";
|
||||||
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 <DashboardForLink link={link} />;
|
return (
|
||||||
|
<Suspense>
|
||||||
|
<DashboardForLink link={link} />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,7 @@ 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();
|
||||||
@ -16,7 +17,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[0];
|
return info?.endpoints?.find(a => a.name == "Best") ?? info?.endpoints?.at(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;
|
||||||
@ -69,31 +70,7 @@ export default function DashboardIntro() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
{!info?.tosAccepted && (
|
{!info?.tosAccepted && <AcceptTos provider={info?.name} tosLink={info?.tosLink} tos={tos} setTos={setTos} />}
|
||||||
<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 () => {
|
||||||
|
39
src/pages/dashboard/tos.tsx
Normal file
39
src/pages/dashboard/tos.tsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import { LIVE_STREAM, SHORTS_KIND, VIDEO_KIND } from "@/const";
|
import { LIVE_STREAM, OLD_SHORTS_KIND, OLD_VIDEO_KIND, 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,20 +20,16 @@ export function LinkHandler() {
|
|||||||
if (!link) return;
|
if (!link) return;
|
||||||
|
|
||||||
if (link.type === NostrPrefix.Event) {
|
if (link.type === NostrPrefix.Event) {
|
||||||
return (
|
return <NostrEventElement link={link} />;
|
||||||
<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) {
|
} else if (link.kind === VIDEO_KIND || link.kind === OLD_VIDEO_KIND) {
|
||||||
return <VideoPage link={link} evPreload={evPreload} />;
|
return <VideoPage link={link} evPreload={evPreload} />;
|
||||||
} else if (link.kind === SHORTS_KIND) {
|
} else if (link.kind === SHORTS_KIND || link.kind === OLD_SHORTS_KIND) {
|
||||||
return <ShortPage link={link} evPreload={evPreload} />;
|
return <ShortPage link={link} evPreload={evPreload} />;
|
||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { SHORTS_KIND } from "@/const";
|
import { OLD_SHORTS_KIND, 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,13 +8,12 @@ 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]);
|
rb.withFilter().kinds([SHORTS_KIND, OLD_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");
|
const pubA = findTag(a, "published_at") ?? a.created_at;
|
||||||
const pubB = findTag(b, "published_at");
|
const pubB = findTag(b, "published_at") ?? b.created_at;
|
||||||
return Number(pubA) > Number(pubB) ? -1 : 1;
|
return Number(pubA) > Number(pubB) ? -1 : 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { VIDEO_KIND } from "@/const";
|
import { SHORTS_KIND, 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 } from "@/service/upload";
|
import { Nip94Tags, UploadResult, nip94TagsToIMeta, readNip94Tags } 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, unixNow, unwrap } from "@snort/shared";
|
import { ExternalStore, removeUndefined, unwrap } from "@snort/shared";
|
||||||
import { EventPublisher, NostrLink } from "@snort/system";
|
import { EventBuilder, EventPublisher, NostrEvent, 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,6 +69,23 @@ 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) {
|
||||||
@ -130,6 +147,27 @@ 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
|
||||||
*/
|
*/
|
||||||
@ -187,21 +225,24 @@ 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 pub.generic(eb => {
|
const ev = await makeEvent().buildAndSign(pub.signer);
|
||||||
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()}`);
|
||||||
@ -229,7 +270,16 @@ 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 => manager.uploadTo(b, new File([data], "thumb.jpg"), pub, "thumb"));
|
servers.servers.forEach(b =>
|
||||||
|
manager.uploadTo(
|
||||||
|
b,
|
||||||
|
new File([data], "thumb.jpg", {
|
||||||
|
type: "image/jpeg",
|
||||||
|
}),
|
||||||
|
pub,
|
||||||
|
"thumb",
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -291,6 +341,17 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,6 +373,7 @@ 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">
|
||||||
@ -359,8 +421,11 @@ export function UploadPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{videos > 0 && (
|
{videos > 0 && (
|
||||||
<div onClick={() => uploadFile()} className="cursor-pointer">
|
<div className="flex flex-col gap-2">
|
||||||
{uploadButton()}
|
<div className="text-xl">
|
||||||
|
<FormattedMessage defaultMessage="Add more content" />
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-4 items-center">{uploadButton()}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{uploads.length > 0 && (
|
{uploads.length > 0 && (
|
||||||
@ -377,7 +442,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" />}
|
{thumb && <img src={proxy(thumb)} className="w-full h-full object-contain" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<DefaultButton onClick={() => uploadThumb()}>
|
<DefaultButton onClick={() => uploadThumb()}>
|
||||||
@ -405,9 +470,13 @@ export function UploadPage() {
|
|||||||
</WarningButton>
|
</WarningButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<pre className="text-xs font-mono overflow-wrap text-pretty">
|
|
||||||
{JSON.stringify(manager.makeIMeta(), undefined, 2)}
|
<div>
|
||||||
</pre>
|
<FormattedMessage defaultMessage="Raw Data:" />
|
||||||
|
<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 />
|
||||||
@ -416,7 +485,19 @@ 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={() => {
|
onPicked={files => {
|
||||||
|
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);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { VIDEO_KIND } from "@/const";
|
import { OLD_VIDEO_KIND, 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]);
|
rb.withFilter().kinds([VIDEO_KIND, OLD_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");
|
const pubA = findTag(a, "published_at") ?? a.created_at;
|
||||||
const pubB = findTag(b, "published_at");
|
const pubB = findTag(b, "published_at") ?? b.created_at;
|
||||||
return Number(pubA) > Number(pubB) ? -1 : 1;
|
return Number(pubA) > Number(pubB) ? -1 : 1;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -16,6 +16,8 @@ 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 {
|
||||||
@ -103,6 +105,14 @@ 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;
|
||||||
@ -126,6 +136,8 @@ 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));
|
||||||
}
|
}
|
||||||
|
@ -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("media_type", file.type);
|
fd.append("content_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);
|
||||||
|
@ -2,7 +2,6 @@ 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;
|
||||||
@ -19,9 +18,12 @@ 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,
|
||||||
@ -31,28 +33,13 @@ 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), unwrap(findTag(ev, "d")), regularTags, VideoInfo.#parseMediaTags(ev.tags));
|
const ret = new VideoInfo(getHost(ev), findTag(ev, "d") ?? ev.id, regularTags, VideoInfo.#parseMediaTags(ev.tags));
|
||||||
|
|
||||||
const matchInto = <K extends keyof VideoInfo>(
|
ret.title = findTag(ev, "title");
|
||||||
tag: Array<string>,
|
ret.summary = findTag(ev, "summary") ?? ev.content;
|
||||||
key: string,
|
ret.contentWarning = findTag(ev, "content-warning");
|
||||||
into: K,
|
ret.goal = findTag(ev, "goal");
|
||||||
fn?: (v: string) => never,
|
ret.publishedAt = Number(findTag(ev, "published_at") ?? ev.created_at);
|
||||||
) => {
|
|
||||||
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;
|
||||||
|
@ -730,6 +730,9 @@
|
|||||||
"gzsn7k": {
|
"gzsn7k": {
|
||||||
"defaultMessage": "{n} الرسائل"
|
"defaultMessage": "{n} الرسائل"
|
||||||
},
|
},
|
||||||
|
"h6NRY6": {
|
||||||
|
"defaultMessage": "No Thanks!"
|
||||||
|
},
|
||||||
"h9mX2/": {
|
"h9mX2/": {
|
||||||
"defaultMessage": "إذا كنت تستخدم استضافة zap.stream الداخلية الخاصة بنا (الأرخص والأسهل)، انسخ عنوان URL للبث ومفتاح البث إلى إعدادات OBS الخاصة بك وستكون جاهزًا للعمل."
|
"defaultMessage": "إذا كنت تستخدم استضافة zap.stream الداخلية الخاصة بنا (الأرخص والأسهل)، انسخ عنوان URL للبث ومفتاح البث إلى إعدادات OBS الخاصة بك وستكون جاهزًا للعمل."
|
||||||
},
|
},
|
||||||
|
@ -730,6 +730,9 @@
|
|||||||
"gzsn7k": {
|
"gzsn7k": {
|
||||||
"defaultMessage": "{n} съобщения"
|
"defaultMessage": "{n} съобщения"
|
||||||
},
|
},
|
||||||
|
"h6NRY6": {
|
||||||
|
"defaultMessage": "No Thanks!"
|
||||||
|
},
|
||||||
"h9mX2/": {
|
"h9mX2/": {
|
||||||
"defaultMessage": "Ако използвате нашия собствен хостинг zap.stream (най-евтиният и най-лесният), копирайте URL адреса на потока и ключа на потока в настройките на OBS и ще можете да започнете работа."
|
"defaultMessage": "Ако използвате нашия собствен хостинг zap.stream (най-евтиният и най-лесният), копирайте URL адреса на потока и ключа на потока в настройките на OBS и ще можете да започнете работа."
|
||||||
},
|
},
|
||||||
|
@ -730,6 +730,9 @@
|
|||||||
"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."
|
||||||
},
|
},
|
||||||
|
@ -730,6 +730,9 @@
|
|||||||
"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."
|
||||||
},
|
},
|
||||||
|
@ -730,6 +730,9 @@
|
|||||||
"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."
|
||||||
},
|
},
|
||||||
|
@ -73,6 +73,7 @@
|
|||||||
"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",
|
||||||
@ -176,6 +177,7 @@
|
|||||||
"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",
|
||||||
@ -210,6 +212,7 @@
|
|||||||
"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",
|
||||||
@ -231,6 +234,7 @@
|
|||||||
"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! 💜",
|
||||||
@ -240,6 +244,7 @@
|
|||||||
"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",
|
||||||
@ -309,6 +314,7 @@
|
|||||||
"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",
|
||||||
|
@ -730,6 +730,9 @@
|
|||||||
"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."
|
||||||
},
|
},
|
||||||
|
@ -730,6 +730,9 @@
|
|||||||
"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."
|
||||||
},
|
},
|
||||||
|
@ -730,6 +730,9 @@
|
|||||||
"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."
|
||||||
},
|
},
|
||||||
|
@ -730,6 +730,9 @@
|
|||||||
"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."
|
||||||
},
|
},
|
||||||
|
@ -730,6 +730,9 @@
|
|||||||
"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."
|
||||||
},
|
},
|
||||||
|
@ -730,6 +730,9 @@
|
|||||||
"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."
|
||||||
},
|
},
|
||||||
|
@ -730,6 +730,9 @@
|
|||||||
"gzsn7k": {
|
"gzsn7k": {
|
||||||
"defaultMessage": "{n} メッセージ"
|
"defaultMessage": "{n} メッセージ"
|
||||||
},
|
},
|
||||||
|
"h6NRY6": {
|
||||||
|
"defaultMessage": "No Thanks!"
|
||||||
|
},
|
||||||
"h9mX2/": {
|
"h9mX2/": {
|
||||||
"defaultMessage": "社内のzap.streamホスティング(最も安くて簡単)をご利用の場合は、ストリームURLとストリームキーをOBSの設定にコピーしてください。"
|
"defaultMessage": "社内のzap.streamホスティング(最も安くて簡単)をご利用の場合は、ストリームURLとストリームキーをOBSの設定にコピーしてください。"
|
||||||
},
|
},
|
||||||
|
@ -730,6 +730,9 @@
|
|||||||
"gzsn7k": {
|
"gzsn7k": {
|
||||||
"defaultMessage": "{n} 메시지"
|
"defaultMessage": "{n} 메시지"
|
||||||
},
|
},
|
||||||
|
"h6NRY6": {
|
||||||
|
"defaultMessage": "No Thanks!"
|
||||||
|
},
|
||||||
"h9mX2/": {
|
"h9mX2/": {
|
||||||
"defaultMessage": "가장 저렴하고 간편한 자체 zap.stream 호스팅을 사용하는 경우 스트림 URL과 스트림 키를 OBS 설정에 복사하면 바로 사용할 수 있습니다."
|
"defaultMessage": "가장 저렴하고 간편한 자체 zap.stream 호스팅을 사용하는 경우 스트림 URL과 스트림 키를 OBS 설정에 복사하면 바로 사용할 수 있습니다."
|
||||||
},
|
},
|
||||||
|
@ -730,6 +730,9 @@
|
|||||||
"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."
|
||||||
},
|
},
|
||||||
|
@ -730,6 +730,9 @@
|
|||||||
"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."
|
||||||
},
|
},
|
||||||
|
@ -730,6 +730,9 @@
|
|||||||
"gzsn7k": {
|
"gzsn7k": {
|
||||||
"defaultMessage": "{n} сообщения"
|
"defaultMessage": "{n} сообщения"
|
||||||
},
|
},
|
||||||
|
"h6NRY6": {
|
||||||
|
"defaultMessage": "No Thanks!"
|
||||||
|
},
|
||||||
"h9mX2/": {
|
"h9mX2/": {
|
||||||
"defaultMessage": "Если вы используете наш собственный хостинг zap.stream (самый дешевый и простой), скопируйте URL-адрес вашего потока и ключ потока в настройки OBS, и все будет готово."
|
"defaultMessage": "Если вы используете наш собственный хостинг zap.stream (самый дешевый и простой), скопируйте URL-адрес вашего потока и ключ потока в настройки OBS, и все будет готово."
|
||||||
},
|
},
|
||||||
|
@ -730,6 +730,9 @@
|
|||||||
"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."
|
||||||
},
|
},
|
||||||
|
@ -730,6 +730,9 @@
|
|||||||
"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."
|
||||||
},
|
},
|
||||||
|
@ -730,6 +730,9 @@
|
|||||||
"gzsn7k": {
|
"gzsn7k": {
|
||||||
"defaultMessage": "{n} ข้อความ"
|
"defaultMessage": "{n} ข้อความ"
|
||||||
},
|
},
|
||||||
|
"h6NRY6": {
|
||||||
|
"defaultMessage": "No Thanks!"
|
||||||
|
},
|
||||||
"h9mX2/": {
|
"h9mX2/": {
|
||||||
"defaultMessage": "หากคุณใช้โฮสติ้ง zap.stream ของเรา (ถูกที่สุดและง่ายที่สุด) ให้คัดลอก URL สตรีมและคีย์สตรีมของคุณไปที่การตั้งค่า OBS เท่านี้คุณก็พร้อมแล้ว"
|
"defaultMessage": "หากคุณใช้โฮสติ้ง zap.stream ของเรา (ถูกที่สุดและง่ายที่สุด) ให้คัดลอก URL สตรีมและคีย์สตรีมของคุณไปที่การตั้งค่า OBS เท่านี้คุณก็พร้อมแล้ว"
|
||||||
},
|
},
|
||||||
|
@ -730,6 +730,9 @@
|
|||||||
"gzsn7k": {
|
"gzsn7k": {
|
||||||
"defaultMessage": "{n} 信息"
|
"defaultMessage": "{n} 信息"
|
||||||
},
|
},
|
||||||
|
"h6NRY6": {
|
||||||
|
"defaultMessage": "No Thanks!"
|
||||||
|
},
|
||||||
"h9mX2/": {
|
"h9mX2/": {
|
||||||
"defaultMessage": "如果您使用我们内部的 zap.stream 托管(最便宜、最简单),请将您的流 URL 和流密钥复制到您的 OBS 设置中,然后就可以使用了。"
|
"defaultMessage": "如果您使用我们内部的 zap.stream 托管(最便宜、最简单),请将您的流 URL 和流密钥复制到您的 OBS 设置中,然后就可以使用了。"
|
||||||
},
|
},
|
||||||
|
@ -730,6 +730,9 @@
|
|||||||
"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."
|
||||||
},
|
},
|
||||||
|
44
yarn.lock
44
yarn.lock
@ -2573,14 +2573,14 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@snort/system-react@npm:^1.5.6":
|
"@snort/system-react@npm:^1.6.1":
|
||||||
version: 1.5.6
|
version: 1.6.1
|
||||||
resolution: "@snort/system-react@npm:1.5.6"
|
resolution: "@snort/system-react@npm:1.6.1"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@snort/shared": "npm:^1.0.17"
|
"@snort/shared": "npm:^1.0.17"
|
||||||
"@snort/system": "npm:^1.5.6"
|
"@snort/system": "npm:^1.6.1"
|
||||||
react: "npm:^18.2.0"
|
react: "npm:^18.2.0"
|
||||||
checksum: 10c0/b7fb8dbc87328603b202dad9ce57dbff2cb8231829128b48346ba10925ee9fa5103652941b6a0456d6221fce8379a7148dc6507ebe8e1d0d35f7efd92738d08a
|
checksum: 10c0/fe3180c1f4341df4fd585c4031ef632a7d59c7637c6aed16ea254a1c22a66e288516d96939c0080efd0fb73940531855a59a24fae5f82c326f6544362eb0394c
|
||||||
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.5.6":
|
"@snort/system@npm:^1.6.1":
|
||||||
version: 1.5.6
|
version: 1.6.1
|
||||||
resolution: "@snort/system@npm:1.5.6"
|
resolution: "@snort/system@npm:1.6.1"
|
||||||
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/38fee2d55240f91a5e6ea0684a4bd94e6f4c56fab9b6a20d9ebef26dcdd17ed6c9a42bf1804c7bcf135a89c6882659baecede6d885dcead4a167d5e3337c9764
|
checksum: 10c0/5da01450970f4a51df98369c4cdd2b8886add3480e7962d11ed17498cf7db421bcd5e585abe532ff754efc0bf26b5734b9b24005f958802357ec390ccb74d281
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@snort/wallet@npm:^0.2.1":
|
"@snort/wallet@npm:^0.2.4":
|
||||||
version: 0.2.1
|
version: 0.2.4
|
||||||
resolution: "@snort/wallet@npm:0.2.1"
|
resolution: "@snort/wallet@npm:0.2.4"
|
||||||
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.5.6"
|
"@snort/system": "npm:^1.6.1"
|
||||||
debug: "npm:^4.3.4"
|
debug: "npm:^4.3.4"
|
||||||
eventemitter3: "npm:^5.0.1"
|
eventemitter3: "npm:^5.0.1"
|
||||||
checksum: 10c0/0dcf4b0336029e336bd6abcd7b79cf60d6fed08b2ab2847a8e791bb2399646e86c95aefc4dcfea08365c3dea417c362cdfa93939d7c36f037762de601936b331
|
checksum: 10c0/7d3e23d1d79595ee99e041b816cec5e7bfc6f34bfce6f0864a556de82cfcf29200f6978744b0717b5f6873c5c498e2dd9005571171f1f533f235cfbadf205a44
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@snort/worker-relay@npm:^1.3.0":
|
"@snort/worker-relay@npm:^1.3.1":
|
||||||
version: 1.3.0
|
version: 1.3.1
|
||||||
resolution: "@snort/worker-relay@npm:1.3.0"
|
resolution: "@snort/worker-relay@npm:1.3.1"
|
||||||
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/1a0eb175f50787bbcaa585641bf710347b59f3d3426cbf0f83182a5574bf7a63beb3e5d66bb41506e2d50c3ee904d55670c85c7f1542018936dd5a4ce06726e8
|
checksum: 10c0/19b89e4f96df425d2d73e87fda1f82844bf7f3a1ba114073d0bf4052c9d5fe3eac9e6ca6d88ad8e36b65bae6dfcf69db5cb47828ef1c195419a94bd87ae2ff53
|
||||||
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.5.6"
|
"@snort/system": "npm:^1.6.1"
|
||||||
"@snort/system-react": "npm:^1.5.6"
|
"@snort/system-react": "npm:^1.6.1"
|
||||||
"@snort/system-wasm": "npm:^1.0.5"
|
"@snort/system-wasm": "npm:^1.0.5"
|
||||||
"@snort/wallet": "npm:^0.2.1"
|
"@snort/wallet": "npm:^0.2.4"
|
||||||
"@snort/worker-relay": "npm:^1.3.0"
|
"@snort/worker-relay": "npm:^1.3.1"
|
||||||
"@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"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user