diff --git a/package.json b/package.json index 2afdea6..1788583 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/public/zap-stream.svg b/public/zap-stream.svg index b0fb3a1..e7f3870 100644 --- a/public/zap-stream.svg +++ b/public/zap-stream.svg @@ -1,3 +1,3 @@ - - + + \ No newline at end of file diff --git a/src/const.ts b/src/const.ts index c21ce43..c6d5466 100644 --- a/src/const.ts +++ b/src/const.ts @@ -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/"; \ No newline at end of file +//export const DefaultProviderUrl = "http://localhost:5295/api/nostr/"; diff --git a/src/element/Event.tsx b/src/element/Event.tsx index 18c4329..a989193 100644 --- a/src/element/Event.tsx +++ b/src/element/Event.tsx @@ -14,7 +14,7 @@ interface EventProps { link: NostrLink; } -export function EventIcon({ kind }: { kind: EventKind }) { +export function EventIcon({ kind }: { kind?: EventKind }) { if (kind === GOAL) { return ; } diff --git a/src/element/async-button.tsx b/src/element/async-button.tsx index b3e42ee..c70cc2a 100644 --- a/src/element/async-button.tsx +++ b/src/element/async-button.tsx @@ -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 { +export interface AsyncButtonProps extends React.ButtonHTMLAttributes { disabled?: boolean; onClick?: (e: React.MouseEvent) => Promise | void; children?: React.ReactNode; @@ -31,7 +30,7 @@ const AsyncButton = forwardRef((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}> diff --git a/src/element/avatar.tsx b/src/element/avatar.tsx index cb19dfa..44a52a6 100644 --- a/src/element/avatar.tsx +++ b/src/element/avatar.tsx @@ -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 & { size?: number; pubkey: string; user?: MetadataCache }; +type AvatarProps = HTMLProps & { 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 ( {user?.name setFailed(true)} style={{ width: `${size ?? 40}px`, + minWidth: `${size ?? 40}px`, height: `${size ?? 40}px`, }} /> diff --git a/src/element/buttons.tsx b/src/element/buttons.tsx new file mode 100644 index 0000000..2a776ac --- /dev/null +++ b/src/element/buttons.tsx @@ -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((props: AsyncButtonProps, ref) => { + return ; +}); +export const PrimaryButton = forwardRef((props: AsyncButtonProps, ref) => { + return ; +}); +export const Layer1Button = forwardRef((props: AsyncButtonProps, ref) => { + return ; +}); +export const Layer2Button = forwardRef((props: AsyncButtonProps, ref) => { + return ; +}); +export const Layer3Button = forwardRef((props: AsyncButtonProps, ref) => { + return ; +}); +export const WarningButton = forwardRef((props: AsyncButtonProps, ref) => { + return ; +}); +export const IconButton = forwardRef(({ iconName, iconSize, ...props }: { iconName: string, iconSize?: number } & AsyncButtonProps, ref) => { + return + + ; +}); +export const BorderButton = forwardRef((props: AsyncButtonProps, ref) => { + return ; +}); \ No newline at end of file diff --git a/src/element/chat-message.tsx b/src/element/chat-message.tsx index 80e21f4..e981728 100644 --- a/src/element/chat-message.tsx +++ b/src/element/chat-message.tsx @@ -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) && (
{hasZaps && ( -
+ - {formatSats(totalZaps)} -
+ {formatSats(totalZaps)} + )} {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 && ( - - + } targetName={profile?.name || ev.pubkey} /> )} - - - + {shouldShowMuteButton && ( - - - + )}
)} diff --git a/src/element/clip-button.tsx b/src/element/clip-button.tsx index cde451d..ee27167 100644 --- a/src/element/clip-button.tsx +++ b/src/element/clip-button.tsx @@ -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(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 ( <> - - -
- - - - - - + + + + + + + {open && setOpen(false)}> +
+

+ +

+ {id && tempClipId &&
+
} ); } diff --git a/src/element/collapsible.tsx b/src/element/collapsible.tsx index f976051..8bccd42 100644 --- a/src/element/collapsible.tsx +++ b/src/element/collapsible.tsx @@ -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 = {url.toString()}; - return ( - - {preview} - - - -
- {url.toString()} - {children} -
- - - - - -
-
-
+ const [open, setOpen] = useState(false); + return (<> + setOpen(true)}>{url.toString()} + {open && setOpen(false)}> + {url.toString()} + {children} + } + ); } @@ -48,23 +33,19 @@ export function CollapsibleEvent({ link }: { link: NostrLink }) { const author = event?.pubkey || link.author; return ( - -
-
- {event && } - {author && } + <> +
+
+ + + }} />
- - - {open ? ( - - ) : ( - - )} - - + setOpen(s => !s)}> + {open ? : } +
- {open && event && } - + {open && event && } + ); } diff --git a/src/element/content-warning.tsx b/src/element/content-warning.tsx index b4df529..205ec21 100644 --- a/src/element/content-warning.tsx +++ b/src/element/content-warning.tsx @@ -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() {
- + - - navigate("/")}> + + navigate("/")}> - +
); diff --git a/src/element/emoji-pack.tsx b/src/element/emoji-pack.tsx index b4dd050..9202861 100644 --- a/src/element/emoji-pack.tsx +++ b/src/element/emoji-pack.tsx @@ -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 }) {

{name}

- {login?.pubkey && ( - - {isUsed ? ( - - ) : ( - - )} - + {login?.pubkey && (isUsed ? + + + : + + + )}
diff --git a/src/element/external-link.tsx b/src/element/external-link.tsx index 5e71dec..f6704a9 100644 --- a/src/element/external-link.tsx +++ b/src/element/external-link.tsx @@ -8,7 +8,7 @@ interface ExternalLinkProps { export function ExternalLink({ children, href }: ExternalLinkProps) { return ( - + {children} ); diff --git a/src/element/file-uploader.css b/src/element/file-uploader.css index 71e6ce7..63ecc4f 100644 --- a/src/element/file-uploader.css +++ b/src/element/file-uploader.css @@ -31,7 +31,3 @@ align-items: flex-start; gap: 12px; } - -.file-uploader-preview .clear-button { - color: var(--text-danger); -} diff --git a/src/element/file-uploader.tsx b/src/element/file-uploader.tsx index 2604d37..85b2d3f 100644 --- a/src/element/file-uploader.tsx +++ b/src/element/file-uploader.tsx @@ -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
{img?.length > 0 && ( - + - + )} {img && }
diff --git a/src/element/follow-button.tsx b/src/element/follow-button.tsx index f64d8f3..f0f55c3 100644 --- a/src/element/follow-button.tsx +++ b/src/element/follow-button.tsx @@ -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 ( - {isFollowing ? ( ) : ( )} - + ); } diff --git a/src/element/goal.tsx b/src/element/goal.tsx index d966b14..c34a9fd 100644 --- a/src/element/goal.tsx +++ b/src/element/goal.tsx @@ -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 = ( -
+
{ev.content.length > 0 &&

{ev.content}

} -
- - - {!isFinished && {formatSats(soFar)}} - - - - - -
- +
+
+
+ {soFar > 0 ? formatSats(soFar) : ""} +
+
+ +
+
+
- {isFinished && previousValue === false && } -
+ {isFinished && previousValue === false && (confetti ?? true) && + } +
); return zapTarget ? ( diff --git a/src/element/live-chat.css b/src/element/live-chat.css index d968ede..5b36058 100644 --- a/src/element/live-chat.css +++ b/src/element/live-chat.css @@ -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; diff --git a/src/element/live-chat.tsx b/src/element/live-chat.tsx index 8eafdc8..1ec5770 100644 --- a/src/element/live-chat.tsx +++ b/src/element/live-chat.tsx @@ -131,7 +131,7 @@ export function LiveChat({ case -2: { return ( {a.kind === -1 ? ( diff --git a/src/element/live-event.tsx b/src/element/live-event.tsx new file mode 100644 index 0000000..b807c37 --- /dev/null +++ b/src/element/live-event.tsx @@ -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 + + +} \ No newline at end of file diff --git a/src/element/login-signup.tsx b/src/element/login-signup.tsx index 9a6803b..36d2b3b 100644 --- a/src/element/login-signup.tsx +++ b/src/element/login-signup.tsx @@ -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 }) {

- + - +

@@ -174,14 +174,14 @@ export function LoginSignup({ close }: { close: () => void }) {
{hasNostrExtension && ( <> - + - + )} - setStage(Stage.LoginInput)}> + setStage(Stage.LoginInput)}> - + {error && {error}}
@@ -208,28 +208,25 @@ export function LoginSignup({ close }: { close: () => void }) { }} />

-
- setNewKey(e.target.value)} - placeholder={formatMessage({ defaultMessage: "eg. nsec1xyz", id: "yzKwBQ" })} - /> -
+ setNewKey(e.target.value)} + placeholder={formatMessage({ defaultMessage: "eg. nsec1xyz", id: "yzKwBQ" })} + />
- { setNewKey(""); setStage(Stage.Login); }}> - - + + - +
{error && {error}} @@ -258,21 +255,19 @@ export function LoginSignup({ close }: { close: () => void }) {
-
- setUsername(e.target.value)} - /> -
+ setUsername(e.target.value)} + />
- + - +
); @@ -303,22 +298,20 @@ export function LoginSignup({ close }: { close: () => void }) {

)}
-
- setLnAddress(e.target.value)} - /> -
+ setLnAddress(e.target.value)} + />
{error && {error}} - + - +
); @@ -337,12 +330,12 @@ export function LoginSignup({ close }: { close: () => void }) { id="H/bNs9" />

-
+
- + - +
); diff --git a/src/element/logo.tsx b/src/element/logo.tsx new file mode 100644 index 0000000..12b4b5b --- /dev/null +++ b/src/element/logo.tsx @@ -0,0 +1,7 @@ +import { HTMLProps } from "react"; + +export default function Logo(props: HTMLProps) { + return + + +} \ No newline at end of file diff --git a/src/element/modal.tsx b/src/element/modal.tsx new file mode 100644 index 0000000..f41c138 --- /dev/null +++ b/src/element/modal.tsx @@ -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( +
{ + e.stopPropagation(); + }}> +
e.stopPropagation()} + onClick={e => { + e.stopPropagation(); + props.onClick?.(e); + }}> +
+ { + e.stopPropagation(); + props.onClose?.(e); + }} + className="rounded-full aspect-square" + iconSize={10} + /> +
+ {props.children} +
+
, + document.body, + ); +} diff --git a/src/element/mute-button.tsx b/src/element/mute-button.tsx index e6add80..7869ce9 100644 --- a/src/element/mute-button.tsx +++ b/src/element/mute-button.tsx @@ -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 ( - (isMuted ? unmute() : mute())} className="font-bold"> + (isMuted ? unmute() : mute())}> {isMuted ? ( ) : ( )} - + ); } diff --git a/src/element/new-goal.css b/src/element/new-goal.css index 0bd397a..0f11ed7 100644 --- a/src/element/new-goal.css +++ b/src/element/new-goal.css @@ -9,11 +9,6 @@ gap: 8px; } -.new-goal .paper { - background: #262626; - height: 32px; -} - .new-goal .btn:disabled { opacity: 0.3; } diff --git a/src/element/new-goal.tsx b/src/element/new-goal.tsx index 8cffe4a..172b79e 100644 --- a/src/element/new-goal.tsx +++ b/src/element/new-goal.tsx @@ -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 ( - - - - - - - - - - - - - - -
-
- -

- -

-
-
-

- -

-
- setGoalName(e.target.value)} - /> -
-
-
-

