refactor: refresh

This commit is contained in:
Kieran 2024-02-27 17:51:31 +00:00
parent 364d2c272f
commit f93a398039
75 changed files with 1434 additions and 2476 deletions

View File

@ -5,18 +5,12 @@
"@emoji-mart/data": "^1.1.2",
"@emoji-mart/react": "^1.1.1",
"@noble/curves": "^1.2.0",
"@radix-ui/react-collapsible": "^1.0.3",
"@radix-ui/react-dialog": "^1.0.4",
"@radix-ui/react-progress": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3",
"@react-hook/resize-observer": "^1.2.6",
"@scure/base": "^1.1.3",
"@snort/shared": "^1.0.12",
"@snort/system": "^1.2.1",
"@snort/system-react": "^1.2.1",
"@snort/shared": "^1.0.14",
"@snort/system": "^1.2.12",
"@snort/system-react": "^1.2.12",
"@snort/system-wasm": "^1.0.2",
"@snort/system-web": "^1.0.4",
"@snort/system-web": "^1.2.11",
"@szhsin/react-menu": "^4.0.2",
"@types/webscopeio__react-textarea-autocomplete": "^4.7.2",
"@void-cat/api": "^1.0.7",

View File

@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="33" height="23" viewBox="0 0 33 23" fill="none">
<path d="M32.7877 1.72093C32.3558 0.67677 31.3439 0 30.216 0H10.6802C10.6738 0 10.6673 0 10.6609 0C10.6545 0 10.648 0 10.6416 0C6.54235 0 3.21012 3.33229 3.21012 7.42513C3.21012 9.5779 4.13825 11.5824 5.70446 12.9746L0.812466 17.8667C0.0132476 18.666 -0.225229 19.8584 0.206607 20.8961C0.638443 21.9402 1.65036 22.617 2.77829 22.617H22.314C22.3205 22.617 22.3269 22.617 22.3398 22.617C22.3463 22.617 22.3527 22.617 22.3656 22.617C26.4584 22.617 29.7906 19.2847 29.7906 15.1919C29.7906 13.0391 28.8625 11.0346 27.2963 9.64236L32.1882 4.75028C32.981 3.95105 33.2195 2.75864 32.7877 1.72093ZM2.71383 19.8584C2.6945 19.8132 2.70739 19.8004 2.73317 19.7746L8.10856 14.3991L22.3914 19.4523C22.4043 19.4587 22.4236 19.4652 22.4365 19.4652C22.4558 19.4716 22.4752 19.4781 22.4945 19.491C22.5267 19.5103 22.5525 19.5425 22.5718 19.5812C22.5783 19.5877 22.5783 19.5941 22.5847 19.6005C22.5912 19.6134 22.5912 19.6263 22.5912 19.6392C22.5912 19.6521 22.5976 19.665 22.5976 19.6779C22.5976 19.7939 22.4687 19.897 22.3269 19.897H2.78473C2.75251 19.9099 2.73317 19.9099 2.71383 19.8584ZM25.208 18.956C25.208 18.9496 25.2015 18.9367 25.2015 18.9302C25.1757 18.8271 25.1435 18.7304 25.1048 18.6337C25.0984 18.6208 25.0984 18.6079 25.092 18.5951C25.0533 18.4919 25.0017 18.3952 24.9502 18.2986C24.9437 18.2857 24.9308 18.2663 24.9244 18.2535C24.8148 18.0601 24.6859 17.8796 24.5377 17.712C24.5248 17.6991 24.5119 17.6863 24.499 17.6734C24.4216 17.596 24.3443 17.5187 24.2605 17.4478C24.2476 17.4413 24.2347 17.4284 24.2283 17.422C24.1509 17.3575 24.0672 17.2995 23.9769 17.2415C23.964 17.2351 23.9511 17.2222 23.9382 17.2157C23.848 17.1577 23.7513 17.1062 23.6547 17.0546C23.6353 17.0417 23.6095 17.0353 23.5902 17.0224C23.4935 16.9773 23.3904 16.9321 23.2808 16.8999L16.6744 14.5602L9.03668 11.8596L9.0109 11.8531C9.00446 11.8531 9.00446 11.8531 8.99801 11.8467C8.8111 11.7758 8.62418 11.692 8.43727 11.5953C6.88395 10.7768 5.9236 9.17829 5.9236 7.42513C5.9236 5.89112 6.65836 4.52469 7.79918 3.661C7.79918 3.66745 7.80563 3.68034 7.80563 3.68678C7.83141 3.78347 7.86364 3.88015 7.90231 3.97683C7.9152 4.00906 7.92809 4.04128 7.94098 4.07351C7.97321 4.14441 8.00543 4.20886 8.03766 4.27976C8.05055 4.31199 8.06989 4.34422 8.08278 4.37C8.1279 4.45379 8.17946 4.53114 8.23747 4.60848C8.26325 4.64071 8.28258 4.67293 8.30836 4.70516C8.35348 4.76317 8.3986 4.82118 8.45016 4.87274C8.4695 4.89852 8.48883 4.92431 8.51462 4.94364C8.57907 5.01454 8.65641 5.079 8.72731 5.14345C8.75309 5.16923 8.77887 5.18857 8.8111 5.20791C8.882 5.26591 8.9529 5.31748 9.03024 5.36904C9.04313 5.38193 9.05602 5.38838 9.07536 5.40127C9.16559 5.45928 9.26227 5.51084 9.35895 5.55596C9.38473 5.56885 9.41051 5.58174 9.43629 5.59463C9.53942 5.63975 9.64254 5.68487 9.74567 5.71709L24.0091 10.7639C24.0156 10.7639 24.022 10.7703 24.022 10.7703C24.209 10.8412 24.3894 10.9186 24.5634 11.0152C26.1168 11.8338 27.0771 13.4323 27.0771 15.1854C27.0836 16.7259 26.3423 18.0923 25.208 18.956ZM30.2675 2.83599L24.8922 8.21147C24.8793 8.20503 24.8599 8.19858 24.847 8.19214L10.6222 3.1647C10.6029 3.15826 10.5836 3.15181 10.5707 3.14537H10.5642C10.5449 3.13892 10.532 3.13248 10.5127 3.11959C10.5062 3.11314 10.4998 3.1067 10.4933 3.10025C10.4804 3.09381 10.474 3.08091 10.4675 3.07447C10.4611 3.06802 10.4547 3.05513 10.4547 3.04869C10.4482 3.0358 10.4418 3.02291 10.4353 3.01001C10.4353 3.00357 10.4289 2.99068 10.4289 2.97779C10.4289 2.9649 10.4224 2.94556 10.4224 2.92622C10.4224 2.91333 10.4289 2.90689 10.4289 2.894C10.4482 2.79732 10.5642 2.71352 10.6867 2.71352H30.2289C30.2611 2.71352 30.2804 2.71352 30.2998 2.75864C30.3062 2.79732 30.2933 2.81665 30.2675 2.83599Z" fill="black"/>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 33 23" fill="none">
<path d="M32.7877 1.72093C32.3558 0.67677 31.3439 0 30.216 0H10.6802C10.6738 0 10.6673 0 10.6609 0C10.6545 0 10.648 0 10.6416 0C6.54235 0 3.21012 3.33229 3.21012 7.42513C3.21012 9.5779 4.13825 11.5824 5.70446 12.9746L0.812466 17.8667C0.0132476 18.666 -0.225229 19.8584 0.206607 20.8961C0.638443 21.9402 1.65036 22.617 2.77829 22.617H22.314C22.3205 22.617 22.3269 22.617 22.3398 22.617C22.3463 22.617 22.3527 22.617 22.3656 22.617C26.4584 22.617 29.7906 19.2847 29.7906 15.1919C29.7906 13.0391 28.8625 11.0346 27.2963 9.64236L32.1882 4.75028C32.981 3.95105 33.2195 2.75864 32.7877 1.72093ZM2.71383 19.8584C2.6945 19.8132 2.70739 19.8004 2.73317 19.7746L8.10856 14.3991L22.3914 19.4523C22.4043 19.4587 22.4236 19.4652 22.4365 19.4652C22.4558 19.4716 22.4752 19.4781 22.4945 19.491C22.5267 19.5103 22.5525 19.5425 22.5718 19.5812C22.5783 19.5877 22.5783 19.5941 22.5847 19.6005C22.5912 19.6134 22.5912 19.6263 22.5912 19.6392C22.5912 19.6521 22.5976 19.665 22.5976 19.6779C22.5976 19.7939 22.4687 19.897 22.3269 19.897H2.78473C2.75251 19.9099 2.73317 19.9099 2.71383 19.8584ZM25.208 18.956C25.208 18.9496 25.2015 18.9367 25.2015 18.9302C25.1757 18.8271 25.1435 18.7304 25.1048 18.6337C25.0984 18.6208 25.0984 18.6079 25.092 18.5951C25.0533 18.4919 25.0017 18.3952 24.9502 18.2986C24.9437 18.2857 24.9308 18.2663 24.9244 18.2535C24.8148 18.0601 24.6859 17.8796 24.5377 17.712C24.5248 17.6991 24.5119 17.6863 24.499 17.6734C24.4216 17.596 24.3443 17.5187 24.2605 17.4478C24.2476 17.4413 24.2347 17.4284 24.2283 17.422C24.1509 17.3575 24.0672 17.2995 23.9769 17.2415C23.964 17.2351 23.9511 17.2222 23.9382 17.2157C23.848 17.1577 23.7513 17.1062 23.6547 17.0546C23.6353 17.0417 23.6095 17.0353 23.5902 17.0224C23.4935 16.9773 23.3904 16.9321 23.2808 16.8999L16.6744 14.5602L9.03668 11.8596L9.0109 11.8531C9.00446 11.8531 9.00446 11.8531 8.99801 11.8467C8.8111 11.7758 8.62418 11.692 8.43727 11.5953C6.88395 10.7768 5.9236 9.17829 5.9236 7.42513C5.9236 5.89112 6.65836 4.52469 7.79918 3.661C7.79918 3.66745 7.80563 3.68034 7.80563 3.68678C7.83141 3.78347 7.86364 3.88015 7.90231 3.97683C7.9152 4.00906 7.92809 4.04128 7.94098 4.07351C7.97321 4.14441 8.00543 4.20886 8.03766 4.27976C8.05055 4.31199 8.06989 4.34422 8.08278 4.37C8.1279 4.45379 8.17946 4.53114 8.23747 4.60848C8.26325 4.64071 8.28258 4.67293 8.30836 4.70516C8.35348 4.76317 8.3986 4.82118 8.45016 4.87274C8.4695 4.89852 8.48883 4.92431 8.51462 4.94364C8.57907 5.01454 8.65641 5.079 8.72731 5.14345C8.75309 5.16923 8.77887 5.18857 8.8111 5.20791C8.882 5.26591 8.9529 5.31748 9.03024 5.36904C9.04313 5.38193 9.05602 5.38838 9.07536 5.40127C9.16559 5.45928 9.26227 5.51084 9.35895 5.55596C9.38473 5.56885 9.41051 5.58174 9.43629 5.59463C9.53942 5.63975 9.64254 5.68487 9.74567 5.71709L24.0091 10.7639C24.0156 10.7639 24.022 10.7703 24.022 10.7703C24.209 10.8412 24.3894 10.9186 24.5634 11.0152C26.1168 11.8338 27.0771 13.4323 27.0771 15.1854C27.0836 16.7259 26.3423 18.0923 25.208 18.956ZM30.2675 2.83599L24.8922 8.21147C24.8793 8.20503 24.8599 8.19858 24.847 8.19214L10.6222 3.1647C10.6029 3.15826 10.5836 3.15181 10.5707 3.14537H10.5642C10.5449 3.13892 10.532 3.13248 10.5127 3.11959C10.5062 3.11314 10.4998 3.1067 10.4933 3.10025C10.4804 3.09381 10.474 3.08091 10.4675 3.07447C10.4611 3.06802 10.4547 3.05513 10.4547 3.04869C10.4482 3.0358 10.4418 3.02291 10.4353 3.01001C10.4353 3.00357 10.4289 2.99068 10.4289 2.97779C10.4289 2.9649 10.4224 2.94556 10.4224 2.92622C10.4224 2.91333 10.4289 2.90689 10.4289 2.894C10.4482 2.79732 10.5642 2.71352 10.6867 2.71352H30.2289C30.2611 2.71352 30.2804 2.71352 30.2998 2.75864C30.3062 2.79732 30.2933 2.81665 30.2675 2.83599Z" fill="currentColor"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@ -21,13 +21,12 @@ export enum StreamState {
}
export const defaultRelays = {
//"ws://localhost:7777": { read: true, write: true },
"ws://localhost:8081": { read: true, write: true },
"wss://relay.snort.social": { read: true, write: true },
"wss://nos.lol": { read: true, write: true },
"wss://relay.damus.io": { read: true, write: true },
"wss://nostr.wine": { read: true, write: true },
};
export const DefaultProviderUrl = "https://api.zap.stream/api/nostr/";
//export const DefaultProviderUrl = "http://localhost:5295/api/nostr/";
//export const DefaultProviderUrl = "http://localhost:5295/api/nostr/";

View File

