refactor: refresh
This commit is contained in:
parent
364d2c272f
commit
f93a398039
14
package.json
14
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",
|
||||
|
@ -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 |
@ -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/";
|
@ -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" />;
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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
31
src/element/buttons.tsx
Normal 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} />;
|
||||
});
|
@ -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(":");
|
||||
@ -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>
|
||||
)}
|
||||
|
@ -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,21 +80,14 @@ 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">
|
||||
<DefaultButton onClick={makeClip}>
|
||||
<Icon name="clapperboard" />
|
||||
<span className="max-lg:hidden">
|
||||
<FormattedMessage defaultMessage="Create Clip" id="PA0ej4" />
|
||||
</span>
|
||||
</AsyncButton>
|
||||
</div>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="dialog-overlay" />
|
||||
<Dialog.Content className="dialog-content">
|
||||
<div className="content-inner">
|
||||
</DefaultButton>
|
||||
{open && <Modal id="create-clip" onClose={() => setOpen(false)}>
|
||||
<div className="flex flex-col">
|
||||
<h1>
|
||||
<FormattedMessage defaultMessage="Create Clip" id="PA0ej4" />
|
||||
</h1>
|
||||
@ -110,17 +104,13 @@ export function ClipButton({ ev }: { ev: TaggedNostrEvent }) {
|
||||
<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}>
|
||||
<DefaultButton onClick={saveClip}>
|
||||
<FormattedMessage defaultMessage="Publish Clip" id="jJLRgo" />
|
||||
</AsyncButton>
|
||||
</DefaultButton>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
</Modal>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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">
|
||||
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}
|
||||
</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>
|
||||
</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} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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 ? (
|
||||
{login?.pubkey && (isUsed ?
|
||||
<WarningButton onClick={toggleEmojiPack}>
|
||||
<FormattedMessage defaultMessage="Remove" id="G/yZLu" />
|
||||
) : (
|
||||
</WarningButton> :
|
||||
<DefaultButton onClick={toggleEmojiPack}>
|
||||
<FormattedMessage defaultMessage="Add" id="2/2yg+" />
|
||||
)}
|
||||
</AsyncButton>
|
||||
</DefaultButton>
|
||||
)}
|
||||
</div>
|
||||
<div className="emoji-pack-emojis">
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -31,7 +31,3 @@
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.file-uploader-preview .clear-button {
|
||||
color: var(--text-danger);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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,22 +39,25 @@ 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">
|
||||
<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) }} />
|
||||
</span>
|
||||
</Progress.Root>
|
||||
<div className="zap-circle">
|
||||
<Icon name="zap-filled" className={isFinished ? "goal-finished" : "goal-unfinished"} />
|
||||
</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} />}
|
||||
{isFinished && previousValue === false && (confetti ?? true) &&
|
||||
<Confetti numberOfPieces={2100} recycle={false} />}
|
||||
</div >
|
||||
);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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" />
|
||||
|
24
src/element/live-event.tsx
Normal file
24
src/element/live-event.tsx
Normal 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>
|
||||
}
|
@ -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>
|
||||
<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>
|
||||
<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>
|
||||
<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
7
src/element/logo.tsx
Normal 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
98
src/element/modal.tsx
Normal 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,
|
||||
);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -9,11 +9,6 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.new-goal .paper {
|
||||
background: #262626;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.new-goal .btn:disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
@ -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,21 +37,14 @@ 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>
|
||||
return (<>
|
||||
<DefaultButton onClick={() => setOpen(true)}>
|
||||
<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">
|
||||
</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} />
|
||||
@ -63,7 +56,6 @@ export function NewGoalDialog() {
|
||||
<p>
|
||||
<FormattedMessage defaultMessage="Name" id="HAlOn1" />
|
||||
</p>
|
||||
<div className="paper">
|
||||
<input
|
||||
type="text"
|
||||
value={goalName}
|
||||
@ -71,12 +63,10 @@ export function NewGoalDialog() {
|
||||
onChange={e => setGoalName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
<FormattedMessage defaultMessage="Amount" id="/0TOL5" />
|
||||
</p>
|
||||
<div className="paper">
|
||||
<input
|
||||
type="number"
|
||||
placeholder="21"
|
||||
@ -86,15 +76,13 @@ export function NewGoalDialog() {
|
||||
onChange={e => setGoalAmount(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="create-goal">
|
||||
<AsyncButton type="button" className="btn btn-primary wide" disabled={!isValid} onClick={publishGoal}>
|
||||
<DefaultButton disabled={!isValid} onClick={publishGoal}>
|
||||
<FormattedMessage defaultMessage="Create Goal" id="X2PZ7D" />
|
||||
</AsyncButton>
|
||||
</DefaultButton>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
</Modal>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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>
|
||||
<div className="flex flex-col gap-4">
|
||||
{providerDialog()}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -103,9 +104,8 @@ 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}>
|
||||
<>
|
||||
<DefaultButton className={props.btnClassName} onClick={() => setOpen(true)}>
|
||||
{props.text && props.text}
|
||||
{!props.text && (
|
||||
<>
|
||||
@ -115,18 +115,12 @@ export function NewStreamDialog(props: NewStreamDialogProps & StreamEditorProps)
|
||||
<Icon name="signal" />
|
||||
</>
|
||||
)}
|
||||
</AsyncButton>
|
||||
</Dialog.Trigger>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="dialog-overlay" />
|
||||
<Dialog.Content className="dialog-content">
|
||||
<div className="content-inner">
|
||||
</DefaultButton>
|
||||
{open && <Modal id="new-stream" onClose={() => setOpen(false)}>
|
||||
<div className="new-stream">
|
||||
<NewStream {...props} onFinish={() => setOpen(false)} />
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
</Modal>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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 <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>
|
||||
</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"
|
||||
<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,10 +417,9 @@ 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">
|
||||
<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>
|
||||
@ -430,17 +427,14 @@ function AddForwardInputs({
|
||||
<option value="trovo">Trovo</option>
|
||||
<option value="custom">Custom</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="paper flex-1">
|
||||
<input
|
||||
type="text"
|
||||
className="flex-1"
|
||||
placeholder={formatMessage({ defaultMessage: "Display name", id: "dOQCL8" })}
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="paper">
|
||||
<input
|
||||
type="text"
|
||||
placeholder={
|
||||
@ -449,10 +443,9 @@ function AddForwardInputs({
|
||||
value={target}
|
||||
onChange={e => setTarget(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<AsyncButton className="btn btn-primary" onClick={doAdd}>
|
||||
<DefaultButton onClick={doAdd}>
|
||||
<FormattedMessage defaultMessage="Add" id="2/2yg+" />
|
||||
</AsyncButton>
|
||||
</DefaultButton>
|
||||
{error && <b className="warning">{error}</b>}
|
||||
</div>
|
||||
);
|
||||
|
@ -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
6
src/element/pill.tsx
Normal 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>
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
const [open, setOpen] = useState(false);
|
||||
return (<>
|
||||
{props.button ? (
|
||||
props.button
|
||||
) : (
|
||||
<AsyncButton className="btn btn-primary zap">
|
||||
<DefaultButton onClick={() => setOpen(true)}>
|
||||
<span className="max-xl:hidden">
|
||||
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
|
||||
</span>
|
||||
<Icon name="zap-filled" size={16} />
|
||||
</AsyncButton>
|
||||
</DefaultButton>
|
||||
)}
|
||||
</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>
|
||||
{open && <Modal id="send-zaps" onClose={() => setOpen(false)}>
|
||||
<SendZaps {...props} onFinish={() => setOpen(false)} />
|
||||
</Modal>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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,28 +50,23 @@ 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">
|
||||
{share && <Modal id="share" onClose={() => setShare(undefined)}>
|
||||
<div className="flex flex-col gap-4">
|
||||
<h2>
|
||||
<FormattedMessage defaultMessage="Share" id="OKhRC6" />
|
||||
</h2>
|
||||
<div className="paper">
|
||||
<Textarea
|
||||
emojis={[]}
|
||||
value={message}
|
||||
@ -71,14 +76,11 @@ export function ShareMenu({ ev }: { ev: NostrEvent }) {
|
||||
}}
|
||||
rows={15}
|
||||
/>
|
||||
</div>
|
||||
<AsyncButton className="btn btn-primary" onClick={sendMessage}>
|
||||
<DefaultButton onClick={sendMessage}>
|
||||
<FormattedMessage defaultMessage="Send" id="9WRlF4" />
|
||||
</AsyncButton>
|
||||
</DefaultButton>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
</Modal>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
64
src/element/stream-cards/add-card.tsx
Normal file
64
src/element/stream-cards/add-card.tsx
Normal 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>
|
||||
);
|
||||
}
|
116
src/element/stream-cards/card-item.tsx
Normal file
116
src/element/stream-cards/card-item.tsx
Normal 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;
|
||||
}
|
84
src/element/stream-cards/edit-card.tsx
Normal file
84
src/element/stream-cards/edit-card.tsx
Normal 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>}
|
||||
</>
|
||||
);
|
||||
}
|
50
src/element/stream-cards/index.tsx
Normal file
50
src/element/stream-cards/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
87
src/element/stream-cards/new-card.tsx
Normal file
87
src/element/stream-cards/new-card.tsx
Normal 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>
|
||||
);
|
||||
}
|
35
src/element/stream-cards/preview.tsx
Normal file
35
src/element/stream-cards/preview.tsx
Normal 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>
|
||||
);
|
||||
});
|
32
src/element/stream-cards/stream-card-editor.tsx
Normal file
32
src/element/stream-cards/stream-card-editor.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -12,9 +12,3 @@
|
||||
border-radius: 12px !important;
|
||||
display: unset !important;
|
||||
}
|
||||
|
||||
.content-warning {
|
||||
padding: 16px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid #ff563f;
|
||||
}
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
@ -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>
|
||||
))}
|
||||
</>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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} />
|
||||
));
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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">
|
||||
<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 && (
|
||||
<span className="pill viewers bg-gray-1">
|
||||
<Pill>
|
||||
<FormattedMessage
|
||||
defaultMessage="{n} viewers"
|
||||
id="3adEeb"
|
||||
values={{ n: formatSats(Number(participants)) }}
|
||||
/>
|
||||
</span>
|
||||
</Pill>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
<h3>{title}</h3>
|
||||
</Link>
|
||||
<div className="video-tile-info">
|
||||
<div className="video-tags">
|
||||
<div className="flex gap-1">
|
||||
<Tags ev={ev} max={3} />
|
||||
</div>
|
||||
{showAuthor && <div>{inView && <Profile pubkey={host} />}</div>}
|
||||
</div>
|
||||
{showAuthor && <Profile pubkey={host} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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
15
src/hooks/clips.ts
Normal 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);
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
231
src/index.css
231
src/index.css
@ -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;
|
||||
}
|
||||
|
@ -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 />,
|
||||
|
@ -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)}>
|
||||
return (<>
|
||||
<DefaultButton onClick={() => setShow(true)}>
|
||||
<FormattedMessage defaultMessage="Raid" id="4iBdw1" />
|
||||
</AsyncButton>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="dialog-overlay" />
|
||||
<Dialog.Content className="dialog-content">
|
||||
</DefaultButton>
|
||||
{show && <Modal id="raid-menu" onClose={() => setShow(false)}>
|
||||
<DashboardRaidMenu link={link} onClose={() => setShow(false)} />
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
</Modal>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -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}>
|
||||
return (<>
|
||||
<BorderButton onClick={() => setShowLogin(true)}>
|
||||
<FormattedMessage defaultMessage="Login" id="AyGauy" />
|
||||
<Icon name="login" />
|
||||
</AsyncButton>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay className="dialog-overlay" />
|
||||
<Dialog.Content className="dialog-content">
|
||||
</BorderButton>
|
||||
{showLogin && <Modal id="login">
|
||||
<LoginSignup close={() => setShowLogin(false)} />
|
||||
</Dialog.Content>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
</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
25
src/pages/mock.tsx
Normal 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>
|
||||
}
|
@ -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,62 +12,85 @@ 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">
|
||||
<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>
|
||||
<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>
|
||||
<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" />
|
||||
@ -89,10 +111,10 @@ export function ProfilePage() {
|
||||
aTag={liveEvent ? `${liveEvent.kind}:${liveEvent.pubkey}:${findTag(liveEvent, "d")}` : undefined}
|
||||
lnurl={zapTarget}
|
||||
button={
|
||||
<AsyncButton className="btn">
|
||||
<DefaultButton>
|
||||
<Icon name="zap-filled" className="zap-button-icon" />
|
||||
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
|
||||
</AsyncButton>
|
||||
</DefaultButton>
|
||||
}
|
||||
targetName={profile?.name || link.id}
|
||||
/>
|
||||
@ -101,40 +123,16 @@ export function ProfilePage() {
|
||||
<MuteButton pubkey={link.id} />
|
||||
</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>
|
||||
</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} />
|
||||
})
|
||||
}
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -61,7 +61,7 @@
|
||||
|
||||
.one-line:before,
|
||||
.one-line:after {
|
||||
background-color: #171717;
|
||||
@apply bg-layer-2;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
|
@ -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,8 +71,13 @@ export function SettingsPage() {
|
||||
onClick={() => Login.setColor(a)}></div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
case Tab.Stream: {
|
||||
return <>
|
||||
<h1>
|
||||
<FormattedMessage defaultMessage="Stream Key" id="LknBsU" />
|
||||
<FormattedMessage defaultMessage="Stream" id="uYw2LD" />
|
||||
</h1>
|
||||
<div className="flex flex-col gap-4">
|
||||
<NostrProviderDialog
|
||||
@ -81,7 +88,6 @@ export function SettingsPage() {
|
||||
/>
|
||||
</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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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]);
|
||||
|
@ -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
769
yarn.lock
@ -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"
|
||||
|
Loading…
x
Reference in New Issue
Block a user