- -

-
- setGoalAmount(e.target.value)} - /> -
-
-
- - - -
-
-
-
-
+ return (<> + setOpen(true)}> + + + + + + {open && setOpen(false)}> +
+
+ +

+ +

+
+
+

+ +

+ setGoalName(e.target.value)} + /> +
+
+

+ +

+ setGoalAmount(e.target.value)} + /> +
+
+ + + +
+
+
} + ); } diff --git a/src/element/new-stream.css b/src/element/new-stream.css deleted file mode 100644 index d7ee34c..0000000 --- a/src/element/new-stream.css +++ /dev/null @@ -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; -} diff --git a/src/element/new-stream.tsx b/src/element/new-stream.tsx index 76e64c3..8cb63fa 100644 --- a/src/element/new-stream.tsx +++ b/src/element/new-stream.tsx @@ -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 & { onFinish: () => void }) { const system = useContext(SnortContext); @@ -53,14 +53,13 @@ function NewStream({ ev, onFinish }: Omit & { onF case StreamProviders.NostrType: { return ( <> - { navigate("/settings"); onFinish?.(); }}> - + & { onF

{providers.map(v => ( - setCurrentProvider(v)}> + setCurrentProvider(v)}> {v.name} - + ))}
- {providerDialog()} +
+ {providerDialog()} +
); } @@ -103,30 +104,23 @@ interface NewStreamDialogProps { export function NewStreamDialog(props: NewStreamDialogProps & StreamEditorProps) { const [open, setOpen] = useState(false); return ( - - - - {props.text && props.text} - {!props.text && ( - <> - - - - - - )} - - - - - -
-
- setOpen(false)} /> -
-
-
-
-
+ <> + setOpen(true)}> + {props.text && props.text} + {!props.text && ( + <> + + + + + + )} + + {open && setOpen(false)}> +
+ setOpen(false)} /> +
+
} + ); } diff --git a/src/element/nostr-provider-dialog.tsx b/src/element/nostr-provider-dialog.tsx index b52bfd9..d9c59bb 100644 --- a/src/element/nostr-provider-dialog.tsx +++ b/src/element/nostr-provider-dialog.tsx @@ -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}`}   or
{`${pm.toLocaleString()} sats/month ($${(rate.ask * pm * 1e-8).toFixed(2)}/mo) streaming ${hrs} hrs/month`} -
+
Hrs setHrs(e.target.valueAsNumber)} />
@@ -135,9 +138,9 @@ export function NostrProviderDialog({
- + - +
); @@ -154,11 +157,11 @@ export function NostrProviderDialog({

{sortEndpoints(info.endpoints).map(a => ( - setEndpoint(a)}> {a.name} - + ))}
@@ -167,23 +170,18 @@ export function NostrProviderDialog({

-
- -
+

-
- -
- + window.navigator.clipboard.writeText(ep?.key ?? "")}> - +
@@ -191,16 +189,16 @@ export function NostrProviderDialog({

-
+
- setTopup(true)}> + setTopup(true)}> - +
@@ -212,7 +210,7 @@ export function NostrProviderDialog({

{ep?.capabilities?.map(a => ( - {parseCapability(a)} + {parseCapability(a)} ))}
@@ -264,18 +262,18 @@ export function NostrProviderDialog({
{info.forwards?.map(a => ( <> -
{a.name}
- {a.name}
+ { await provider.removeForward(a.id); + await loadInfo(); }}> - + ))}
- {}} /> + ); } @@ -419,40 +417,35 @@ function AddForwardInputs({ } return ( -
+
-
- -
-
- setName(e.target.value)} - /> -
-
-
+ setTarget(e.target.value)} + className="flex-1" + placeholder={formatMessage({ defaultMessage: "Display name", id: "dOQCL8" })} + value={name} + onChange={e => setName(e.target.value)} />
- + setTarget(e.target.value)} + /> + - + {error && {error}}
); diff --git a/src/element/notifications-button.tsx b/src/element/notifications-button.tsx index 02f5cc6..36d552c 100644 --- a/src/element/notifications-button.tsx +++ b/src/element/notifications-button.tsx @@ -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 ( - + - + ); } diff --git a/src/element/pill.tsx b/src/element/pill.tsx new file mode 100644 index 0000000..66a27a5 --- /dev/null +++ b/src/element/pill.tsx @@ -0,0 +1,6 @@ +import classNames from "classnames"; +import { HTMLProps } from "react"; + +export default function Pill({ children, selected, className, ...props }: HTMLProps) { + return {children} +} \ No newline at end of file diff --git a/src/element/raid-menu.tsx b/src/element/raid-menu.tsx index df24891..a40cfa7 100644 --- a/src/element/raid-menu.tsx +++ b/src/element/raid-menu.tsx @@ -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:
-

+

{livePubkeys.map(a => (
{ const liveEvent = live.find(b => getHost(b) === a); if (liveEvent) { @@ -60,7 +60,7 @@ export function DashboardRaidMenu({ link, onClose }: { link: NostrLink; onClose:
-

+

@@ -68,16 +68,16 @@ export function DashboardRaidMenu({ link, onClose }: { link: NostrLink; onClose:
-

+

setMsg(e.target.value)} />
- + - +
); } diff --git a/src/element/send-zap.css b/src/element/send-zap.css index de83ada..5a11927 100644 --- a/src/element/send-zap.css +++ b/src/element/send-zap.css @@ -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; diff --git a/src/element/send-zap.tsx b/src/element/send-zap.tsx index 80fa086..4604e82 100644 --- a/src/element/send-zap.tsx +++ b/src/element/send-zap.tsx @@ -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 ( <>
- { setIsFiat(false); setAmount(satsAmounts[0]); }}> SATS - - + { setIsFiat(true); setAmount(usdAmounts[0]); }}> USD - +
- + )} -
+
{(isFiat ? usdAmounts : satsAmounts).map(a => ( - setAmount(a)}> + setAmount(a)}> {isFiat ? `$${a.toLocaleString()}` : formatSats(a)} - + ))}
@@ -167,9 +168,9 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: Se
)}
- + - +
); @@ -185,18 +186,18 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: Se
- onFinish()}> + onFinish()}> - + ); } return ( -
+

- +

{input()} {payInvoice()} @@ -205,29 +206,21 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: Se } export function SendZapsDialog(props: Omit) { - const [isOpen, setIsOpen] = useState(false); - return ( - - - {props.button ? ( - props.button - ) : ( - - - - - - - )} - - - - -
- setIsOpen(false)} /> -
-
-
-
+ const [open, setOpen] = useState(false); + return (<> + {props.button ? ( + props.button + ) : ( + setOpen(true)}> + + + + + + )} + {open && setOpen(false)}> + setOpen(false)} /> + } + ); } diff --git a/src/element/share-menu.tsx b/src/element/share-menu.tsx index 3162470..4f20a58 100644 --- a/src/element/share-menu.tsx +++ b/src/element/share-menu.tsx @@ -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(); - 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={ - + - + }> { - setMessage(`Come check out my stream on zap.stream!\n\n${link}\n\nnostr:${naddr}`); setShare("nostr"); }}> - setShare(undefined)}> - - - -
-

- -

-
-