@ -14,7 +14,7 @@ interface EventProps {
link: NostrLink;
}
export function EventIcon({ kind }: { kind: EventKind }) {
export function EventIcon({ kind }: { kind?: EventKind }) {
if (kind === GOAL) {
return <Icon name="piggybank" />;
}

View File

@ -1,9 +1,8 @@
import "./async-button.css";
import { forwardRef, useState } from "react";
import Spinner from "./spinner";
import classNames from "classnames";
interface AsyncButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
export interface AsyncButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
disabled?: boolean;
onClick?: (e: React.MouseEvent) => Promise<void> | void;
children?: React.ReactNode;
@ -31,7 +30,7 @@ const AsyncButton = forwardRef<HTMLButtonElement, AsyncButtonProps>((props: Asyn
disabled={loading || props.disabled}
{...props}
onClick={handle}
className={classNames("px-3 py-2 bg-gray-2 rounded-full", props.className)}>
className={props.className}>
<span
style={{ visibility: loading ? "hidden" : "visible" }}
className="whitespace-nowrap flex gap-2 items-center justify-center">

View File

@ -1,21 +1,23 @@
import { MetadataCache } from "@snort/system";
import { HTMLProps, useState } from "react";
import classNames from "classnames";
import { getPlaceholder } from "@/utils";
import { UserMetadata } from "@snort/system";
type AvatarProps = HTMLProps<HTMLImageElement> & { size?: number; pubkey: string; user?: MetadataCache };
type AvatarProps = HTMLProps<HTMLImageElement> & { size?: number; pubkey: string; user?: UserMetadata };
export function Avatar({ pubkey, size, user, ...props }: AvatarProps) {
const [failed, setFailed] = useState(false);
const src = user?.picture && !failed ? user.picture : getPlaceholder(pubkey);
return (
<img
{...props}
className={classNames("aspect-square rounded-full bg-gray-1", props.className)}
alt={user?.name || user?.pubkey}
className={classNames("aspect-square rounded-full bg-layer-1", props.className)}
alt={user?.name}
src={src}
onError={() => setFailed(true)}
style={{
width: `${size ?? 40}px`,
minWidth: `${size ?? 40}px`,
height: `${size ?? 40}px`,
}}
/>

31
src/element/buttons.tsx Normal file
View File

@ -0,0 +1,31 @@
import { forwardRef } from "react"
import AsyncButton, { AsyncButtonProps } from "./async-button"
import { Icon } from "./icon";
import classNames from "classnames";
export const DefaultButton = forwardRef<HTMLButtonElement, AsyncButtonProps>((props: AsyncButtonProps, ref) => {
return <AsyncButton {...props} className={classNames(props.className, "px-3 py-2 font-semibold rounded-xl bg-white text-black")} ref={ref} />;
});
export const PrimaryButton = forwardRef<HTMLButtonElement, AsyncButtonProps>((props: AsyncButtonProps, ref) => {
return <AsyncButton {...props} className={classNames(props.className, "px-3 py-2 font-semibold rounded-xl bg-primary")} ref={ref} />;
});
export const Layer1Button = forwardRef<HTMLButtonElement, AsyncButtonProps>((props: AsyncButtonProps, ref) => {
return <AsyncButton {...props} className={classNames(props.className, "px-3 py-2 font-semibold rounded-xl bg-layer-1")} ref={ref} />;
});
export const Layer2Button = forwardRef<HTMLButtonElement, AsyncButtonProps>((props: AsyncButtonProps, ref) => {
return <AsyncButton {...props} className={classNames(props.className, "px-3 py-2 font-semibold rounded-xl bg-layer-2")} ref={ref} />;
});
export const Layer3Button = forwardRef<HTMLButtonElement, AsyncButtonProps>((props: AsyncButtonProps, ref) => {
return <AsyncButton {...props} className={classNames(props.className, "px-3 py-2 font-semibold rounded-xl bg-layer-3")} ref={ref} />;
});
export const WarningButton = forwardRef<HTMLButtonElement, AsyncButtonProps>((props: AsyncButtonProps, ref) => {
return <AsyncButton {...props} className={classNames(props.className, "px-3 py-2 font-semibold rounded-xl bg-warning")} ref={ref} />;
});
export const IconButton = forwardRef<HTMLButtonElement, { iconName: string, iconSize?: number } & AsyncButtonProps>(({ iconName, iconSize, ...props }: { iconName: string, iconSize?: number } & AsyncButtonProps, ref) => {
return <AsyncButton {...props} className={classNames(props.className)} ref={ref}>
<Icon name={iconName} size={iconSize} />
</AsyncButton>;
});
export const BorderButton = forwardRef<HTMLButtonElement, AsyncButtonProps>((props: AsyncButtonProps, ref) => {
return <AsyncButton {...props} className={classNames(props.className, "px-3 py-2 font-semibold rounded-xl btn-border")} ref={ref} />;
});

View File

@ -16,7 +16,8 @@ import { CollapsibleEvent } from "./collapsible";
import { useLogin } from "@/hooks/login";
import { formatSats } from "@/number";
import type { Badge, Emoji, EmojiPack } from "@/types";
import AsyncButton from "./async-button";
import { IconButton } from "./buttons";
import Pill from "./pill";
function emojifyReaction(reaction: string) {
if (reaction === "+") {
@ -149,10 +150,10 @@ export function ChatMessage({
{(hasReactions || hasZaps) && (
<div className="message-reactions">
{hasZaps && (
<div className="zap-pill">
<Pill>
<Icon name="zap-filled" className="text-zap" size={12} />
<span className="zap-pill-amount">{formatSats(totalZaps)}</span>
</div>
<span className="text-xs">{formatSats(totalZaps)}</span>
</Pill>
)}
{dedupe(filteredReactions.map(v => emojifyReaction(v.content))).map(e => {
const isCustomEmojiReaction = e.length > 1 && e.startsWith(":") && e.endsWith(":");
@ -178,15 +179,15 @@ export function ChatMessage({
style={
isTablet
? {
display: showZapDialog || isHovering ? "flex" : "none",
}
display: showZapDialog || isHovering ? "flex" : "none",
}
: {
position: "fixed",
top: topOffset ? topOffset - 12 : 0,
left: leftOffset ? leftOffset - 32 : 0,
opacity: showZapDialog || isHovering ? 1 : 0,
pointerEvents: showZapDialog || isHovering ? "auto" : "none",
}
position: "fixed",
top: topOffset ? topOffset - 12 : 0,
left: leftOffset ? leftOffset - 32 : 0,
opacity: showZapDialog || isHovering ? 1 : 0,
pointerEvents: showZapDialog || isHovering ? "auto" : "none",
}
}>
{zapTarget && (
<SendZapsDialog
@ -194,20 +195,14 @@ export function ChatMessage({
eTag={ev.id}
pubkey={ev.pubkey}
button={
<AsyncButton className="message-zap-button">
<Icon name="zap" className="message-zap-button-icon" />
</AsyncButton>
<IconButton iconName="zap" iconSize={14} className="rounded-full bg-layer-2 aspect-square" />
}
targetName={profile?.name || ev.pubkey}
/>
)}
<AsyncButton className="message-zap-button" onClick={pickEmoji}>
<Icon name="face" className="message-zap-button-icon" />
</AsyncButton>
<IconButton onClick={pickEmoji} iconName="face" iconSize={14} className="rounded-full bg-layer-2 aspect-square" />
{shouldShowMuteButton && (
<AsyncButton className="message-zap-button" onClick={muteUser}>
<Icon name="user-x" className="message-zap-button-icon" />
</AsyncButton>
<IconButton onClick={muteUser} iconName="user-x" iconSize={14} className="rounded-full bg-layer-2 aspect-square" />
)}
</div>
)}

View File

@ -1,4 +1,3 @@
import * as Dialog from "@radix-ui/react-dialog";
import { useLogin } from "@/hooks/login";
import { useContext, useEffect, useRef, useState } from "react";
import { NostrStreamProvider } from "@/providers";
@ -6,16 +5,17 @@ import { FormattedMessage } from "react-intl";
import { SnortContext } from "@snort/system-react";
import { NostrLink, TaggedNostrEvent } from "@snort/system";
import AsyncButton from "./async-button";
import { LIVE_STREAM_CLIP, StreamState } from "@/const";
import { extractStreamInfo } from "@/utils";
import { Icon } from "./icon";
import { unwrap } from "@snort/shared";
import { TimelineBar } from "./timeline";
import { DefaultButton } from "./buttons";
import Modal from "./modal";
export function ClipButton({ ev }: { ev: TaggedNostrEvent }) {
const system = useContext(SnortContext);
const { id, service, status } = extractStreamInfo(ev);
const { id, service, status, host } = extractStreamInfo(ev);
const ref = useRef<HTMLVideoElement | null>(null);
const login = useLogin();
const [open, setOpen] = useState(false);
@ -68,6 +68,7 @@ export function ClipButton({ ev }: { ev: TaggedNostrEvent }) {
return eb
.kind(LIVE_STREAM_CLIP)
.tag(unwrap(NostrLink.fromEvent(ev).toEventTag("root")))
.tag(["p", host ?? ev.pubkey])
.tag(["r", newClip.url])
.tag(["title", title])
.tag(["alt", `Live stream clip created on https://zap.stream\n${newClip.url}`]);
@ -79,48 +80,37 @@ export function ClipButton({ ev }: { ev: TaggedNostrEvent }) {
return (
<>
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger asChild>
<div className="contents">
<AsyncButton onClick={makeClip} className="btn btn-primary">
<Icon name="clapperboard" />
<span className="max-lg:hidden">
<FormattedMessage defaultMessage="Create Clip" id="PA0ej4" />
</span>
</AsyncButton>
<DefaultButton onClick={makeClip}>
<Icon name="clapperboard" />
<span className="max-lg:hidden">
<FormattedMessage defaultMessage="Create Clip" id="PA0ej4" />
</span>
</DefaultButton>
{open && <Modal id="create-clip" onClose={() => setOpen(false)}>
<div className="flex flex-col">
<h1>
<FormattedMessage defaultMessage="Create Clip" id="PA0ej4" />
</h1>
{id && tempClipId && <video ref={ref} src={provider.getTempClipUrl(id, tempClipId)} controls muted />}
<TimelineBar
length={length}
offset={start}
width={300}
height={60}
setOffset={setStart}
setLength={setLength}
/>
<div className="flex flex-col gap-1">
<small>
<FormattedMessage defaultMessage="Clip title" id="YwzT/0" />
</small>
<input type="text" value={title} onChange={e => setTitle(e.target.value)} placeholder="Epic combo!" />
</div>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="dialog-overlay" />
<Dialog.Content className="dialog-content">
<div className="content-inner">
<h1>
<FormattedMessage defaultMessage="Create Clip" id="PA0ej4" />
</h1>
{id && tempClipId && <video ref={ref} src={provider.getTempClipUrl(id, tempClipId)} controls muted />}
<TimelineBar
length={length}
offset={start}
width={300}
height={60}
setOffset={setStart}
setLength={setLength}
/>
<div className="flex flex-col gap-1">
<small>
<FormattedMessage defaultMessage="Clip title" id="YwzT/0" />
</small>
<div className="paper">
<input type="text" value={title} onChange={e => setTitle(e.target.value)} placeholder="Epic combo!" />
</div>
</div>
<AsyncButton onClick={saveClip}>
<FormattedMessage defaultMessage="Publish Clip" id="jJLRgo" />
</AsyncButton>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
<DefaultButton onClick={saveClip}>
<FormattedMessage defaultMessage="Publish Clip" id="jJLRgo" />
</DefaultButton>
</div>
</Modal>}
</>
);
}

View File

@ -1,18 +1,14 @@
import "./collapsible.css";
import type { ReactNode } from "react";
import { useState } from "react";
import { FormattedMessage } from "react-intl";
import * as Dialog from "@radix-ui/react-dialog";
import * as Collapsible from "@radix-ui/react-collapsible";
import type { NostrLink } from "@snort/system";
import { Mention } from "./mention";
import { EventIcon, NostrEvent } from "./Event";
import { ExternalLink } from "./external-link";
import AsyncButton from "./async-button";
import { useEventFeed } from "@snort/system-react";
import Modal from "./modal";
import { DefaultButton } from "./buttons";
interface MediaURLProps {
url: URL;
@ -20,25 +16,14 @@ interface MediaURLProps {
}
export function MediaURL({ url, children }: MediaURLProps) {
const preview = <span className="url-preview">{url.toString()}</span>;
return (
<Dialog.Root>
<Dialog.Trigger asChild>{preview}</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="dialog-overlay" />
<Dialog.Content className="dialog-content">
<div className="collapsible-media">
<ExternalLink href={url.toString()}>{url.toString()}</ExternalLink>
{children}
</div>
<Dialog.Close asChild>
<AsyncButton className="btn delete-button" aria-label="Close">
<FormattedMessage defaultMessage="Close" id="rbrahO" />
</AsyncButton>
</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
const [open, setOpen] = useState(false);
return (<>
<span onClick={() => setOpen(true)}>{url.toString()}</span>
{open && <Modal id="media-preview" onClose={() => setOpen(false)}>
<ExternalLink href={url.toString()}>{url.toString()}</ExternalLink>
{children}
</Modal>}
</>
);
}
@ -48,23 +33,19 @@ export function CollapsibleEvent({ link }: { link: NostrLink }) {
const author = event?.pubkey || link.author;
return (
<Collapsible.Root className="collapsible" open={open} onOpenChange={setOpen}>
<div className="collapsed-event">
<div className="collapsed-event-header">
{event && <EventIcon kind={event.kind} />}
{author && <Mention pubkey={author} />}
<>
<div className="flex justify-between">
<div className="flex gap-2">
<EventIcon kind={event?.kind} />
<FormattedMessage defaultMessage="Note by {name}" id="ALdW69" values={{
name: <Mention pubkey={author ?? ""} />
}} />
</div>
<Collapsible.Trigger asChild>
<AsyncButton className={`${open ? "btn btn-small delete-button" : "btn btn-small"}`}>
{open ? (
<FormattedMessage defaultMessage="Hide" id="VA/Z1S" />
) : (
<FormattedMessage defaultMessage="Show" id="K7AkdL" />
)}
</AsyncButton>
</Collapsible.Trigger>
<DefaultButton onClick={() => setOpen(s => !s)}>
{open ? <FormattedMessage defaultMessage="Hide" id="VA/Z1S" /> : <FormattedMessage defaultMessage="Show" id="K7AkdL" />}
</DefaultButton>
</div>
<Collapsible.Content>{open && event && <NostrEvent ev={event} />}</Collapsible.Content>
</Collapsible.Root>
{open && event && <NostrEvent ev={event} />}
</>
);
}

View File

@ -1,7 +1,7 @@
import { useState } from "react";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import AsyncButton from "./async-button";
import { Layer1Button, WarningButton } from "./buttons";
export function isContentWarningAccepted() {
return Boolean(window.localStorage.getItem("accepted-content-warning"));
@ -26,12 +26,12 @@ export function ContentWarningOverlay() {
<FormattedMessage defaultMessage="Confirm your age" id="s7V+5p" />
</h2>
<div className="flex gap-3">
<AsyncButton className="btn btn-warning" onClick={grownUp}>
<WarningButton onClick={grownUp}>
<FormattedMessage defaultMessage="Yes, I am over 18" id="O2Cy6m" />
</AsyncButton>
<AsyncButton className="btn" onClick={() => navigate("/")}>
</WarningButton>
<Layer1Button onClick={() => navigate("/")}>
<FormattedMessage defaultMessage="No, I am under 18" id="KkIL3s" />
</AsyncButton>
</Layer1Button>
</div>
</div>
);

View File

@ -6,11 +6,11 @@ import { SnortContext } from "@snort/system-react";
import { useLogin } from "@/hooks/login";
import { toEmojiPack } from "@/hooks/emoji";
import AsyncButton from "./async-button";
import { findTag } from "@/utils";
import { USER_EMOJIS } from "@/const";
import { Login } from "@/index";
import type { EmojiPack as EmojiPackType } from "@/types";
import { DefaultButton, WarningButton } from "./buttons";
export function EmojiPack({ ev }: { ev: NostrEvent }) {
const system = useContext(SnortContext);
@ -45,16 +45,13 @@ export function EmojiPack({ ev }: { ev: NostrEvent }) {
<div className="outline emoji-pack">
<div className="emoji-pack-title">
<h4>{name}</h4>
{login?.pubkey && (
<AsyncButton
className={`btn btn-small btn-primary ${isUsed ? "delete-button" : ""}`}
onClick={toggleEmojiPack}>
{isUsed ? (
<FormattedMessage defaultMessage="Remove" id="G/yZLu" />
) : (
<FormattedMessage defaultMessage="Add" id="2/2yg+" />
)}
</AsyncButton>
{login?.pubkey && (isUsed ?
<WarningButton onClick={toggleEmojiPack}>
<FormattedMessage defaultMessage="Remove" id="G/yZLu" />
</WarningButton> :
<DefaultButton onClick={toggleEmojiPack}>
<FormattedMessage defaultMessage="Add" id="2/2yg+" />
</DefaultButton>
)}
</div>
<div className="emoji-pack-emojis">

View File

@ -8,7 +8,7 @@ interface ExternalLinkProps {
export function ExternalLink({ children, href }: ExternalLinkProps) {
return (
<a href={href} rel="noopener noreferrer" target="_blank">
<a href={href} rel="noopener noreferrer" target="_blank" className="text-primary">
{children}
</a>
);

View File

@ -31,7 +31,3 @@
align-items: flex-start;
gap: 12px;
}
.file-uploader-preview .clear-button {
color: var(--text-danger);
}

View File

@ -3,7 +3,7 @@ import type { ChangeEvent } from "react";
import { VoidApi } from "@void-cat/api";
import { useState } from "react";
import { FormattedMessage } from "react-intl";
import AsyncButton from "./async-button";
import { DefaultButton } from "./buttons";
const voidCatHost = "https://void.cat";
const fileExtensionRegex = /\.([\w]{1,7})$/i;
@ -87,9 +87,9 @@ export function FileUploader({ defaultImage, onClear, onFileUpload }: FileUpload
</label>
<div className="file-uploader-preview">
{img?.length > 0 && (
<AsyncButton className="btn btn-primary clear-button" onClick={clearImage}>
<DefaultButton onClick={clearImage}>
<FormattedMessage defaultMessage="Clear" id="/GCoTA" />
</AsyncButton>
</DefaultButton>
)}
{img && <img className="image-preview" src={img} />}
</div>

View File

@ -4,8 +4,8 @@ import { useContext } from "react";
import { SnortContext } from "@snort/system-react";
import { useLogin } from "@/hooks/login";
import AsyncButton from "./async-button";
import { Login } from "@/index";
import { DefaultButton } from "./buttons";
export function LoggedInFollowButton({
tag,
@ -60,17 +60,15 @@ export function LoggedInFollowButton({
if (isFollowing && hideWhenFollowing) return;
return (
<AsyncButton
<DefaultButton
disabled={timestamp ? timestamp === 0 : true}
type="button"
className="btn btn-primary"
onClick={isFollowing ? unfollow : follow}>
{isFollowing ? (
<FormattedMessage defaultMessage="Unfollow" id="izWS4J" />
) : (
<FormattedMessage defaultMessage="Follow" id="ieGrWo" />
)}
</AsyncButton>
</DefaultButton>
);
}

View File

@ -1,6 +1,4 @@
import "./goal.css";
import { useMemo } from "react";
import * as Progress from "@radix-ui/react-progress";
import Confetti from "react-confetti";
import { FormattedMessage } from "react-intl";
@ -14,8 +12,9 @@ import { SendZapsDialog } from "./send-zap";
import { getName } from "./profile";
import { Icon } from "./icon";
import { useZaps } from "@/hooks/zaps";
import classNames from "classnames";
export function Goal({ ev }: { ev: NostrEvent }) {
export function Goal({ ev, confetti }: { ev: NostrEvent, confetti?: boolean }) {
const profile = useUserProfile(ev.pubkey);
const zapTarget = profile?.lud16 ?? profile?.lud06;
const link = NostrLink.fromEvent(ev);
@ -40,23 +39,26 @@ export function Goal({ ev }: { ev: NostrEvent }) {
const previousValue = usePreviousValue(isFinished);
const goalContent = (
<div className="goal" style={{ cursor: zapTarget ? "pointer" : "auto" }}>
<div className="flex flex-col gap-2 cursor-pointer">
{ev.content.length > 0 && <p>{ev.content}</p>}
<div className={`progress-container ${isFinished ? "finished" : ""}`}>
<Progress.Root className="progress-root" value={progress}>
<Progress.Indicator className="progress-indicator" style={{ transform: `translateX(-${100 - progress}%)` }}>
{!isFinished && <span className="amount so-far">{formatSats(soFar)}</span>}
</Progress.Indicator>
<span className="amount target">
<FormattedMessage defaultMessage="Goal: {amount}" id="QceMQZ" values={{ amount: formatSats(goalAmount) }} />
</span>
</Progress.Root>
<div className="zap-circle">
<Icon name="zap-filled" className={isFinished ? "goal-finished" : "goal-unfinished"} />
<div className="relative h-10">
<div className="absolute bg-layer-2 h-3 rounded-full my-4 w-full"></div>
<div className="absolute bg-zap h-3 rounded-full text-xs font-medium my-4 leading-3 pl-2" style={{
width: `${progress}%`
}}>
{soFar > 0 ? formatSats(soFar) : ""}
</div>
<div className="absolute text-right text-xs right-10 font-medium my-4 leading-3">
<FormattedMessage defaultMessage="Goal: {amount}" id="QceMQZ" values={{ amount: formatSats(goalAmount) }} />
</div>
<div className={classNames("absolute right-0 rounded-full p-2 my-1",
{ "bg-zap": isFinished, "bg-layer-2": !isFinished })}>
<Icon name="zap-filled" />
</div>
</div>
{isFinished && previousValue === false && <Confetti numberOfPieces={2100} recycle={false} />}
</div>
{isFinished && previousValue === false && (confetti ?? true) &&
<Confetti numberOfPieces={2100} recycle={false} />}
</div >
);
return zapTarget ? (

View File

@ -48,7 +48,7 @@
margin-top: auto;
padding-top: 8px;
border-top: 1px solid var(--border, #171717);
border-top: 1px solid var(--border);
}
.live-chat > .write-message > div:nth-child(1) {
@ -96,7 +96,7 @@
display: flex;
flex-direction: column;
gap: var(--gap-s);
border-bottom: 1px solid var(--border, #171717);
border-bottom: 1px solid var(--border);
padding-bottom: var(--gap-s);
}
@ -269,10 +269,6 @@
gap: 8px;
}
.write-message-container .paper {
flex: 1;
}
.write-emoji-button {
color: #ffffff80;
cursor: pointer;

View File

@ -131,7 +131,7 @@ export function LiveChat({
case -2: {
return (
<b
className="border px-3 py-2 text-center border-gray-2 rounded-xl bg-primary uppercase"
className="border px-3 py-2 text-center border-layer-2 rounded-xl bg-primary uppercase"
key={`${a.kind}-${a.created_at}`}>
{a.kind === -1 ? (
<FormattedMessage defaultMessage="Stream Started" id="5tM0VD" />

View File

@ -0,0 +1,24 @@
import { StreamState } from "@/const";
import { extractStreamInfo } from "@/utils";
import { TaggedNostrEvent } from "@snort/system";
import { Suspense } from "react";
import LiveVideoPlayer from "./live-video-player";
export default function LiveEvent({ ev }: { ev: TaggedNostrEvent }) {
const {
title,
image,
status,
stream,
recording,
} = extractStreamInfo(ev);
return <Suspense>
<LiveVideoPlayer
title={title}
stream={status === StreamState.Live ? stream : recording}
poster={image}
status={status}
/>
</Suspense>
}

View File

@ -19,7 +19,6 @@ import { LNURL, bech32ToHex, getPublicKey, hexToBech32 } from "@snort/shared";
import { VoidApi } from "@void-cat/api";
import { SnortContext } from "@snort/system-react";
import AsyncButton from "./async-button";
import { Login } from "@/index";
import { Icon } from "./icon";
import Copy from "./copy";
@ -27,6 +26,7 @@ import { openFile } from "@/utils";
import { LoginType } from "@/login";
import { DefaultProvider, StreamProviderInfo } from "@/providers";
import { NostrStreamProvider } from "@/providers/zsz";
import { DefaultButton, Layer1Button } from "./buttons";
enum Stage {
Login = 0,
@ -81,7 +81,7 @@ export function LoginSignup({ close }: { close: () => void }) {
function createAccount() {
const newKey = bytesToHex(schnorr.utils.randomPrivateKey());
setNewKey(newKey);
setLnAddress(`${getPublicKey(newKey)}@zap.stream`);
setLnAddress(`${getPublicKey(newKey)}@${window.location.host}`);
setStage(Stage.Details);
}
@ -163,9 +163,9 @@ export function LoginSignup({ close }: { close: () => void }) {
<h3>
<FormattedMessage defaultMessage="No emails, just awesomeness!" id="+AcVD+" />
</h3>
<AsyncButton className="btn btn-primary btn-block" onClick={createAccount}>
<DefaultButton onClick={createAccount}>
<FormattedMessage defaultMessage="Create Account" id="5JcXdV" />
</AsyncButton>
</DefaultButton>
<div className="or-divider">
<hr />
@ -174,14 +174,14 @@ export function LoginSignup({ close }: { close: () => void }) {
</div>
{hasNostrExtension && (
<>
<AsyncButton className="btn btn-primary btn-block" onClick={loginNip7}>
<DefaultButton onClick={loginNip7}>
<FormattedMessage defaultMessage="Nostr Extension" id="ebmhes" />
</AsyncButton>
</DefaultButton>
</>
)}
<AsyncButton className="btn btn-primary btn-block" onClick={() => setStage(Stage.LoginInput)}>
<DefaultButton onClick={() => setStage(Stage.LoginInput)}>
<FormattedMessage defaultMessage="Login with Private Key (insecure)" id="feZ/kG" />
</AsyncButton>
</DefaultButton>
{error && <b className="error">{error}</b>}
</div>
</>
@ -208,28 +208,25 @@ export function LoginSignup({ close }: { close: () => void }) {
}}
/>
</p>
<div className="paper">
<input
type="text"
value={key}
onChange={e => setNewKey(e.target.value)}
placeholder={formatMessage({ defaultMessage: "eg. nsec1xyz", id: "yzKwBQ" })}
/>
</div>
<input
type="text"
value={key}
onChange={e => setNewKey(e.target.value)}
placeholder={formatMessage({ defaultMessage: "eg. nsec1xyz", id: "yzKwBQ" })}
/>
<div className="flex justify-between">
<div></div>
<div className="flex gap-1">
<AsyncButton
className="btn btn-secondary"
<Layer1Button
onClick={() => {
setNewKey("");
setStage(Stage.Login);
}}>
<FormattedMessage defaultMessage="Cancel" id="47FYwb" />
</AsyncButton>
<AsyncButton onClick={doLoginNsec} className="btn btn-primary">
</Layer1Button>
<DefaultButton onClick={doLoginNsec}>
<FormattedMessage defaultMessage="Log In" id="r2Jjms" />
</AsyncButton>
</DefaultButton>
</div>
</div>
{error && <b className="error">{error}</b>}
@ -258,21 +255,19 @@ export function LoginSignup({ close }: { close: () => void }) {
</div>
</div>
<div className="username">
<div className="paper">
<input
type="text"
placeholder="Username"
value={username}
onChange={e => setUsername(e.target.value)}
/>
</div>
<input
type="text"
placeholder="Username"
value={username}
onChange={e => setUsername(e.target.value)}
/>
<small>
<FormattedMessage defaultMessage="You can change this later" id="ZmqxZs" />
</small>
</div>
<AsyncButton type="button" className="btn btn-primary" onClick={setupProfile}>
<DefaultButton onClick={setupProfile}>
<FormattedMessage defaultMessage="Save" id="jvo0vs" />
</AsyncButton>
</DefaultButton>
</div>
</>
);
@ -303,22 +298,20 @@ export function LoginSignup({ close }: { close: () => void }) {
</p>
)}
<div className="username">
<div className="paper">
<input
type="text"
placeholder={formatMessage({ defaultMessage: "eg. name@wallet.com", id: "1qsXCO" })}
value={lnAddress}
onChange={e => setLnAddress(e.target.value)}
/>
</div>
<input
type="text"
placeholder={formatMessage({ defaultMessage: "eg. name@wallet.com", id: "1qsXCO" })}
value={lnAddress}
onChange={e => setLnAddress(e.target.value)}
/>
<small>
<FormattedMessage defaultMessage="You can always replace it with your own address later." id="FjDlus" />
</small>
</div>
{error && <b className="error">{error}</b>}
<AsyncButton type="button" className="btn btn-primary" onClick={saveProfile}>
<DefaultButton onClick={saveProfile}>
<FormattedMessage defaultMessage="Amazing! Continue.." id="tM6fNW" />
</AsyncButton>
</DefaultButton>
</div>
</>
);
@ -337,12 +330,12 @@ export function LoginSignup({ close }: { close: () => void }) {
id="H/bNs9"
/>
</p>
<div className="paper">
<div className="bg-layer-1 rounded-xl px-3 py-2">
<Copy text={hexToBech32("nsec", key)} />
</div>
<AsyncButton className="btn btn-primary" onClick={loginWithKey}>
<DefaultButton onClick={loginWithKey}>
<FormattedMessage defaultMessage="Ok, it's safe" id="My6HwN" />
</AsyncButton>
</DefaultButton>
</div>
</>
);

7
src/element/logo.tsx Normal file
View File

@ -0,0 +1,7 @@
import { HTMLProps } from "react";
export default function Logo(props: HTMLProps<SVGSVGElement>) {
return <svg {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 33 23" fill="none">
<path d="M32.7877 1.72093C32.3558 0.67677 31.3439 0 30.216 0H10.6802C10.6738 0 10.6673 0 10.6609 0C10.6545 0 10.648 0 10.6416 0C6.54235 0 3.21012 3.33229 3.21012 7.42513C3.21012 9.5779 4.13825 11.5824 5.70446 12.9746L0.812466 17.8667C0.0132476 18.666 -0.225229 19.8584 0.206607 20.8961C0.638443 21.9402 1.65036 22.617 2.77829 22.617H22.314C22.3205 22.617 22.3269 22.617 22.3398 22.617C22.3463 22.617 22.3527 22.617 22.3656 22.617C26.4584 22.617 29.7906 19.2847 29.7906 15.1919C29.7906 13.0391 28.8625 11.0346 27.2963 9.64236L32.1882 4.75028C32.981 3.95105 33.2195 2.75864 32.7877 1.72093ZM2.71383 19.8584C2.6945 19.8132 2.70739 19.8004 2.73317 19.7746L8.10856 14.3991L22.3914 19.4523C22.4043 19.4587 22.4236 19.4652 22.4365 19.4652C22.4558 19.4716 22.4752 19.4781 22.4945 19.491C22.5267 19.5103 22.5525 19.5425 22.5718 19.5812C22.5783 19.5877 22.5783 19.5941 22.5847 19.6005C22.5912 19.6134 22.5912 19.6263 22.5912 19.6392C22.5912 19.6521 22.5976 19.665 22.5976 19.6779C22.5976 19.7939 22.4687 19.897 22.3269 19.897H2.78473C2.75251 19.9099 2.73317 19.9099 2.71383 19.8584ZM25.208 18.956C25.208 18.9496 25.2015 18.9367 25.2015 18.9302C25.1757 18.8271 25.1435 18.7304 25.1048 18.6337C25.0984 18.6208 25.0984 18.6079 25.092 18.5951C25.0533 18.4919 25.0017 18.3952 24.9502 18.2986C24.9437 18.2857 24.9308 18.2663 24.9244 18.2535C24.8148 18.0601 24.6859 17.8796 24.5377 17.712C24.5248 17.6991 24.5119 17.6863 24.499 17.6734C24.4216 17.596 24.3443 17.5187 24.2605 17.4478C24.2476 17.4413 24.2347 17.4284 24.2283 17.422C24.1509 17.3575 24.0672 17.2995 23.9769 17.2415C23.964 17.2351 23.9511 17.2222 23.9382 17.2157C23.848 17.1577 23.7513 17.1062 23.6547 17.0546C23.6353 17.0417 23.6095 17.0353 23.5902 17.0224C23.4935 16.9773 23.3904 16.9321 23.2808 16.8999L16.6744 14.5602L9.03668 11.8596L9.0109 11.8531C9.00446 11.8531 9.00446 11.8531 8.99801 11.8467C8.8111 11.7758 8.62418 11.692 8.43727 11.5953C6.88395 10.7768 5.9236 9.17829 5.9236 7.42513C5.9236 5.89112 6.65836 4.52469 7.79918 3.661C7.79918 3.66745 7.80563 3.68034 7.80563 3.68678C7.83141 3.78347 7.86364 3.88015 7.90231 3.97683C7.9152 4.00906 7.92809 4.04128 7.94098 4.07351C7.97321 4.14441 8.00543 4.20886 8.03766 4.27976C8.05055 4.31199 8.06989 4.34422 8.08278 4.37C8.1279 4.45379 8.17946 4.53114 8.23747 4.60848C8.26325 4.64071 8.28258 4.67293 8.30836 4.70516C8.35348 4.76317 8.3986 4.82118 8.45016 4.87274C8.4695 4.89852 8.48883 4.92431 8.51462 4.94364C8.57907 5.01454 8.65641 5.079 8.72731 5.14345C8.75309 5.16923 8.77887 5.18857 8.8111 5.20791C8.882 5.26591 8.9529 5.31748 9.03024 5.36904C9.04313 5.38193 9.05602 5.38838 9.07536 5.40127C9.16559 5.45928 9.26227 5.51084 9.35895 5.55596C9.38473 5.56885 9.41051 5.58174 9.43629 5.59463C9.53942 5.63975 9.64254 5.68487 9.74567 5.71709L24.0091 10.7639C24.0156 10.7639 24.022 10.7703 24.022 10.7703C24.209 10.8412 24.3894 10.9186 24.5634 11.0152C26.1168 11.8338 27.0771 13.4323 27.0771 15.1854C27.0836 16.7259 26.3423 18.0923 25.208 18.956ZM30.2675 2.83599L24.8922 8.21147C24.8793 8.20503 24.8599 8.19858 24.847 8.19214L10.6222 3.1647C10.6029 3.15826 10.5836 3.15181 10.5707 3.14537H10.5642C10.5449 3.13892 10.532 3.13248 10.5127 3.11959C10.5062 3.11314 10.4998 3.1067 10.4933 3.10025C10.4804 3.09381 10.474 3.08091 10.4675 3.07447C10.4611 3.06802 10.4547 3.05513 10.4547 3.04869C10.4482 3.0358 10.4418 3.02291 10.4353 3.01001C10.4353 3.00357 10.4289 2.99068 10.4289 2.97779C10.4289 2.9649 10.4224 2.94556 10.4224 2.92622C10.4224 2.91333 10.4289 2.90689 10.4289 2.894C10.4482 2.79732 10.5642 2.71352 10.6867 2.71352H30.2289C30.2611 2.71352 30.2804 2.71352 30.2998 2.75864C30.3062 2.79732 30.2933 2.81665 30.2675 2.83599Z" fill="currentColor" />
</svg>
}

98
src/element/modal.tsx Normal file
View File

@ -0,0 +1,98 @@
import classNames from "classnames";
import React, { ReactNode, useEffect } from "react";
import { createPortal } from "react-dom";
import { IconButton } from "./buttons";
export interface ModalProps {
id: string;
className?: string;
bodyClassName?: string;
onClose?: (e: React.MouseEvent | KeyboardEvent) => void;
onClick?: (e: React.MouseEvent) => void;
children: ReactNode;
}
let scrollbarWidth: number | null = null;
const getScrollbarWidth = () => {
if (scrollbarWidth !== null) {
return scrollbarWidth;
}
const outer = document.createElement("div");
outer.style.visibility = "hidden";
outer.style.width = "100px";
document.body.appendChild(outer);
const widthNoScroll = outer.offsetWidth;
outer.style.overflow = "scroll";
const inner = document.createElement("div");
inner.style.width = "100%";
outer.appendChild(inner);
const widthWithScroll = inner.offsetWidth;
outer.parentNode?.removeChild(outer);
scrollbarWidth = widthNoScroll - widthWithScroll;
return scrollbarWidth;
};
export default function Modal(props: ModalProps) {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === "Escape" && props.onClose) {
props.onClose(e);
}
};
useEffect(() => {
document.body.classList.add("scroll-lock");
document.body.style.paddingRight = `${getScrollbarWidth()}px`;
document.addEventListener("keydown", handleKeyDown);
return () => {
document.body.classList.remove("scroll-lock");
document.body.style.paddingRight = "";
document.removeEventListener("keydown", handleKeyDown);
};
}, []);
const handleBackdropClick = (e: React.MouseEvent) => {
e.stopPropagation();
props.onClose?.(e);
};
return createPortal(
<div
className={classNames("z-[42] w-screen h-screen top-0 left-0 fixed bg-black/80 flex justify-center overflow-y-auto")}
onMouseDown={handleBackdropClick}
onClick={e => {
e.stopPropagation();
}}>
<div
className={"bg-layer-1 p-8 rounded-3xl my-auto lg:w-[500px] max-lg:w-full"}
onMouseDown={e => e.stopPropagation()}
onClick={e => {
e.stopPropagation();
props.onClick?.(e);
}}>
<div className="absolute right-4 top-4">
<IconButton
iconName="x"
onClick={(e) => {
e.stopPropagation();
props.onClose?.(e);
}}
className="rounded-full aspect-square"
iconSize={10}
/>
</div>
{props.children}
</div>
</div>,
document.body,
);
}

View File

@ -3,9 +3,9 @@ import { FormattedMessage } from "react-intl";
import { SnortContext } from "@snort/system-react";
import { useLogin } from "@/hooks/login";
import AsyncButton from "./async-button";
import { Login } from "@/index";
import { MUTED } from "@/const";
import { DefaultButton } from "./buttons";
export function useMute(pubkey: string) {
const system = useContext(SnortContext);
@ -55,13 +55,13 @@ export function LoggedInMuteButton({ pubkey }: { pubkey: string }) {
const { isMuted, mute, unmute } = useMute(pubkey);
return (
<AsyncButton onClick={() => (isMuted ? unmute() : mute())} className="font-bold">
<DefaultButton onClick={() => (isMuted ? unmute() : mute())}>
{isMuted ? (
<FormattedMessage defaultMessage="Unmute" id="W9355R" />
) : (
<FormattedMessage defaultMessage="Mute" id="x82IOl" />
)}
</AsyncButton>
</DefaultButton>
);
}

View File

@ -9,11 +9,6 @@
gap: 8px;
}
.new-goal .paper {
background: #262626;
height: 32px;
}
.new-goal .btn:disabled {
opacity: 0.3;
}

View File

@ -1,14 +1,14 @@
import "./new-goal.css";
import * as Dialog from "@radix-ui/react-dialog";
import { FormattedMessage } from "react-intl";
import { useContext, useState } from "react";
import { SnortContext } from "@snort/system-react";
import AsyncButton from "./async-button";
import { Icon } from "./icon";
import { GOAL } from "@/const";
import { useLogin } from "@/hooks/login";
import { defaultRelays } from "@/const";
import { DefaultButton } from "./buttons";
import Modal from "./modal";
export function NewGoalDialog() {
const system = useContext(SnortContext);
@ -37,64 +37,52 @@ export function NewGoalDialog() {
}
const isValid = goalName.length && Number(goalAmount) > 0;
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger asChild>
<AsyncButton className="btn btn-primary">
<span>
<Icon name="zap-filled" size={12} />
<span>
<FormattedMessage defaultMessage="Add stream goal" id="wOy57k" />
</span>
</span>
</AsyncButton>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="dialog-overlay" />
<Dialog.Content className="dialog-content">
<div className="new-goal content-inner">
<div className="zap-goals">
<Icon name="zap-filled" className="stream-zap-goals-icon" size={16} />
<h3>
<FormattedMessage defaultMessage="Stream Zap Goals" id="0GfNiL" />
</h3>
</div>
<div>
<p>
<FormattedMessage defaultMessage="Name" id="HAlOn1" />
</p>
<div className="paper">
<input
type="text"
value={goalName}
placeholder="e.g. New Laptop"
onChange={e => setGoalName(e.target.value)}
/>
</div>
</div>
<div>
<p>
<FormattedMessage defaultMessage="Amount" id="/0TOL5" />
</p>
<div className="paper">
<input
type="number"
placeholder="21"
min="1"
max="2100000000000000"
value={goalAmount}
onChange={e => setGoalAmount(e.target.value)}
/>
</div>
</div>
<div className="create-goal">
<AsyncButton type="button" className="btn btn-primary wide" disabled={!isValid} onClick={publishGoal}>
<FormattedMessage defaultMessage="Create Goal" id="X2PZ7D" />
</AsyncButton>
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
return (<>
<DefaultButton onClick={() => setOpen(true)}>
<Icon name="zap-filled" size={12} />
<span>
<FormattedMessage defaultMessage="Add stream goal" id="wOy57k" />
</span>
</DefaultButton>
{open && <Modal id="new-goal" onClose={() => setOpen(false)}>
<div className="new-goal content-inner">
<div className="zap-goals">
<Icon name="zap-filled" className="stream-zap-goals-icon" size={16} />
<h3>
<FormattedMessage defaultMessage="Stream Zap Goals" id="0GfNiL" />
</h3>
</div>
<div>
<p>
<FormattedMessage defaultMessage="Name" id="HAlOn1" />
</p>
<input
type="text"
value={goalName}
placeholder="e.g. New Laptop"
onChange={e => setGoalName(e.target.value)}
/>
</div>
<div>
<p>
<FormattedMessage defaultMessage="Amount" id="/0TOL5" />
</p>
<input
type="number"
placeholder="21"
min="1"
max="2100000000000000"
value={goalAmount}
onChange={e => setGoalAmount(e.target.value)}
/>
</div>
<div className="create-goal">
<DefaultButton disabled={!isValid} onClick={publishGoal}>
<FormattedMessage defaultMessage="Create Goal" id="X2PZ7D" />
</DefaultButton>
</div>
</div>
</Modal>}
</>
);
}

View File

@ -1,56 +0,0 @@
.new-stream {
display: flex;
flex-direction: column;
gap: 24px;
}
.new-stream h3 {
font-size: 24px;
margin: 0;
}
.new-stream p {
margin: 0 0 8px 0;
}
.new-stream small {
display: block;
margin: 8px 0 0 0;
}
.new-stream .btn.wide {
padding: 12px 16px;
border-radius: 16px;
width: 100%;
}
.new-stream div.paper {
background: #262626;
padding: 12px 16px;
}
.new-stream .btn:disabled {
opacity: 0.3;
}
.new-stream .pill {
border-radius: 16px;
background: #262626;
padding: 8px 12px;
text-align: center;
text-transform: uppercase;
}
.new-stream .pill.active {
color: inherit;
background: #353535;
}
.new-stream .tos-link {
cursor: pointer;
color: var(--primary);
}
.new-stream .tos-link:hover {
text-decoration: underline;
}

View File

@ -1,5 +1,3 @@
import "./new-stream.css";
import * as Dialog from "@radix-ui/react-dialog";
import { useContext, useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { unwrap } from "@snort/shared";
@ -12,7 +10,9 @@ import { NostrStreamProvider, StreamProvider, StreamProviders } from "@/provider
import { StreamEditor, StreamEditorProps } from "./stream-editor";
import { eventLink } from "@/utils";
import { NostrProviderDialog } from "./nostr-provider-dialog";
import AsyncButton from "./async-button";
import { DefaultButton } from "./buttons";
import Pill from "./pill";
import Modal from "./modal";
function NewStream({ ev, onFinish }: Omit<StreamEditorProps, "onFinish"> & { onFinish: () => void }) {
const system = useContext(SnortContext);
@ -53,14 +53,13 @@ function NewStream({ ev, onFinish }: Omit<StreamEditorProps, "onFinish"> & { onF
case StreamProviders.NostrType: {
return (
<>
<AsyncButton
className="btn btn-secondary"
<DefaultButton
onClick={() => {
navigate("/settings");
onFinish?.();
}}>
<FormattedMessage defaultMessage="Get Stream Key" id="Vn2WiP" />
</AsyncButton>
</DefaultButton>
<NostrProviderDialog
provider={currentProvider as NostrStreamProvider}
onFinish={onFinish}
@ -85,12 +84,14 @@ function NewStream({ ev, onFinish }: Omit<StreamEditorProps, "onFinish"> & { onF
</p>
<div className="flex gap-2">
{providers.map(v => (
<span className={`pill${v === currentProvider ? " active" : ""}`} onClick={() => setCurrentProvider(v)}>
<Pill className={`${v === currentProvider ? " text-bold" : ""}`} onClick={() => setCurrentProvider(v)}>
{v.name}
</span>
</Pill>
))}
</div>
{providerDialog()}
<div className="flex flex-col gap-4">
{providerDialog()}
</div>
</>
);
}
@ -103,30 +104,23 @@ interface NewStreamDialogProps {
export function NewStreamDialog(props: NewStreamDialogProps & StreamEditorProps) {
const [open, setOpen] = useState(false);
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger asChild>
<AsyncButton className={props.btnClassName}>
{props.text && props.text}
{!props.text && (
<>
<span className="max-xl:hidden">
<FormattedMessage defaultMessage="Stream" id="uYw2LD" />
</span>
<Icon name="signal" />
</>
)}
</AsyncButton>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="dialog-overlay" />
<Dialog.Content className="dialog-content">
<div className="content-inner">
<div className="new-stream">
<NewStream {...props} onFinish={() => setOpen(false)} />
</div>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
<>
<DefaultButton className={props.btnClassName} onClick={() => setOpen(true)}>
{props.text && props.text}
{!props.text && (
<>
<span className="max-xl:hidden">
<FormattedMessage defaultMessage="Stream" id="uYw2LD" />
</span>
<Icon name="signal" />
</>
)}
</DefaultButton>
{open && <Modal id="new-stream" onClose={() => setOpen(false)}>
<div className="new-stream">
<NewStream {...props} onFinish={() => setOpen(false)} />
</div>
</Modal>}
</>
);
}

View File

@ -7,9 +7,10 @@ import { NostrStreamProvider, StreamProviderEndpoint, StreamProviderInfo } from
import { SendZaps } from "./send-zap";
import { StreamEditor, StreamEditorProps } from "./stream-editor";
import Spinner from "./spinner";
import AsyncButton from "./async-button";
import { unwrap } from "@snort/shared";
import { useRates } from "@/hooks/rates";
import { DefaultButton } from "./buttons";
import Pill from "./pill";
export function NostrProviderDialog({
provider,
@ -35,12 +36,14 @@ export function NostrProviderDialog({
return arr.sort((a, b) => ((a.rate ?? 0) > (b.rate ?? 0) ? -1 : 1));
}
async function loadInfo() {
const info = await provider.info();
setInfo(info);
setTos(info.tosAccepted ?? true);
setEndpoint(sortEndpoints(info.endpoints)[0]);
}
useEffect(() => {
provider.info().then(v => {
setInfo(v);
setTos(v.tosAccepted ?? true);
setEndpoint(sortEndpoints(v.endpoints)[0]);
});
loadInfo();
}, [provider]);
if (!info) {
@ -80,7 +83,7 @@ export function NostrProviderDialog({
{`${(raw / 60).toFixed(0)} hour @ ${ep.rate} sats/${ep.unit}`}
&nbsp; or <br />
{`${pm.toLocaleString()} sats/month ($${(rate.ask * pm * 1e-8).toFixed(2)}/mo) streaming ${hrs} hrs/month`}
<div className="paper">
<div className="bg-layer-2 rounded-xl flex items-center px-2">
Hrs
<input type="number" value={hrs} onChange={e => setHrs(e.target.valueAsNumber)} />
</div>
@ -135,9 +138,9 @@ export function NostrProviderDialog({
</div>
</div>
<div>
<AsyncButton type="button" className="btn btn-primary wide" disabled={!tos} onClick={acceptTos}>
<DefaultButton disabled={!tos} onClick={acceptTos}>
<FormattedMessage defaultMessage="Continue" id="acrOoz" />
</AsyncButton>
</DefaultButton>
</div>
</>
);
@ -154,11 +157,11 @@ export function NostrProviderDialog({
</p>
<div className="flex gap-2">
{sortEndpoints(info.endpoints).map(a => (
<span
className={`pill bg-gray-1${ep?.name === a.name ? " active" : ""}`}
<Pill
selected={ep?.name === a.name}
onClick={() => setEndpoint(a)}>
{a.name}
</span>
</Pill>
))}
</div>
</div>
@ -167,23 +170,18 @@ export function NostrProviderDialog({
<p>
<FormattedMessage defaultMessage="Server Url" id="5kx+2v" />
</p>
<div className="paper">
<input type="text" value={ep?.url} disabled />
</div>
<input type="text" value={ep?.url} disabled />
</div>
<div>
<p>
<FormattedMessage defaultMessage="Stream Key" id="LknBsU" />
</p>
<div className="flex gap-2">
<div className="paper grow">
<input type="password" value={ep?.key} disabled />
</div>
<AsyncButton
className="btn btn-primary"
<input type="password" value={ep?.key} disabled />
<DefaultButton
onClick={() => window.navigator.clipboard.writeText(ep?.key ?? "")}>
<FormattedMessage defaultMessage="Copy" id="4l6vz1" />
</AsyncButton>
</DefaultButton>
</div>
</div>
<div>
@ -191,16 +189,16 @@ export function NostrProviderDialog({
<FormattedMessage defaultMessage="Balance" id="H5+NAX" />
</p>
<div className="flex gap-2">
<div className="paper grow">
<div className="bg-layer-2 rounded-xl w-full flex items-center px-3">
<FormattedMessage
defaultMessage="{amount} sats"
id="vrTOHJ"
values={{ amount: info.balance?.toLocaleString() }}
/>
</div>
<AsyncButton className="btn btn-primary" onClick={() => setTopup(true)}>
<DefaultButton onClick={() => setTopup(true)}>
<FormattedMessage defaultMessage="Topup" id="nBCvvJ" />
</AsyncButton>
</DefaultButton>
</div>
<small>
<FormattedMessage defaultMessage="About {estimate}" id="Q3au2v" values={{ estimate: calcEstimate() }} />
@ -212,7 +210,7 @@ export function NostrProviderDialog({
</p>
<div className="flex gap-2">
{ep?.capabilities?.map(a => (
<span className="pill bg-gray-1">{parseCapability(a)}</span>
<Pill>{parseCapability(a)}</Pill>
))}
</div>
</div>
@ -264,18 +262,18 @@ export function NostrProviderDialog({
<div className="grid grid-cols-2 gap-2">
{info.forwards?.map(a => (
<>
<div className="paper">{a.name}</div>
<AsyncButton
className="btn btn-primary"
<div className="bg-layer-2 rounded-xl px-3 flex items-center">{a.name}</div>
<DefaultButton
onClick={async () => {
await provider.removeForward(a.id);
await loadInfo();
}}>
<FormattedMessage defaultMessage="Remove" id="G/yZLu" />
</AsyncButton>
</DefaultButton>
</>
))}
</div>
<AddForwardInputs provider={provider} onAdd={() => {}} />
<AddForwardInputs provider={provider} onAdd={loadInfo} />
</div>
);
}
@ -419,40 +417,35 @@ function AddForwardInputs({
}
return (
<div className="flex flex-col p-4 gap-2 bg-gray-3 rounded-xl">
<div className="flex flex-col p-4 gap-2 bg-layer-3 rounded-xl">
<div className="flex gap-2">
<div className="paper flex-1">
<select value={svc} onChange={e => setService(e.target.value as ForwardService)} className="bg-gray-1">
<option value="twitch">Twitch</option>
<option value="youtube">Youtube</option>
<option value="facebook">Facebook Gaming</option>
<option value="kick">Kick</option>
<option value="trovo">Trovo</option>
<option value="custom">Custom</option>
</select>
</div>
<div className="paper flex-1">
<input
type="text"
placeholder={formatMessage({ defaultMessage: "Display name", id: "dOQCL8" })}
value={name}
onChange={e => setName(e.target.value)}
/>
</div>
</div>
<div className="paper">
<select value={svc} onChange={e => setService(e.target.value as ForwardService)} className="flex-1">
<option value="twitch">Twitch</option>
<option value="youtube">Youtube</option>
<option value="facebook">Facebook Gaming</option>
<option value="kick">Kick</option>
<option value="trovo">Trovo</option>
<option value="custom">Custom</option>
</select>
<input
type="text"
placeholder={
svc === ForwardService.Custom ? "rtmp://" : formatMessage({ defaultMessage: "Stream key", id: "QWlMq9" })
}
value={target}
onChange={e => setTarget(e.target.value)}
className="flex-1"
placeholder={formatMessage({ defaultMessage: "Display name", id: "dOQCL8" })}
value={name}
onChange={e => setName(e.target.value)}
/>
</div>
<AsyncButton className="btn btn-primary" onClick={doAdd}>
<input
type="text"
placeholder={
svc === ForwardService.Custom ? "rtmp://" : formatMessage({ defaultMessage: "Stream key", id: "QWlMq9" })
}
value={target}
onChange={e => setTarget(e.target.value)}
/>
<DefaultButton onClick={doAdd}>
<FormattedMessage defaultMessage="Add" id="2/2yg+" />
</AsyncButton>
</DefaultButton>
{error && <b className="warning">{error}</b>}
</div>
);

View File

@ -1,10 +1,10 @@
import AsyncButton from "./async-button";
import { useLogin } from "@/hooks/login";
import { NostrStreamProvider } from "@/providers";
import { base64 } from "@scure/base";
import { unwrap } from "@snort/shared";
import { useEffect, useState } from "react";
import { Icon } from "./icon";
import { DefaultButton } from "./buttons";
export function NotificationsButton({ host, service }: { host: string; service: string }) {
const login = useLogin();
@ -80,8 +80,8 @@ export function NotificationsButton({ host, service }: { host: string; service:
}, []);
return (
<AsyncButton onClick={subscribed ? unsubscribe : subscribe}>
<DefaultButton onClick={subscribed ? unsubscribe : subscribe}>
<Icon name={subscribed ? "bell-off" : "bell-ringing"} />
</AsyncButton>
</DefaultButton>
);
}

6
src/element/pill.tsx Normal file
View File

@ -0,0 +1,6 @@
import classNames from "classnames";
import { HTMLProps } from "react";
export default function Pill({ children, selected, className, ...props }: HTMLProps<HTMLSpanElement>) {
return <span {...props} className={classNames(className, { "bg-layer-3 font-bold": selected }, "px-2 py-1 font-semibold rounded-lg bg-layer-2 cursor-pointer text-sm")}>{children}</span>
}

View File

@ -6,9 +6,9 @@ import { Profile } from "./profile";
import { useLogin } from "@/hooks/login";
import { useContext, useState } from "react";
import { NostrLink, parseNostrLink } from "@snort/system";
import AsyncButton from "./async-button";
import { SnortContext } from "@snort/system-react";
import { LIVE_STREAM_RAID } from "@/const";
import { DefaultButton } from "./buttons";
export function DashboardRaidMenu({ link, onClose }: { link: NostrLink; onClose: () => void }) {
const system = useContext(SnortContext);
@ -41,13 +41,13 @@ export function DashboardRaidMenu({ link, onClose }: { link: NostrLink; onClose:
<FormattedMessage defaultMessage="Start Raid" id="MTHO1W" />
</h2>
<div className="flex flex-col gap-1">
<p className="text-gray-3 uppercase font-semibold text-sm">
<p className="text-layer-3 uppercase font-semibold text-sm">
<FormattedMessage defaultMessage="Live now" id="+sdKx8" />
</p>
<div className="flex gap-2 flex-wrap">
{livePubkeys.map(a => (
<div
className="border border-gray-1 rounded-full px-4 py-2 bg-gray-2 pointer"
className="border border-layer-1 rounded-full px-4 py-2 bg-layer-2 pointer"
onClick={() => {
const liveEvent = live.find(b => getHost(b) === a);
if (liveEvent) {
@ -60,7 +60,7 @@ export function DashboardRaidMenu({ link, onClose }: { link: NostrLink; onClose:
</div>
</div>
<div className="flex flex-col gap-1">
<p className="text-gray-3 uppercase font-semibold text-sm">
<p className="text-layer-3 uppercase font-semibold text-sm">
<FormattedMessage defaultMessage="Raid target" id="Zse7yG" />
</p>
<div className="paper">
@ -68,16 +68,16 @@ export function DashboardRaidMenu({ link, onClose }: { link: NostrLink; onClose:
</div>
</div>
<div className="flex flex-col gap-1">
<p className="text-gray-3 uppercase font-semibold text-sm">
<p className="text-layer-3 uppercase font-semibold text-sm">
<FormattedMessage defaultMessage="Raid Message" id="RS6smY" />
</p>
<div className="paper">
<input type="text" value={msg} onChange={e => setMsg(e.target.value)} />
</div>
</div>
<AsyncButton className="btn btn-primary" onClick={raid}>
<DefaultButton onClick={raid}>
<FormattedMessage defaultMessage="Raid!" id="aqjZxs" />
</AsyncButton>
</DefaultButton>
</div>
);
}

View File

@ -19,11 +19,6 @@
text-align: center;
}
.send-zap .pill.active {
color: inherit;
background: #353535;
}
.send-zap p {
margin: 0 0 8px 0;
font-weight: 500;

View File

@ -1,5 +1,4 @@
import "./send-zap.css";
import * as Dialog from "@radix-ui/react-dialog";
import { type ReactNode, useEffect, useState } from "react";
import { LNURL } from "@snort/shared";
import { EventPublisher, NostrEvent } from "@snort/system";
@ -9,12 +8,14 @@ import { FormattedMessage, FormattedNumber } from "react-intl";
import { formatSats } from "../number";
import { Icon } from "./icon";
import AsyncButton from "./async-button";
import QrCode from "./qr-code";
import { useLogin } from "@/hooks/login";
import Copy from "./copy";
import { defaultRelays } from "@/const";
import { useRates } from "@/hooks/rates";
import { DefaultButton } from "./buttons";
import Modal from "./modal";
import Pill from "./pill";
export interface LNURLLike {
get name(): string;
@ -110,25 +111,25 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: Se
return (
<>
<div className="flex gap-2">
<span
className={`pill${isFiat ? "" : " active"}`}
<Pill
selected={!isFiat}
onClick={() => {
setIsFiat(false);
setAmount(satsAmounts[0]);
}}>
SATS
</span>
<span
className={`pill${isFiat ? " active" : ""}`}
</Pill>
<Pill
selected={isFiat}
onClick={() => {
setIsFiat(true);
setAmount(usdAmounts[0]);
}}>
USD
</span>
</Pill>
</div>
<div>
<small>
<small className="mb-2">
<FormattedMessage
defaultMessage="Zap amount in {currency}"
id="IJDKz3"
@ -148,11 +149,11 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: Se
</>
)}
</small>
<div className="amounts">
<div className="grid grid-cols-5 gap-2 text-center">
{(isFiat ? usdAmounts : satsAmounts).map(a => (
<span key={a} className={`pill${a === amount ? " active" : ""}`} onClick={() => setAmount(a)}>
<Pill key={a} selected={a === amount} onClick={() => setAmount(a)}>
{isFiat ? `$${a.toLocaleString()}` : formatSats(a)}
</span>
</Pill>
))}
</div>
</div>
@ -167,9 +168,9 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: Se
</div>
)}
<div>
<AsyncButton onClick={send} className="btn btn-primary">
<DefaultButton onClick={send}>
<FormattedMessage defaultMessage="Zap!" id="3HwrQo" />
</AsyncButton>
</DefaultButton>
</div>
</>
);
@ -185,18 +186,18 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: Se
<div className="flex items-center">
<Copy text={invoice} />
</div>
<AsyncButton className="btn btn-primary wide" onClick={() => onFinish()}>
<DefaultButton onClick={() => onFinish()}>
<FormattedMessage defaultMessage="Back" id="cyR7Kh" />
</AsyncButton>
</DefaultButton>
</>
);
}
return (
<div className="send-zap">
<div className="flex flex-col gap-4">
<h3 className="flex gap-2 items-center">
<FormattedMessage defaultMessage="Zap {name}" id="oHPB8Q" values={{ name }} />
<Icon name="zap" />
<Icon name="zap-filled" />
</h3>
{input()}
{payInvoice()}
@ -205,29 +206,21 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: Se
}
export function SendZapsDialog(props: Omit<SendZapsProps, "onFinish">) {
const [isOpen, setIsOpen] = useState(false);
return (
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
<Dialog.Trigger asChild>
{props.button ? (
props.button
) : (
<AsyncButton className="btn btn-primary zap">
<span className="max-xl:hidden">
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
</span>
<Icon name="zap-filled" size={16} />
</AsyncButton>
)}
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="dialog-overlay" />
<Dialog.Content className="dialog-content">
<div className="content-inner">
<SendZaps {...props} onFinish={() => setIsOpen(false)} />
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
const [open, setOpen] = useState(false);
return (<>
{props.button ? (
props.button
) : (
<DefaultButton onClick={() => setOpen(true)}>
<span className="max-xl:hidden">
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
</span>
<Icon name="zap-filled" size={16} />
</DefaultButton>
)}
{open && <Modal id="send-zaps" onClose={() => setOpen(false)}>
<SendZaps {...props} onFinish={() => setOpen(false)} />
</Modal>}
</>
);
}

View File

@ -1,27 +1,37 @@
import { Menu, MenuItem } from "@szhsin/react-menu";
import * as Dialog from "@radix-ui/react-dialog";
import { unwrap } from "@snort/shared";
import { NostrEvent, NostrPrefix, encodeTLV } from "@snort/system";
import { FormattedMessage } from "react-intl";
import { NostrEvent, NostrLink, NostrPrefix } from "@snort/system";
import { FormattedMessage, useIntl } from "react-intl";
import { useContext, useState } from "react";
import { SnortContext } from "@snort/system-react";
import { Icon } from "./icon";
import { Textarea } from "./textarea";
import { findTag } from "@/utils";
import AsyncButton from "./async-button";
import { getHost } from "@/utils";
import { useLogin } from "@/hooks/login";
import { DefaultButton } from "./buttons";
import Modal from "./modal";
type ShareOn = "nostr" | "twitter";
export function ShareMenu({ ev }: { ev: NostrEvent }) {
const system = useContext(SnortContext);
const [share, setShare] = useState<ShareOn>();
const [message, setMessage] = useState("");
const login = useLogin();
const { formatMessage } = useIntl();
const host = getHost(ev);
const naddr = encodeTLV(NostrPrefix.Address, unwrap(findTag(ev, "d")), undefined, ev.kind, ev.pubkey);
const link = `https://zap.stream/${naddr}`;
const defaultMyMsg = formatMessage({
defaultMessage: "Come check out my stream on zap.stream!\n\n{link}\n\n", id: 'HsgeUk'
}, {
link: `https://${window.location.host}/${NostrLink.fromEvent(ev).encode()}`
});
const defaultHostMsg = formatMessage({
defaultMessage: "Come check out {name} stream on zap.stream!\n\n{link}", id: 'PUymyQ'
}, {
name: `nostr:${new NostrLink(NostrPrefix.PublicKey, host ?? ev.pubkey).encode()}`,
link: `https://${window.location.host}/${NostrLink.fromEvent(ev).encode()}`
});
const [message, setMessage] = useState(login?.pubkey === host ? defaultMyMsg : defaultHostMsg);
async function sendMessage() {
const pub = login?.publisher();
@ -40,45 +50,37 @@ export function ShareMenu({ ev }: { ev: NostrEvent }) {
gap={5}
menuClassName="ctx-menu"
menuButton={
<AsyncButton className="btn btn-secondary">
<DefaultButton>
<FormattedMessage defaultMessage="Share" id="OKhRC6" />
</AsyncButton>
</DefaultButton>
}>
<MenuItem
onClick={() => {
setMessage(`Come check out my stream on zap.stream!\n\n${link}\n\nnostr:${naddr}`);
setShare("nostr");
}}>
<Icon name="nostrich" size={24} />
<FormattedMessage defaultMessage="Broadcast on Nostr" id="wCIL7o" />
</MenuItem>
</Menu>
<Dialog.Root open={Boolean(share)} onOpenChange={() => setShare(undefined)}>
<Dialog.Portal>
<Dialog.Overlay className="dialog-overlay" />
<Dialog.Content className="dialog-content">
<div className="content-inner">
<h2>
<FormattedMessage defaultMessage="Share" id="OKhRC6" />
</h2>
<div className="paper">
<Textarea
emojis={[]}
value={message}
onChange={e => setMessage(e.target.value)}
onKeyDown={() => {
//noop
}}
rows={15}
/>
</div>
<AsyncButton className="btn btn-primary" onClick={sendMessage}>
<FormattedMessage defaultMessage="Send" id="9WRlF4" />
</AsyncButton>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
{share && <Modal id="share" onClose={() => setShare(undefined)}>
<div className="flex flex-col gap-4">
<h2>
<FormattedMessage defaultMessage="Share" id="OKhRC6" />
</h2>
<Textarea
emojis={[]}
value={message}
onChange={e => setMessage(e.target.value)}
onKeyDown={() => {
//noop
}}
rows={15}
/>
<DefaultButton onClick={sendMessage}>
<FormattedMessage defaultMessage="Send" id="9WRlF4" />
</DefaultButton>
</div>
</Modal>}
</>
);
}

View File

@ -2,19 +2,20 @@ import { HTMLProps } from "react";
import "./state-pill.css";
import classNames from "classnames";
import { StreamState } from "@/const";
import Pill from "./pill";
type StatePillProps = { state: StreamState } & HTMLProps<HTMLSpanElement>;
export function StatePill({ state, ...props }: StatePillProps) {
return (
<span
<Pill
{...props}
className={classNames(
"uppercase font-white pill",
state === StreamState.Live ? "bg-primary" : "bg-gray-1",
"uppercase font-white",
state === StreamState.Live ? "bg-primary" : "bg-layer-1",
props.className
)}>
{state}
</span>
</Pill>
);
}

View File

@ -1,177 +0,0 @@
.stream-cards,
.edit-container {
display: none;
}
@media (min-width: 1020px) {
.edit-container {
display: block;
}
.stream-cards {
display: grid;
align-items: flex-start;
grid-template-columns: repeat(2, 1fr);
gap: 32px;
margin-top: 12px;
flex-wrap: wrap;
}
}
@media (min-width: 1600px) {
.stream-cards {
grid-template-columns: repeat(3, 1fr);
}
}
@media (min-width: 2100px) {
.stream-cards {
grid-template-columns: repeat(4, 1fr);
}
}
.card-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.editor-buttons {
display: flex;
flex-direction: column;
gap: 12px;
}
.stream-card {
display: flex;
align-self: flex-start;
flex-direction: column;
gap: 16px;
flex: 1;
width: 100%;
overflow-wrap: break-word;
}
.stream-card.image-card {
padding: 0;
background: transparent;
}
.stream-card .card-title {
margin: 0;
font-size: 22px;
font-style: normal;
font-weight: 600;
line-height: normal;
}
@media (min-width: 1900px) {
.stream-card {
width: 342px;
}
}
.add-card {
align-items: center;
justify-content: center;
}
.add-card .add-icon {
color: var(--text-muted);
cursor: pointer;
width: 24px;
height: 24px;
}
.new-card {
display: flex;
flex-direction: column;
gap: 12px;
}
.new-card h3 {
margin: 0;
margin-bottom: 12px;
font-weight: 500;
}
.new-card input[type="text"] {
background: #262626;
padding: 8px 16px;
border-radius: 16px;
width: unset;
margin-bottom: 8px;
font-size: 16px;
font-weight: 500;
line-height: 20px;
}
.new-card textarea {
width: unset;
background: #262626;
padding: 8px 16px;
border-radius: 16px;
margin-bottom: 8px;
resize: vertical;
min-height: 210px;
}
.form-control {
display: flex;
flex-direction: column;
}
.form-control label {
margin-bottom: 8px;
}
.new-card-buttons {
display: flex;
flex-direction: column;
gap: 12px;
margin-top: 12px;
}
.help-text {
color: var(--text-muted);
font-size: 14px;
margin-left: 6px;
}
.help-text a {
color: var(--primary);
}
.add-button {
height: 50px;
}
.delete-button {
background: transparent;
color: var(--text-danger);
}
@keyframes shake {
0% {
transform: rotate(0deg);
}
25% {
transform: rotate(5deg);
}
50% {
transform: rotate(0eg);
}
75% {
transform: rotate(-5deg);
}
100% {
transform: rotate(0deg);
}
}
.stream-card .card-image {
max-width: 343px;
}
.stream-card {
max-width: 343px;
}

View File

@ -1,455 +0,0 @@
import "./stream-cards.css";
import { Suspense, forwardRef, lazy, useContext, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import * as Dialog from "@radix-ui/react-dialog";
import { DndProvider, useDrag, useDrop } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { removeUndefined, unwrap } from "@snort/shared";
import { NostrLink, TaggedNostrEvent } from "@snort/system";
import { SnortContext } from "@snort/system-react";
const Markdown = lazy(() => import("./markdown"));
import { Toggle } from "./toggle";
import { Icon } from "./icon";
import { ExternalLink } from "./external-link";
import { FileUploader } from "./file-uploader";
import { useLogin } from "@/hooks/login";
import { useCards, useUserCards } from "@/hooks/cards";
import { CARD, USER_CARDS } from "@/const";
import { findTag } from "@/utils";
import { Login } from "@/index";
import type { Tags } from "@/types";
import AsyncButton from "./async-button";
interface CardType {
identifier: string;
content: string;
title?: string;
image?: string;
link?: string;
}
type NewCard = Omit<CardType, "identifier">;
function isEmpty(s?: string) {
return !s || s.trim().length === 0;
}
interface CardPreviewProps extends NewCard {
style: object;
}
const CardPreview = forwardRef(({ style, title, link, image, content }: CardPreviewProps, ref) => {
const isImageOnly = !isEmpty(image) && isEmpty(content) && isEmpty(title);
return (
<div
className={`stream-card ${isImageOnly ? "image-card" : ""}`}
// @ts-expect-error: Type 'ForwardRef<unknown>'
ref={ref}
style={style}>
{title && <h1 className="card-title">{title}</h1>}
{image &&
(link && link?.length > 0 ? (
<ExternalLink href={link}>
<img className="card-image" src={image} alt={title} />
</ExternalLink>
) : (
<img className="card-image" src={image} alt={title} />
))}
<Suspense>
<Markdown content={content} />
</Suspense>
</div>
);
});
interface CardProps {
canEdit?: boolean;
ev: TaggedNostrEvent;
cards: TaggedNostrEvent[];
}
interface CardItem {
identifier: string;
}
function Card({ canEdit, ev, cards }: CardProps) {
const system = useContext(SnortContext);
const login = useLogin();
const identifier = findTag(ev, "d") ?? "";
const title = findTag(ev, "title") || findTag(ev, "subject");
const image = findTag(ev, "image");
const link = findTag(ev, "r");
const content = ev.content;
const evCard = { title, image, link, content, identifier };
const tags = removeUndefined(cards.map(a => NostrLink.fromEvent(a).toEventTag()));
const [style, dragRef] = useDrag(
() => ({
type: "card",
item: { identifier } as CardItem,
canDrag: () => {
return Boolean(canEdit);
},
collect: monitor => {
const isDragging = monitor.isDragging();
return {
opacity: isDragging ? 0.1 : 1,
cursor: !canEdit ? "auto" : isDragging ? "grabbing" : "grab",
};
},
}),
[canEdit, identifier]
);
function findTagByIdentifier(d: string) {
return tags.find(t => t[1].endsWith(`:${d}`));
}
const [dropStyle, dropRef] = useDrop(
() => ({
accept: ["card"],
canDrop: () => {
return Boolean(canEdit);
},
collect: monitor => {
const isOvering = monitor.isOver({ shallow: true });
return {
opacity: isOvering ? 0.3 : 1,
animation: isOvering ? "shake 0.1s 3" : "",
};
},
async drop(item) {
const typed = item as CardItem;
if (identifier === typed.identifier) {
return;
}
const newItem = findTagByIdentifier(typed.identifier);
const oldItem = findTagByIdentifier(identifier);
const newTags = tags.map(t => {
if (t === oldItem) {
return newItem;
}
if (t === newItem) {
return oldItem;
}
return t;
}) as Tags;
const pub = login?.publisher();
if (pub) {
const userCardsEv = await pub.generic(eb => {
eb.kind(USER_CARDS).content("");
for (const tag of newTags) {
eb.tag(tag);
}
return eb;
});
console.debug(userCardsEv);
await system.BroadcastEvent(userCardsEv);
Login.setCards(newTags, userCardsEv.created_at);
}
},
}),
[canEdit, tags, identifier]
);
const card = (
<CardPreview ref={dropRef} title={title} link={link} image={image} content={content} style={dropStyle} />
);
const editor = canEdit && (
<div className="editor-buttons">
<EditCard card={evCard} cards={cards} />
</div>
);
return canEdit ? (
<div className="card-container" ref={dragRef} style={style}>
{card}
{editor}
</div>
) : (
<div className="card-container">{card}</div>
);
}
interface CardDialogProps {
header?: string;
cta?: string;
cancelCta?: string;
card?: CardType;
onSave(ev: NewCard): void;
onCancel(): void;
}
function CardDialog({ header, cta, cancelCta, card, onSave, onCancel }: CardDialogProps) {
const [title, setTitle] = useState(card?.title ?? "");
const [image, setImage] = useState(card?.image ?? "");
const [content, setContent] = useState(card?.content ?? "");
const [link, setLink] = useState(card?.link ?? "");
const { formatMessage } = useIntl();
return (
<div className="new-card">
<h3>{header || <FormattedMessage defaultMessage="Add card" id="nwA8Os" />}</h3>
<div className="form-control">
<label htmlFor="card-title">
<FormattedMessage defaultMessage="Title" id="9a9+ww" />
</label>
<input
id="card-title"
type="text"
value={title}
onChange={e => setTitle(e.target.value)}
placeholder={formatMessage({ defaultMessage: "e.g. about me", id: "k21gTS" })}
/>
</div>
<div className="form-control">
<label htmlFor="card-image">
<FormattedMessage defaultMessage="Image" id="+0zv6g" />
</label>
<FileUploader defaultImage={image} onFileUpload={setImage} onClear={() => setImage("")} />
</div>
<div className="form-control">
<label htmlFor="card-image-link">
<FormattedMessage defaultMessage="Image Link" id="s5ksS7" />
</label>
<input
id="card-image-link"
type="text"
placeholder="https://"
value={link}
onChange={e => setLink(e.target.value)}
/>
</div>
<div className="form-control">
<label htmlFor="card-content">
<FormattedMessage defaultMessage="Content" id="Jq3FDz" />
</label>
<textarea
placeholder={formatMessage({ defaultMessage: "Start typing", id: "w0Xm2F" })}
value={content}
onChange={e => setContent(e.target.value)}
/>
<span className="help-text">
<FormattedMessage
defaultMessage="Supports {markdown}"
id="I1kjHI"
values={{
markdown: (
<ExternalLink href="https://www.markdownguide.org/cheat-sheet">
<FormattedMessage defaultMessage="Markdown" id="jr4+vD" />
</ExternalLink>
),
}}
/>
</span>
</div>
<div className="new-card-buttons">
<AsyncButton className="btn btn-primary add-button" onClick={() => onSave({ title, image, content, link })}>
{cta || <FormattedMessage defaultMessage="Add Card" id="UJBFYK" />}
</AsyncButton>
<AsyncButton className="btn delete-button" onClick={onCancel}>
{cancelCta || <FormattedMessage defaultMessage="Cancel" id="47FYwb" />}
</AsyncButton>
</div>
</div>
);
}
interface EditCardProps {
card: CardType;
cards: TaggedNostrEvent[];
}
function EditCard({ card, cards }: EditCardProps) {
const system = useContext(SnortContext);
const login = useLogin();
const [isOpen, setIsOpen] = useState(false);
const identifier = card.identifier;
const tags = removeUndefined(cards.map(a => NostrLink.fromEvent(a).toEventTag()));
const { formatMessage } = useIntl();
async function editCard({ title, image, link, content }: CardType) {
const pub = login?.publisher();
if (pub) {
const ev = await pub.generic(eb => {
eb.kind(CARD).content(content).tag(["d", card.identifier]);
if (title && title?.length > 0) {
eb.tag(["title", title]);
}
if (image && image?.length > 0) {
eb.tag(["image", image]);
}
if (link && link?.length > 0) {
eb.tag(["r", link]);
}
return eb;
});
console.debug(ev);
await system.BroadcastEvent(ev);
setIsOpen(false);
}
}
async function onCancel() {
const pub = login?.publisher();
if (pub) {
const newTags = tags.filter(t => !t[1].endsWith(`:${identifier}`));
const userCardsEv = await pub.generic(eb => {
eb.kind(USER_CARDS).content("");
for (const tag of newTags) {
eb.tag(tag);
}
return eb;
});
console.debug(userCardsEv);
await system.BroadcastEvent(userCardsEv);
Login.setCards(newTags, userCardsEv.created_at);
setIsOpen(false);
}
}
return (
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
<Dialog.Trigger asChild>
<AsyncButton className="btn btn-primary">
<FormattedMessage defaultMessage="Edit" id="wEQDC6" />
</AsyncButton>
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="dialog-overlay" />
<Dialog.Content className="dialog-content">
<div className="content-inner">
<CardDialog
header={formatMessage({ defaultMessage: "Edit card", id: "OWgHbg" })}
cta={formatMessage({ defaultMessage: "Save card", id: "rfC1Zq" })}
cancelCta={formatMessage({ defaultMessage: "Delete", id: "K3r6DQ" })}
card={card}
onSave={editCard}
onCancel={onCancel}
/>
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
interface AddCardProps {
cards: TaggedNostrEvent[];
}
function AddCard({ cards }: AddCardProps) {
const system = useContext(SnortContext);
const login = useLogin();
const tags = removeUndefined(cards.map(a => NostrLink.fromEvent(a).toEventTag()));
const [isOpen, setIsOpen] = useState(false);
async function createCard({ title, image, link, content }: NewCard) {
const pub = login?.publisher();
if (pub) {
const ev = await pub.generic(eb => {
const d = String(Date.now());
eb.kind(CARD).content(content).tag(["d", d]);
if (title && title?.length > 0) {
eb.tag(["title", title]);
}
if (image && image?.length > 0) {
eb.tag(["image", image]);
}
if (link && link?.length > 0) {
eb.tag(["r", link]);
}
return eb;
});
const userCardsEv = await pub.generic(eb => {
eb.kind(USER_CARDS).content("");
tags.forEach(a => eb.tag(a));
eb.tag(unwrap(NostrLink.fromEvent(ev).toEventTag()));
return eb;
});
console.debug(ev);
console.debug(userCardsEv);
await system.BroadcastEvent(ev);
await system.BroadcastEvent(userCardsEv);
setIsOpen(false);
}
}
function onCancel() {
setIsOpen(false);
}
return (
<div className="stream-card add-card">
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
<Dialog.Trigger asChild>
<Icon name="plus" className="add-icon" />
</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className="dialog-overlay" />
<Dialog.Content className="dialog-content">
<div className="content-inner">
<CardDialog onSave={createCard} onCancel={onCancel} />
</div>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
</div>
);
}
interface StreamCardEditorProps {
pubkey: string;
tags: Tags;
}
export function StreamCardEditor({ pubkey, tags }: StreamCardEditorProps) {
const cards = useUserCards(pubkey, tags, true);
const [isEditing, setIsEditing] = useState(false);
return (
<>
<div className="stream-cards">
{cards.map(ev => (
<Card canEdit={isEditing} cards={cards} key={ev.id} ev={ev} />
))}
{isEditing && <AddCard cards={cards} />}
</div>
<div className="edit-container">
<Toggle pressed={isEditing} onPressedChange={setIsEditing} label="Toggle edit mode" text="Edit cards" />
</div>
</>
);
}
interface StreamCardsProps {
host: string;
}
export function ReadOnlyStreamCards({ host }: StreamCardsProps) {
const cards = useCards(host);
return (
<div className="stream-cards">
{cards.map(ev => (
<Card cards={cards} key={ev.id} ev={ev} />
))}
</div>
);
}
export function StreamCards({ host }: StreamCardsProps) {
const login = useLogin();
const canEdit = login?.pubkey === host;
return (
<DndProvider backend={HTML5Backend}>
{canEdit ? (
<StreamCardEditor tags={login.cards.tags} pubkey={login.pubkey} />
) : (
<ReadOnlyStreamCards host={host} />
)}
</DndProvider>
);
}

View File

@ -0,0 +1,64 @@
import { useContext, useState } from "react";
import { removeUndefined, unwrap } from "@snort/shared";
import { NostrLink, TaggedNostrEvent } from "@snort/system";
import { SnortContext } from "@snort/system-react";
import { Icon } from "../icon";
import { useLogin } from "@/hooks/login";
import { CARD, USER_CARDS } from "@/const";
import Modal from "../modal";
import { NewCard } from ".";
import { CardDialog } from "./new-card";
interface AddCardProps {
cards: TaggedNostrEvent[];
}
export function AddCard({ cards }: AddCardProps) {
const system = useContext(SnortContext);
const login = useLogin();
const tags = removeUndefined(cards.map(a => NostrLink.fromEvent(a).toEventTag()));
const [open, setOpen] = useState(false);
async function createCard({ title, image, link, content }: NewCard) {
const pub = login?.publisher();
if (pub) {
const ev = await pub.generic(eb => {
const d = String(Date.now());
eb.kind(CARD).content(content).tag(["d", d]);
if (title && title?.length > 0) {
eb.tag(["title", title]);
}
if (image && image?.length > 0) {
eb.tag(["image", image]);
}
if (link && link?.length > 0) {
eb.tag(["r", link]);
}
return eb;
});
const userCardsEv = await pub.generic(eb => {
eb.kind(USER_CARDS).content("");
tags.forEach(a => eb.tag(a));
eb.tag(unwrap(NostrLink.fromEvent(ev).toEventTag()));
return eb;
});
console.debug(ev);
console.debug(userCardsEv);
await system.BroadcastEvent(ev);
await system.BroadcastEvent(userCardsEv);
setOpen(false);
}
}
return (
<div className="flex flex-col items-center justify-center bg-layer-1 rounded-xl gap-4 p-2 cursor-pointer" onClick={() => setOpen(true)}>
<Icon name="plus" />
{open && <Modal id="add-card" onClose={() => setOpen(false)}>
<CardDialog onSave={createCard} onCancel={() => setOpen(false)} />
</Modal>}
</div>
);
}

View File

@ -0,0 +1,116 @@
import { useContext } from "react";
import { useDrag, useDrop } from "react-dnd";
import { removeUndefined } from "@snort/shared";
import { NostrLink, TaggedNostrEvent } from "@snort/system";
import { SnortContext } from "@snort/system-react";
import { CardItem } from ".";
import { USER_CARDS } from "@/const";
import { useLogin } from "@/hooks/login";
import { Login } from "@/index";
import { Tags } from "@/types";
import { findTag } from "@/utils";
import { EditCard } from "./edit-card";
import { CardPreview } from "./preview";
interface CardProps {
canEdit?: boolean;
ev: TaggedNostrEvent;
cards: TaggedNostrEvent[];
}
export function Card({ canEdit, ev, cards }: CardProps) {
const system = useContext(SnortContext);
const login = useLogin();
const identifier = findTag(ev, "d") ?? "";
const title = findTag(ev, "title") || findTag(ev, "subject");
const image = findTag(ev, "image");
const link = findTag(ev, "r");
const content = ev.content;
const evCard = { title, image, link, content, identifier };
const tags = removeUndefined(cards.map(a => NostrLink.fromEvent(a).toEventTag()));
const [style, dragRef] = useDrag(
() => ({
type: "card",
item: { identifier } as CardItem,
canDrag: () => {
return Boolean(canEdit);
},
collect: monitor => {
const isDragging = monitor.isDragging();
return {
opacity: isDragging ? 0.1 : 1,
cursor: !canEdit ? "auto" : isDragging ? "grabbing" : "grab",
};
},
}),
[canEdit, identifier]
);
function findTagByIdentifier(d: string) {
return tags.find(t => t[1].endsWith(`:${d}`));
}
const [dropStyle, dropRef] = useDrop(
() => ({
accept: ["card"],
canDrop: () => {
return Boolean(canEdit);
},
collect: monitor => {
const isOvering = monitor.isOver({ shallow: true });
return {
opacity: isOvering ? 0.3 : 1,
animation: isOvering ? "shake 0.1s 3" : "",
};
},
async drop(item) {
const typed = item as CardItem;
if (identifier === typed.identifier) {
return;
}
const newItem = findTagByIdentifier(typed.identifier);
const oldItem = findTagByIdentifier(identifier);
const newTags = tags.map(t => {
if (t === oldItem) {
return newItem;
}
if (t === newItem) {
return oldItem;
}
return t;
}) as Tags;
const pub = login?.publisher();
if (pub) {
const userCardsEv = await pub.generic(eb => {
eb.kind(USER_CARDS).content("");
for (const tag of newTags) {
eb.tag(tag);
}
return eb;
});
console.debug(userCardsEv);
await system.BroadcastEvent(userCardsEv);
Login.setCards(newTags, userCardsEv.created_at);
}
},
}),
[canEdit, tags, identifier]
);
const card = (
<CardPreview
ref={dropRef}
title={title}
link={link}
image={image}
content={content}
style={dropStyle}
/>
);
if (canEdit) {
return <div ref={dragRef} style={style}>
{card}
<EditCard card={evCard} cards={cards} />
</div>
}
return card;
}

View File

@ -0,0 +1,84 @@
import { CARD, USER_CARDS } from "@/const";
import { useLogin } from "@/hooks/login";
import { Login } from "@/index";
import { removeUndefined } from "@snort/shared";
import { TaggedNostrEvent, NostrLink } from "@snort/system";
import { SnortContext } from "@snort/system-react";
import { useContext, useState } from "react";
import { useIntl, FormattedMessage } from "react-intl";
import { DefaultButton } from "../buttons";
import Modal from "../modal";
import { CardDialog } from "./new-card";
import { CardType } from ".";
interface EditCardProps {
card: CardType;
cards: TaggedNostrEvent[];
}
export function EditCard({ card, cards }: EditCardProps) {
const system = useContext(SnortContext);
const login = useLogin();
const [open, setOpen] = useState(false);
const identifier = card.identifier;
const tags = removeUndefined(cards.map(a => NostrLink.fromEvent(a).toEventTag()));
const { formatMessage } = useIntl();
async function editCard({ title, image, link, content }: CardType) {
const pub = login?.publisher();
if (pub) {
const ev = await pub.generic(eb => {
eb.kind(CARD).content(content).tag(["d", card.identifier]);
if (title && title?.length > 0) {
eb.tag(["title", title]);
}
if (image && image?.length > 0) {
eb.tag(["image", image]);
}
if (link && link?.length > 0) {
eb.tag(["r", link]);
}
return eb;
});
console.debug(ev);
await system.BroadcastEvent(ev);
setOpen(false);
}
}
async function onCancel() {
const pub = login?.publisher();
if (pub) {
const newTags = tags.filter(t => !t[1].endsWith(`:${identifier}`));
const userCardsEv = await pub.generic(eb => {
eb.kind(USER_CARDS).content("");
for (const tag of newTags) {
eb.tag(tag);
}
return eb;
});
console.debug(userCardsEv);
await system.BroadcastEvent(userCardsEv);
Login.setCards(newTags, userCardsEv.created_at);
setOpen(false);
}
}
return (<>
<DefaultButton onClick={() => setOpen(true)}>
<FormattedMessage defaultMessage="Edit" id="wEQDC6" />
</DefaultButton>
{open && <Modal id="edit-stream-card" onClose={() => setOpen(false)}>
<CardDialog
header={formatMessage({ defaultMessage: "Edit", id: 'wEQDC6' })}
cta={formatMessage({ defaultMessage: "Save", id: 'jvo0vs' })}
cancelCta={formatMessage({ defaultMessage: "Delete", id: "K3r6DQ" })}
card={card}
onSave={editCard}
onCancel={onCancel}
/>
</Modal>}
</>
);
}

View File

@ -0,0 +1,50 @@
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { useLogin } from "@/hooks/login";
import { useCards } from "@/hooks/cards";
import { StreamCardEditor } from "./stream-card-editor";
import { Card } from "./card-item";
export interface CardType {
identifier: string;
content: string;
title?: string;
image?: string;
link?: string;
}
export type NewCard = Omit<CardType, "identifier">;
export interface CardItem {
identifier: string;
}
interface StreamCardsProps {
host: string;
}
export function ReadOnlyStreamCards({ host }: StreamCardsProps) {
const cards = useCards(host);
return (
<div className="max-xl:hidden grid lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-6">
{cards.map(ev => (
<Card cards={cards} key={ev.id} ev={ev} />
))}
</div>
);
}
export function StreamCards({ host }: StreamCardsProps) {
const login = useLogin();
const canEdit = login?.pubkey === host;
return (
<DndProvider backend={HTML5Backend}>
{canEdit ? (
<StreamCardEditor tags={login.cards.tags} pubkey={login.pubkey} />
) : (
<ReadOnlyStreamCards host={host} />
)}
</DndProvider>
);
}

View File

@ -0,0 +1,87 @@
import { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { ExternalLink } from "../external-link";
import { FileUploader } from "../file-uploader";
import { DefaultButton, WarningButton } from "../buttons";
import { CardType, NewCard } from ".";
interface CardDialogProps {
header?: string;
cta?: string;
cancelCta?: string;
card?: CardType;
onSave(ev: NewCard): void;
onCancel(): void;
}
export function CardDialog({ header, cta, cancelCta, card, onSave, onCancel }: CardDialogProps) {
const [title, setTitle] = useState(card?.title ?? "");
const [image, setImage] = useState(card?.image ?? "");
const [content, setContent] = useState(card?.content ?? "");
const [link, setLink] = useState(card?.link ?? "");
const { formatMessage } = useIntl();
return (
<div className="flex flex-col gap-2">
<h3>{header || <FormattedMessage defaultMessage="Add card" id="nwA8Os" />}</h3>
{/* TITLE */}
<label htmlFor="card-title">
<FormattedMessage defaultMessage="Title" id="9a9+ww" />
</label>
<input
id="card-title"
type="text"
value={title}
onChange={e => setTitle(e.target.value)}
placeholder={formatMessage({ defaultMessage: "e.g. about me", id: "k21gTS" })} />
{/* IMAGE */}
<label htmlFor="card-image">
<FormattedMessage defaultMessage="Image" id="+0zv6g" />
</label>
<FileUploader defaultImage={image} onFileUpload={setImage} onClear={() => setImage("")} />
{image.length > 0 && <>
{/* IMAGE LINK */}
<label htmlFor="card-image-link">
<FormattedMessage defaultMessage="Image Link" id="s5ksS7" />
</label>
<input
id="card-image-link"
type="text"
placeholder="https://"
value={link}
onChange={e => setLink(e.target.value)} />
</>}
{/* CONTENT */}
<label htmlFor="card-content">
<FormattedMessage defaultMessage="Content" id="Jq3FDz" />
</label>
<textarea
placeholder={formatMessage({ defaultMessage: "Start typing", id: "w0Xm2F" })}
value={content}
rows={5}
onChange={e => setContent(e.target.value)} />
<span className="help-text">
<FormattedMessage
defaultMessage="Supports {markdown}"
id="I1kjHI"
values={{
markdown: (
<ExternalLink href="https://www.markdownguide.org/cheat-sheet">
<FormattedMessage defaultMessage="Markdown" id="jr4+vD" />
</ExternalLink>
),
}} />
</span>
<div className="flex justify-between">
<WarningButton onClick={onCancel}>
{cancelCta || <FormattedMessage defaultMessage="Cancel" id="47FYwb" />}
</WarningButton>
<DefaultButton onClick={() => onSave({ title, image, content, link })}>
{cta || <FormattedMessage defaultMessage="Add Card" id="UJBFYK" />}
</DefaultButton>
</div>
</div>
);
}

View File

@ -0,0 +1,35 @@
import { isEmpty } from "lodash";
import { forwardRef, lazy, Suspense } from "react";
import { ExternalLink } from "../external-link";
import { NewCard } from ".";
import classNames from "classnames";
const Markdown = lazy(() => import("../markdown"));
interface CardPreviewProps extends NewCard {
style: object;
}
export const CardPreview = forwardRef<HTMLDivElement, CardPreviewProps>(({ style, title, link, image, content }: CardPreviewProps, ref) => {
const isImageOnly = !isEmpty(image) && isEmpty(content) && isEmpty(title);
return (
<div
className={classNames("flex flex-col gap-4", { "": isImageOnly })}
ref={ref}
style={style}>
{title && <h2>{title}</h2>}
{image &&
(link && link?.length > 0 ? (
<ExternalLink href={link}>
<img src={image} alt={title} />
</ExternalLink>
) : (
<img src={image} alt={title} />
))}
{content &&
<Suspense>
<Markdown content={content} />
</Suspense>
}
</div>
);
});

View File

@ -0,0 +1,32 @@
import { useState } from "react";
import { FormattedMessage } from "react-intl";
import { Toggle } from "../toggle";
import { useUserCards } from "@/hooks/cards";
import { AddCard } from "./add-card";
import { Tags } from "@/types";
import { Card } from "./card-item";
interface StreamCardEditorProps {
pubkey: string;
tags: Tags;
}
export function StreamCardEditor({ pubkey, tags }: StreamCardEditorProps) {
const cards = useUserCards(pubkey, tags, true);
const [isEditing, setIsEditing] = useState(false);
return (
<>
<div className="text-xl flex items-center gap-2">
<FormattedMessage defaultMessage="Edit Cards" id="bD/ZwY" />
<Toggle onClick={() => setIsEditing(s => !s)} checked={isEditing} size={40} />
</div>
<div className="max-xl:hidden grid lg:grid-cols-3 xl:grid-cols-4 gap-6">
{cards.map(ev => (
<Card canEdit={isEditing} cards={cards} key={ev.id} ev={ev} />
))}
{isEditing && <AddCard cards={cards} />}
</div>
</>
);
}

View File

@ -11,10 +11,4 @@
padding: 4px 10px !important;
border-radius: 12px !important;
display: unset !important;
}
.content-warning {
padding: 16px;
border-radius: 16px;
border: 1px solid #ff563f;
}
}

View File

@ -5,12 +5,13 @@ import { unixNow } from "@snort/shared";
import { TagsInput } from "react-tag-input-component";
import { FormattedMessage, useIntl } from "react-intl";
import AsyncButton from "./async-button";
import { extractStreamInfo, findTag } from "@/utils";
import { useLogin } from "@/hooks/login";
import { NewGoalDialog } from "./new-goal";
import { useGoals } from "@/hooks/goals";
import { StreamState } from "@/const";
import { DefaultButton } from "./buttons";
import Pill from "./pill";
export interface StreamEditorProps {
ev?: NostrEvent;
@ -206,9 +207,9 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
</p>
<div className="flex gap-2">
{[StreamState.Live, StreamState.Planned, StreamState.Ended].map(v => (
<span className={`pill${status === v ? " active" : ""}`} onClick={() => setStatus(v)} key={v}>
<Pill className={status === v ? " active" : ""} onClick={() => setStatus(v)} key={v}>
{v}
</span>
</Pill>
))}
</div>
</div>
@ -262,12 +263,12 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
</>
)}
{(options?.canSetContentWarning ?? true) && (
<div className="flex gap-2 content-warning">
<div className="flex gap-2 rounded-xl border border-warning px-4 py-3">
<div>
<input type="checkbox" checked={contentWarning} onChange={e => setContentWarning(e.target.checked)} />
</div>
<div>
<div className="warning">
<div className="text-warning">
<FormattedMessage defaultMessage="NSFW Content" id="Atr2p4" />
</div>
<FormattedMessage
@ -278,13 +279,13 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
</div>
)}
<div>
<AsyncButton type="button" className="btn btn-primary wide" disabled={!isValid} onClick={publishStream}>
<DefaultButton disabled={!isValid} onClick={publishStream}>
{ev ? (
<FormattedMessage defaultMessage="Save" id="jvo0vs" />
) : (
<FormattedMessage defaultMessage="Start Stream" id="TaTRKo" />
)}
</AsyncButton>
</DefaultButton>
</div>
</>
);

View File

@ -4,6 +4,7 @@ import { FormattedMessage } from "react-intl";
import { NostrEvent } from "@snort/system";
import { findTag, getTagValues } from "@/utils";
import { StreamState } from "@/const";
import Pill from "./pill";
export function Tags({ children, max, ev }: { children?: ReactNode; max?: number; ev: NostrEvent }) {
const status = findTag(ev, "status");
@ -14,13 +15,13 @@ export function Tags({ children, max, ev }: { children?: ReactNode; max?: number
<>
{children}
{status === StreamState.Planned && (
<span className="pill bg-gray-1">
<Pill>
{status === StreamState.Planned ? <FormattedMessage defaultMessage="Starts " id="0hNxBy" /> : ""}
</span>
</Pill>
)}
{tags.map(a => (
<a href={`/t/${encodeURIComponent(a)}`} className="pill bg-gray-1" key={a}>
{a}
<a href={`/t/${encodeURIComponent(a)}`} key={a}>
<Pill>{a}</Pill>
</a>
))}
</>

View File

@ -15,7 +15,7 @@
.emoji-item,
.user-item {
color: white;
background: #171717;
@apply bg-layer-2;
display: flex;
flex-direction: row;
gap: 8px;
@ -26,7 +26,7 @@
.emoji-item:hover,
.user-item:hover {
color: #171717;
@apply text-layer-2;
background: white;
}

View File

@ -1,27 +0,0 @@
.toggle-container {
display: flex;
align-items: center;
gap: 6px;
}
.toggle {
display: flex;
align-items: center;
justify-content: center;
background: transparent;
border: none;
}
.toggle svg {
color: var(--text-muted);
height: 32px;
width: 32px;
}
.toggle:hover {
cursor: pointer;
}
.toggle:hover svg {
color: white;
}
.toggle[data-state="on"] svg {
color: var(--primary);
}

View File

@ -1,22 +1,32 @@
import * as BaseToggle from "@radix-ui/react-toggle";
import "./toggle.css";
import { Icon } from "./icon";
import { HTMLProps } from "react";
interface ToggleProps {
label: string;
text: string;
pressed?: boolean;
onPressedChange?: (b: boolean) => void;
}
export function Toggle({ label, text, ...rest }: ToggleProps) {
const { pressed } = rest;
export function Toggle({ size, className, checked, ...props }: HTMLProps<SVGSVGElement>) {
return (
<div className="toggle-container">
<BaseToggle.Root className="toggle" aria-label={label} {...rest}>
{pressed ? <Icon name="toggle-on" /> : <Icon name="toggle-off" />}
</BaseToggle.Root>
<span className="toggle-text">{text}</span>
</div>
<svg
{...props}
viewBox="0 0 33.015999 19.353487"
fill="none"
width={size}
height={props.height ?? size}
className={className}
onClick={props.onClick}
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="m 10.265361,0.05752611 c 1.126446,-0.06242431 2.251588,-0.05743114046 3.378034,-0.05743114046 0.0077,0 5.70779,0 5.70779,0 1.148521,0 2.273663,-0.0049931695 3.399407,0.05743114046 1.023495,0.05619252 2.020399,0.17605763 3.012887,0.44387929 2.089033,0.5631251 3.913363,1.739307 5.232065,3.4068227 1.310876,1.6569003 2.020399,3.685279 2.020399,5.7678955 0,2.0852294 -0.709523,4.1123014 -2.020399,5.7692404 -1.318702,1.666906 -3.143032,2.843688 -5.232065,3.406775 -0.992488,0.26785 -1.989392,0.387067 -3.012887,0.443869 -1.125744,0.06241 -2.250886,0.0568 -3.377332,0.0568 -0.0078,0 -5.709194,6.78e-4 -5.709194,6.78e-4 -1.147117,-6.78e-4 -2.272259,0.0049 -3.398705,-0.05748 C 9.2425688,19.239206 8.2455645,19.119989 7.253176,18.852139 5.1640828,18.289052 3.3397837,17.11227 2.021091,15.445364 0.71016486,13.788425 0,11.761353 0,9.676801 0,7.5935071 0.71016486,5.5651284 2.021091,3.9082281 3.3397837,2.2407124 5.1640828,1.0645305 7.253176,0.5014054 8.2455645,0.23358374 9.2425688,0.11371863 10.265361,0.05752611 Z"
className={checked ? "transition-colors fill-success" : "transition-colors fill-layer-3"}
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="m 10.118173,18.367375 c 4.8102,0 8.7097,-3.8995 8.7097,-8.7096999 0,-4.8101905 -3.8995,-8.7096505 -8.7097,-8.7096505 -4.8102196,0 -8.7096796,3.89946 -8.7096796,8.7096505 0,4.8101999 3.89946,8.7096999 8.7096796,8.7096999 z"
fill="#ffffff"
className="transition-transform"
style={{
transform: checked ? "translateX(12px)" : "translateX(0px)"
}}
/>
</svg>
);
}

View File

@ -2,11 +2,9 @@ import { ParsedZap } from "@snort/system";
import useTopZappers from "@/hooks/top-zappers";
import { ZapperRow } from "./zapper-row";
export function TopZappers({ zaps, limit }: { zaps: ParsedZap[]; limit?: number }) {
export function TopZappers({ zaps, limit, avatarSize, showName, className }: { zaps: ParsedZap[]; limit?: number, avatarSize?: number, showName?: boolean, className?: string }) {
const zappers = useTopZappers(zaps);
return zappers.slice(0, limit ?? 10).map(({ pubkey, total }) => (
<div className="border rounded-full px-2 py-1 border-gray-1 grow-0 shrink-0 basis-auto font-bold">
<ZapperRow pubkey={pubkey} total={total} key={pubkey} showName={false} />
</div>
<ZapperRow pubkey={pubkey} total={total} key={pubkey} showName={showName ?? false} avatarSize={avatarSize} className={className} />
));
}

View File

@ -1,67 +0,0 @@
.video-tile {
position: relative;
}
.video-tile-container {
display: flex;
flex-direction: column;
}
.video-tile-info {
display: flex;
flex-direction: column;
gap: 6px;
}
.video-tile.nsfw > div:nth-child(1) {
filter: blur(3px);
}
.video-tile > div:nth-child(1) {
border-radius: 16px;
width: 100%;
aspect-ratio: 16 / 10;
background-size: cover;
background-position: center;
background-repeat: no-repeat;
}
.video-tile h3 {
font-size: 20px;
line-height: 25px;
margin: 16px 0 6px 0;
word-break: break-all;
word-wrap: break-word;
}
.video-tile .pill-box {
margin: 16px 20px;
text-transform: uppercase;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: flex-end;
position: absolute;
top: 0;
right: 0;
gap: 8px;
}
.video-tile .pill-box .pill {
width: fit-content;
}
.video-tile .pill-box .pill.viewers {
text-transform: lowercase;
}
.video-tags {
display: flex;
align-items: flex-start;
flex-wrap: wrap;
gap: 4px;
}
.video-tags .pill {
font-size: 12px;
}

View File

@ -1,16 +1,16 @@
import "./video-tile.css";
import { Link } from "react-router-dom";
import { Profile } from "./profile";
import { NostrEvent, NostrPrefix, encodeTLV } from "@snort/system";
import { useInView } from "react-intersection-observer";
import { NostrEvent, NostrLink } from "@snort/system";
import { FormattedMessage } from "react-intl";
import { StatePill } from "./state-pill";
import { extractStreamInfo, findTag, getHost } from "@/utils";
import { extractStreamInfo, getHost } from "@/utils";
import { formatSats } from "@/number";
import { isContentWarningAccepted } from "./content-warning";
import { Tags } from "./tags";
import { StreamState } from "@/const";
import Pill from "./pill";
import classNames from "classnames";
import Logo from "./logo";
export function VideoTile({
ev,
@ -21,43 +21,39 @@ export function VideoTile({
showAuthor?: boolean;
showStatus?: boolean;
}) {
const { inView, ref } = useInView({ triggerOnce: true });
const id = findTag(ev, "d") ?? "";
const { title, image, status, participants, contentWarning } = extractStreamInfo(ev);
const host = getHost(ev);
const link = encodeTLV(NostrPrefix.Address, id, undefined, ev.kind, ev.pubkey);
const link = NostrLink.fromEvent(ev);
const hasImg = (image?.length ?? 0) > 0;
return (
<div className="video-tile-container">
<div className="flex flex-col gap-2">
<Link
to={`/${link}`}
className={`video-tile${contentWarning && !isContentWarningAccepted() ? " nsfw" : ""}`}
ref={ref}
to={`/${link.encode()}`}
className={classNames({ "blur": contentWarning }, "h-full")}
state={ev}>
<div
style={{
backgroundImage: `url(${inView ? ((image?.length ?? 0) > 0 ? image : "/zap-stream.svg") : ""})`,
}}></div>
<span className="pill-box">
{showStatus && <StatePill state={status as StreamState} />}
{participants && (
<span className="pill viewers bg-gray-1">
<FormattedMessage
defaultMessage="{n} viewers"
id="3adEeb"
values={{ n: formatSats(Number(participants)) }}
/>
</span>
)}
</span>
<div className="relative mb-2 aspect-video">
{hasImg ? <img loading="lazy" className="aspect-video object-cover rounded-xl" src={image} /> :
<Logo className="text-white aspect-video" />}
<span className="flex flex-col justify-between absolute top-0 h-full right-4 items-end py-2">
{showStatus && <StatePill state={status as StreamState} />}
{participants && (
<Pill>
<FormattedMessage
defaultMessage="{n} viewers"
id="3adEeb"
values={{ n: formatSats(Number(participants)) }}
/>
</Pill>
)}
</span>
</div>
<h3>{title}</h3>
</Link>
<div className="video-tile-info">
<div className="video-tags">
<Tags ev={ev} max={3} />
</div>
{showAuthor && <div>{inView && <Profile pubkey={host} />}</div>}
<div className="flex gap-1">
<Tags ev={ev} max={3} />
</div>
{showAuthor && <Profile pubkey={host} />}
</div>
);
}

View File

@ -6,12 +6,12 @@ import { unixNowMs } from "@snort/shared";
const EmojiPicker = lazy(() => import("./emoji-picker"));
import { useLogin } from "@/hooks/login";
import AsyncButton from "./async-button";
import { Icon } from "./icon";
import { Textarea } from "./textarea";
import type { Emoji, EmojiPack } from "@/types";
import { LIVE_STREAM_CHAT } from "@/const";
import { TimeSync } from "@/index";
import { BorderButton } from "./buttons";
export function WriteMessage({ link, emojiPacks }: { link: NostrLink; emojiPacks: EmojiPack[] }) {
const system = useContext(SnortContext);
@ -100,9 +100,9 @@ export function WriteMessage({ link, emojiPacks }: { link: NostrLink; emojiPacks
</Suspense>
)}
</div>
<AsyncButton onClick={sendChatMessage} className="btn btn-border">
<BorderButton onClick={sendChatMessage}>
<FormattedMessage defaultMessage="Send" id="9WRlF4" />
</AsyncButton>
</BorderButton>
</>
);
}

View File

@ -2,16 +2,17 @@ import { formatSats } from "@/number";
import { Icon } from "./icon";
import { Profile } from "./profile";
import { FormattedMessage } from "react-intl";
import classNames from "classnames";
export function ZapperRow({ pubkey, total, showName }: { pubkey: string; total: number; showName?: boolean }) {
export function ZapperRow({ pubkey, total, showName, avatarSize, className }: { pubkey: string; total: number; showName?: boolean, avatarSize?: number, className?: string }) {
return (
<div className="flex gap-1 justify-between items-center">
<div className={classNames(className, "flex gap-1 justify-between items-center")}>
{pubkey === "anon" ? (
<span>
<span style={{ height: avatarSize }}>
<FormattedMessage defaultMessage="Anon" id="bfvyfs" />
</span>
) : (
<Profile pubkey={pubkey} options={{ showName }} />
<Profile pubkey={pubkey} options={{ showName }} avatarSize={avatarSize} />
)}
<div className="flex items-center gap-2">
<Icon name="zap-filled" className="text-zap" />

15
src/hooks/clips.ts Normal file
View File

@ -0,0 +1,15 @@
import { LIVE_STREAM_CLIP } from "@/const";
import { NostrLink, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { useMemo } from "react";
export function useClips(link?: NostrLink, limit?: number) {
const sub = useMemo(() => {
if (!link) return;
const rb = new RequestBuilder(`clips:${link.id.slice(0, 12)}`);
rb.withFilter().kinds([LIVE_STREAM_CLIP]).tag("p", [link.id]).limit(limit);
return rb;
}, [link]);
return useRequestBuilder(sub);
}

View File

@ -15,15 +15,14 @@ export function useZapGoal(id?: string) {
return data.at(0);
}
export function useGoals(pubkey?: string, leaveOpen = false) {
export function useGoals(pubkey?: string, leaveOpen?: boolean, limit?: number) {
const sub = useMemo(() => {
if (!pubkey) return null;
const b = new RequestBuilder(`goals:${pubkey.slice(0, 12)}`);
b.withOptions({ leaveOpen });
b.withFilter().kinds([GOAL]).authors([pubkey]);
b.withFilter().kinds([GOAL]).authors([pubkey]).limit(limit);
return b;
}, [pubkey, leaveOpen]);
}, [pubkey, leaveOpen, limit]);
const data = useRequestBuilder(sub);
return data;
return useRequestBuilder(sub);
}

View File

@ -20,7 +20,7 @@ body {
--primary: #f838d9;
--secondary: #34d2fe;
--zap: #ff8d2b;
--text-danger: #ff563f;
--success: green;
--surface: #222;
--border: #171717;
--border-2: #393939;
@ -29,7 +29,18 @@ body {
--gradient-orange: linear-gradient(270deg, #ff5b27 0%, rgba(255, 182, 39, 0.99) 100%);
}
@media (max-width: 1020px) {
.btn-border {
border: 1px solid transparent;
color: inherit;
background: linear-gradient(black, black) padding-box, linear-gradient(94.73deg, #2bd9ff 0%, #f838d9 100%) border-box;
transition: 0.3s;
}
.btn-border:hover {
background: linear-gradient(black, black) padding-box, linear-gradient(94.73deg, #14b4d8 0%, #ba179f 100%) border-box;
}
@media screen(xl) {
:root {
--gap-l: 24px;
--gap-m: 16px;
@ -66,225 +77,18 @@ a {
outline: none;
}
.pill {
padding: 4px 8px;
border-radius: 9px;
font-weight: 700;
font-size: 14px;
line-height: 18px;
cursor: pointer;
user-select: none;
}
.w-max {
width: stretch;
width: -webkit-fill-available;
width: -moz-available;
}
.pointer {
cursor: pointer;
}
.btn {
border: none;
outline: none;
cursor: pointer;
font-weight: 700;
font-size: 16px;
line-height: 20px;
padding: 8px 16px;
border-radius: 16px;
background: white;
color: black;
}
.btn-block {
width: 100%;
}
.btn-small {
font-size: 14px;
line-height: 18px;
padding: 4px 8px;
}
.btn-border {
border: 1px solid transparent;
color: inherit;
background: linear-gradient(black, black) padding-box, linear-gradient(94.73deg, #2bd9ff 0%, #f838d9 100%) border-box;
transition: 0.3s;
}
.btn-border:hover {
background: linear-gradient(black, black) padding-box, linear-gradient(94.73deg, #14b4d8 0%, #ba179f 100%) border-box;
}
.btn-primary {
background: #fff;
color: #0a0a0a;
}
.btn-primary:hover {
opacity: 0.9;
}
.btn-secondary {
color: white;
background: #222;
}
.btn-warning {
background: #ff563f;
color: white;
}
input[type="text"],
textarea,
input[type="datetime-local"],
input[type="password"],
input[type="number"] {
font-family: inherit;
border: unset;
background-color: unset;
color: inherit;
width: 100%;
font-size: 16px;
font-weight: 500;
}
input[type="number"],
select {
font-family: inherit;
border: unset;
background-color: #262626;
color: inherit;
width: 100%;
font-size: 16px;
font-weight: 500;
}
input[type="checkbox"] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
width: 20px;
height: 20px;
border-radius: 4px;
border: 2px solid #333;
background-color: transparent;
}
input[type="checkbox"]:after {
content: " ";
position: relative;
left: 40%;
top: 20%;
width: 15%;
height: 40%;
border: solid #fff;
border-width: 0 2px 2px 0;
transform: rotate(50deg);
display: none;
}
input[type="checkbox"]:checked:after {
display: block;
}
.plain-paper {
background: #171717;
border-radius: 16px;
padding: 8px 16px;
}
div.paper {
background: #171717;
border-radius: 16px;
padding: 8px 16px;
display: flex;
gap: 10px;
align-items: center;
@apply bg-layer-2 w-full font-medium px-4 py-2 rounded-xl;
}
.scroll-lock {
overflow: hidden;
height: 100vh;
}
.warning {
color: #ff563f;
}
.border-warning {
border: 1px solid #ff563f;
}
.dialog-overlay {
background-color: rgba(0, 0, 0, 0.8);
position: fixed;
inset: 0;
z-index: 99;
}
.dialog-content {
display: flex;
flex-direction: column;
gap: 12px;
z-index: 2;
background-color: #171717;
border-radius: 24px;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 500px;
max-width: 90vw;
max-height: 85vh;
overflow-y: auto;
z-index: 100;
}
.dialog-content .header-image {
width: 100%;
height: auto;
}
.dialog-content .content-inner {
width: 100%;
display: flex;
flex-direction: column;
padding: 25px;
box-sizing: border-box;
gap: 16px;
}
.dialog-content .username,
.dialog-content .username input {
width: 100%;
}
.dialog-content div.paper {
background: #262626;
width: 100%;
box-sizing: border-box;
}
.dialog-content h2 {
font-size: 24px;
font-weight: 500;
margin: 0;
}
.dialog-content h3 {
font-size: 16px;
font-weight: 500;
margin: 0;
margin-bottom: 24px;
}
.dialog-content small {
display: block;
color: #868686;
margin: 6px;
height: 100dvh;
}
.ctx-menu {
@ -354,7 +158,8 @@ div.paper {
.full-page-height .live-chat {
padding: 24px 16px 8px 24px;
border: 1px solid #171717;
border: 1px solid;
@apply border-layer-2;
border-radius: 24px;
height: inherit;
}

View File

@ -33,12 +33,14 @@ import { WasmOptimizer, WasmPath, wasmInit } from "./wasm";
const DashboardPage = lazy(() => import("./pages/dashboard"));
import Faq from "@/faq.md";
import MockPage from "./pages/mock";
const hasWasm = "WebAssembly" in globalThis;
const db = new SnortSystemDb();
const System = new NostrSystem({
db,
optimizer: hasWasm ? WasmOptimizer : undefined,
automaticOutboxModel: false,
});
export const Login = new LoginStore();
@ -57,7 +59,9 @@ async function doInit() {
db.ready = await db.isAvailable();
await System.Init();
try {
const req = await fetch("https://api.zap.stream/api/time");
const req = await fetch("https://api.zap.stream/api/time", {
signal: AbortSignal.timeout(1000)
});
const nowAtServer = (await req.json()).time as number;
const now = unixNowMs();
TimeSync = now - nowAtServer;
@ -75,6 +79,10 @@ const router = createBrowserRouter([
return null;
},
children: [
{
path: "/mock",
element: <MockPage />
},
{
path: "/",
element: <RootPage />,

View File

@ -1,4 +1,3 @@
import AsyncButton from "@/element/async-button";
import { ChatZap, LiveChat } from "@/element/live-chat";
import LiveVideoPlayer from "@/element/live-video-player";
import { MuteButton } from "@/element/mute-button";
@ -15,8 +14,9 @@ import { HTMLProps, ReactNode, useEffect, useMemo, useState } from "react";
import { FormattedMessage, FormattedNumber } from "react-intl";
import { Text } from "@/element/text";
import { StreamTimer } from "@/element/stream-time";
import * as Dialog from "@radix-ui/react-dialog";
import { DashboardRaidMenu } from "@/element/raid-menu";
import { DefaultButton } from "@/element/buttons";
import Modal from "@/element/modal";
export default function DashboardPage() {
const login = useLogin();
@ -77,7 +77,7 @@ function DashboardForLink({ link }: { link: NostrLink }) {
function DashboardCard(props: HTMLProps<HTMLDivElement>) {
return (
<div {...props} className={classNames("px-4 py-6 rounded-3xl border border-gray-1", props.className)}>
<div {...props} className={classNames("px-4 py-6 rounded-3xl border border-layer-1", props.className)}>
{props.children}
</div>
);
@ -91,8 +91,8 @@ function DashboardStatsCard({
return (
<div
{...props}
className={classNames("flex-1 bg-gray-1 flex flex-col gap-1 px-4 py-2 rounded-xl", props.className)}>
<div className="text-gray-3 font-medium">{name}</div>
className={classNames("flex-1 bg-layer-1 flex flex-col gap-1 px-4 py-2 rounded-xl", props.className)}>
<div className="text-layer-3 font-medium">{name}</div>
<div>{value}</div>
</div>
);
@ -106,13 +106,13 @@ function DashboardChatList({ link }: { link: NostrLink }) {
}, [feed]);
return pubkeys.map(a => (
<div className="flex justify-between items-center px-4 py-2 border-b border-gray-1">
<div className="flex justify-between items-center px-4 py-2 border-b border-layer-1">
<Profile pubkey={a} avatarSize={32} gap={4} />
<div className="flex gap-2">
<MuteButton pubkey={a} />
<AsyncButton onClick={() => {}} className="font-bold">
<DefaultButton onClick={() => { }} className="font-bold">
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
</AsyncButton>
</DefaultButton>
</div>
</div>
));
@ -144,7 +144,7 @@ function DashboardZapColumn({ link }: { link: NostrLink }) {
function DashboardHighlightZap({ zap }: { zap: ParsedZap }) {
return (
<div className="px-4 py-6 bg-gray-1 flex flex-col gap-4 rounded-xl animate-flash">
<div className="px-4 py-6 bg-layer-1 flex flex-col gap-4 rounded-xl animate-flash">
<div className="flex justify-between items-center text-zap text-2xl font-semibold">
<Profile
pubkey={zap.sender ?? "anon"}
@ -173,17 +173,13 @@ function DashboardHighlightZap({ zap }: { zap: ParsedZap }) {
function DashboardRaidButton({ link }: { link: NostrLink }) {
const [show, setShow] = useState(false);
return (
<Dialog.Root open={show} onOpenChange={setShow}>
<AsyncButton className="btn btn-primary" onClick={() => setShow(true)}>
<FormattedMessage defaultMessage="Raid" id="4iBdw1" />
</AsyncButton>
<Dialog.Portal>
<Dialog.Overlay className="dialog-overlay" />
<Dialog.Content className="dialog-content">
<DashboardRaidMenu link={link} onClose={() => setShow(false)} />
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
return (<>
<DefaultButton onClick={() => setShow(true)}>
<FormattedMessage defaultMessage="Raid" id="4iBdw1" />
</DefaultButton>
{show && <Modal id="raid-menu" onClose={() => setShow(false)}>
<DashboardRaidMenu link={link} onClose={() => setShow(false)} />
</Modal>}
</>
);
}

View File

@ -1,7 +1,6 @@
import "./layout.css";
import { CSSProperties, useEffect, useState, useSyncExternalStore } from "react";
import * as Dialog from "@radix-ui/react-dialog";
import { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
import { Helmet } from "react-helmet";
import { FormattedMessage } from "react-intl";
@ -17,8 +16,10 @@ import { Login } from "@/index";
import { useLang } from "@/hooks/lang";
import { AllLocales } from "@/intl";
import { NewVersion } from "@/serviceWorker";
import AsyncButton from "@/element/async-button";
import { trackEvent } from "@/utils";
import { BorderButton, DefaultButton } from "@/element/buttons";
import Modal from "@/element/modal";
import Logo from "@/element/logo";
export function LayoutPage() {
const navigate = useNavigate();
@ -109,24 +110,15 @@ export function LayoutPage() {
function loggedOut() {
if (login) return;
function handleLogin() {
setShowLogin(true);
}
return (
<Dialog.Root open={showLogin} onOpenChange={setShowLogin}>
<AsyncButton className="btn btn-border" onClick={handleLogin}>
<FormattedMessage defaultMessage="Login" id="AyGauy" />
<Icon name="login" />
</AsyncButton>
<Dialog.Portal>
<Dialog.Overlay className="dialog-overlay" />
<Dialog.Content className="dialog-content">
<LoginSignup close={() => setShowLogin(false)} />
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
return (<>
<BorderButton onClick={() => setShowLogin(true)}>
<FormattedMessage defaultMessage="Login" id="AyGauy" />
<Icon name="login" />
</BorderButton>
{showLogin && <Modal id="login">
<LoginSignup close={() => setShowLogin(false)} />
</Modal>}
</>
);
}
@ -141,16 +133,16 @@ export function LayoutPage() {
</Helmet>
<header>
<div
className="bg-white flex items-center pointer rounded-2xl aspect-square px-1"
className="bg-white text-black flex items-center cursor-pointer rounded-2xl aspect-square px-1"
onClick={() => navigate("/")}>
<img src="/zap-stream.svg" width={40} />
<Logo width={40} height={40} />
</div>
<div className="grow flex items-center gap-2"></div>
<div className="flex items-center gap-3">
<Link
to="https://discord.gg/Wtg6NVDdbT"
target="_blank"
className="flex items-center max-md:hidden gap-1 bg-gray-1 hover:bg-gray-2 font-bold p-2 rounded-xl">
className="flex items-center max-md:hidden gap-1 bg-layer-1 hover:bg-layer-2 font-bold p-2 rounded-xl">
<Icon name="link" />
Discord
</Link>
@ -182,9 +174,9 @@ function NewVersionBanner() {
<FormattedMessage defaultMessage="Refresh the page to use the latest version" id="Gmiwnd" />
</p>
</div>
<AsyncButton onClick={() => window.location.reload()} className="btn">
<DefaultButton onClick={() => window.location.reload()} className="btn">
<FormattedMessage defaultMessage="Refresh" id="rELDbB" />
</AsyncButton>
</DefaultButton>
</div>
);
}

25
src/pages/mock.tsx Normal file
View File

@ -0,0 +1,25 @@
import { LIVE_STREAM } from "@/const";
import { LiveChat } from "@/element/live-chat";
import { SendZapsDialog } from "@/element/send-zap";
import { EventBuilder, NostrLink } from "@snort/system";
export default function MockPage() {
const pubkey = "cf45a6ba1363ad7ed213a078e710d24115ae721c9b47bd1ebf4458eaefb4c2a5";
const fakeStream = new EventBuilder()
.kind(LIVE_STREAM)
.pubKey(pubkey)
.tag(["d", "mock"])
.tag(["title", "Example Stream"])
.tag(["summary", "An example mock stream for debugging"])
.tag(["streaming", "https://example.com/live.m3u8"])
.tag(["t", "nostr"])
.tag(["t", "mock"])
.processContent()
.build();
const fakeStreamLink = NostrLink.fromEvent(fakeStream);
return <div className="">
<LiveChat link={fakeStreamLink} ev={fakeStream} height={600} />
<SendZapsDialog lnurl="donate@snort.social" aTag={fakeStreamLink.toEventTag()![1]} pubkey={pubkey} />
</div>
}

View File

@ -1,8 +1,7 @@
import "./profile-page.css";
import { useMemo } from "react";
import { useNavigate, useParams } from "react-router-dom";
import * as Tabs from "@radix-ui/react-tabs";
import { NostrPrefix, ParsedZap, TaggedNostrEvent, encodeTLV, parseNostrLink } from "@snort/system";
import { CachedMetadata, NostrEvent, NostrLink, TaggedNostrEvent, parseNostrLink } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { unwrap } from "@snort/shared";
import { FormattedMessage } from "react-intl";
@ -13,128 +12,127 @@ import { VideoTile } from "@/element/video-tile";
import { FollowButton } from "@/element/follow-button";
import { MuteButton } from "@/element/mute-button";
import { useProfile } from "@/hooks/profile";
import useTopZappers from "@/hooks/top-zappers";
import { Text } from "@/element/text";
import { findTag } from "@/utils";
import { StatePill } from "@/element/state-pill";
import { Avatar } from "@/element/avatar";
import { ZapperRow } from "@/element/zapper-row";
import { StreamState } from "@/const";
import AsyncButton from "@/element/async-button";
function TopZappers({ zaps }: { zaps: ParsedZap[] }) {
const zappers = useTopZappers(zaps);
return (
<section className="flex flex-col gap-2">
{zappers.map(z => (
<ZapperRow key={z.pubkey} pubkey={z.pubkey} total={z.total} />
))}
</section>
);
}
import { DefaultButton } from "@/element/buttons";
import { useGoals } from "@/hooks/goals";
import { Goal } from "@/element/goal";
import { TopZappers } from "@/element/top-zappers";
import { useClips } from "@/hooks/clips";
const defaultBanner = "https://void.cat/d/Hn1AdN5UKmceuDkgDW847q.webp";
export function ProfilePage() {
const navigate = useNavigate();
const params = useParams();
const link = parseNostrLink(unwrap(params.npub));
const profile = useUserProfile(link.id);
const zapTarget = profile?.lud16 ?? profile?.lud06;
const { streams, zaps } = useProfile(link, true);
const liveEvent = useMemo(() => {
return streams.find(ev => findTag(ev, "status") === StreamState.Live);
}, [streams]);
const profile = useUserProfile(link.id);
const pastStreams = useMemo(() => {
return streams.filter(ev => findTag(ev, "status") === StreamState.Ended);
}, [streams]);
const futureStreams = useMemo(() => {
return streams.filter(ev => findTag(ev, "status") === StreamState.Planned);
}, [streams]);
const isLive = Boolean(liveEvent);
function goToLive() {
if (liveEvent) {
const d = findTag(liveEvent, "d") || "";
const naddr = encodeTLV(NostrPrefix.Address, d, undefined, liveEvent.kind, liveEvent.pubkey);
navigate(`/${naddr}`);
}
}
return (
<div className="flex flex-col gap-3 max-sm:px-4">
<div className="flex flex-col gap-3 px-4">
<img
className="rounded-xl object-cover h-[360px]"
alt={profile?.name || link.id}
src={profile?.banner ? profile?.banner : defaultBanner}
/>
<div className="flex justify-between">
<div className="flex items-center gap-3">
<div className="relative flex flex-col items-center">
<Avatar user={profile} pubkey={link.id} size={88} className="border border-4" />
{isLive && <StatePill state={StreamState.Live} onClick={goToLive} className="absolute bottom-0 -mb-2" />}
</div>
<div className="flex flex-col gap-1">
{profile?.name && <h1 className="name">{profile.name}</h1>}
{profile?.about && (
<p className="text-neutral-400">
<Text content={profile.about} tags={[]} />
</p>
)}
<ProfileHeader link={link} profile={profile} streams={streams} />
<div className="grid lg:grid-cols-2 gap-4 py-2">
<div>
<h3 className="text-xl py-2">
<FormattedMessage defaultMessage="All Time Top Zappers" id="FIDK5Y" />
</h3>
<div className="flex flex-col gap-4">
<TopZappers zaps={zaps} limit={10} avatarSize={40} showName={true} />
</div>
</div>
<div className="flex gap-2 items-center">
{zapTarget && (
<SendZapsDialog
aTag={liveEvent ? `${liveEvent.kind}:${liveEvent.pubkey}:${findTag(liveEvent, "d")}` : undefined}
lnurl={zapTarget}
button={
<AsyncButton className="btn">
<Icon name="zap-filled" className="zap-button-icon" />
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
</AsyncButton>
}
targetName={profile?.name || link.id}
/>
)}
<FollowButton pubkey={link.id} />
<MuteButton pubkey={link.id} />
<div>
<h3 className="text-xl py-2">
<FormattedMessage defaultMessage="Zap Goals" id="LEmxc8" />
</h3>
<div className="flex flex-col gap-2">
<ProfileZapGoals link={link} />
</div>
</div>
</div>
<Tabs.Root className="tabs-root" defaultValue="top-zappers">
<Tabs.List className="tabs-list" aria-label={`Information about ${profile ? profile.name : link.id}`}>
<Tabs.Trigger className="tabs-tab" value="top-zappers">
<FormattedMessage defaultMessage="Top Zappers" id="dVD/AR" />
<div className="tab-border"></div>
</Tabs.Trigger>
<Tabs.Trigger className="tabs-tab" value="past-streams">
<FormattedMessage defaultMessage="Past Streams" id="UfSot5" />
<div className="tab-border"></div>
</Tabs.Trigger>
<Tabs.Trigger className="tabs-tab" value="schedule">
<FormattedMessage defaultMessage="Schedule" id="hGQqkW" />
<div className="tab-border"></div>
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content className="tabs-content" value="top-zappers">
<TopZappers zaps={zaps} />
</Tabs.Content>
<Tabs.Content className="tabs-content" value="past-streams">
<ProfileStreamList streams={pastStreams} />
</Tabs.Content>
<Tabs.Content className="tabs-content" value="schedule">
<ProfileStreamList streams={futureStreams} />
</Tabs.Content>
</Tabs.Root>
<h1>
<FormattedMessage defaultMessage="Recent Clips" id="XMGfiA" />
</h1>
<div className="flex gap-4">
<ProfileClips link={link} />
</div>
<h1>
<FormattedMessage defaultMessage="Past Streams" id="UfSot5" />
</h1>
<ProfileStreamList streams={pastStreams} />
</div>
);
}
function ProfileHeader({ profile, link, streams }: { profile?: CachedMetadata, link: NostrLink, streams: Array<NostrEvent> }) {
const navigate = useNavigate();
const liveEvent = useMemo(() => {
return streams.find(ev => findTag(ev, "status") === StreamState.Live);
}, [streams]);
const zapTarget = profile?.lud16 ?? profile?.lud06;
const isLive = Boolean(liveEvent);
function goToLive() {
if (liveEvent) {
const evLink = NostrLink.fromEvent(liveEvent);
navigate(`/${evLink.encode()}`);
}
}
return <div className="flex max-sm:flex-col gap-3 justify-between">
<div className="flex items-center gap-3">
<div className="relative flex flex-col items-center">
<Avatar user={profile} pubkey={link.id} size={88} className="border border-4" />
{isLive && <StatePill state={StreamState.Live} onClick={goToLive} className="absolute bottom-0 -mb-2" />}
</div>
<div className="flex flex-col gap-1">
{profile?.name && <h1 className="name">{profile.name}</h1>}
{profile?.about && (
<p className="text-neutral-400">
<Text content={profile.about} tags={[]} />
</p>
)}
</div>
</div>
<div className="flex gap-2 items-center">
{zapTarget && (
<SendZapsDialog
aTag={liveEvent ? `${liveEvent.kind}:${liveEvent.pubkey}:${findTag(liveEvent, "d")}` : undefined}
lnurl={zapTarget}
button={
<DefaultButton>
<Icon name="zap-filled" className="zap-button-icon" />
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
</DefaultButton>
}
targetName={profile?.name || link.id}
/>
)}
<FollowButton pubkey={link.id} />
<MuteButton pubkey={link.id} />
</div>
</div>
}
function ProfileStreamList({ streams }: { streams: Array<TaggedNostrEvent> }) {
if (streams.length === 0) {
return <FormattedMessage defaultMessage="No streams yet" id="0rVLjV" />
}
return (
<div className="flex gap-3 flex-wrap justify-center">
<div className="grid gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-8">
{streams.map(ev => (
<div key={ev.id} className="flex flex-col gap-1 sm:w-64 w-full">
<div key={ev.id} className="flex flex-col gap-1">
<VideoTile ev={ev} showAuthor={false} showStatus={false} />
<span className="text-neutral-500">
<FormattedMessage
@ -150,3 +148,28 @@ function ProfileStreamList({ streams }: { streams: Array<TaggedNostrEvent> }) {
</div>
);
}
function ProfileZapGoals({ link }: { link: NostrLink }) {
const limit = 5;
const goals = useGoals(link.id, false, limit);
if (goals.length === 0) {
return <FormattedMessage defaultMessage="No goals yet" id="ZaNcK4" />
}
return goals
.sort((a, b) => a.created_at > b.created_at ? -1 : 1)
.slice(0, limit)
.map(a => <div key={a.id} className="bg-layer-1 rounded-xl px-4 py-3">
<Goal ev={a} confetti={false} />
</div>);
}
function ProfileClips({ link }: { link: NostrLink }) {
const clips = useClips(link, 10);
if (clips.length === 0) {
return <FormattedMessage defaultMessage="No clips yet" id="ObZZEz" />
}
return clips.map(a => {
const r = findTag(a, "r");
return <video src={r} />
})
}

View File

@ -6,7 +6,7 @@ import Owncast from "@/owncast.png";
import Cloudflare from "@/cloudflare.png";
import { ConfigureOwncast } from "./owncast";
import { ConfigureNostrType } from "./nostr";
import AsyncButton from "@/element/async-button";
import { DefaultButton } from "@/element/buttons";
export function StreamProvidersPage() {
const navigate = useNavigate();
@ -38,9 +38,9 @@ export function StreamProvidersPage() {
<div className="paper">
<h3>{mapName(p)}</h3>
{mapLogo(p)}
<AsyncButton className="btn btn-border" onClick={() => navigate(p)}>
<DefaultButton onClick={() => navigate(p)}>
+ Configure
</AsyncButton>
</DefaultButton>
</div>
);
}

View File

@ -2,11 +2,11 @@ import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { FormattedMessage } from "react-intl";
import AsyncButton from "@/element/async-button";
import { StatePill } from "@/element/state-pill";
import { StreamProviderInfo, StreamProviderStore } from "@/providers";
import { NostrStreamProvider } from "@/providers/zsz";
import { StreamState } from "@/const";
import { DefaultButton } from "@/element/buttons";
export function ConfigureNostrType() {
const [url, setUrl] = useState("");
@ -55,14 +55,13 @@ export function ConfigureNostrType() {
</div>
)}
<div>
<AsyncButton
className="btn btn-border"
<DefaultButton
onClick={() => {
StreamProviderStore.add(new NostrStreamProvider(new URL(url).host, url));
navigate("/");
}}>
<FormattedMessage defaultMessage="Save" id="jvo0vs" />
</AsyncButton>
</DefaultButton>
</div>
</>
);
@ -77,9 +76,9 @@ export function ConfigureNostrType() {
<input type="text" placeholder="https://" value={url} onChange={e => setUrl(e.target.value)} />
</div>
</div>
<AsyncButton className="btn btn-primary" onClick={tryConnect}>
<DefaultButton onClick={tryConnect}>
<FormattedMessage defaultMessage="Connect" id="+vVZ/G" />
</AsyncButton>
</DefaultButton>
</div>
<div>{status()}</div>
</div>

View File

@ -1,11 +1,11 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import AsyncButton from "@/element/async-button";
import { StatePill } from "@/element/state-pill";
import { StreamProviderInfo, StreamProviderStore } from "@/providers";
import { OwncastProvider } from "@/providers/owncast";
import { StreamState } from "@/const";
import { DefaultButton } from "@/element/buttons";
export function ConfigureOwncast() {
const [url, setUrl] = useState("");
@ -55,14 +55,13 @@ export function ConfigureOwncast() {
</div>
)}
<div>
<AsyncButton
className="btn btn-border"
<DefaultButton
onClick={() => {
StreamProviderStore.add(new OwncastProvider(url, token));
navigate("/");
}}>
Save
</AsyncButton>
</DefaultButton>
</div>
</>
);
@ -83,9 +82,9 @@ export function ConfigureOwncast() {
<input type="password" value={token} onChange={e => setToken(e.target.value)} />
</div>
</div>
<AsyncButton className="btn btn-primary" onClick={tryConnect}>
<DefaultButton onClick={tryConnect}>
Connect
</AsyncButton>
</DefaultButton>
</div>
<div>{status()}</div>
</div>

View File

@ -61,7 +61,7 @@
.one-line:before,
.one-line:after {
background-color: #171717;
@apply bg-layer-2;
}
::-webkit-scrollbar {

View File

@ -11,11 +11,11 @@ import { Login } from "..";
import { StatePill } from "@/element/state-pill";
import { NostrStreamProvider } from "@/providers";
import { StreamState } from "@/const";
import AsyncButton from "@/element/async-button";
import { Layer1Button } from "@/element/buttons";
const enum Tab {
Account,
Notifications,
Stream,
}
export function SettingsPage() {
@ -51,7 +51,9 @@ export function SettingsPage() {
<p>
<FormattedMessage defaultMessage="Private key" id="Bep/gA" />
</p>
<Copy text={hexToBech32("nsec", login.privateKey)} />
<Layer1Button>
<FormattedMessage defaultMessage="Copy" id="4l6vz1" />
</Layer1Button>
</div>
)}
<h1>
@ -69,20 +71,24 @@ export function SettingsPage() {
onClick={() => Login.setColor(a)}></div>
))}
</div>
<h1>
<FormattedMessage defaultMessage="Stream Key" id="LknBsU" />
</h1>
<div className="flex flex-col gap-4">
<NostrProviderDialog
provider={unwrap(providers.find(a => a.name === "zap.stream")) as NostrStreamProvider}
showEndpoints={true}
showEditor={false}
showForwards={true}
/>
</div>
</>
);
}
case Tab.Stream: {
return <>
<h1>
<FormattedMessage defaultMessage="Stream" id="uYw2LD" />
</h1>
<div className="flex flex-col gap-4">
<NostrProviderDialog
provider={unwrap(providers.find(a => a.name === "zap.stream")) as NostrStreamProvider}
showEndpoints={true}
showEditor={false}
showForwards={true}
/>
</div>
</>
}
}
}
@ -90,6 +96,8 @@ export function SettingsPage() {
switch (t) {
case Tab.Account:
return <FormattedMessage defaultMessage="Account" id="TwyMau" />;
case Tab.Stream:
return <FormattedMessage defaultMessage="Stream" id="uYw2LD" />;
}
}
@ -101,13 +109,13 @@ export function SettingsPage() {
</h1>
<div className="flex flex-col gap-2">
<div className="flex gap-2">
{[Tab.Account].map(t => (
<AsyncButton onClick={() => setTab(t)} className="rounded-xl px-3 py-2 bg-gray-2 hover:bg-gray-1">
{[Tab.Account, Tab.Stream].map(t => (
<Layer1Button onClick={() => setTab(t)} className={t === tab ? "active" : ""}>
{tabName(t)}
</AsyncButton>
</Layer1Button>
))}
</div>
<div className="p-5 bg-gray-2 rounded-3xl flex flex-col gap-3">{tabContent()}</div>
<div className="p-5 bg-layer-1 rounded-3xl flex flex-col gap-3">{tabContent()}</div>
</div>
</div>
</div>

View File

@ -11,7 +11,6 @@ const LiveVideoPlayer = lazy(() => import("@/element/live-video-player"));
import { extractStreamInfo, findTag, getEventFromLocationState, getHost } from "@/utils";
import { Profile, getName } from "@/element/profile";
import { LiveChat } from "@/element/live-chat";
import AsyncButton from "@/element/async-button";
import { useLogin } from "@/hooks/login";
import { useZapGoal } from "@/hooks/goals";
import { SendZapsDialog } from "@/element/send-zap";
@ -29,6 +28,8 @@ import { FollowButton } from "@/element/follow-button";
import { ClipButton } from "@/element/clip-button";
import { StreamState } from "@/const";
import { NotificationsButton } from "@/element/notifications-button";
import { WarningButton } from "@/element/buttons";
import Pill from "@/element/pill";
function ProfileInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedNostrEvent }) {
const system = useContext(SnortContext);
@ -60,22 +61,22 @@ function ProfileInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedNostrEv
<p>{summary}</p>
<div className="tags">
<StatePill state={status as StreamState} />
<span className="pill bg-gray-1">
<Pill>
<FormattedMessage defaultMessage="{n} viewers" id="3adEeb" values={{ n: formatSats(viewers) }} />
</span>
</Pill>
{status === StreamState.Live && (
<span className="pill bg-gray-1">
<Pill>
<StreamTimer ev={ev} />
</span>
</Pill>
)}
{ev && <Tags ev={ev} />}
</div>
{isMine && (
<div className="actions">
{ev && <NewStreamDialog text="Edit" ev={ev} btnClassName="btn" />}
<AsyncButton type="button" className="btn btn-warning" onClick={deleteStream}>
<WarningButton onClick={deleteStream}>
<FormattedMessage defaultMessage="Delete" id="K3r6DQ" />
</AsyncButton>
</WarningButton>
</div>
)}
</div>

View File

@ -14,7 +14,7 @@ import { Views } from "./widgets/views";
import { Music } from "./widgets/music";
import groupBy from "lodash/groupBy";
import { hexToBech32 } from "@snort/shared";
import AsyncButton from "@/element/async-button";
import { DefaultButton } from "@/element/buttons";
interface ZapAlertConfigurationProps {
npub: string;
@ -153,9 +153,9 @@ function ZapAlertConfiguration({ npub, baseUrl }: ZapAlertConfigurationProps) {
onChange={ev => setTestText(ev.target.value)}
/>
</div>
<AsyncButton disabled={testText.length === 0} className="btn" onClick={testVoice}>
<DefaultButton disabled={testText.length === 0} onClick={testVoice}>
<FormattedMessage defaultMessage="Test voice" id="d5zWyh" />
</AsyncButton>
</DefaultButton>
</>
)}
</>

View File

@ -14,7 +14,7 @@ export function TopZappersWidget({ link }: { link: NostrLink }) {
<FormattedMessage defaultMessage="Top Zappers" id="dVD/AR" />
</div>
<div className="flex gap-1">
<TopZappers zaps={zaps} limit={3} />
<TopZappers zaps={zaps} limit={3} className="border rounded-full px-2 py-1 border-layer-1 font-bold" />
</div>
</div>
);

View File

@ -98,10 +98,13 @@ interface StreamInfo {
starts?: string;
ends?: string;
service?: string;
host?: string;
}
export function extractStreamInfo(ev?: NostrEvent) {
const ret = {} as StreamInfo;
const ret = {
host: getHost(ev)
} as StreamInfo;
const matchTag = (tag: Array<string>, k: string, into: (v: string) => void) => {
if (tag[0] === k) {
into(tag[1]);

View File

@ -4,12 +4,14 @@ module.exports = {
theme: {
extend: {
colors: {
"gray-1": "#171717",
"gray-2": "#222",
"gray-3": "#797979",
"layer-1": "rgb(23 23 23 / <alpha-value>)",
"layer-2": "rgb(34 34 34 / <alpha-value>)",
"layer-3": "rgb(50 50 50 / <alpha-value>)",
primary: "var(--primary)",
secondary: "var(--secondary)",
zap: "var(--zap)",
success: "rgb(0 127 0 / <alpha-value>)",
warning: "rgb(255 86 63 / <alpha-value>)"
},
animation: {
"ping-once": "ping 1s cubic-bezier(0, 0, 0.2, 1);",

769
yarn.lock
View File

@ -1467,7 +1467,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2":
"@babel/runtime@npm:^7.11.2, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2":
version: 7.22.11
resolution: "@babel/runtime@npm:7.22.11"
dependencies:
@ -2052,13 +2052,6 @@ __metadata:
languageName: node
linkType: hard
"@juggle/resize-observer@npm:^3.3.1":
version: 3.4.0
resolution: "@juggle/resize-observer@npm:3.4.0"
checksum: 2505028c05cc2e17639fcad06218b1c4b60f932a4ebb4b41ab546ef8c157031ae377e3f560903801f6d01706dbefd4943b6c4704bf19ed86dfa1c62f1473a570
languageName: node
linkType: hard
"@noble/curves@npm:^1.2.0":
version: 1.2.0
resolution: "@noble/curves@npm:1.2.0"
@ -2118,457 +2111,6 @@ __metadata:
languageName: node
linkType: hard
"@radix-ui/primitive@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/primitive@npm:1.0.1"
dependencies:
"@babel/runtime": ^7.13.10
checksum: 2b93e161d3fdabe9a64919def7fa3ceaecf2848341e9211520c401181c9eaebb8451c630b066fad2256e5c639c95edc41de0ba59c40eff37e799918d019822d1
languageName: node
linkType: hard
"@radix-ui/react-collapsible@npm:^1.0.3":
version: 1.0.3
resolution: "@radix-ui/react-collapsible@npm:1.0.3"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/primitive": 1.0.1
"@radix-ui/react-compose-refs": 1.0.1
"@radix-ui/react-context": 1.0.1
"@radix-ui/react-id": 1.0.1
"@radix-ui/react-presence": 1.0.1
"@radix-ui/react-primitive": 1.0.3
"@radix-ui/react-use-controllable-state": 1.0.1
"@radix-ui/react-use-layout-effect": 1.0.1
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 26976e4a72a3e0f4b2c62af2898b3e205c3652af46a3b41cda9a43567fe8381d9ef6afb0b29e3214c450b847f4f2099a533cffc5045844ecab290e9fa6114ca9
languageName: node
linkType: hard
"@radix-ui/react-collection@npm:1.0.3":
version: 1.0.3
resolution: "@radix-ui/react-collection@npm:1.0.3"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/react-compose-refs": 1.0.1
"@radix-ui/react-context": 1.0.1
"@radix-ui/react-primitive": 1.0.3
"@radix-ui/react-slot": 1.0.2
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: acfbc9b0b2c553d343c22f02c9f098bc5cfa99e6e48df91c0d671855013f8b877ade9c657b7420a7aa523b5aceadea32a60dd72c23b1291f415684fb45d00cff
languageName: node
linkType: hard
"@radix-ui/react-compose-refs@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-compose-refs@npm:1.0.1"
dependencies:
"@babel/runtime": ^7.13.10
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 2b9a613b6db5bff8865588b6bf4065f73021b3d16c0a90b2d4c23deceeb63612f1f15de188227ebdc5f88222cab031be617a9dd025874c0487b303be3e5cc2a8
languageName: node
linkType: hard
"@radix-ui/react-context@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-context@npm:1.0.1"
dependencies:
"@babel/runtime": ^7.13.10
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 60e9b81d364f40c91a6213ec953f7c64fcd9d75721205a494a5815b3e5ae0719193429b62ee6c7002cd6aaf70f8c0e2f08bdbaba9ffcc233044d32b56d2127d1
languageName: node
linkType: hard
"@radix-ui/react-dialog@npm:^1.0.4":
version: 1.0.4
resolution: "@radix-ui/react-dialog@npm:1.0.4"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/primitive": 1.0.1
"@radix-ui/react-compose-refs": 1.0.1
"@radix-ui/react-context": 1.0.1
"@radix-ui/react-dismissable-layer": 1.0.4
"@radix-ui/react-focus-guards": 1.0.1
"@radix-ui/react-focus-scope": 1.0.3
"@radix-ui/react-id": 1.0.1
"@radix-ui/react-portal": 1.0.3
"@radix-ui/react-presence": 1.0.1
"@radix-ui/react-primitive": 1.0.3
"@radix-ui/react-slot": 1.0.2
"@radix-ui/react-use-controllable-state": 1.0.1
aria-hidden: ^1.1.1
react-remove-scroll: 2.5.5
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 01ad549a3685e221628950e6fbec306494170aa3b92cbe00732b1531c16e1cf681138cd4a79d658f4f97d4096676a40d08642090fdea1675d0b7dc78df66d962
languageName: node
linkType: hard
"@radix-ui/react-direction@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-direction@npm:1.0.1"
dependencies:
"@babel/runtime": ^7.13.10
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 5336a8b0d4f1cde585d5c2b4448af7b3d948bb63a1aadb37c77771b0e5902dc6266e409cf35fd0edaca7f33e26424be19e64fb8f9d7f7be2d6f1714ea2764210
languageName: node
linkType: hard
"@radix-ui/react-dismissable-layer@npm:1.0.4":
version: 1.0.4
resolution: "@radix-ui/react-dismissable-layer@npm:1.0.4"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/primitive": 1.0.1
"@radix-ui/react-compose-refs": 1.0.1
"@radix-ui/react-primitive": 1.0.3
"@radix-ui/react-use-callback-ref": 1.0.1
"@radix-ui/react-use-escape-keydown": 1.0.3
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: ea86004ed56a10609dd84eef39dc1e57b400d687a35be41bb4aaa06dc7ad6dbd0a8da281e08c8c077fdbd523122e4d860cb7438a60c664f024f77c8b41299ec6
languageName: node
linkType: hard
"@radix-ui/react-focus-guards@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-focus-guards@npm:1.0.1"
dependencies:
"@babel/runtime": ^7.13.10
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 1f8ca8f83b884b3612788d0742f3f054e327856d90a39841a47897dbed95e114ee512362ae314177de226d05310047cabbf66b686ae86ad1b65b6b295be24ef7
languageName: node
linkType: hard
"@radix-ui/react-focus-scope@npm:1.0.3":
version: 1.0.3
resolution: "@radix-ui/react-focus-scope@npm:1.0.3"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/react-compose-refs": 1.0.1
"@radix-ui/react-primitive": 1.0.3
"@radix-ui/react-use-callback-ref": 1.0.1
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: e5b1a089071fbe77aca11124a4ad9623fc2bcaf4c019759b0cd044bf0878ecc924131ee09c6a22d38a3f094684ef68ed18fa65c8d891918412e0afc685a464e0
languageName: node
linkType: hard
"@radix-ui/react-id@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-id@npm:1.0.1"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/react-use-layout-effect": 1.0.1
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 446a453d799cc790dd2a1583ff8328da88271bff64530b5a17c102fa7fb35eece3cf8985359d416f65e330cd81aa7b8fe984ea125fc4f4eaf4b3801d698e49fe
languageName: node
linkType: hard
"@radix-ui/react-portal@npm:1.0.3":
version: 1.0.3
resolution: "@radix-ui/react-portal@npm:1.0.3"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/react-primitive": 1.0.3
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: d352bcd6ad65eb43c9e0d72d0755c2aae85e03fb287770866262be3a2d5302b2885aee3cd99f2bbf62ecd14fcb1460703f1dcdc40351f77ad887b931c6f0012a
languageName: node
linkType: hard
"@radix-ui/react-presence@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-presence@npm:1.0.1"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/react-compose-refs": 1.0.1
"@radix-ui/react-use-layout-effect": 1.0.1
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: ed2ff9faf9e4257a4065034d3771459e5a91c2d840b2fcec94661761704dbcb65bcdd927d28177a2a129b3dab5664eb90a9b88309afe0257a9f8ba99338c0d95
languageName: node
linkType: hard
"@radix-ui/react-primitive@npm:1.0.3":
version: 1.0.3
resolution: "@radix-ui/react-primitive@npm:1.0.3"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/react-slot": 1.0.2
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 9402bc22923c8e5c479051974a721c301535c36521c0237b83e5fa213d013174e77f3ad7905e6d60ef07e14f88ec7f4ea69891dc7a2b39047f8d3640e8f8d713
languageName: node
linkType: hard
"@radix-ui/react-progress@npm:^1.0.3":
version: 1.0.3
resolution: "@radix-ui/react-progress@npm:1.0.3"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/react-context": 1.0.1
"@radix-ui/react-primitive": 1.0.3
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: a4398812315ae5b25f8637a6553daf85bd36e6e08d15f7486248538a994a99268752e2c21e4af15277c17e861c1825fdd7292938c515c112dc2cdce63ec1a892
languageName: node
linkType: hard
"@radix-ui/react-roving-focus@npm:1.0.4":
version: 1.0.4
resolution: "@radix-ui/react-roving-focus@npm:1.0.4"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/primitive": 1.0.1
"@radix-ui/react-collection": 1.0.3
"@radix-ui/react-compose-refs": 1.0.1
"@radix-ui/react-context": 1.0.1
"@radix-ui/react-direction": 1.0.1
"@radix-ui/react-id": 1.0.1
"@radix-ui/react-primitive": 1.0.3
"@radix-ui/react-use-callback-ref": 1.0.1
"@radix-ui/react-use-controllable-state": 1.0.1
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 69b1c82c2d9db3ba71549a848f2704200dab1b2cd22d050c1e081a78b9a567dbfdc7fd0403ee010c19b79652de69924d8ca2076cd031d6552901e4213493ffc7
languageName: node
linkType: hard
"@radix-ui/react-slot@npm:1.0.2":
version: 1.0.2
resolution: "@radix-ui/react-slot@npm:1.0.2"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/react-compose-refs": 1.0.1
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: edf5edf435ff594bea7e198bf16d46caf81b6fb559493acad4fa8c308218896136acb16f9b7238c788fd13e94a904f2fd0b6d834e530e4cae94522cdb8f77ce9
languageName: node
linkType: hard
"@radix-ui/react-tabs@npm:^1.0.4":
version: 1.0.4
resolution: "@radix-ui/react-tabs@npm:1.0.4"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/primitive": 1.0.1
"@radix-ui/react-context": 1.0.1
"@radix-ui/react-direction": 1.0.1
"@radix-ui/react-id": 1.0.1
"@radix-ui/react-presence": 1.0.1
"@radix-ui/react-primitive": 1.0.3
"@radix-ui/react-roving-focus": 1.0.4
"@radix-ui/react-use-controllable-state": 1.0.1
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: 1daf0550da3ba527c1c2d8d7efd3a6618628f1f101a40f16c62eafb28df64a6bc7ee17ccb970b883907f99d601864c8f3c229c05e7bc7faf7f8c95b087141353
languageName: node
linkType: hard
"@radix-ui/react-toggle@npm:^1.0.3":
version: 1.0.3
resolution: "@radix-ui/react-toggle@npm:1.0.3"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/primitive": 1.0.1
"@radix-ui/react-primitive": 1.0.3
"@radix-ui/react-use-controllable-state": 1.0.1
peerDependencies:
"@types/react": "*"
"@types/react-dom": "*"
react: ^16.8 || ^17.0 || ^18.0
react-dom: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
"@types/react-dom":
optional: true
checksum: ed5407f48254f20cda542017774f259d0b2c0007ea4bd7287d10d751016dbf269cb13d1142591432c269c3ab768cde2f1ba0344743027d36bbec10af909f19de
languageName: node
linkType: hard
"@radix-ui/react-use-callback-ref@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-use-callback-ref@npm:1.0.1"
dependencies:
"@babel/runtime": ^7.13.10
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: b9fd39911c3644bbda14a84e4fca080682bef84212b8d8931fcaa2d2814465de242c4cfd8d7afb3020646bead9c5e539d478cea0a7031bee8a8a3bb164f3bc4c
languageName: node
linkType: hard
"@radix-ui/react-use-controllable-state@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-use-controllable-state@npm:1.0.1"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/react-use-callback-ref": 1.0.1
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: dee2be1937d293c3a492cb6d279fc11495a8f19dc595cdbfe24b434e917302f9ac91db24e8cc5af9a065f3f209c3423115b5442e65a5be9fd1e9091338972be9
languageName: node
linkType: hard
"@radix-ui/react-use-escape-keydown@npm:1.0.3":
version: 1.0.3
resolution: "@radix-ui/react-use-escape-keydown@npm:1.0.3"
dependencies:
"@babel/runtime": ^7.13.10
"@radix-ui/react-use-callback-ref": 1.0.1
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: c6ed0d9ce780f67f924980eb305af1f6cce2a8acbaf043a58abe0aa3cc551d9aa76ccee14531df89bbee302ead7ecc7fce330886f82d4672c5eda52f357ef9b8
languageName: node
linkType: hard
"@radix-ui/react-use-layout-effect@npm:1.0.1":
version: 1.0.1
resolution: "@radix-ui/react-use-layout-effect@npm:1.0.1"
dependencies:
"@babel/runtime": ^7.13.10
peerDependencies:
"@types/react": "*"
react: ^16.8 || ^17.0 || ^18.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: bed9c7e8de243a5ec3b93bb6a5860950b0dba359b6680c84d57c7a655e123dec9b5891c5dfe81ab970652e7779fe2ad102a23177c7896dde95f7340817d47ae5
languageName: node
linkType: hard
"@react-dnd/asap@npm:^5.0.1":
version: 5.0.2
resolution: "@react-dnd/asap@npm:5.0.2"
@ -2590,37 +2132,6 @@ __metadata:
languageName: node
linkType: hard
"@react-hook/latest@npm:^1.0.2":
version: 1.0.3
resolution: "@react-hook/latest@npm:1.0.3"
peerDependencies:
react: ">=16.8"
checksum: 2408c9cd35c5cfa7697b6da3bc5eebef254a932ade70955074c474f23be7dd3e2f81bbba12edcc9208bd0f89c6ed366d6b11d4f6d7b1052877a0bac8f74afad4
languageName: node
linkType: hard
"@react-hook/passive-layout-effect@npm:^1.2.0":
version: 1.2.1
resolution: "@react-hook/passive-layout-effect@npm:1.2.1"
peerDependencies:
react: ">=16.8"
checksum: 217cb8aa90fb8e677672319a9a466d7752890cf4357c76df000b207696e9cc717cf2ee88080671cc9dae238a82f92093ab4f61ab2f6032d2a8db958fc7d99b5d
languageName: node
linkType: hard
"@react-hook/resize-observer@npm:^1.2.6":
version: 1.2.6
resolution: "@react-hook/resize-observer@npm:1.2.6"
dependencies:
"@juggle/resize-observer": ^3.3.1
"@react-hook/latest": ^1.0.2
"@react-hook/passive-layout-effect": ^1.2.0
peerDependencies:
react: ">=16.8"
checksum: d2ff6c50e847514acad774f2a4010fb1e6782a231ae00c9507c1a98028b3a26399e35f094170918f11b1eeafc581d60da7641bc178d496abf00c56eee8a6b36b
languageName: node
linkType: hard
"@remix-run/router@npm:1.8.0":
version: 1.8.0
resolution: "@remix-run/router@npm:1.8.0"
@ -2791,9 +2302,9 @@ __metadata:
languageName: node
linkType: hard
"@snort/shared@npm:^1.0.11":
version: 1.0.11
resolution: "@snort/shared@npm:1.0.11"
"@snort/shared@npm:^1.0.14":
version: 1.0.14
resolution: "@snort/shared@npm:1.0.14"
dependencies:
"@noble/curves": ^1.2.0
"@noble/hashes": ^1.3.2
@ -2801,32 +2312,18 @@ __metadata:
debug: ^4.3.4
eventemitter3: ^5.0.1
light-bolt11-decoder: ^3.0.0
checksum: 4a5441c9a1b2636283a9d39533821fcf4606dbbfbb39329aa814e6512282adf48768e761553e906f0c9c62c3862a0cb48b05a5a84cc871562a2d1120c7a902ac
checksum: 9f260aecc0f35232471259b68071183fda74cd4f0db93ad8b50d3af07f106a434896452c196db8d954caf16795fa182770f2f3c12ee7e34b5e36b83694453bb8
languageName: node
linkType: hard
"@snort/shared@npm:^1.0.12":
version: 1.0.12
resolution: "@snort/shared@npm:1.0.12"
"@snort/system-react@npm:^1.2.12":
version: 1.2.12
resolution: "@snort/system-react@npm:1.2.12"
dependencies:
"@noble/curves": ^1.2.0
"@noble/hashes": ^1.3.2
"@scure/base": ^1.1.2
debug: ^4.3.4
eventemitter3: ^5.0.1
light-bolt11-decoder: ^3.0.0
checksum: b31ee1a1977db5c7fa2e5d29f364a956e6de38fab3d8b0e8f2b233a362e7a1095ee615b839c0c2a7d8d48b1c3445ec7cecc0f8470cf45863d0ddeb673053fcbb
languageName: node
linkType: hard
"@snort/system-react@npm:^1.2.1":
version: 1.2.1
resolution: "@snort/system-react@npm:1.2.1"
dependencies:
"@snort/shared": ^1.0.12
"@snort/system": ^1.2.1
"@snort/shared": ^1.0.14
"@snort/system": ^1.2.12
react: ^18.2.0
checksum: 7f354f2251fdd4ed8e2dace0dd54c8d0b06de2d5c5eac5468850ac6416bf24d97a16fb0d6fff211a34b2fb22899f4b01c1ecc7de210066709d5b85fb436dc906
checksum: e0fa13c7d9efbd4cb5e226386db329f5953f9868dd1be3cf306f7fe249bf08ff32dbbaf624a6b67fa2c61309af29d783a098f3f7cfc394a18d04d33a558a668d
languageName: node
linkType: hard
@ -2837,44 +2334,25 @@ __metadata:
languageName: node
linkType: hard
"@snort/system-web@npm:^1.0.4":
version: 1.0.4
resolution: "@snort/system-web@npm:1.0.4"
"@snort/system-web@npm:^1.2.11":
version: 1.2.11
resolution: "@snort/system-web@npm:1.2.11"
dependencies:
"@snort/shared": ^1.0.11
"@snort/system": ^1.2.0
"@snort/shared": ^1.0.14
"@snort/system": ^1.2.11
dexie: ^3.2.4
checksum: ee705a586009cdc35c167aaeb4e505afac4d4c5f8eb80ae3b8519e631a7e9cfae828a7e00cc589eeff509bb5bd5e9398888763e03e26e1ce888393d3f5daf11b
checksum: 851db0c1c830df3507297a54f0c944da42e8bc35a97fcb6fd8de1e7891b97d50e2b919ccea7c5da62ccdba3cb2c6c788207b317d404fd92a003771a6ebe070f3
languageName: node
linkType: hard
"@snort/system@npm:^1.2.0":
version: 1.2.0
resolution: "@snort/system@npm:1.2.0"
"@snort/system@npm:^1.2.11":
version: 1.2.11
resolution: "@snort/system@npm:1.2.11"
dependencies:
"@noble/curves": ^1.2.0
"@noble/hashes": ^1.3.2
"@scure/base": ^1.1.2
"@snort/shared": ^1.0.11
"@stablelib/xchacha20": ^1.0.1
debug: ^4.3.4
eventemitter3: ^5.0.1
isomorphic-ws: ^5.0.0
lokijs: ^1.5.12
uuid: ^9.0.0
ws: ^8.14.0
checksum: f5fb8a89f3d35db87c0c9a9e6cc49081d3c93c5092c51a6105ca020ff6679ae24b688d59c66482124e81eb839313ad5d723087eac948cad76f460a7f34876213
languageName: node
linkType: hard
"@snort/system@npm:^1.2.1":
version: 1.2.1
resolution: "@snort/system@npm:1.2.1"
dependencies:
"@noble/curves": ^1.2.0
"@noble/hashes": ^1.3.2
"@scure/base": ^1.1.2
"@snort/shared": ^1.0.12
"@snort/shared": ^1.0.14
"@stablelib/xchacha20": ^1.0.1
debug: ^4.3.4
eventemitter3: ^5.0.1
@ -2883,7 +2361,27 @@ __metadata:
lru-cache: ^10.2.0
uuid: ^9.0.0
ws: ^8.14.0
checksum: 78557fddba7f8ebba4ee1df0b0896f475c16d622f833aa71a849e40e9a1a2fc693bcfdcddf0ea9ec413f37297854881597eb3ae9cdebb8bd4e18b251eff1ee1c
checksum: 6aa810f8be82f9d8825cb93097ad4da4f8da546294058627fb6968818f8264886ad73b1d42822a7e92c2f4840496be7f21a489abe962262db344ca0d221239ee
languageName: node
linkType: hard
"@snort/system@npm:^1.2.12":
version: 1.2.12
resolution: "@snort/system@npm:1.2.12"
dependencies:
"@noble/curves": ^1.2.0
"@noble/hashes": ^1.3.2
"@scure/base": ^1.1.2
"@snort/shared": ^1.0.14
"@stablelib/xchacha20": ^1.0.1
debug: ^4.3.4
eventemitter3: ^5.0.1
isomorphic-ws: ^5.0.0
lokijs: ^1.5.12
lru-cache: ^10.2.0
uuid: ^9.0.0
ws: ^8.14.0
checksum: d9546267bd6d95114528542a265f266320657b41f579b9c84deabc048a4295e73306ff04605168ef824aa51e74dccf1e5866f14cee6b4ae2a5a3247a605564f7
languageName: node
linkType: hard
@ -3672,15 +3170,6 @@ __metadata:
languageName: node
linkType: hard
"aria-hidden@npm:^1.1.1":
version: 1.2.3
resolution: "aria-hidden@npm:1.2.3"
dependencies:
tslib: ^2.0.0
checksum: 7d7d211629eef315e94ed3b064c6823d13617e609d3f9afab1c2ed86399bb8e90405f9bdd358a85506802766f3ecb468af985c67c846045a34b973bcc0289db9
languageName: node
linkType: hard
"aria-query@npm:5.1.3":
version: 5.1.3
resolution: "aria-query@npm:5.1.3"
@ -3859,20 +3348,6 @@ __metadata:
languageName: node
linkType: hard
"browserslist@npm:^4.23.0":
version: 4.23.0
resolution: "browserslist@npm:4.23.0"
dependencies:
caniuse-lite: ^1.0.30001587
electron-to-chromium: ^1.4.668
node-releases: ^2.0.14
update-browserslist-db: ^1.0.13
bin:
browserslist: cli.js
checksum: 436f49e796782ca751ebab7edc010cfc9c29f68536f387666cd70ea22f7105563f04dd62c6ff89cb24cc3254d17cba385f979eeeb3484d43e012412ff7e75def
languageName: node
linkType: hard
"buffer-from@npm:^1.0.0":
version: 1.1.2
resolution: "buffer-from@npm:1.1.2"
@ -3955,13 +3430,6 @@ __metadata:
languageName: node
linkType: hard
"caniuse-lite@npm:^1.0.30001587":
version: 1.0.30001588
resolution: "caniuse-lite@npm:1.0.30001588"
checksum: 2ab5fcec8fd3ee5d817a44bf1fb69804a6924d190e476863fb519692cd3e85a3a775bf4a2b6ba793f8db592ca61255b7f77f3d773ff7d42b452216f180bcdd2f
languageName: node
linkType: hard
"chalk@npm:^2.4.2":
version: 2.4.2
resolution: "chalk@npm:2.4.2"
@ -4350,13 +3818,6 @@ __metadata:
languageName: node
linkType: hard
"detect-node-es@npm:^1.1.0":
version: 1.1.0
resolution: "detect-node-es@npm:1.1.0"
checksum: e46307d7264644975b71c104b9f028ed1d3d34b83a15b8a22373640ce5ea630e5640b1078b8ea15f202b54641da71e4aa7597093bd4b91f113db520a26a37449
languageName: node
linkType: hard
"dexie@npm:^3.2.4":
version: 3.2.4
resolution: "dexie@npm:3.2.4"
@ -4448,13 +3909,6 @@ __metadata:
languageName: node
linkType: hard
"electron-to-chromium@npm:^1.4.668":
version: 1.4.679
resolution: "electron-to-chromium@npm:1.4.679"
checksum: 1884239565cec13308298d08a746a2721ed73f9c0128b7660ef5251404f00b0e1c0fb44afbd73be7d3e9c6bb15c24e7f6374e7148416c127140be9ec86cd9f6f
languageName: node
linkType: hard
"emoji-mart@npm:^5.5.2":
version: 5.5.2
resolution: "emoji-mart@npm:5.5.2"
@ -5128,13 +4582,6 @@ __metadata:
languageName: node
linkType: hard
"get-nonce@npm:^1.0.0":
version: 1.0.1
resolution: "get-nonce@npm:1.0.1"
checksum: e2614e43b4694c78277bb61b0f04583d45786881289285c73770b07ded246a98be7e1f78b940c80cbe6f2b07f55f0b724e6db6fd6f1bcbd1e8bdac16521074ed
languageName: node
linkType: hard
"get-own-enumerable-property-symbols@npm:^3.0.0":
version: 3.0.2
resolution: "get-own-enumerable-property-symbols@npm:3.0.2"
@ -5498,15 +4945,6 @@ __metadata:
languageName: node
linkType: hard
"invariant@npm:^2.2.4":
version: 2.2.4
resolution: "invariant@npm:2.2.4"
dependencies:
loose-envify: ^1.0.0
checksum: cc3182d793aad82a8d1f0af697b462939cb46066ec48bbf1707c150ad5fad6406137e91a262022c269702e01621f35ef60269f6c0d7fd178487959809acdfb14
languageName: node
linkType: hard
"ip@npm:^2.0.0":
version: 2.0.0
resolution: "ip@npm:2.0.0"
@ -6066,7 +5504,7 @@ __metadata:
languageName: node
linkType: hard
"loose-envify@npm:^1.0.0, loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0":
"loose-envify@npm:^1.1.0, loose-envify@npm:^1.4.0":
version: 1.4.0
resolution: "loose-envify@npm:1.4.0"
dependencies:
@ -6395,13 +5833,6 @@ __metadata:
languageName: node
linkType: hard
"node-releases@npm:^2.0.14":
version: 2.0.14
resolution: "node-releases@npm:2.0.14"
checksum: 59443a2f77acac854c42d321bf1b43dea0aef55cd544c6a686e9816a697300458d4e82239e2d794ea05f7bbbc8a94500332e2d3ac3f11f52e4b16cbe638b3c41
languageName: node
linkType: hard
"nopt@npm:^6.0.0":
version: 6.0.0
resolution: "nopt@npm:6.0.0"
@ -6953,41 +6384,6 @@ __metadata:
languageName: node
linkType: hard
"react-remove-scroll-bar@npm:^2.3.3":
version: 2.3.4
resolution: "react-remove-scroll-bar@npm:2.3.4"
dependencies:
react-style-singleton: ^2.2.1
tslib: ^2.0.0
peerDependencies:
"@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: b5ce5f2f98d65c97a3e975823ae4043a4ba2a3b63b5ba284b887e7853f051b5cd6afb74abde6d57b421931c52f2e1fdbb625dc858b1cb5a32c27c14ab85649d4
languageName: node
linkType: hard
"react-remove-scroll@npm:2.5.5":
version: 2.5.5
resolution: "react-remove-scroll@npm:2.5.5"
dependencies:
react-remove-scroll-bar: ^2.3.3
react-style-singleton: ^2.2.1
tslib: ^2.1.0
use-callback-ref: ^1.3.0
use-sidecar: ^1.1.2
peerDependencies:
"@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 2c7fe9cbd766f5e54beb4bec2e2efb2de3583037b23fef8fa511ab426ed7f1ae992382db5acd8ab5bfb030a4b93a06a2ebca41377d6eeaf0e6791bb0a59616a4
languageName: node
linkType: hard
"react-resize-detector@npm:^8.0.4":
version: 8.1.0
resolution: "react-resize-detector@npm:8.1.0"
@ -7047,23 +6443,6 @@ __metadata:
languageName: node
linkType: hard
"react-style-singleton@npm:^2.2.1":
version: 2.2.1
resolution: "react-style-singleton@npm:2.2.1"
dependencies:
get-nonce: ^1.0.0
invariant: ^2.2.4
tslib: ^2.0.0
peerDependencies:
"@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 7ee8ef3aab74c7ae1d70ff34a27643d11ba1a8d62d072c767827d9ff9a520905223e567002e0bf6c772929d8ea1c781a3ba0cc4a563e92b1e3dc2eaa817ecbe8
languageName: node
linkType: hard
"react-tag-input-component@npm:^2.0.2":
version: 2.0.2
resolution: "react-tag-input-component@npm:2.0.2"
@ -7715,18 +7094,12 @@ __metadata:
"@formatjs/cli": ^6.1.3
"@formatjs/ts-transformer": ^3.13.3
"@noble/curves": ^1.2.0
"@radix-ui/react-collapsible": ^1.0.3
"@radix-ui/react-dialog": ^1.0.4
"@radix-ui/react-progress": ^1.0.3
"@radix-ui/react-tabs": ^1.0.4
"@radix-ui/react-toggle": ^1.0.3
"@react-hook/resize-observer": ^1.2.6
"@scure/base": ^1.1.3
"@snort/shared": ^1.0.12
"@snort/system": ^1.2.1
"@snort/system-react": ^1.2.1
"@snort/shared": ^1.0.14
"@snort/system": ^1.2.12
"@snort/system-react": ^1.2.12
"@snort/system-wasm": ^1.0.2
"@snort/system-web": ^1.0.4
"@snort/system-web": ^1.2.11
"@szhsin/react-menu": ^4.0.2
"@testing-library/dom": ^9.3.1
"@types/lodash": ^4.14.195
@ -7743,7 +7116,6 @@ __metadata:
"@webbtc/webln-types": ^1.0.12
"@webscopeio/react-textarea-autocomplete": ^4.9.2
autoprefixer: ^10.4.16
browserslist: ^4.23.0
buffer: ^6.0.3
classnames: ^2.3.2
emoji-mart: ^5.5.2
@ -8111,7 +7483,7 @@ __metadata:
languageName: node
linkType: hard
"tslib@npm:2.6.2, tslib@npm:^2.0.0, tslib@npm:^2.1.0, tslib@npm:^2.4.0":
"tslib@npm:2.6.2, tslib@npm:^2.4.0":
version: 2.6.2
resolution: "tslib@npm:2.6.2"
checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad
@ -8349,20 +7721,6 @@ __metadata:
languageName: node
linkType: hard
"update-browserslist-db@npm:^1.0.13":
version: 1.0.13
resolution: "update-browserslist-db@npm:1.0.13"
dependencies:
escalade: ^3.1.1
picocolors: ^1.0.0
peerDependencies:
browserslist: ">= 4.21.0"
bin:
update-browserslist-db: cli.js
checksum: 1e47d80182ab6e4ad35396ad8b61008ae2a1330221175d0abd37689658bdb61af9b705bfc41057fd16682474d79944fb2d86767c5ed5ae34b6276b9bed353322
languageName: node
linkType: hard
"uri-js@npm:^4.2.2":
version: 4.4.1
resolution: "uri-js@npm:4.4.1"
@ -8372,37 +7730,6 @@ __metadata:
languageName: node
linkType: hard
"use-callback-ref@npm:^1.3.0":
version: 1.3.0
resolution: "use-callback-ref@npm:1.3.0"
dependencies:
tslib: ^2.0.0
peerDependencies:
"@types/react": ^16.8.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 7913df383a5a6fcb399212eedefaac2e0c6f843555202d4e3010bac3848afe38ecaa3d0d6500ad1d936fbeffd637e6c517e68edb024af5e6beca7f27f3ce7b21
languageName: node
linkType: hard
"use-sidecar@npm:^1.1.2":
version: 1.1.2
resolution: "use-sidecar@npm:1.1.2"
dependencies:
detect-node-es: ^1.1.0
tslib: ^2.0.0
peerDependencies:
"@types/react": ^16.9.0 || ^17.0.0 || ^18.0.0
react: ^16.8.0 || ^17.0.0 || ^18.0.0
peerDependenciesMeta:
"@types/react":
optional: true
checksum: 925d1922f9853e516eaad526b6fed1be38008073067274f0ecc3f56b17bb8ab63480140dd7c271f94150027c996cea4efe83d3e3525e8f3eda22055f6a39220b
languageName: node
linkType: hard
"usehooks-ts@npm:^2.9.1":
version: 2.9.1
resolution: "usehooks-ts@npm:2.9.1"