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/data": "^1.1.2",
|
||||||
"@emoji-mart/react": "^1.1.1",
|
"@emoji-mart/react": "^1.1.1",
|
||||||
"@noble/curves": "^1.2.0",
|
"@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",
|
"@scure/base": "^1.1.3",
|
||||||
"@snort/shared": "^1.0.12",
|
"@snort/shared": "^1.0.14",
|
||||||
"@snort/system": "^1.2.1",
|
"@snort/system": "^1.2.12",
|
||||||
"@snort/system-react": "^1.2.1",
|
"@snort/system-react": "^1.2.12",
|
||||||
"@snort/system-wasm": "^1.0.2",
|
"@snort/system-wasm": "^1.0.2",
|
||||||
"@snort/system-web": "^1.0.4",
|
"@snort/system-web": "^1.2.11",
|
||||||
"@szhsin/react-menu": "^4.0.2",
|
"@szhsin/react-menu": "^4.0.2",
|
||||||
"@types/webscopeio__react-textarea-autocomplete": "^4.7.2",
|
"@types/webscopeio__react-textarea-autocomplete": "^4.7.2",
|
||||||
"@void-cat/api": "^1.0.7",
|
"@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">
|
<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="black"/>
|
<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>
|
</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 = {
|
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://relay.snort.social": { read: true, write: true },
|
||||||
"wss://nos.lol": { read: true, write: true },
|
"wss://nos.lol": { read: true, write: true },
|
||||||
"wss://relay.damus.io": { read: true, write: true },
|
"wss://relay.damus.io": { read: true, write: true },
|
||||||
"wss://nostr.wine": { read: true, write: true },
|
"wss://nostr.wine": { read: true, write: true },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const DefaultProviderUrl = "https://api.zap.stream/api/nostr/";
|
export const DefaultProviderUrl = "https://api.zap.stream/api/nostr/";
|
||||||
//export const DefaultProviderUrl = "http://localhost:5295/api/nostr/";
|
//export const DefaultProviderUrl = "http://localhost:5295/api/nostr/";
|
||||||
|
@ -14,7 +14,7 @@ interface EventProps {
|
|||||||
link: NostrLink;
|
link: NostrLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function EventIcon({ kind }: { kind: EventKind }) {
|
export function EventIcon({ kind }: { kind?: EventKind }) {
|
||||||
if (kind === GOAL) {
|
if (kind === GOAL) {
|
||||||
return <Icon name="piggybank" />;
|
return <Icon name="piggybank" />;
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import "./async-button.css";
|
import "./async-button.css";
|
||||||
import { forwardRef, useState } from "react";
|
import { forwardRef, useState } from "react";
|
||||||
import Spinner from "./spinner";
|
import Spinner from "./spinner";
|
||||||
import classNames from "classnames";
|
|
||||||
|
|
||||||
interface AsyncButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
export interface AsyncButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
onClick?: (e: React.MouseEvent) => Promise<void> | void;
|
onClick?: (e: React.MouseEvent) => Promise<void> | void;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
@ -31,7 +30,7 @@ const AsyncButton = forwardRef<HTMLButtonElement, AsyncButtonProps>((props: Asyn
|
|||||||
disabled={loading || props.disabled}
|
disabled={loading || props.disabled}
|
||||||
{...props}
|
{...props}
|
||||||
onClick={handle}
|
onClick={handle}
|
||||||
className={classNames("px-3 py-2 bg-gray-2 rounded-full", props.className)}>
|
className={props.className}>
|
||||||
<span
|
<span
|
||||||
style={{ visibility: loading ? "hidden" : "visible" }}
|
style={{ visibility: loading ? "hidden" : "visible" }}
|
||||||
className="whitespace-nowrap flex gap-2 items-center justify-center">
|
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 { HTMLProps, useState } from "react";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { getPlaceholder } from "@/utils";
|
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) {
|
export function Avatar({ pubkey, size, user, ...props }: AvatarProps) {
|
||||||
const [failed, setFailed] = useState(false);
|
const [failed, setFailed] = useState(false);
|
||||||
const src = user?.picture && !failed ? user.picture : getPlaceholder(pubkey);
|
const src = user?.picture && !failed ? user.picture : getPlaceholder(pubkey);
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
{...props}
|
{...props}
|
||||||
className={classNames("aspect-square rounded-full bg-gray-1", props.className)}
|
className={classNames("aspect-square rounded-full bg-layer-1", props.className)}
|
||||||
alt={user?.name || user?.pubkey}
|
alt={user?.name}
|
||||||
src={src}
|
src={src}
|
||||||
onError={() => setFailed(true)}
|
onError={() => setFailed(true)}
|
||||||
style={{
|
style={{
|
||||||
width: `${size ?? 40}px`,
|
width: `${size ?? 40}px`,
|
||||||
|
minWidth: `${size ?? 40}px`,
|
||||||
height: `${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 { useLogin } from "@/hooks/login";
|
||||||
import { formatSats } from "@/number";
|
import { formatSats } from "@/number";
|
||||||
import type { Badge, Emoji, EmojiPack } from "@/types";
|
import type { Badge, Emoji, EmojiPack } from "@/types";
|
||||||
import AsyncButton from "./async-button";
|
import { IconButton } from "./buttons";
|
||||||
|
import Pill from "./pill";
|
||||||
|
|
||||||
function emojifyReaction(reaction: string) {
|
function emojifyReaction(reaction: string) {
|
||||||
if (reaction === "+") {
|
if (reaction === "+") {
|
||||||
@ -149,10 +150,10 @@ export function ChatMessage({
|
|||||||
{(hasReactions || hasZaps) && (
|
{(hasReactions || hasZaps) && (
|
||||||
<div className="message-reactions">
|
<div className="message-reactions">
|
||||||
{hasZaps && (
|
{hasZaps && (
|
||||||
<div className="zap-pill">
|
<Pill>
|
||||||
<Icon name="zap-filled" className="text-zap" size={12} />
|
<Icon name="zap-filled" className="text-zap" size={12} />
|
||||||
<span className="zap-pill-amount">{formatSats(totalZaps)}</span>
|
<span className="text-xs">{formatSats(totalZaps)}</span>
|
||||||
</div>
|
</Pill>
|
||||||
)}
|
)}
|
||||||
{dedupe(filteredReactions.map(v => emojifyReaction(v.content))).map(e => {
|
{dedupe(filteredReactions.map(v => emojifyReaction(v.content))).map(e => {
|
||||||
const isCustomEmojiReaction = e.length > 1 && e.startsWith(":") && e.endsWith(":");
|
const isCustomEmojiReaction = e.length > 1 && e.startsWith(":") && e.endsWith(":");
|
||||||
@ -178,15 +179,15 @@ export function ChatMessage({
|
|||||||
style={
|
style={
|
||||||
isTablet
|
isTablet
|
||||||
? {
|
? {
|
||||||
display: showZapDialog || isHovering ? "flex" : "none",
|
display: showZapDialog || isHovering ? "flex" : "none",
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
top: topOffset ? topOffset - 12 : 0,
|
top: topOffset ? topOffset - 12 : 0,
|
||||||
left: leftOffset ? leftOffset - 32 : 0,
|
left: leftOffset ? leftOffset - 32 : 0,
|
||||||
opacity: showZapDialog || isHovering ? 1 : 0,
|
opacity: showZapDialog || isHovering ? 1 : 0,
|
||||||
pointerEvents: showZapDialog || isHovering ? "auto" : "none",
|
pointerEvents: showZapDialog || isHovering ? "auto" : "none",
|
||||||
}
|
}
|
||||||
}>
|
}>
|
||||||
{zapTarget && (
|
{zapTarget && (
|
||||||
<SendZapsDialog
|
<SendZapsDialog
|
||||||
@ -194,20 +195,14 @@ export function ChatMessage({
|
|||||||
eTag={ev.id}
|
eTag={ev.id}
|
||||||
pubkey={ev.pubkey}
|
pubkey={ev.pubkey}
|
||||||
button={
|
button={
|
||||||
<AsyncButton className="message-zap-button">
|
<IconButton iconName="zap" iconSize={14} className="rounded-full bg-layer-2 aspect-square" />
|
||||||
<Icon name="zap" className="message-zap-button-icon" />
|
|
||||||
</AsyncButton>
|
|
||||||
}
|
}
|
||||||
targetName={profile?.name || ev.pubkey}
|
targetName={profile?.name || ev.pubkey}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<AsyncButton className="message-zap-button" onClick={pickEmoji}>
|
<IconButton onClick={pickEmoji} iconName="face" iconSize={14} className="rounded-full bg-layer-2 aspect-square" />
|
||||||
<Icon name="face" className="message-zap-button-icon" />
|
|
||||||
</AsyncButton>
|
|
||||||
{shouldShowMuteButton && (
|
{shouldShowMuteButton && (
|
||||||
<AsyncButton className="message-zap-button" onClick={muteUser}>
|
<IconButton onClick={muteUser} iconName="user-x" iconSize={14} className="rounded-full bg-layer-2 aspect-square" />
|
||||||
<Icon name="user-x" className="message-zap-button-icon" />
|
|
||||||
</AsyncButton>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import * as Dialog from "@radix-ui/react-dialog";
|
|
||||||
import { useLogin } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
import { useContext, useEffect, useRef, useState } from "react";
|
import { useContext, useEffect, useRef, useState } from "react";
|
||||||
import { NostrStreamProvider } from "@/providers";
|
import { NostrStreamProvider } from "@/providers";
|
||||||
@ -6,16 +5,17 @@ import { FormattedMessage } from "react-intl";
|
|||||||
import { SnortContext } from "@snort/system-react";
|
import { SnortContext } from "@snort/system-react";
|
||||||
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||||
|
|
||||||
import AsyncButton from "./async-button";
|
|
||||||
import { LIVE_STREAM_CLIP, StreamState } from "@/const";
|
import { LIVE_STREAM_CLIP, StreamState } from "@/const";
|
||||||
import { extractStreamInfo } from "@/utils";
|
import { extractStreamInfo } from "@/utils";
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
import { unwrap } from "@snort/shared";
|
import { unwrap } from "@snort/shared";
|
||||||
import { TimelineBar } from "./timeline";
|
import { TimelineBar } from "./timeline";
|
||||||
|
import { DefaultButton } from "./buttons";
|
||||||
|
import Modal from "./modal";
|
||||||
|
|
||||||
export function ClipButton({ ev }: { ev: TaggedNostrEvent }) {
|
export function ClipButton({ ev }: { ev: TaggedNostrEvent }) {
|
||||||
const system = useContext(SnortContext);
|
const system = useContext(SnortContext);
|
||||||
const { id, service, status } = extractStreamInfo(ev);
|
const { id, service, status, host } = extractStreamInfo(ev);
|
||||||
const ref = useRef<HTMLVideoElement | null>(null);
|
const ref = useRef<HTMLVideoElement | null>(null);
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@ -68,6 +68,7 @@ export function ClipButton({ ev }: { ev: TaggedNostrEvent }) {
|
|||||||
return eb
|
return eb
|
||||||
.kind(LIVE_STREAM_CLIP)
|
.kind(LIVE_STREAM_CLIP)
|
||||||
.tag(unwrap(NostrLink.fromEvent(ev).toEventTag("root")))
|
.tag(unwrap(NostrLink.fromEvent(ev).toEventTag("root")))
|
||||||
|
.tag(["p", host ?? ev.pubkey])
|
||||||
.tag(["r", newClip.url])
|
.tag(["r", newClip.url])
|
||||||
.tag(["title", title])
|
.tag(["title", title])
|
||||||
.tag(["alt", `Live stream clip created on https://zap.stream\n${newClip.url}`]);
|
.tag(["alt", `Live stream clip created on https://zap.stream\n${newClip.url}`]);
|
||||||
@ -79,48 +80,37 @@ export function ClipButton({ ev }: { ev: TaggedNostrEvent }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog.Root open={open} onOpenChange={setOpen}>
|
<DefaultButton onClick={makeClip}>
|
||||||
<Dialog.Trigger asChild>
|
<Icon name="clapperboard" />
|
||||||
<div className="contents">
|
<span className="max-lg:hidden">
|
||||||
<AsyncButton onClick={makeClip} className="btn btn-primary">
|
<FormattedMessage defaultMessage="Create Clip" id="PA0ej4" />
|
||||||
<Icon name="clapperboard" />
|
</span>
|
||||||
<span className="max-lg:hidden">
|
</DefaultButton>
|
||||||
<FormattedMessage defaultMessage="Create Clip" id="PA0ej4" />
|
{open && <Modal id="create-clip" onClose={() => setOpen(false)}>
|
||||||
</span>
|
<div className="flex flex-col">
|
||||||
</AsyncButton>
|
<h1>
|
||||||
|
<FormattedMessage defaultMessage="Create Clip" id="PA0ej4" />
|
||||||
|
</h1>
|
||||||
|
{id && tempClipId && <video ref={ref} src={provider.getTempClipUrl(id, tempClipId)} controls muted />}
|
||||||
|
<TimelineBar
|
||||||
|
length={length}
|
||||||
|
offset={start}
|
||||||
|
width={300}
|
||||||
|
height={60}
|
||||||
|
setOffset={setStart}
|
||||||
|
setLength={setLength}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<small>
|
||||||
|
<FormattedMessage defaultMessage="Clip title" id="YwzT/0" />
|
||||||
|
</small>
|
||||||
|
<input type="text" value={title} onChange={e => setTitle(e.target.value)} placeholder="Epic combo!" />
|
||||||
</div>
|
</div>
|
||||||
</Dialog.Trigger>
|
<DefaultButton onClick={saveClip}>
|
||||||
<Dialog.Portal>
|
<FormattedMessage defaultMessage="Publish Clip" id="jJLRgo" />
|
||||||
<Dialog.Overlay className="dialog-overlay" />
|
</DefaultButton>
|
||||||
<Dialog.Content className="dialog-content">
|
</div>
|
||||||
<div className="content-inner">
|
</Modal>}
|
||||||
<h1>
|
|
||||||
<FormattedMessage defaultMessage="Create Clip" id="PA0ej4" />
|
|
||||||
</h1>
|
|
||||||
{id && tempClipId && <video ref={ref} src={provider.getTempClipUrl(id, tempClipId)} controls muted />}
|
|
||||||
<TimelineBar
|
|
||||||
length={length}
|
|
||||||
offset={start}
|
|
||||||
width={300}
|
|
||||||
height={60}
|
|
||||||
setOffset={setStart}
|
|
||||||
setLength={setLength}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col gap-1">
|
|
||||||
<small>
|
|
||||||
<FormattedMessage defaultMessage="Clip title" id="YwzT/0" />
|
|
||||||
</small>
|
|
||||||
<div className="paper">
|
|
||||||
<input type="text" value={title} onChange={e => setTitle(e.target.value)} placeholder="Epic combo!" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<AsyncButton onClick={saveClip}>
|
|
||||||
<FormattedMessage defaultMessage="Publish Clip" id="jJLRgo" />
|
|
||||||
</AsyncButton>
|
|
||||||
</div>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Portal>
|
|
||||||
</Dialog.Root>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,18 +1,14 @@
|
|||||||
import "./collapsible.css";
|
import "./collapsible.css";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { FormattedMessage } from "react-intl";
|
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 type { NostrLink } from "@snort/system";
|
||||||
|
|
||||||
import { Mention } from "./mention";
|
import { Mention } from "./mention";
|
||||||
import { EventIcon, NostrEvent } from "./Event";
|
import { EventIcon, NostrEvent } from "./Event";
|
||||||
import { ExternalLink } from "./external-link";
|
import { ExternalLink } from "./external-link";
|
||||||
import AsyncButton from "./async-button";
|
|
||||||
import { useEventFeed } from "@snort/system-react";
|
import { useEventFeed } from "@snort/system-react";
|
||||||
|
import Modal from "./modal";
|
||||||
|
import { DefaultButton } from "./buttons";
|
||||||
|
|
||||||
interface MediaURLProps {
|
interface MediaURLProps {
|
||||||
url: URL;
|
url: URL;
|
||||||
@ -20,25 +16,14 @@ interface MediaURLProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function MediaURL({ url, children }: MediaURLProps) {
|
export function MediaURL({ url, children }: MediaURLProps) {
|
||||||
const preview = <span className="url-preview">{url.toString()}</span>;
|
const [open, setOpen] = useState(false);
|
||||||
return (
|
return (<>
|
||||||
<Dialog.Root>
|
<span onClick={() => setOpen(true)}>{url.toString()}</span>
|
||||||
<Dialog.Trigger asChild>{preview}</Dialog.Trigger>
|
{open && <Modal id="media-preview" onClose={() => setOpen(false)}>
|
||||||
<Dialog.Portal>
|
<ExternalLink href={url.toString()}>{url.toString()}</ExternalLink>
|
||||||
<Dialog.Overlay className="dialog-overlay" />
|
{children}
|
||||||
<Dialog.Content className="dialog-content">
|
</Modal>}
|
||||||
<div className="collapsible-media">
|
</>
|
||||||
<ExternalLink href={url.toString()}>{url.toString()}</ExternalLink>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
<Dialog.Close asChild>
|
|
||||||
<AsyncButton className="btn delete-button" aria-label="Close">
|
|
||||||
<FormattedMessage defaultMessage="Close" id="rbrahO" />
|
|
||||||
</AsyncButton>
|
|
||||||
</Dialog.Close>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Portal>
|
|
||||||
</Dialog.Root>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,23 +33,19 @@ export function CollapsibleEvent({ link }: { link: NostrLink }) {
|
|||||||
const author = event?.pubkey || link.author;
|
const author = event?.pubkey || link.author;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Collapsible.Root className="collapsible" open={open} onOpenChange={setOpen}>
|
<>
|
||||||
<div className="collapsed-event">
|
<div className="flex justify-between">
|
||||||
<div className="collapsed-event-header">
|
<div className="flex gap-2">
|
||||||
{event && <EventIcon kind={event.kind} />}
|
<EventIcon kind={event?.kind} />
|
||||||
{author && <Mention pubkey={author} />}
|
<FormattedMessage defaultMessage="Note by {name}" id="ALdW69" values={{
|
||||||
|
name: <Mention pubkey={author ?? ""} />
|
||||||
|
}} />
|
||||||
</div>
|
</div>
|
||||||
<Collapsible.Trigger asChild>
|
<DefaultButton onClick={() => setOpen(s => !s)}>
|
||||||
<AsyncButton className={`${open ? "btn btn-small delete-button" : "btn btn-small"}`}>
|
{open ? <FormattedMessage defaultMessage="Hide" id="VA/Z1S" /> : <FormattedMessage defaultMessage="Show" id="K7AkdL" />}
|
||||||
{open ? (
|
</DefaultButton>
|
||||||
<FormattedMessage defaultMessage="Hide" id="VA/Z1S" />
|
|
||||||
) : (
|
|
||||||
<FormattedMessage defaultMessage="Show" id="K7AkdL" />
|
|
||||||
)}
|
|
||||||
</AsyncButton>
|
|
||||||
</Collapsible.Trigger>
|
|
||||||
</div>
|
</div>
|
||||||
<Collapsible.Content>{open && event && <NostrEvent ev={event} />}</Collapsible.Content>
|
{open && event && <NostrEvent ev={event} />}
|
||||||
</Collapsible.Root>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import AsyncButton from "./async-button";
|
import { Layer1Button, WarningButton } from "./buttons";
|
||||||
|
|
||||||
export function isContentWarningAccepted() {
|
export function isContentWarningAccepted() {
|
||||||
return Boolean(window.localStorage.getItem("accepted-content-warning"));
|
return Boolean(window.localStorage.getItem("accepted-content-warning"));
|
||||||
@ -26,12 +26,12 @@ export function ContentWarningOverlay() {
|
|||||||
<FormattedMessage defaultMessage="Confirm your age" id="s7V+5p" />
|
<FormattedMessage defaultMessage="Confirm your age" id="s7V+5p" />
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<AsyncButton className="btn btn-warning" onClick={grownUp}>
|
<WarningButton onClick={grownUp}>
|
||||||
<FormattedMessage defaultMessage="Yes, I am over 18" id="O2Cy6m" />
|
<FormattedMessage defaultMessage="Yes, I am over 18" id="O2Cy6m" />
|
||||||
</AsyncButton>
|
</WarningButton>
|
||||||
<AsyncButton className="btn" onClick={() => navigate("/")}>
|
<Layer1Button onClick={() => navigate("/")}>
|
||||||
<FormattedMessage defaultMessage="No, I am under 18" id="KkIL3s" />
|
<FormattedMessage defaultMessage="No, I am under 18" id="KkIL3s" />
|
||||||
</AsyncButton>
|
</Layer1Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -6,11 +6,11 @@ import { SnortContext } from "@snort/system-react";
|
|||||||
|
|
||||||
import { useLogin } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
import { toEmojiPack } from "@/hooks/emoji";
|
import { toEmojiPack } from "@/hooks/emoji";
|
||||||
import AsyncButton from "./async-button";
|
|
||||||
import { findTag } from "@/utils";
|
import { findTag } from "@/utils";
|
||||||
import { USER_EMOJIS } from "@/const";
|
import { USER_EMOJIS } from "@/const";
|
||||||
import { Login } from "@/index";
|
import { Login } from "@/index";
|
||||||
import type { EmojiPack as EmojiPackType } from "@/types";
|
import type { EmojiPack as EmojiPackType } from "@/types";
|
||||||
|
import { DefaultButton, WarningButton } from "./buttons";
|
||||||
|
|
||||||
export function EmojiPack({ ev }: { ev: NostrEvent }) {
|
export function EmojiPack({ ev }: { ev: NostrEvent }) {
|
||||||
const system = useContext(SnortContext);
|
const system = useContext(SnortContext);
|
||||||
@ -45,16 +45,13 @@ export function EmojiPack({ ev }: { ev: NostrEvent }) {
|
|||||||
<div className="outline emoji-pack">
|
<div className="outline emoji-pack">
|
||||||
<div className="emoji-pack-title">
|
<div className="emoji-pack-title">
|
||||||
<h4>{name}</h4>
|
<h4>{name}</h4>
|
||||||
{login?.pubkey && (
|
{login?.pubkey && (isUsed ?
|
||||||
<AsyncButton
|
<WarningButton onClick={toggleEmojiPack}>
|
||||||
className={`btn btn-small btn-primary ${isUsed ? "delete-button" : ""}`}
|
<FormattedMessage defaultMessage="Remove" id="G/yZLu" />
|
||||||
onClick={toggleEmojiPack}>
|
</WarningButton> :
|
||||||
{isUsed ? (
|
<DefaultButton onClick={toggleEmojiPack}>
|
||||||
<FormattedMessage defaultMessage="Remove" id="G/yZLu" />
|
<FormattedMessage defaultMessage="Add" id="2/2yg+" />
|
||||||
) : (
|
</DefaultButton>
|
||||||
<FormattedMessage defaultMessage="Add" id="2/2yg+" />
|
|
||||||
)}
|
|
||||||
</AsyncButton>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="emoji-pack-emojis">
|
<div className="emoji-pack-emojis">
|
||||||
|
@ -8,7 +8,7 @@ interface ExternalLinkProps {
|
|||||||
|
|
||||||
export function ExternalLink({ children, href }: ExternalLinkProps) {
|
export function ExternalLink({ children, href }: ExternalLinkProps) {
|
||||||
return (
|
return (
|
||||||
<a href={href} rel="noopener noreferrer" target="_blank">
|
<a href={href} rel="noopener noreferrer" target="_blank" className="text-primary">
|
||||||
{children}
|
{children}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
@ -31,7 +31,3 @@
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 12px;
|
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 { VoidApi } from "@void-cat/api";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import AsyncButton from "./async-button";
|
import { DefaultButton } from "./buttons";
|
||||||
|
|
||||||
const voidCatHost = "https://void.cat";
|
const voidCatHost = "https://void.cat";
|
||||||
const fileExtensionRegex = /\.([\w]{1,7})$/i;
|
const fileExtensionRegex = /\.([\w]{1,7})$/i;
|
||||||
@ -87,9 +87,9 @@ export function FileUploader({ defaultImage, onClear, onFileUpload }: FileUpload
|
|||||||
</label>
|
</label>
|
||||||
<div className="file-uploader-preview">
|
<div className="file-uploader-preview">
|
||||||
{img?.length > 0 && (
|
{img?.length > 0 && (
|
||||||
<AsyncButton className="btn btn-primary clear-button" onClick={clearImage}>
|
<DefaultButton onClick={clearImage}>
|
||||||
<FormattedMessage defaultMessage="Clear" id="/GCoTA" />
|
<FormattedMessage defaultMessage="Clear" id="/GCoTA" />
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
)}
|
)}
|
||||||
{img && <img className="image-preview" src={img} />}
|
{img && <img className="image-preview" src={img} />}
|
||||||
</div>
|
</div>
|
||||||
|
@ -4,8 +4,8 @@ import { useContext } from "react";
|
|||||||
import { SnortContext } from "@snort/system-react";
|
import { SnortContext } from "@snort/system-react";
|
||||||
|
|
||||||
import { useLogin } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
import AsyncButton from "./async-button";
|
|
||||||
import { Login } from "@/index";
|
import { Login } from "@/index";
|
||||||
|
import { DefaultButton } from "./buttons";
|
||||||
|
|
||||||
export function LoggedInFollowButton({
|
export function LoggedInFollowButton({
|
||||||
tag,
|
tag,
|
||||||
@ -60,17 +60,15 @@ export function LoggedInFollowButton({
|
|||||||
|
|
||||||
if (isFollowing && hideWhenFollowing) return;
|
if (isFollowing && hideWhenFollowing) return;
|
||||||
return (
|
return (
|
||||||
<AsyncButton
|
<DefaultButton
|
||||||
disabled={timestamp ? timestamp === 0 : true}
|
disabled={timestamp ? timestamp === 0 : true}
|
||||||
type="button"
|
|
||||||
className="btn btn-primary"
|
|
||||||
onClick={isFollowing ? unfollow : follow}>
|
onClick={isFollowing ? unfollow : follow}>
|
||||||
{isFollowing ? (
|
{isFollowing ? (
|
||||||
<FormattedMessage defaultMessage="Unfollow" id="izWS4J" />
|
<FormattedMessage defaultMessage="Unfollow" id="izWS4J" />
|
||||||
) : (
|
) : (
|
||||||
<FormattedMessage defaultMessage="Follow" id="ieGrWo" />
|
<FormattedMessage defaultMessage="Follow" id="ieGrWo" />
|
||||||
)}
|
)}
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,4 @@
|
|||||||
import "./goal.css";
|
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import * as Progress from "@radix-ui/react-progress";
|
|
||||||
import Confetti from "react-confetti";
|
import Confetti from "react-confetti";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
@ -14,8 +12,9 @@ import { SendZapsDialog } from "./send-zap";
|
|||||||
import { getName } from "./profile";
|
import { getName } from "./profile";
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
import { useZaps } from "@/hooks/zaps";
|
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 profile = useUserProfile(ev.pubkey);
|
||||||
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
||||||
const link = NostrLink.fromEvent(ev);
|
const link = NostrLink.fromEvent(ev);
|
||||||
@ -40,23 +39,26 @@ export function Goal({ ev }: { ev: NostrEvent }) {
|
|||||||
const previousValue = usePreviousValue(isFinished);
|
const previousValue = usePreviousValue(isFinished);
|
||||||
|
|
||||||
const goalContent = (
|
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>}
|
{ev.content.length > 0 && <p>{ev.content}</p>}
|
||||||
<div className={`progress-container ${isFinished ? "finished" : ""}`}>
|
<div className="relative h-10">
|
||||||
<Progress.Root className="progress-root" value={progress}>
|
<div className="absolute bg-layer-2 h-3 rounded-full my-4 w-full"></div>
|
||||||
<Progress.Indicator className="progress-indicator" style={{ transform: `translateX(-${100 - progress}%)` }}>
|
<div className="absolute bg-zap h-3 rounded-full text-xs font-medium my-4 leading-3 pl-2" style={{
|
||||||
{!isFinished && <span className="amount so-far">{formatSats(soFar)}</span>}
|
width: `${progress}%`
|
||||||
</Progress.Indicator>
|
}}>
|
||||||
<span className="amount target">
|
{soFar > 0 ? formatSats(soFar) : ""}
|
||||||
<FormattedMessage defaultMessage="Goal: {amount}" id="QceMQZ" values={{ amount: formatSats(goalAmount) }} />
|
</div>
|
||||||
</span>
|
<div className="absolute text-right text-xs right-10 font-medium my-4 leading-3">
|
||||||
</Progress.Root>
|
<FormattedMessage defaultMessage="Goal: {amount}" id="QceMQZ" values={{ amount: formatSats(goalAmount) }} />
|
||||||
<div className="zap-circle">
|
</div>
|
||||||
<Icon name="zap-filled" className={isFinished ? "goal-finished" : "goal-unfinished"} />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{isFinished && previousValue === false && <Confetti numberOfPieces={2100} recycle={false} />}
|
{isFinished && previousValue === false && (confetti ?? true) &&
|
||||||
</div>
|
<Confetti numberOfPieces={2100} recycle={false} />}
|
||||||
|
</div >
|
||||||
);
|
);
|
||||||
|
|
||||||
return zapTarget ? (
|
return zapTarget ? (
|
||||||
|
@ -48,7 +48,7 @@
|
|||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
|
|
||||||
padding-top: 8px;
|
padding-top: 8px;
|
||||||
border-top: 1px solid var(--border, #171717);
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
.live-chat > .write-message > div:nth-child(1) {
|
.live-chat > .write-message > div:nth-child(1) {
|
||||||
@ -96,7 +96,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: var(--gap-s);
|
gap: var(--gap-s);
|
||||||
border-bottom: 1px solid var(--border, #171717);
|
border-bottom: 1px solid var(--border);
|
||||||
padding-bottom: var(--gap-s);
|
padding-bottom: var(--gap-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,10 +269,6 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.write-message-container .paper {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.write-emoji-button {
|
.write-emoji-button {
|
||||||
color: #ffffff80;
|
color: #ffffff80;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -131,7 +131,7 @@ export function LiveChat({
|
|||||||
case -2: {
|
case -2: {
|
||||||
return (
|
return (
|
||||||
<b
|
<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}`}>
|
key={`${a.kind}-${a.created_at}`}>
|
||||||
{a.kind === -1 ? (
|
{a.kind === -1 ? (
|
||||||
<FormattedMessage defaultMessage="Stream Started" id="5tM0VD" />
|
<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 { VoidApi } from "@void-cat/api";
|
||||||
import { SnortContext } from "@snort/system-react";
|
import { SnortContext } from "@snort/system-react";
|
||||||
|
|
||||||
import AsyncButton from "./async-button";
|
|
||||||
import { Login } from "@/index";
|
import { Login } from "@/index";
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
import Copy from "./copy";
|
import Copy from "./copy";
|
||||||
@ -27,6 +26,7 @@ import { openFile } from "@/utils";
|
|||||||
import { LoginType } from "@/login";
|
import { LoginType } from "@/login";
|
||||||
import { DefaultProvider, StreamProviderInfo } from "@/providers";
|
import { DefaultProvider, StreamProviderInfo } from "@/providers";
|
||||||
import { NostrStreamProvider } from "@/providers/zsz";
|
import { NostrStreamProvider } from "@/providers/zsz";
|
||||||
|
import { DefaultButton, Layer1Button } from "./buttons";
|
||||||
|
|
||||||
enum Stage {
|
enum Stage {
|
||||||
Login = 0,
|
Login = 0,
|
||||||
@ -81,7 +81,7 @@ export function LoginSignup({ close }: { close: () => void }) {
|
|||||||
function createAccount() {
|
function createAccount() {
|
||||||
const newKey = bytesToHex(schnorr.utils.randomPrivateKey());
|
const newKey = bytesToHex(schnorr.utils.randomPrivateKey());
|
||||||
setNewKey(newKey);
|
setNewKey(newKey);
|
||||||
setLnAddress(`${getPublicKey(newKey)}@zap.stream`);
|
setLnAddress(`${getPublicKey(newKey)}@${window.location.host}`);
|
||||||
setStage(Stage.Details);
|
setStage(Stage.Details);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,9 +163,9 @@ export function LoginSignup({ close }: { close: () => void }) {
|
|||||||
<h3>
|
<h3>
|
||||||
<FormattedMessage defaultMessage="No emails, just awesomeness!" id="+AcVD+" />
|
<FormattedMessage defaultMessage="No emails, just awesomeness!" id="+AcVD+" />
|
||||||
</h3>
|
</h3>
|
||||||
<AsyncButton className="btn btn-primary btn-block" onClick={createAccount}>
|
<DefaultButton onClick={createAccount}>
|
||||||
<FormattedMessage defaultMessage="Create Account" id="5JcXdV" />
|
<FormattedMessage defaultMessage="Create Account" id="5JcXdV" />
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
|
|
||||||
<div className="or-divider">
|
<div className="or-divider">
|
||||||
<hr />
|
<hr />
|
||||||
@ -174,14 +174,14 @@ export function LoginSignup({ close }: { close: () => void }) {
|
|||||||
</div>
|
</div>
|
||||||
{hasNostrExtension && (
|
{hasNostrExtension && (
|
||||||
<>
|
<>
|
||||||
<AsyncButton className="btn btn-primary btn-block" onClick={loginNip7}>
|
<DefaultButton onClick={loginNip7}>
|
||||||
<FormattedMessage defaultMessage="Nostr Extension" id="ebmhes" />
|
<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" />
|
<FormattedMessage defaultMessage="Login with Private Key (insecure)" id="feZ/kG" />
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
{error && <b className="error">{error}</b>}
|
{error && <b className="error">{error}</b>}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@ -208,28 +208,25 @@ export function LoginSignup({ close }: { close: () => void }) {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<div className="paper">
|
<input
|
||||||
<input
|
type="text"
|
||||||
type="text"
|
value={key}
|
||||||
value={key}
|
onChange={e => setNewKey(e.target.value)}
|
||||||
onChange={e => setNewKey(e.target.value)}
|
placeholder={formatMessage({ defaultMessage: "eg. nsec1xyz", id: "yzKwBQ" })}
|
||||||
placeholder={formatMessage({ defaultMessage: "eg. nsec1xyz", id: "yzKwBQ" })}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<div></div>
|
<div></div>
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
<AsyncButton
|
<Layer1Button
|
||||||
className="btn btn-secondary"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setNewKey("");
|
setNewKey("");
|
||||||
setStage(Stage.Login);
|
setStage(Stage.Login);
|
||||||
}}>
|
}}>
|
||||||
<FormattedMessage defaultMessage="Cancel" id="47FYwb" />
|
<FormattedMessage defaultMessage="Cancel" id="47FYwb" />
|
||||||
</AsyncButton>
|
</Layer1Button>
|
||||||
<AsyncButton onClick={doLoginNsec} className="btn btn-primary">
|
<DefaultButton onClick={doLoginNsec}>
|
||||||
<FormattedMessage defaultMessage="Log In" id="r2Jjms" />
|
<FormattedMessage defaultMessage="Log In" id="r2Jjms" />
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{error && <b className="error">{error}</b>}
|
{error && <b className="error">{error}</b>}
|
||||||
@ -258,21 +255,19 @@ export function LoginSignup({ close }: { close: () => void }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="username">
|
<div className="username">
|
||||||
<div className="paper">
|
<input
|
||||||
<input
|
type="text"
|
||||||
type="text"
|
placeholder="Username"
|
||||||
placeholder="Username"
|
value={username}
|
||||||
value={username}
|
onChange={e => setUsername(e.target.value)}
|
||||||
onChange={e => setUsername(e.target.value)}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<small>
|
<small>
|
||||||
<FormattedMessage defaultMessage="You can change this later" id="ZmqxZs" />
|
<FormattedMessage defaultMessage="You can change this later" id="ZmqxZs" />
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
<AsyncButton type="button" className="btn btn-primary" onClick={setupProfile}>
|
<DefaultButton onClick={setupProfile}>
|
||||||
<FormattedMessage defaultMessage="Save" id="jvo0vs" />
|
<FormattedMessage defaultMessage="Save" id="jvo0vs" />
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -303,22 +298,20 @@ export function LoginSignup({ close }: { close: () => void }) {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<div className="username">
|
<div className="username">
|
||||||
<div className="paper">
|
<input
|
||||||
<input
|
type="text"
|
||||||
type="text"
|
placeholder={formatMessage({ defaultMessage: "eg. name@wallet.com", id: "1qsXCO" })}
|
||||||
placeholder={formatMessage({ defaultMessage: "eg. name@wallet.com", id: "1qsXCO" })}
|
value={lnAddress}
|
||||||
value={lnAddress}
|
onChange={e => setLnAddress(e.target.value)}
|
||||||
onChange={e => setLnAddress(e.target.value)}
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<small>
|
<small>
|
||||||
<FormattedMessage defaultMessage="You can always replace it with your own address later." id="FjDlus" />
|
<FormattedMessage defaultMessage="You can always replace it with your own address later." id="FjDlus" />
|
||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
{error && <b className="error">{error}</b>}
|
{error && <b className="error">{error}</b>}
|
||||||
<AsyncButton type="button" className="btn btn-primary" onClick={saveProfile}>
|
<DefaultButton onClick={saveProfile}>
|
||||||
<FormattedMessage defaultMessage="Amazing! Continue.." id="tM6fNW" />
|
<FormattedMessage defaultMessage="Amazing! Continue.." id="tM6fNW" />
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -337,12 +330,12 @@ export function LoginSignup({ close }: { close: () => void }) {
|
|||||||
id="H/bNs9"
|
id="H/bNs9"
|
||||||
/>
|
/>
|
||||||
</p>
|
</p>
|
||||||
<div className="paper">
|
<div className="bg-layer-1 rounded-xl px-3 py-2">
|
||||||
<Copy text={hexToBech32("nsec", key)} />
|
<Copy text={hexToBech32("nsec", key)} />
|
||||||
</div>
|
</div>
|
||||||
<AsyncButton className="btn btn-primary" onClick={loginWithKey}>
|
<DefaultButton onClick={loginWithKey}>
|
||||||
<FormattedMessage defaultMessage="Ok, it's safe" id="My6HwN" />
|
<FormattedMessage defaultMessage="Ok, it's safe" id="My6HwN" />
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
</div>
|
</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 { SnortContext } from "@snort/system-react";
|
||||||
|
|
||||||
import { useLogin } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
import AsyncButton from "./async-button";
|
|
||||||
import { Login } from "@/index";
|
import { Login } from "@/index";
|
||||||
import { MUTED } from "@/const";
|
import { MUTED } from "@/const";
|
||||||
|
import { DefaultButton } from "./buttons";
|
||||||
|
|
||||||
export function useMute(pubkey: string) {
|
export function useMute(pubkey: string) {
|
||||||
const system = useContext(SnortContext);
|
const system = useContext(SnortContext);
|
||||||
@ -55,13 +55,13 @@ export function LoggedInMuteButton({ pubkey }: { pubkey: string }) {
|
|||||||
const { isMuted, mute, unmute } = useMute(pubkey);
|
const { isMuted, mute, unmute } = useMute(pubkey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncButton onClick={() => (isMuted ? unmute() : mute())} className="font-bold">
|
<DefaultButton onClick={() => (isMuted ? unmute() : mute())}>
|
||||||
{isMuted ? (
|
{isMuted ? (
|
||||||
<FormattedMessage defaultMessage="Unmute" id="W9355R" />
|
<FormattedMessage defaultMessage="Unmute" id="W9355R" />
|
||||||
) : (
|
) : (
|
||||||
<FormattedMessage defaultMessage="Mute" id="x82IOl" />
|
<FormattedMessage defaultMessage="Mute" id="x82IOl" />
|
||||||
)}
|
)}
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,11 +9,6 @@
|
|||||||
gap: 8px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.new-goal .paper {
|
|
||||||
background: #262626;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.new-goal .btn:disabled {
|
.new-goal .btn:disabled {
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
import "./new-goal.css";
|
import "./new-goal.css";
|
||||||
import * as Dialog from "@radix-ui/react-dialog";
|
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
import { SnortContext } from "@snort/system-react";
|
import { SnortContext } from "@snort/system-react";
|
||||||
|
|
||||||
import AsyncButton from "./async-button";
|
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
import { GOAL } from "@/const";
|
import { GOAL } from "@/const";
|
||||||
import { useLogin } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
import { defaultRelays } from "@/const";
|
import { defaultRelays } from "@/const";
|
||||||
|
import { DefaultButton } from "./buttons";
|
||||||
|
import Modal from "./modal";
|
||||||
|
|
||||||
export function NewGoalDialog() {
|
export function NewGoalDialog() {
|
||||||
const system = useContext(SnortContext);
|
const system = useContext(SnortContext);
|
||||||
@ -37,64 +37,52 @@ export function NewGoalDialog() {
|
|||||||
}
|
}
|
||||||
const isValid = goalName.length && Number(goalAmount) > 0;
|
const isValid = goalName.length && Number(goalAmount) > 0;
|
||||||
|
|
||||||
return (
|
return (<>
|
||||||
<Dialog.Root open={open} onOpenChange={setOpen}>
|
<DefaultButton onClick={() => setOpen(true)}>
|
||||||
<Dialog.Trigger asChild>
|
<Icon name="zap-filled" size={12} />
|
||||||
<AsyncButton className="btn btn-primary">
|
<span>
|
||||||
<span>
|
<FormattedMessage defaultMessage="Add stream goal" id="wOy57k" />
|
||||||
<Icon name="zap-filled" size={12} />
|
</span>
|
||||||
<span>
|
</DefaultButton>
|
||||||
<FormattedMessage defaultMessage="Add stream goal" id="wOy57k" />
|
{open && <Modal id="new-goal" onClose={() => setOpen(false)}>
|
||||||
</span>
|
<div className="new-goal content-inner">
|
||||||
</span>
|
<div className="zap-goals">
|
||||||
</AsyncButton>
|
<Icon name="zap-filled" className="stream-zap-goals-icon" size={16} />
|
||||||
</Dialog.Trigger>
|
<h3>
|
||||||
<Dialog.Portal>
|
<FormattedMessage defaultMessage="Stream Zap Goals" id="0GfNiL" />
|
||||||
<Dialog.Overlay className="dialog-overlay" />
|
</h3>
|
||||||
<Dialog.Content className="dialog-content">
|
</div>
|
||||||
<div className="new-goal content-inner">
|
<div>
|
||||||
<div className="zap-goals">
|
<p>
|
||||||
<Icon name="zap-filled" className="stream-zap-goals-icon" size={16} />
|
<FormattedMessage defaultMessage="Name" id="HAlOn1" />
|
||||||
<h3>
|
</p>
|
||||||
<FormattedMessage defaultMessage="Stream Zap Goals" id="0GfNiL" />
|
<input
|
||||||
</h3>
|
type="text"
|
||||||
</div>
|
value={goalName}
|
||||||
<div>
|
placeholder="e.g. New Laptop"
|
||||||
<p>
|
onChange={e => setGoalName(e.target.value)}
|
||||||
<FormattedMessage defaultMessage="Name" id="HAlOn1" />
|
/>
|
||||||
</p>
|
</div>
|
||||||
<div className="paper">
|
<div>
|
||||||
<input
|
<p>
|
||||||
type="text"
|
<FormattedMessage defaultMessage="Amount" id="/0TOL5" />
|
||||||
value={goalName}
|
</p>
|
||||||
placeholder="e.g. New Laptop"
|
<input
|
||||||
onChange={e => setGoalName(e.target.value)}
|
type="number"
|
||||||
/>
|
placeholder="21"
|
||||||
</div>
|
min="1"
|
||||||
</div>
|
max="2100000000000000"
|
||||||
<div>
|
value={goalAmount}
|
||||||
<p>
|
onChange={e => setGoalAmount(e.target.value)}
|
||||||
<FormattedMessage defaultMessage="Amount" id="/0TOL5" />
|
/>
|
||||||
</p>
|
</div>
|
||||||
<div className="paper">
|
<div className="create-goal">
|
||||||
<input
|
<DefaultButton disabled={!isValid} onClick={publishGoal}>
|
||||||
type="number"
|
<FormattedMessage defaultMessage="Create Goal" id="X2PZ7D" />
|
||||||
placeholder="21"
|
</DefaultButton>
|
||||||
min="1"
|
</div>
|
||||||
max="2100000000000000"
|
</div>
|
||||||
value={goalAmount}
|
</Modal>}
|
||||||
onChange={e => setGoalAmount(e.target.value)}
|
</>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="create-goal">
|
|
||||||
<AsyncButton type="button" className="btn btn-primary wide" disabled={!isValid} onClick={publishGoal}>
|
|
||||||
<FormattedMessage defaultMessage="Create Goal" id="X2PZ7D" />
|
|
||||||
</AsyncButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Portal>
|
|
||||||
</Dialog.Root>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -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 { useContext, useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { unwrap } from "@snort/shared";
|
import { unwrap } from "@snort/shared";
|
||||||
@ -12,7 +10,9 @@ import { NostrStreamProvider, StreamProvider, StreamProviders } from "@/provider
|
|||||||
import { StreamEditor, StreamEditorProps } from "./stream-editor";
|
import { StreamEditor, StreamEditorProps } from "./stream-editor";
|
||||||
import { eventLink } from "@/utils";
|
import { eventLink } from "@/utils";
|
||||||
import { NostrProviderDialog } from "./nostr-provider-dialog";
|
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 }) {
|
function NewStream({ ev, onFinish }: Omit<StreamEditorProps, "onFinish"> & { onFinish: () => void }) {
|
||||||
const system = useContext(SnortContext);
|
const system = useContext(SnortContext);
|
||||||
@ -53,14 +53,13 @@ function NewStream({ ev, onFinish }: Omit<StreamEditorProps, "onFinish"> & { onF
|
|||||||
case StreamProviders.NostrType: {
|
case StreamProviders.NostrType: {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AsyncButton
|
<DefaultButton
|
||||||
className="btn btn-secondary"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate("/settings");
|
navigate("/settings");
|
||||||
onFinish?.();
|
onFinish?.();
|
||||||
}}>
|
}}>
|
||||||
<FormattedMessage defaultMessage="Get Stream Key" id="Vn2WiP" />
|
<FormattedMessage defaultMessage="Get Stream Key" id="Vn2WiP" />
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
<NostrProviderDialog
|
<NostrProviderDialog
|
||||||
provider={currentProvider as NostrStreamProvider}
|
provider={currentProvider as NostrStreamProvider}
|
||||||
onFinish={onFinish}
|
onFinish={onFinish}
|
||||||
@ -85,12 +84,14 @@ function NewStream({ ev, onFinish }: Omit<StreamEditorProps, "onFinish"> & { onF
|
|||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{providers.map(v => (
|
{providers.map(v => (
|
||||||
<span className={`pill${v === currentProvider ? " active" : ""}`} onClick={() => setCurrentProvider(v)}>
|
<Pill className={`${v === currentProvider ? " text-bold" : ""}`} onClick={() => setCurrentProvider(v)}>
|
||||||
{v.name}
|
{v.name}
|
||||||
</span>
|
</Pill>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{providerDialog()}
|
<div className="flex flex-col gap-4">
|
||||||
|
{providerDialog()}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -103,30 +104,23 @@ interface NewStreamDialogProps {
|
|||||||
export function NewStreamDialog(props: NewStreamDialogProps & StreamEditorProps) {
|
export function NewStreamDialog(props: NewStreamDialogProps & StreamEditorProps) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
return (
|
return (
|
||||||
<Dialog.Root open={open} onOpenChange={setOpen}>
|
<>
|
||||||
<Dialog.Trigger asChild>
|
<DefaultButton className={props.btnClassName} onClick={() => setOpen(true)}>
|
||||||
<AsyncButton className={props.btnClassName}>
|
{props.text && props.text}
|
||||||
{props.text && props.text}
|
{!props.text && (
|
||||||
{!props.text && (
|
<>
|
||||||
<>
|
<span className="max-xl:hidden">
|
||||||
<span className="max-xl:hidden">
|
<FormattedMessage defaultMessage="Stream" id="uYw2LD" />
|
||||||
<FormattedMessage defaultMessage="Stream" id="uYw2LD" />
|
</span>
|
||||||
</span>
|
<Icon name="signal" />
|
||||||
<Icon name="signal" />
|
</>
|
||||||
</>
|
)}
|
||||||
)}
|
</DefaultButton>
|
||||||
</AsyncButton>
|
{open && <Modal id="new-stream" onClose={() => setOpen(false)}>
|
||||||
</Dialog.Trigger>
|
<div className="new-stream">
|
||||||
<Dialog.Portal>
|
<NewStream {...props} onFinish={() => setOpen(false)} />
|
||||||
<Dialog.Overlay className="dialog-overlay" />
|
</div>
|
||||||
<Dialog.Content className="dialog-content">
|
</Modal>}
|
||||||
<div className="content-inner">
|
</>
|
||||||
<div className="new-stream">
|
|
||||||
<NewStream {...props} onFinish={() => setOpen(false)} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Portal>
|
|
||||||
</Dialog.Root>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,10 @@ import { NostrStreamProvider, StreamProviderEndpoint, StreamProviderInfo } from
|
|||||||
import { SendZaps } from "./send-zap";
|
import { SendZaps } from "./send-zap";
|
||||||
import { StreamEditor, StreamEditorProps } from "./stream-editor";
|
import { StreamEditor, StreamEditorProps } from "./stream-editor";
|
||||||
import Spinner from "./spinner";
|
import Spinner from "./spinner";
|
||||||
import AsyncButton from "./async-button";
|
|
||||||
import { unwrap } from "@snort/shared";
|
import { unwrap } from "@snort/shared";
|
||||||
import { useRates } from "@/hooks/rates";
|
import { useRates } from "@/hooks/rates";
|
||||||
|
import { DefaultButton } from "./buttons";
|
||||||
|
import Pill from "./pill";
|
||||||
|
|
||||||
export function NostrProviderDialog({
|
export function NostrProviderDialog({
|
||||||
provider,
|
provider,
|
||||||
@ -35,12 +36,14 @@ export function NostrProviderDialog({
|
|||||||
return arr.sort((a, b) => ((a.rate ?? 0) > (b.rate ?? 0) ? -1 : 1));
|
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(() => {
|
useEffect(() => {
|
||||||
provider.info().then(v => {
|
loadInfo();
|
||||||
setInfo(v);
|
|
||||||
setTos(v.tosAccepted ?? true);
|
|
||||||
setEndpoint(sortEndpoints(v.endpoints)[0]);
|
|
||||||
});
|
|
||||||
}, [provider]);
|
}, [provider]);
|
||||||
|
|
||||||
if (!info) {
|
if (!info) {
|
||||||
@ -80,7 +83,7 @@ export function NostrProviderDialog({
|
|||||||
{`${(raw / 60).toFixed(0)} hour @ ${ep.rate} sats/${ep.unit}`}
|
{`${(raw / 60).toFixed(0)} hour @ ${ep.rate} sats/${ep.unit}`}
|
||||||
or <br />
|
or <br />
|
||||||
{`${pm.toLocaleString()} sats/month ($${(rate.ask * pm * 1e-8).toFixed(2)}/mo) streaming ${hrs} hrs/month`}
|
{`${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
|
Hrs
|
||||||
<input type="number" value={hrs} onChange={e => setHrs(e.target.valueAsNumber)} />
|
<input type="number" value={hrs} onChange={e => setHrs(e.target.valueAsNumber)} />
|
||||||
</div>
|
</div>
|
||||||
@ -135,9 +138,9 @@ export function NostrProviderDialog({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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" />
|
<FormattedMessage defaultMessage="Continue" id="acrOoz" />
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -154,11 +157,11 @@ export function NostrProviderDialog({
|
|||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{sortEndpoints(info.endpoints).map(a => (
|
{sortEndpoints(info.endpoints).map(a => (
|
||||||
<span
|
<Pill
|
||||||
className={`pill bg-gray-1${ep?.name === a.name ? " active" : ""}`}
|
selected={ep?.name === a.name}
|
||||||
onClick={() => setEndpoint(a)}>
|
onClick={() => setEndpoint(a)}>
|
||||||
{a.name}
|
{a.name}
|
||||||
</span>
|
</Pill>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -167,23 +170,18 @@ export function NostrProviderDialog({
|
|||||||
<p>
|
<p>
|
||||||
<FormattedMessage defaultMessage="Server Url" id="5kx+2v" />
|
<FormattedMessage defaultMessage="Server Url" id="5kx+2v" />
|
||||||
</p>
|
</p>
|
||||||
<div className="paper">
|
<input type="text" value={ep?.url} disabled />
|
||||||
<input type="text" value={ep?.url} disabled />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
<FormattedMessage defaultMessage="Stream Key" id="LknBsU" />
|
<FormattedMessage defaultMessage="Stream Key" id="LknBsU" />
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div className="paper grow">
|
<input type="password" value={ep?.key} disabled />
|
||||||
<input type="password" value={ep?.key} disabled />
|
<DefaultButton
|
||||||
</div>
|
|
||||||
<AsyncButton
|
|
||||||
className="btn btn-primary"
|
|
||||||
onClick={() => window.navigator.clipboard.writeText(ep?.key ?? "")}>
|
onClick={() => window.navigator.clipboard.writeText(ep?.key ?? "")}>
|
||||||
<FormattedMessage defaultMessage="Copy" id="4l6vz1" />
|
<FormattedMessage defaultMessage="Copy" id="4l6vz1" />
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -191,16 +189,16 @@ export function NostrProviderDialog({
|
|||||||
<FormattedMessage defaultMessage="Balance" id="H5+NAX" />
|
<FormattedMessage defaultMessage="Balance" id="H5+NAX" />
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<div className="paper grow">
|
<div className="bg-layer-2 rounded-xl w-full flex items-center px-3">
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="{amount} sats"
|
defaultMessage="{amount} sats"
|
||||||
id="vrTOHJ"
|
id="vrTOHJ"
|
||||||
values={{ amount: info.balance?.toLocaleString() }}
|
values={{ amount: info.balance?.toLocaleString() }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<AsyncButton className="btn btn-primary" onClick={() => setTopup(true)}>
|
<DefaultButton onClick={() => setTopup(true)}>
|
||||||
<FormattedMessage defaultMessage="Topup" id="nBCvvJ" />
|
<FormattedMessage defaultMessage="Topup" id="nBCvvJ" />
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
</div>
|
</div>
|
||||||
<small>
|
<small>
|
||||||
<FormattedMessage defaultMessage="About {estimate}" id="Q3au2v" values={{ estimate: calcEstimate() }} />
|
<FormattedMessage defaultMessage="About {estimate}" id="Q3au2v" values={{ estimate: calcEstimate() }} />
|
||||||
@ -212,7 +210,7 @@ export function NostrProviderDialog({
|
|||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{ep?.capabilities?.map(a => (
|
{ep?.capabilities?.map(a => (
|
||||||
<span className="pill bg-gray-1">{parseCapability(a)}</span>
|
<Pill>{parseCapability(a)}</Pill>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -264,18 +262,18 @@ export function NostrProviderDialog({
|
|||||||
<div className="grid grid-cols-2 gap-2">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{info.forwards?.map(a => (
|
{info.forwards?.map(a => (
|
||||||
<>
|
<>
|
||||||
<div className="paper">{a.name}</div>
|
<div className="bg-layer-2 rounded-xl px-3 flex items-center">{a.name}</div>
|
||||||
<AsyncButton
|
<DefaultButton
|
||||||
className="btn btn-primary"
|
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await provider.removeForward(a.id);
|
await provider.removeForward(a.id);
|
||||||
|
await loadInfo();
|
||||||
}}>
|
}}>
|
||||||
<FormattedMessage defaultMessage="Remove" id="G/yZLu" />
|
<FormattedMessage defaultMessage="Remove" id="G/yZLu" />
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
</>
|
</>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<AddForwardInputs provider={provider} onAdd={() => {}} />
|
<AddForwardInputs provider={provider} onAdd={loadInfo} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -419,40 +417,35 @@ function AddForwardInputs({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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="flex gap-2">
|
||||||
<div className="paper flex-1">
|
<select value={svc} onChange={e => setService(e.target.value as ForwardService)} className="flex-1">
|
||||||
<select value={svc} onChange={e => setService(e.target.value as ForwardService)} className="bg-gray-1">
|
<option value="twitch">Twitch</option>
|
||||||
<option value="twitch">Twitch</option>
|
<option value="youtube">Youtube</option>
|
||||||
<option value="youtube">Youtube</option>
|
<option value="facebook">Facebook Gaming</option>
|
||||||
<option value="facebook">Facebook Gaming</option>
|
<option value="kick">Kick</option>
|
||||||
<option value="kick">Kick</option>
|
<option value="trovo">Trovo</option>
|
||||||
<option value="trovo">Trovo</option>
|
<option value="custom">Custom</option>
|
||||||
<option value="custom">Custom</option>
|
</select>
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div className="paper flex-1">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder={formatMessage({ defaultMessage: "Display name", id: "dOQCL8" })}
|
|
||||||
value={name}
|
|
||||||
onChange={e => setName(e.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="paper">
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={
|
className="flex-1"
|
||||||
svc === ForwardService.Custom ? "rtmp://" : formatMessage({ defaultMessage: "Stream key", id: "QWlMq9" })
|
placeholder={formatMessage({ defaultMessage: "Display name", id: "dOQCL8" })}
|
||||||
}
|
value={name}
|
||||||
value={target}
|
onChange={e => setName(e.target.value)}
|
||||||
onChange={e => setTarget(e.target.value)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<AsyncButton className="btn btn-primary" onClick={doAdd}>
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={
|
||||||
|
svc === ForwardService.Custom ? "rtmp://" : formatMessage({ defaultMessage: "Stream key", id: "QWlMq9" })
|
||||||
|
}
|
||||||
|
value={target}
|
||||||
|
onChange={e => setTarget(e.target.value)}
|
||||||
|
/>
|
||||||
|
<DefaultButton onClick={doAdd}>
|
||||||
<FormattedMessage defaultMessage="Add" id="2/2yg+" />
|
<FormattedMessage defaultMessage="Add" id="2/2yg+" />
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
{error && <b className="warning">{error}</b>}
|
{error && <b className="warning">{error}</b>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
import AsyncButton from "./async-button";
|
|
||||||
import { useLogin } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
import { NostrStreamProvider } from "@/providers";
|
import { NostrStreamProvider } from "@/providers";
|
||||||
import { base64 } from "@scure/base";
|
import { base64 } from "@scure/base";
|
||||||
import { unwrap } from "@snort/shared";
|
import { unwrap } from "@snort/shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
|
import { DefaultButton } from "./buttons";
|
||||||
|
|
||||||
export function NotificationsButton({ host, service }: { host: string; service: string }) {
|
export function NotificationsButton({ host, service }: { host: string; service: string }) {
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
@ -80,8 +80,8 @@ export function NotificationsButton({ host, service }: { host: string; service:
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AsyncButton onClick={subscribed ? unsubscribe : subscribe}>
|
<DefaultButton onClick={subscribed ? unsubscribe : subscribe}>
|
||||||
<Icon name={subscribed ? "bell-off" : "bell-ringing"} />
|
<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 { useLogin } from "@/hooks/login";
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
import { NostrLink, parseNostrLink } from "@snort/system";
|
import { NostrLink, parseNostrLink } from "@snort/system";
|
||||||
import AsyncButton from "./async-button";
|
|
||||||
import { SnortContext } from "@snort/system-react";
|
import { SnortContext } from "@snort/system-react";
|
||||||
import { LIVE_STREAM_RAID } from "@/const";
|
import { LIVE_STREAM_RAID } from "@/const";
|
||||||
|
import { DefaultButton } from "./buttons";
|
||||||
|
|
||||||
export function DashboardRaidMenu({ link, onClose }: { link: NostrLink; onClose: () => void }) {
|
export function DashboardRaidMenu({ link, onClose }: { link: NostrLink; onClose: () => void }) {
|
||||||
const system = useContext(SnortContext);
|
const system = useContext(SnortContext);
|
||||||
@ -41,13 +41,13 @@ export function DashboardRaidMenu({ link, onClose }: { link: NostrLink; onClose:
|
|||||||
<FormattedMessage defaultMessage="Start Raid" id="MTHO1W" />
|
<FormattedMessage defaultMessage="Start Raid" id="MTHO1W" />
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex flex-col gap-1">
|
<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" />
|
<FormattedMessage defaultMessage="Live now" id="+sdKx8" />
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2 flex-wrap">
|
<div className="flex gap-2 flex-wrap">
|
||||||
{livePubkeys.map(a => (
|
{livePubkeys.map(a => (
|
||||||
<div
|
<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={() => {
|
onClick={() => {
|
||||||
const liveEvent = live.find(b => getHost(b) === a);
|
const liveEvent = live.find(b => getHost(b) === a);
|
||||||
if (liveEvent) {
|
if (liveEvent) {
|
||||||
@ -60,7 +60,7 @@ export function DashboardRaidMenu({ link, onClose }: { link: NostrLink; onClose:
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<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" />
|
<FormattedMessage defaultMessage="Raid target" id="Zse7yG" />
|
||||||
</p>
|
</p>
|
||||||
<div className="paper">
|
<div className="paper">
|
||||||
@ -68,16 +68,16 @@ export function DashboardRaidMenu({ link, onClose }: { link: NostrLink; onClose:
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<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" />
|
<FormattedMessage defaultMessage="Raid Message" id="RS6smY" />
|
||||||
</p>
|
</p>
|
||||||
<div className="paper">
|
<div className="paper">
|
||||||
<input type="text" value={msg} onChange={e => setMsg(e.target.value)} />
|
<input type="text" value={msg} onChange={e => setMsg(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AsyncButton className="btn btn-primary" onClick={raid}>
|
<DefaultButton onClick={raid}>
|
||||||
<FormattedMessage defaultMessage="Raid!" id="aqjZxs" />
|
<FormattedMessage defaultMessage="Raid!" id="aqjZxs" />
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -19,11 +19,6 @@
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.send-zap .pill.active {
|
|
||||||
color: inherit;
|
|
||||||
background: #353535;
|
|
||||||
}
|
|
||||||
|
|
||||||
.send-zap p {
|
.send-zap p {
|
||||||
margin: 0 0 8px 0;
|
margin: 0 0 8px 0;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import "./send-zap.css";
|
import "./send-zap.css";
|
||||||
import * as Dialog from "@radix-ui/react-dialog";
|
|
||||||
import { type ReactNode, useEffect, useState } from "react";
|
import { type ReactNode, useEffect, useState } from "react";
|
||||||
import { LNURL } from "@snort/shared";
|
import { LNURL } from "@snort/shared";
|
||||||
import { EventPublisher, NostrEvent } from "@snort/system";
|
import { EventPublisher, NostrEvent } from "@snort/system";
|
||||||
@ -9,12 +8,14 @@ import { FormattedMessage, FormattedNumber } from "react-intl";
|
|||||||
|
|
||||||
import { formatSats } from "../number";
|
import { formatSats } from "../number";
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
import AsyncButton from "./async-button";
|
|
||||||
import QrCode from "./qr-code";
|
import QrCode from "./qr-code";
|
||||||
import { useLogin } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
import Copy from "./copy";
|
import Copy from "./copy";
|
||||||
import { defaultRelays } from "@/const";
|
import { defaultRelays } from "@/const";
|
||||||
import { useRates } from "@/hooks/rates";
|
import { useRates } from "@/hooks/rates";
|
||||||
|
import { DefaultButton } from "./buttons";
|
||||||
|
import Modal from "./modal";
|
||||||
|
import Pill from "./pill";
|
||||||
|
|
||||||
export interface LNURLLike {
|
export interface LNURLLike {
|
||||||
get name(): string;
|
get name(): string;
|
||||||
@ -110,25 +111,25 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: Se
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<span
|
<Pill
|
||||||
className={`pill${isFiat ? "" : " active"}`}
|
selected={!isFiat}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsFiat(false);
|
setIsFiat(false);
|
||||||
setAmount(satsAmounts[0]);
|
setAmount(satsAmounts[0]);
|
||||||
}}>
|
}}>
|
||||||
SATS
|
SATS
|
||||||
</span>
|
</Pill>
|
||||||
<span
|
<Pill
|
||||||
className={`pill${isFiat ? " active" : ""}`}
|
selected={isFiat}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsFiat(true);
|
setIsFiat(true);
|
||||||
setAmount(usdAmounts[0]);
|
setAmount(usdAmounts[0]);
|
||||||
}}>
|
}}>
|
||||||
USD
|
USD
|
||||||
</span>
|
</Pill>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<small>
|
<small className="mb-2">
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
defaultMessage="Zap amount in {currency}"
|
defaultMessage="Zap amount in {currency}"
|
||||||
id="IJDKz3"
|
id="IJDKz3"
|
||||||
@ -148,11 +149,11 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: Se
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</small>
|
</small>
|
||||||
<div className="amounts">
|
<div className="grid grid-cols-5 gap-2 text-center">
|
||||||
{(isFiat ? usdAmounts : satsAmounts).map(a => (
|
{(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)}
|
{isFiat ? `$${a.toLocaleString()}` : formatSats(a)}
|
||||||
</span>
|
</Pill>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -167,9 +168,9 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: Se
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<AsyncButton onClick={send} className="btn btn-primary">
|
<DefaultButton onClick={send}>
|
||||||
<FormattedMessage defaultMessage="Zap!" id="3HwrQo" />
|
<FormattedMessage defaultMessage="Zap!" id="3HwrQo" />
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -185,18 +186,18 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: Se
|
|||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<Copy text={invoice} />
|
<Copy text={invoice} />
|
||||||
</div>
|
</div>
|
||||||
<AsyncButton className="btn btn-primary wide" onClick={() => onFinish()}>
|
<DefaultButton onClick={() => onFinish()}>
|
||||||
<FormattedMessage defaultMessage="Back" id="cyR7Kh" />
|
<FormattedMessage defaultMessage="Back" id="cyR7Kh" />
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="send-zap">
|
<div className="flex flex-col gap-4">
|
||||||
<h3 className="flex gap-2 items-center">
|
<h3 className="flex gap-2 items-center">
|
||||||
<FormattedMessage defaultMessage="Zap {name}" id="oHPB8Q" values={{ name }} />
|
<FormattedMessage defaultMessage="Zap {name}" id="oHPB8Q" values={{ name }} />
|
||||||
<Icon name="zap" />
|
<Icon name="zap-filled" />
|
||||||
</h3>
|
</h3>
|
||||||
{input()}
|
{input()}
|
||||||
{payInvoice()}
|
{payInvoice()}
|
||||||
@ -205,29 +206,21 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: Se
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function SendZapsDialog(props: Omit<SendZapsProps, "onFinish">) {
|
export function SendZapsDialog(props: Omit<SendZapsProps, "onFinish">) {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
return (
|
return (<>
|
||||||
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
|
{props.button ? (
|
||||||
<Dialog.Trigger asChild>
|
props.button
|
||||||
{props.button ? (
|
) : (
|
||||||
props.button
|
<DefaultButton onClick={() => setOpen(true)}>
|
||||||
) : (
|
<span className="max-xl:hidden">
|
||||||
<AsyncButton className="btn btn-primary zap">
|
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
|
||||||
<span className="max-xl:hidden">
|
</span>
|
||||||
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
|
<Icon name="zap-filled" size={16} />
|
||||||
</span>
|
</DefaultButton>
|
||||||
<Icon name="zap-filled" size={16} />
|
)}
|
||||||
</AsyncButton>
|
{open && <Modal id="send-zaps" onClose={() => setOpen(false)}>
|
||||||
)}
|
<SendZaps {...props} onFinish={() => setOpen(false)} />
|
||||||
</Dialog.Trigger>
|
</Modal>}
|
||||||
<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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,37 @@
|
|||||||
import { Menu, MenuItem } from "@szhsin/react-menu";
|
import { Menu, MenuItem } from "@szhsin/react-menu";
|
||||||
import * as Dialog from "@radix-ui/react-dialog";
|
import { NostrEvent, NostrLink, NostrPrefix } from "@snort/system";
|
||||||
import { unwrap } from "@snort/shared";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { NostrEvent, NostrPrefix, encodeTLV } from "@snort/system";
|
|
||||||
import { FormattedMessage } from "react-intl";
|
|
||||||
import { useContext, useState } from "react";
|
import { useContext, useState } from "react";
|
||||||
import { SnortContext } from "@snort/system-react";
|
import { SnortContext } from "@snort/system-react";
|
||||||
|
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
import { Textarea } from "./textarea";
|
import { Textarea } from "./textarea";
|
||||||
import { findTag } from "@/utils";
|
import { getHost } from "@/utils";
|
||||||
import AsyncButton from "./async-button";
|
|
||||||
import { useLogin } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
|
import { DefaultButton } from "./buttons";
|
||||||
|
import Modal from "./modal";
|
||||||
|
|
||||||
type ShareOn = "nostr" | "twitter";
|
type ShareOn = "nostr" | "twitter";
|
||||||
|
|
||||||
export function ShareMenu({ ev }: { ev: NostrEvent }) {
|
export function ShareMenu({ ev }: { ev: NostrEvent }) {
|
||||||
const system = useContext(SnortContext);
|
const system = useContext(SnortContext);
|
||||||
const [share, setShare] = useState<ShareOn>();
|
const [share, setShare] = useState<ShareOn>();
|
||||||
const [message, setMessage] = useState("");
|
|
||||||
const login = useLogin();
|
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 defaultMyMsg = formatMessage({
|
||||||
const link = `https://zap.stream/${naddr}`;
|
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() {
|
async function sendMessage() {
|
||||||
const pub = login?.publisher();
|
const pub = login?.publisher();
|
||||||
@ -40,45 +50,37 @@ export function ShareMenu({ ev }: { ev: NostrEvent }) {
|
|||||||
gap={5}
|
gap={5}
|
||||||
menuClassName="ctx-menu"
|
menuClassName="ctx-menu"
|
||||||
menuButton={
|
menuButton={
|
||||||
<AsyncButton className="btn btn-secondary">
|
<DefaultButton>
|
||||||
<FormattedMessage defaultMessage="Share" id="OKhRC6" />
|
<FormattedMessage defaultMessage="Share" id="OKhRC6" />
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
}>
|
}>
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMessage(`Come check out my stream on zap.stream!\n\n${link}\n\nnostr:${naddr}`);
|
|
||||||
setShare("nostr");
|
setShare("nostr");
|
||||||
}}>
|
}}>
|
||||||
<Icon name="nostrich" size={24} />
|
<Icon name="nostrich" size={24} />
|
||||||
<FormattedMessage defaultMessage="Broadcast on Nostr" id="wCIL7o" />
|
<FormattedMessage defaultMessage="Broadcast on Nostr" id="wCIL7o" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
<Dialog.Root open={Boolean(share)} onOpenChange={() => setShare(undefined)}>
|
{share && <Modal id="share" onClose={() => setShare(undefined)}>
|
||||||
<Dialog.Portal>
|
<div className="flex flex-col gap-4">
|
||||||
<Dialog.Overlay className="dialog-overlay" />
|
<h2>
|
||||||
<Dialog.Content className="dialog-content">
|
<FormattedMessage defaultMessage="Share" id="OKhRC6" />
|
||||||
<div className="content-inner">
|
</h2>
|
||||||
<h2>
|
<Textarea
|
||||||
<FormattedMessage defaultMessage="Share" id="OKhRC6" />
|
emojis={[]}
|
||||||
</h2>
|
value={message}
|
||||||
<div className="paper">
|
onChange={e => setMessage(e.target.value)}
|
||||||
<Textarea
|
onKeyDown={() => {
|
||||||
emojis={[]}
|
//noop
|
||||||
value={message}
|
}}
|
||||||
onChange={e => setMessage(e.target.value)}
|
rows={15}
|
||||||
onKeyDown={() => {
|
/>
|
||||||
//noop
|
<DefaultButton onClick={sendMessage}>
|
||||||
}}
|
<FormattedMessage defaultMessage="Send" id="9WRlF4" />
|
||||||
rows={15}
|
</DefaultButton>
|
||||||
/>
|
</div>
|
||||||
</div>
|
</Modal>}
|
||||||
<AsyncButton className="btn btn-primary" onClick={sendMessage}>
|
|
||||||
<FormattedMessage defaultMessage="Send" id="9WRlF4" />
|
|
||||||
</AsyncButton>
|
|
||||||
</div>
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Portal>
|
|
||||||
</Dialog.Root>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,19 +2,20 @@ import { HTMLProps } from "react";
|
|||||||
import "./state-pill.css";
|
import "./state-pill.css";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { StreamState } from "@/const";
|
import { StreamState } from "@/const";
|
||||||
|
import Pill from "./pill";
|
||||||
|
|
||||||
type StatePillProps = { state: StreamState } & HTMLProps<HTMLSpanElement>;
|
type StatePillProps = { state: StreamState } & HTMLProps<HTMLSpanElement>;
|
||||||
|
|
||||||
export function StatePill({ state, ...props }: StatePillProps) {
|
export function StatePill({ state, ...props }: StatePillProps) {
|
||||||
return (
|
return (
|
||||||
<span
|
<Pill
|
||||||
{...props}
|
{...props}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"uppercase font-white pill",
|
"uppercase font-white",
|
||||||
state === StreamState.Live ? "bg-primary" : "bg-gray-1",
|
state === StreamState.Live ? "bg-primary" : "bg-layer-1",
|
||||||
props.className
|
props.className
|
||||||
)}>
|
)}>
|
||||||
{state}
|
{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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
@ -11,10 +11,4 @@
|
|||||||
padding: 4px 10px !important;
|
padding: 4px 10px !important;
|
||||||
border-radius: 12px !important;
|
border-radius: 12px !important;
|
||||||
display: unset !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 { TagsInput } from "react-tag-input-component";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
import AsyncButton from "./async-button";
|
|
||||||
import { extractStreamInfo, findTag } from "@/utils";
|
import { extractStreamInfo, findTag } from "@/utils";
|
||||||
import { useLogin } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
import { NewGoalDialog } from "./new-goal";
|
import { NewGoalDialog } from "./new-goal";
|
||||||
import { useGoals } from "@/hooks/goals";
|
import { useGoals } from "@/hooks/goals";
|
||||||
import { StreamState } from "@/const";
|
import { StreamState } from "@/const";
|
||||||
|
import { DefaultButton } from "./buttons";
|
||||||
|
import Pill from "./pill";
|
||||||
|
|
||||||
export interface StreamEditorProps {
|
export interface StreamEditorProps {
|
||||||
ev?: NostrEvent;
|
ev?: NostrEvent;
|
||||||
@ -206,9 +207,9 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
|
|||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{[StreamState.Live, StreamState.Planned, StreamState.Ended].map(v => (
|
{[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}
|
{v}
|
||||||
</span>
|
</Pill>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -262,12 +263,12 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{(options?.canSetContentWarning ?? true) && (
|
{(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>
|
<div>
|
||||||
<input type="checkbox" checked={contentWarning} onChange={e => setContentWarning(e.target.checked)} />
|
<input type="checkbox" checked={contentWarning} onChange={e => setContentWarning(e.target.checked)} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="warning">
|
<div className="text-warning">
|
||||||
<FormattedMessage defaultMessage="NSFW Content" id="Atr2p4" />
|
<FormattedMessage defaultMessage="NSFW Content" id="Atr2p4" />
|
||||||
</div>
|
</div>
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
@ -278,13 +279,13 @@ export function StreamEditor({ ev, onFinish, options }: StreamEditorProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<AsyncButton type="button" className="btn btn-primary wide" disabled={!isValid} onClick={publishStream}>
|
<DefaultButton disabled={!isValid} onClick={publishStream}>
|
||||||
{ev ? (
|
{ev ? (
|
||||||
<FormattedMessage defaultMessage="Save" id="jvo0vs" />
|
<FormattedMessage defaultMessage="Save" id="jvo0vs" />
|
||||||
) : (
|
) : (
|
||||||
<FormattedMessage defaultMessage="Start Stream" id="TaTRKo" />
|
<FormattedMessage defaultMessage="Start Stream" id="TaTRKo" />
|
||||||
)}
|
)}
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -4,6 +4,7 @@ import { FormattedMessage } from "react-intl";
|
|||||||
import { NostrEvent } from "@snort/system";
|
import { NostrEvent } from "@snort/system";
|
||||||
import { findTag, getTagValues } from "@/utils";
|
import { findTag, getTagValues } from "@/utils";
|
||||||
import { StreamState } from "@/const";
|
import { StreamState } from "@/const";
|
||||||
|
import Pill from "./pill";
|
||||||
|
|
||||||
export function Tags({ children, max, ev }: { children?: ReactNode; max?: number; ev: NostrEvent }) {
|
export function Tags({ children, max, ev }: { children?: ReactNode; max?: number; ev: NostrEvent }) {
|
||||||
const status = findTag(ev, "status");
|
const status = findTag(ev, "status");
|
||||||
@ -14,13 +15,13 @@ export function Tags({ children, max, ev }: { children?: ReactNode; max?: number
|
|||||||
<>
|
<>
|
||||||
{children}
|
{children}
|
||||||
{status === StreamState.Planned && (
|
{status === StreamState.Planned && (
|
||||||
<span className="pill bg-gray-1">
|
<Pill>
|
||||||
{status === StreamState.Planned ? <FormattedMessage defaultMessage="Starts " id="0hNxBy" /> : ""}
|
{status === StreamState.Planned ? <FormattedMessage defaultMessage="Starts " id="0hNxBy" /> : ""}
|
||||||
</span>
|
</Pill>
|
||||||
)}
|
)}
|
||||||
{tags.map(a => (
|
{tags.map(a => (
|
||||||
<a href={`/t/${encodeURIComponent(a)}`} className="pill bg-gray-1" key={a}>
|
<a href={`/t/${encodeURIComponent(a)}`} key={a}>
|
||||||
{a}
|
<Pill>{a}</Pill>
|
||||||
</a>
|
</a>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
|
@ -15,7 +15,7 @@
|
|||||||
.emoji-item,
|
.emoji-item,
|
||||||
.user-item {
|
.user-item {
|
||||||
color: white;
|
color: white;
|
||||||
background: #171717;
|
@apply bg-layer-2;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@ -26,7 +26,7 @@
|
|||||||
|
|
||||||
.emoji-item:hover,
|
.emoji-item:hover,
|
||||||
.user-item:hover {
|
.user-item:hover {
|
||||||
color: #171717;
|
@apply text-layer-2;
|
||||||
background: white;
|
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 { HTMLProps } from "react";
|
||||||
import "./toggle.css";
|
|
||||||
import { Icon } from "./icon";
|
|
||||||
|
|
||||||
interface ToggleProps {
|
export function Toggle({ size, className, checked, ...props }: HTMLProps<SVGSVGElement>) {
|
||||||
label: string;
|
|
||||||
text: string;
|
|
||||||
pressed?: boolean;
|
|
||||||
onPressedChange?: (b: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Toggle({ label, text, ...rest }: ToggleProps) {
|
|
||||||
const { pressed } = rest;
|
|
||||||
return (
|
return (
|
||||||
<div className="toggle-container">
|
<svg
|
||||||
<BaseToggle.Root className="toggle" aria-label={label} {...rest}>
|
{...props}
|
||||||
{pressed ? <Icon name="toggle-on" /> : <Icon name="toggle-off" />}
|
viewBox="0 0 33.015999 19.353487"
|
||||||
</BaseToggle.Root>
|
fill="none"
|
||||||
<span className="toggle-text">{text}</span>
|
width={size}
|
||||||
</div>
|
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 useTopZappers from "@/hooks/top-zappers";
|
||||||
import { ZapperRow } from "./zapper-row";
|
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);
|
const zappers = useTopZappers(zaps);
|
||||||
return zappers.slice(0, limit ?? 10).map(({ pubkey, total }) => (
|
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={showName ?? false} avatarSize={avatarSize} className={className} />
|
||||||
<ZapperRow pubkey={pubkey} total={total} key={pubkey} showName={false} />
|
|
||||||
</div>
|
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -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 { Link } from "react-router-dom";
|
||||||
import { Profile } from "./profile";
|
import { Profile } from "./profile";
|
||||||
import { NostrEvent, NostrPrefix, encodeTLV } from "@snort/system";
|
import { NostrEvent, NostrLink } from "@snort/system";
|
||||||
import { useInView } from "react-intersection-observer";
|
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import { StatePill } from "./state-pill";
|
import { StatePill } from "./state-pill";
|
||||||
import { extractStreamInfo, findTag, getHost } from "@/utils";
|
import { extractStreamInfo, getHost } from "@/utils";
|
||||||
import { formatSats } from "@/number";
|
import { formatSats } from "@/number";
|
||||||
import { isContentWarningAccepted } from "./content-warning";
|
|
||||||
import { Tags } from "./tags";
|
import { Tags } from "./tags";
|
||||||
import { StreamState } from "@/const";
|
import { StreamState } from "@/const";
|
||||||
|
import Pill from "./pill";
|
||||||
|
import classNames from "classnames";
|
||||||
|
import Logo from "./logo";
|
||||||
|
|
||||||
export function VideoTile({
|
export function VideoTile({
|
||||||
ev,
|
ev,
|
||||||
@ -21,43 +21,39 @@ export function VideoTile({
|
|||||||
showAuthor?: boolean;
|
showAuthor?: boolean;
|
||||||
showStatus?: boolean;
|
showStatus?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { inView, ref } = useInView({ triggerOnce: true });
|
|
||||||
const id = findTag(ev, "d") ?? "";
|
|
||||||
const { title, image, status, participants, contentWarning } = extractStreamInfo(ev);
|
const { title, image, status, participants, contentWarning } = extractStreamInfo(ev);
|
||||||
const host = getHost(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 (
|
return (
|
||||||
<div className="video-tile-container">
|
<div className="flex flex-col gap-2">
|
||||||
<Link
|
<Link
|
||||||
to={`/${link}`}
|
to={`/${link.encode()}`}
|
||||||
className={`video-tile${contentWarning && !isContentWarningAccepted() ? " nsfw" : ""}`}
|
className={classNames({ "blur": contentWarning }, "h-full")}
|
||||||
ref={ref}
|
|
||||||
state={ev}>
|
state={ev}>
|
||||||
<div
|
<div className="relative mb-2 aspect-video">
|
||||||
style={{
|
{hasImg ? <img loading="lazy" className="aspect-video object-cover rounded-xl" src={image} /> :
|
||||||
backgroundImage: `url(${inView ? ((image?.length ?? 0) > 0 ? image : "/zap-stream.svg") : ""})`,
|
<Logo className="text-white aspect-video" />}
|
||||||
}}></div>
|
<span className="flex flex-col justify-between absolute top-0 h-full right-4 items-end py-2">
|
||||||
<span className="pill-box">
|
{showStatus && <StatePill state={status as StreamState} />}
|
||||||
{showStatus && <StatePill state={status as StreamState} />}
|
{participants && (
|
||||||
{participants && (
|
<Pill>
|
||||||
<span className="pill viewers bg-gray-1">
|
<FormattedMessage
|
||||||
<FormattedMessage
|
defaultMessage="{n} viewers"
|
||||||
defaultMessage="{n} viewers"
|
id="3adEeb"
|
||||||
id="3adEeb"
|
values={{ n: formatSats(Number(participants)) }}
|
||||||
values={{ n: formatSats(Number(participants)) }}
|
/>
|
||||||
/>
|
</Pill>
|
||||||
</span>
|
)}
|
||||||
)}
|
</span>
|
||||||
</span>
|
</div>
|
||||||
<h3>{title}</h3>
|
<h3>{title}</h3>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="video-tile-info">
|
<div className="flex gap-1">
|
||||||
<div className="video-tags">
|
<Tags ev={ev} max={3} />
|
||||||
<Tags ev={ev} max={3} />
|
|
||||||
</div>
|
|
||||||
{showAuthor && <div>{inView && <Profile pubkey={host} />}</div>}
|
|
||||||
</div>
|
</div>
|
||||||
|
{showAuthor && <Profile pubkey={host} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,12 +6,12 @@ import { unixNowMs } from "@snort/shared";
|
|||||||
|
|
||||||
const EmojiPicker = lazy(() => import("./emoji-picker"));
|
const EmojiPicker = lazy(() => import("./emoji-picker"));
|
||||||
import { useLogin } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
import AsyncButton from "./async-button";
|
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
import { Textarea } from "./textarea";
|
import { Textarea } from "./textarea";
|
||||||
import type { Emoji, EmojiPack } from "@/types";
|
import type { Emoji, EmojiPack } from "@/types";
|
||||||
import { LIVE_STREAM_CHAT } from "@/const";
|
import { LIVE_STREAM_CHAT } from "@/const";
|
||||||
import { TimeSync } from "@/index";
|
import { TimeSync } from "@/index";
|
||||||
|
import { BorderButton } from "./buttons";
|
||||||
|
|
||||||
export function WriteMessage({ link, emojiPacks }: { link: NostrLink; emojiPacks: EmojiPack[] }) {
|
export function WriteMessage({ link, emojiPacks }: { link: NostrLink; emojiPacks: EmojiPack[] }) {
|
||||||
const system = useContext(SnortContext);
|
const system = useContext(SnortContext);
|
||||||
@ -100,9 +100,9 @@ export function WriteMessage({ link, emojiPacks }: { link: NostrLink; emojiPacks
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<AsyncButton onClick={sendChatMessage} className="btn btn-border">
|
<BorderButton onClick={sendChatMessage}>
|
||||||
<FormattedMessage defaultMessage="Send" id="9WRlF4" />
|
<FormattedMessage defaultMessage="Send" id="9WRlF4" />
|
||||||
</AsyncButton>
|
</BorderButton>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,16 +2,17 @@ import { formatSats } from "@/number";
|
|||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
import { Profile } from "./profile";
|
import { Profile } from "./profile";
|
||||||
import { FormattedMessage } from "react-intl";
|
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 (
|
return (
|
||||||
<div className="flex gap-1 justify-between items-center">
|
<div className={classNames(className, "flex gap-1 justify-between items-center")}>
|
||||||
{pubkey === "anon" ? (
|
{pubkey === "anon" ? (
|
||||||
<span>
|
<span style={{ height: avatarSize }}>
|
||||||
<FormattedMessage defaultMessage="Anon" id="bfvyfs" />
|
<FormattedMessage defaultMessage="Anon" id="bfvyfs" />
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<Profile pubkey={pubkey} options={{ showName }} />
|
<Profile pubkey={pubkey} options={{ showName }} avatarSize={avatarSize} />
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Icon name="zap-filled" className="text-zap" />
|
<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);
|
return data.at(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useGoals(pubkey?: string, leaveOpen = false) {
|
export function useGoals(pubkey?: string, leaveOpen?: boolean, limit?: number) {
|
||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
if (!pubkey) return null;
|
if (!pubkey) return null;
|
||||||
const b = new RequestBuilder(`goals:${pubkey.slice(0, 12)}`);
|
const b = new RequestBuilder(`goals:${pubkey.slice(0, 12)}`);
|
||||||
b.withOptions({ leaveOpen });
|
b.withOptions({ leaveOpen });
|
||||||
b.withFilter().kinds([GOAL]).authors([pubkey]);
|
b.withFilter().kinds([GOAL]).authors([pubkey]).limit(limit);
|
||||||
return b;
|
return b;
|
||||||
}, [pubkey, leaveOpen]);
|
}, [pubkey, leaveOpen, limit]);
|
||||||
|
|
||||||
const data = useRequestBuilder(sub);
|
return useRequestBuilder(sub);
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
|
231
src/index.css
231
src/index.css
@ -20,7 +20,7 @@ body {
|
|||||||
--primary: #f838d9;
|
--primary: #f838d9;
|
||||||
--secondary: #34d2fe;
|
--secondary: #34d2fe;
|
||||||
--zap: #ff8d2b;
|
--zap: #ff8d2b;
|
||||||
--text-danger: #ff563f;
|
--success: green;
|
||||||
--surface: #222;
|
--surface: #222;
|
||||||
--border: #171717;
|
--border: #171717;
|
||||||
--border-2: #393939;
|
--border-2: #393939;
|
||||||
@ -29,7 +29,18 @@ body {
|
|||||||
--gradient-orange: linear-gradient(270deg, #ff5b27 0%, rgba(255, 182, 39, 0.99) 100%);
|
--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 {
|
:root {
|
||||||
--gap-l: 24px;
|
--gap-l: 24px;
|
||||||
--gap-m: 16px;
|
--gap-m: 16px;
|
||||||
@ -66,225 +77,18 @@ a {
|
|||||||
outline: none;
|
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"],
|
input[type="text"],
|
||||||
textarea,
|
textarea,
|
||||||
input[type="datetime-local"],
|
input[type="datetime-local"],
|
||||||
input[type="password"],
|
input[type="password"],
|
||||||
input[type="number"] {
|
input[type="number"],
|
||||||
font-family: inherit;
|
|
||||||
border: unset;
|
|
||||||
background-color: unset;
|
|
||||||
color: inherit;
|
|
||||||
width: 100%;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
select {
|
||||||
font-family: inherit;
|
@apply bg-layer-2 w-full font-medium px-4 py-2 rounded-xl;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.scroll-lock {
|
.scroll-lock {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
height: 100vh;
|
height: 100dvh;
|
||||||
}
|
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.ctx-menu {
|
.ctx-menu {
|
||||||
@ -354,7 +158,8 @@ div.paper {
|
|||||||
|
|
||||||
.full-page-height .live-chat {
|
.full-page-height .live-chat {
|
||||||
padding: 24px 16px 8px 24px;
|
padding: 24px 16px 8px 24px;
|
||||||
border: 1px solid #171717;
|
border: 1px solid;
|
||||||
|
@apply border-layer-2;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
height: inherit;
|
height: inherit;
|
||||||
}
|
}
|
||||||
|
@ -33,12 +33,14 @@ import { WasmOptimizer, WasmPath, wasmInit } from "./wasm";
|
|||||||
const DashboardPage = lazy(() => import("./pages/dashboard"));
|
const DashboardPage = lazy(() => import("./pages/dashboard"));
|
||||||
|
|
||||||
import Faq from "@/faq.md";
|
import Faq from "@/faq.md";
|
||||||
|
import MockPage from "./pages/mock";
|
||||||
|
|
||||||
const hasWasm = "WebAssembly" in globalThis;
|
const hasWasm = "WebAssembly" in globalThis;
|
||||||
const db = new SnortSystemDb();
|
const db = new SnortSystemDb();
|
||||||
const System = new NostrSystem({
|
const System = new NostrSystem({
|
||||||
db,
|
db,
|
||||||
optimizer: hasWasm ? WasmOptimizer : undefined,
|
optimizer: hasWasm ? WasmOptimizer : undefined,
|
||||||
|
automaticOutboxModel: false,
|
||||||
});
|
});
|
||||||
export const Login = new LoginStore();
|
export const Login = new LoginStore();
|
||||||
|
|
||||||
@ -57,7 +59,9 @@ async function doInit() {
|
|||||||
db.ready = await db.isAvailable();
|
db.ready = await db.isAvailable();
|
||||||
await System.Init();
|
await System.Init();
|
||||||
try {
|
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 nowAtServer = (await req.json()).time as number;
|
||||||
const now = unixNowMs();
|
const now = unixNowMs();
|
||||||
TimeSync = now - nowAtServer;
|
TimeSync = now - nowAtServer;
|
||||||
@ -75,6 +79,10 @@ const router = createBrowserRouter([
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
|
{
|
||||||
|
path: "/mock",
|
||||||
|
element: <MockPage />
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
element: <RootPage />,
|
element: <RootPage />,
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import AsyncButton from "@/element/async-button";
|
|
||||||
import { ChatZap, LiveChat } from "@/element/live-chat";
|
import { ChatZap, LiveChat } from "@/element/live-chat";
|
||||||
import LiveVideoPlayer from "@/element/live-video-player";
|
import LiveVideoPlayer from "@/element/live-video-player";
|
||||||
import { MuteButton } from "@/element/mute-button";
|
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 { FormattedMessage, FormattedNumber } from "react-intl";
|
||||||
import { Text } from "@/element/text";
|
import { Text } from "@/element/text";
|
||||||
import { StreamTimer } from "@/element/stream-time";
|
import { StreamTimer } from "@/element/stream-time";
|
||||||
import * as Dialog from "@radix-ui/react-dialog";
|
|
||||||
import { DashboardRaidMenu } from "@/element/raid-menu";
|
import { DashboardRaidMenu } from "@/element/raid-menu";
|
||||||
|
import { DefaultButton } from "@/element/buttons";
|
||||||
|
import Modal from "@/element/modal";
|
||||||
|
|
||||||
export default function DashboardPage() {
|
export default function DashboardPage() {
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
@ -77,7 +77,7 @@ function DashboardForLink({ link }: { link: NostrLink }) {
|
|||||||
|
|
||||||
function DashboardCard(props: HTMLProps<HTMLDivElement>) {
|
function DashboardCard(props: HTMLProps<HTMLDivElement>) {
|
||||||
return (
|
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}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -91,8 +91,8 @@ function DashboardStatsCard({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
{...props}
|
{...props}
|
||||||
className={classNames("flex-1 bg-gray-1 flex flex-col gap-1 px-4 py-2 rounded-xl", props.className)}>
|
className={classNames("flex-1 bg-layer-1 flex flex-col gap-1 px-4 py-2 rounded-xl", props.className)}>
|
||||||
<div className="text-gray-3 font-medium">{name}</div>
|
<div className="text-layer-3 font-medium">{name}</div>
|
||||||
<div>{value}</div>
|
<div>{value}</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -106,13 +106,13 @@ function DashboardChatList({ link }: { link: NostrLink }) {
|
|||||||
}, [feed]);
|
}, [feed]);
|
||||||
|
|
||||||
return pubkeys.map(a => (
|
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} />
|
<Profile pubkey={a} avatarSize={32} gap={4} />
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<MuteButton pubkey={a} />
|
<MuteButton pubkey={a} />
|
||||||
<AsyncButton onClick={() => {}} className="font-bold">
|
<DefaultButton onClick={() => { }} className="font-bold">
|
||||||
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
|
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
@ -144,7 +144,7 @@ function DashboardZapColumn({ link }: { link: NostrLink }) {
|
|||||||
|
|
||||||
function DashboardHighlightZap({ zap }: { zap: ParsedZap }) {
|
function DashboardHighlightZap({ zap }: { zap: ParsedZap }) {
|
||||||
return (
|
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">
|
<div className="flex justify-between items-center text-zap text-2xl font-semibold">
|
||||||
<Profile
|
<Profile
|
||||||
pubkey={zap.sender ?? "anon"}
|
pubkey={zap.sender ?? "anon"}
|
||||||
@ -173,17 +173,13 @@ function DashboardHighlightZap({ zap }: { zap: ParsedZap }) {
|
|||||||
|
|
||||||
function DashboardRaidButton({ link }: { link: NostrLink }) {
|
function DashboardRaidButton({ link }: { link: NostrLink }) {
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
return (
|
return (<>
|
||||||
<Dialog.Root open={show} onOpenChange={setShow}>
|
<DefaultButton onClick={() => setShow(true)}>
|
||||||
<AsyncButton className="btn btn-primary" onClick={() => setShow(true)}>
|
<FormattedMessage defaultMessage="Raid" id="4iBdw1" />
|
||||||
<FormattedMessage defaultMessage="Raid" id="4iBdw1" />
|
</DefaultButton>
|
||||||
</AsyncButton>
|
{show && <Modal id="raid-menu" onClose={() => setShow(false)}>
|
||||||
<Dialog.Portal>
|
<DashboardRaidMenu link={link} onClose={() => setShow(false)} />
|
||||||
<Dialog.Overlay className="dialog-overlay" />
|
</Modal>}
|
||||||
<Dialog.Content className="dialog-content">
|
</>
|
||||||
<DashboardRaidMenu link={link} onClose={() => setShow(false)} />
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Portal>
|
|
||||||
</Dialog.Root>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
import "./layout.css";
|
import "./layout.css";
|
||||||
|
|
||||||
import { CSSProperties, useEffect, useState, useSyncExternalStore } from "react";
|
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 { Link, Outlet, useLocation, useNavigate } from "react-router-dom";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
@ -17,8 +16,10 @@ import { Login } from "@/index";
|
|||||||
import { useLang } from "@/hooks/lang";
|
import { useLang } from "@/hooks/lang";
|
||||||
import { AllLocales } from "@/intl";
|
import { AllLocales } from "@/intl";
|
||||||
import { NewVersion } from "@/serviceWorker";
|
import { NewVersion } from "@/serviceWorker";
|
||||||
import AsyncButton from "@/element/async-button";
|
|
||||||
import { trackEvent } from "@/utils";
|
import { trackEvent } from "@/utils";
|
||||||
|
import { BorderButton, DefaultButton } from "@/element/buttons";
|
||||||
|
import Modal from "@/element/modal";
|
||||||
|
import Logo from "@/element/logo";
|
||||||
|
|
||||||
export function LayoutPage() {
|
export function LayoutPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -109,24 +110,15 @@ export function LayoutPage() {
|
|||||||
|
|
||||||
function loggedOut() {
|
function loggedOut() {
|
||||||
if (login) return;
|
if (login) return;
|
||||||
|
return (<>
|
||||||
function handleLogin() {
|
<BorderButton onClick={() => setShowLogin(true)}>
|
||||||
setShowLogin(true);
|
<FormattedMessage defaultMessage="Login" id="AyGauy" />
|
||||||
}
|
<Icon name="login" />
|
||||||
|
</BorderButton>
|
||||||
return (
|
{showLogin && <Modal id="login">
|
||||||
<Dialog.Root open={showLogin} onOpenChange={setShowLogin}>
|
<LoginSignup close={() => setShowLogin(false)} />
|
||||||
<AsyncButton className="btn btn-border" onClick={handleLogin}>
|
</Modal>}
|
||||||
<FormattedMessage defaultMessage="Login" id="AyGauy" />
|
</>
|
||||||
<Icon name="login" />
|
|
||||||
</AsyncButton>
|
|
||||||
<Dialog.Portal>
|
|
||||||
<Dialog.Overlay className="dialog-overlay" />
|
|
||||||
<Dialog.Content className="dialog-content">
|
|
||||||
<LoginSignup close={() => setShowLogin(false)} />
|
|
||||||
</Dialog.Content>
|
|
||||||
</Dialog.Portal>
|
|
||||||
</Dialog.Root>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,16 +133,16 @@ export function LayoutPage() {
|
|||||||
</Helmet>
|
</Helmet>
|
||||||
<header>
|
<header>
|
||||||
<div
|
<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("/")}>
|
onClick={() => navigate("/")}>
|
||||||
<img src="/zap-stream.svg" width={40} />
|
<Logo width={40} height={40} />
|
||||||
</div>
|
</div>
|
||||||
<div className="grow flex items-center gap-2"></div>
|
<div className="grow flex items-center gap-2"></div>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link
|
<Link
|
||||||
to="https://discord.gg/Wtg6NVDdbT"
|
to="https://discord.gg/Wtg6NVDdbT"
|
||||||
target="_blank"
|
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" />
|
<Icon name="link" />
|
||||||
Discord
|
Discord
|
||||||
</Link>
|
</Link>
|
||||||
@ -182,9 +174,9 @@ function NewVersionBanner() {
|
|||||||
<FormattedMessage defaultMessage="Refresh the page to use the latest version" id="Gmiwnd" />
|
<FormattedMessage defaultMessage="Refresh the page to use the latest version" id="Gmiwnd" />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<AsyncButton onClick={() => window.location.reload()} className="btn">
|
<DefaultButton onClick={() => window.location.reload()} className="btn">
|
||||||
<FormattedMessage defaultMessage="Refresh" id="rELDbB" />
|
<FormattedMessage defaultMessage="Refresh" id="rELDbB" />
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
</div>
|
</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 "./profile-page.css";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import * as Tabs from "@radix-ui/react-tabs";
|
import { CachedMetadata, NostrEvent, NostrLink, TaggedNostrEvent, parseNostrLink } from "@snort/system";
|
||||||
import { NostrPrefix, ParsedZap, TaggedNostrEvent, encodeTLV, parseNostrLink } from "@snort/system";
|
|
||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
import { unwrap } from "@snort/shared";
|
import { unwrap } from "@snort/shared";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
@ -13,128 +12,127 @@ import { VideoTile } from "@/element/video-tile";
|
|||||||
import { FollowButton } from "@/element/follow-button";
|
import { FollowButton } from "@/element/follow-button";
|
||||||
import { MuteButton } from "@/element/mute-button";
|
import { MuteButton } from "@/element/mute-button";
|
||||||
import { useProfile } from "@/hooks/profile";
|
import { useProfile } from "@/hooks/profile";
|
||||||
import useTopZappers from "@/hooks/top-zappers";
|
|
||||||
import { Text } from "@/element/text";
|
import { Text } from "@/element/text";
|
||||||
import { findTag } from "@/utils";
|
import { findTag } from "@/utils";
|
||||||
import { StatePill } from "@/element/state-pill";
|
import { StatePill } from "@/element/state-pill";
|
||||||
import { Avatar } from "@/element/avatar";
|
import { Avatar } from "@/element/avatar";
|
||||||
import { ZapperRow } from "@/element/zapper-row";
|
|
||||||
import { StreamState } from "@/const";
|
import { StreamState } from "@/const";
|
||||||
import AsyncButton from "@/element/async-button";
|
import { DefaultButton } from "@/element/buttons";
|
||||||
|
import { useGoals } from "@/hooks/goals";
|
||||||
function TopZappers({ zaps }: { zaps: ParsedZap[] }) {
|
import { Goal } from "@/element/goal";
|
||||||
const zappers = useTopZappers(zaps);
|
import { TopZappers } from "@/element/top-zappers";
|
||||||
return (
|
import { useClips } from "@/hooks/clips";
|
||||||
<section className="flex flex-col gap-2">
|
|
||||||
{zappers.map(z => (
|
|
||||||
<ZapperRow key={z.pubkey} pubkey={z.pubkey} total={z.total} />
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultBanner = "https://void.cat/d/Hn1AdN5UKmceuDkgDW847q.webp";
|
const defaultBanner = "https://void.cat/d/Hn1AdN5UKmceuDkgDW847q.webp";
|
||||||
|
|
||||||
export function ProfilePage() {
|
export function ProfilePage() {
|
||||||
const navigate = useNavigate();
|
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const link = parseNostrLink(unwrap(params.npub));
|
const link = parseNostrLink(unwrap(params.npub));
|
||||||
const profile = useUserProfile(link.id);
|
|
||||||
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
|
||||||
const { streams, zaps } = useProfile(link, true);
|
const { streams, zaps } = useProfile(link, true);
|
||||||
const liveEvent = useMemo(() => {
|
const profile = useUserProfile(link.id);
|
||||||
return streams.find(ev => findTag(ev, "status") === StreamState.Live);
|
|
||||||
}, [streams]);
|
|
||||||
const pastStreams = useMemo(() => {
|
const pastStreams = useMemo(() => {
|
||||||
return streams.filter(ev => findTag(ev, "status") === StreamState.Ended);
|
return streams.filter(ev => findTag(ev, "status") === StreamState.Ended);
|
||||||
}, [streams]);
|
}, [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 (
|
return (
|
||||||
<div className="flex flex-col gap-3 max-sm:px-4">
|
<div className="flex flex-col gap-3 px-4">
|
||||||
<img
|
<img
|
||||||
className="rounded-xl object-cover h-[360px]"
|
className="rounded-xl object-cover h-[360px]"
|
||||||
alt={profile?.name || link.id}
|
alt={profile?.name || link.id}
|
||||||
src={profile?.banner ? profile?.banner : defaultBanner}
|
src={profile?.banner ? profile?.banner : defaultBanner}
|
||||||
/>
|
/>
|
||||||
<div className="flex justify-between">
|
<ProfileHeader link={link} profile={profile} streams={streams} />
|
||||||
<div className="flex items-center gap-3">
|
<div className="grid lg:grid-cols-2 gap-4 py-2">
|
||||||
<div className="relative flex flex-col items-center">
|
<div>
|
||||||
<Avatar user={profile} pubkey={link.id} size={88} className="border border-4" />
|
<h3 className="text-xl py-2">
|
||||||
{isLive && <StatePill state={StreamState.Live} onClick={goToLive} className="absolute bottom-0 -mb-2" />}
|
<FormattedMessage defaultMessage="All Time Top Zappers" id="FIDK5Y" />
|
||||||
</div>
|
</h3>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-4">
|
||||||
{profile?.name && <h1 className="name">{profile.name}</h1>}
|
<TopZappers zaps={zaps} limit={10} avatarSize={40} showName={true} />
|
||||||
{profile?.about && (
|
|
||||||
<p className="text-neutral-400">
|
|
||||||
<Text content={profile.about} tags={[]} />
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center">
|
<div>
|
||||||
{zapTarget && (
|
<h3 className="text-xl py-2">
|
||||||
<SendZapsDialog
|
<FormattedMessage defaultMessage="Zap Goals" id="LEmxc8" />
|
||||||
aTag={liveEvent ? `${liveEvent.kind}:${liveEvent.pubkey}:${findTag(liveEvent, "d")}` : undefined}
|
</h3>
|
||||||
lnurl={zapTarget}
|
<div className="flex flex-col gap-2">
|
||||||
button={
|
<ProfileZapGoals link={link} />
|
||||||
<AsyncButton className="btn">
|
</div>
|
||||||
<Icon name="zap-filled" className="zap-button-icon" />
|
|
||||||
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
|
|
||||||
</AsyncButton>
|
|
||||||
}
|
|
||||||
targetName={profile?.name || link.id}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<FollowButton pubkey={link.id} />
|
|
||||||
<MuteButton pubkey={link.id} />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Tabs.Root className="tabs-root" defaultValue="top-zappers">
|
<h1>
|
||||||
<Tabs.List className="tabs-list" aria-label={`Information about ${profile ? profile.name : link.id}`}>
|
<FormattedMessage defaultMessage="Recent Clips" id="XMGfiA" />
|
||||||
<Tabs.Trigger className="tabs-tab" value="top-zappers">
|
</h1>
|
||||||
<FormattedMessage defaultMessage="Top Zappers" id="dVD/AR" />
|
<div className="flex gap-4">
|
||||||
<div className="tab-border"></div>
|
<ProfileClips link={link} />
|
||||||
</Tabs.Trigger>
|
</div>
|
||||||
<Tabs.Trigger className="tabs-tab" value="past-streams">
|
<h1>
|
||||||
<FormattedMessage defaultMessage="Past Streams" id="UfSot5" />
|
<FormattedMessage defaultMessage="Past Streams" id="UfSot5" />
|
||||||
<div className="tab-border"></div>
|
</h1>
|
||||||
</Tabs.Trigger>
|
<ProfileStreamList streams={pastStreams} />
|
||||||
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function ProfileHeader({ profile, link, streams }: { profile?: CachedMetadata, link: NostrLink, streams: Array<NostrEvent> }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const liveEvent = useMemo(() => {
|
||||||
|
return streams.find(ev => findTag(ev, "status") === StreamState.Live);
|
||||||
|
}, [streams]);
|
||||||
|
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
||||||
|
const isLive = Boolean(liveEvent);
|
||||||
|
|
||||||
|
function goToLive() {
|
||||||
|
if (liveEvent) {
|
||||||
|
const evLink = NostrLink.fromEvent(liveEvent);
|
||||||
|
navigate(`/${evLink.encode()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="flex max-sm:flex-col gap-3 justify-between">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="relative flex flex-col items-center">
|
||||||
|
<Avatar user={profile} pubkey={link.id} size={88} className="border border-4" />
|
||||||
|
{isLive && <StatePill state={StreamState.Live} onClick={goToLive} className="absolute bottom-0 -mb-2" />}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
{profile?.name && <h1 className="name">{profile.name}</h1>}
|
||||||
|
{profile?.about && (
|
||||||
|
<p className="text-neutral-400">
|
||||||
|
<Text content={profile.about} tags={[]} />
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
{zapTarget && (
|
||||||
|
<SendZapsDialog
|
||||||
|
aTag={liveEvent ? `${liveEvent.kind}:${liveEvent.pubkey}:${findTag(liveEvent, "d")}` : undefined}
|
||||||
|
lnurl={zapTarget}
|
||||||
|
button={
|
||||||
|
<DefaultButton>
|
||||||
|
<Icon name="zap-filled" className="zap-button-icon" />
|
||||||
|
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
|
||||||
|
</DefaultButton>
|
||||||
|
}
|
||||||
|
targetName={profile?.name || link.id}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<FollowButton pubkey={link.id} />
|
||||||
|
<MuteButton pubkey={link.id} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
function ProfileStreamList({ streams }: { streams: Array<TaggedNostrEvent> }) {
|
function ProfileStreamList({ streams }: { streams: Array<TaggedNostrEvent> }) {
|
||||||
|
if (streams.length === 0) {
|
||||||
|
return <FormattedMessage defaultMessage="No streams yet" id="0rVLjV" />
|
||||||
|
}
|
||||||
return (
|
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 => (
|
{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} />
|
<VideoTile ev={ev} showAuthor={false} showStatus={false} />
|
||||||
<span className="text-neutral-500">
|
<span className="text-neutral-500">
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
@ -150,3 +148,28 @@ function ProfileStreamList({ streams }: { streams: Array<TaggedNostrEvent> }) {
|
|||||||
</div>
|
</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 Cloudflare from "@/cloudflare.png";
|
||||||
import { ConfigureOwncast } from "./owncast";
|
import { ConfigureOwncast } from "./owncast";
|
||||||
import { ConfigureNostrType } from "./nostr";
|
import { ConfigureNostrType } from "./nostr";
|
||||||
import AsyncButton from "@/element/async-button";
|
import { DefaultButton } from "@/element/buttons";
|
||||||
|
|
||||||
export function StreamProvidersPage() {
|
export function StreamProvidersPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -38,9 +38,9 @@ export function StreamProvidersPage() {
|
|||||||
<div className="paper">
|
<div className="paper">
|
||||||
<h3>{mapName(p)}</h3>
|
<h3>{mapName(p)}</h3>
|
||||||
{mapLogo(p)}
|
{mapLogo(p)}
|
||||||
<AsyncButton className="btn btn-border" onClick={() => navigate(p)}>
|
<DefaultButton onClick={() => navigate(p)}>
|
||||||
+ Configure
|
+ Configure
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,11 +2,11 @@ import { useState } from "react";
|
|||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
import AsyncButton from "@/element/async-button";
|
|
||||||
import { StatePill } from "@/element/state-pill";
|
import { StatePill } from "@/element/state-pill";
|
||||||
import { StreamProviderInfo, StreamProviderStore } from "@/providers";
|
import { StreamProviderInfo, StreamProviderStore } from "@/providers";
|
||||||
import { NostrStreamProvider } from "@/providers/zsz";
|
import { NostrStreamProvider } from "@/providers/zsz";
|
||||||
import { StreamState } from "@/const";
|
import { StreamState } from "@/const";
|
||||||
|
import { DefaultButton } from "@/element/buttons";
|
||||||
|
|
||||||
export function ConfigureNostrType() {
|
export function ConfigureNostrType() {
|
||||||
const [url, setUrl] = useState("");
|
const [url, setUrl] = useState("");
|
||||||
@ -55,14 +55,13 @@ export function ConfigureNostrType() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<AsyncButton
|
<DefaultButton
|
||||||
className="btn btn-border"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
StreamProviderStore.add(new NostrStreamProvider(new URL(url).host, url));
|
StreamProviderStore.add(new NostrStreamProvider(new URL(url).host, url));
|
||||||
navigate("/");
|
navigate("/");
|
||||||
}}>
|
}}>
|
||||||
<FormattedMessage defaultMessage="Save" id="jvo0vs" />
|
<FormattedMessage defaultMessage="Save" id="jvo0vs" />
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -77,9 +76,9 @@ export function ConfigureNostrType() {
|
|||||||
<input type="text" placeholder="https://" value={url} onChange={e => setUrl(e.target.value)} />
|
<input type="text" placeholder="https://" value={url} onChange={e => setUrl(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AsyncButton className="btn btn-primary" onClick={tryConnect}>
|
<DefaultButton onClick={tryConnect}>
|
||||||
<FormattedMessage defaultMessage="Connect" id="+vVZ/G" />
|
<FormattedMessage defaultMessage="Connect" id="+vVZ/G" />
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
</div>
|
</div>
|
||||||
<div>{status()}</div>
|
<div>{status()}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import AsyncButton from "@/element/async-button";
|
|
||||||
import { StatePill } from "@/element/state-pill";
|
import { StatePill } from "@/element/state-pill";
|
||||||
import { StreamProviderInfo, StreamProviderStore } from "@/providers";
|
import { StreamProviderInfo, StreamProviderStore } from "@/providers";
|
||||||
import { OwncastProvider } from "@/providers/owncast";
|
import { OwncastProvider } from "@/providers/owncast";
|
||||||
import { StreamState } from "@/const";
|
import { StreamState } from "@/const";
|
||||||
|
import { DefaultButton } from "@/element/buttons";
|
||||||
|
|
||||||
export function ConfigureOwncast() {
|
export function ConfigureOwncast() {
|
||||||
const [url, setUrl] = useState("");
|
const [url, setUrl] = useState("");
|
||||||
@ -55,14 +55,13 @@ export function ConfigureOwncast() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div>
|
<div>
|
||||||
<AsyncButton
|
<DefaultButton
|
||||||
className="btn btn-border"
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
StreamProviderStore.add(new OwncastProvider(url, token));
|
StreamProviderStore.add(new OwncastProvider(url, token));
|
||||||
navigate("/");
|
navigate("/");
|
||||||
}}>
|
}}>
|
||||||
Save
|
Save
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@ -83,9 +82,9 @@ export function ConfigureOwncast() {
|
|||||||
<input type="password" value={token} onChange={e => setToken(e.target.value)} />
|
<input type="password" value={token} onChange={e => setToken(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AsyncButton className="btn btn-primary" onClick={tryConnect}>
|
<DefaultButton onClick={tryConnect}>
|
||||||
Connect
|
Connect
|
||||||
</AsyncButton>
|
</DefaultButton>
|
||||||
</div>
|
</div>
|
||||||
<div>{status()}</div>
|
<div>{status()}</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
|
|
||||||
.one-line:before,
|
.one-line:before,
|
||||||
.one-line:after {
|
.one-line:after {
|
||||||
background-color: #171717;
|
@apply bg-layer-2;
|
||||||
}
|
}
|
||||||
|
|
||||||
::-webkit-scrollbar {
|
::-webkit-scrollbar {
|
||||||
|
@ -11,11 +11,11 @@ import { Login } from "..";
|
|||||||
import { StatePill } from "@/element/state-pill";
|
import { StatePill } from "@/element/state-pill";
|
||||||
import { NostrStreamProvider } from "@/providers";
|
import { NostrStreamProvider } from "@/providers";
|
||||||
import { StreamState } from "@/const";
|
import { StreamState } from "@/const";
|
||||||
import AsyncButton from "@/element/async-button";
|
import { Layer1Button } from "@/element/buttons";
|
||||||
|
|
||||||
const enum Tab {
|
const enum Tab {
|
||||||
Account,
|
Account,
|
||||||
Notifications,
|
Stream,
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SettingsPage() {
|
export function SettingsPage() {
|
||||||
@ -51,7 +51,9 @@ export function SettingsPage() {
|
|||||||
<p>
|
<p>
|
||||||
<FormattedMessage defaultMessage="Private key" id="Bep/gA" />
|
<FormattedMessage defaultMessage="Private key" id="Bep/gA" />
|
||||||
</p>
|
</p>
|
||||||
<Copy text={hexToBech32("nsec", login.privateKey)} />
|
<Layer1Button>
|
||||||
|
<FormattedMessage defaultMessage="Copy" id="4l6vz1" />
|
||||||
|
</Layer1Button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<h1>
|
<h1>
|
||||||
@ -69,20 +71,24 @@ export function SettingsPage() {
|
|||||||
onClick={() => Login.setColor(a)}></div>
|
onClick={() => Login.setColor(a)}></div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<h1>
|
|
||||||
<FormattedMessage defaultMessage="Stream Key" id="LknBsU" />
|
|
||||||
</h1>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<NostrProviderDialog
|
|
||||||
provider={unwrap(providers.find(a => a.name === "zap.stream")) as NostrStreamProvider}
|
|
||||||
showEndpoints={true}
|
|
||||||
showEditor={false}
|
|
||||||
showForwards={true}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
case Tab.Stream: {
|
||||||
|
return <>
|
||||||
|
<h1>
|
||||||
|
<FormattedMessage defaultMessage="Stream" id="uYw2LD" />
|
||||||
|
</h1>
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<NostrProviderDialog
|
||||||
|
provider={unwrap(providers.find(a => a.name === "zap.stream")) as NostrStreamProvider}
|
||||||
|
showEndpoints={true}
|
||||||
|
showEditor={false}
|
||||||
|
showForwards={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,6 +96,8 @@ export function SettingsPage() {
|
|||||||
switch (t) {
|
switch (t) {
|
||||||
case Tab.Account:
|
case Tab.Account:
|
||||||
return <FormattedMessage defaultMessage="Account" id="TwyMau" />;
|
return <FormattedMessage defaultMessage="Account" id="TwyMau" />;
|
||||||
|
case Tab.Stream:
|
||||||
|
return <FormattedMessage defaultMessage="Stream" id="uYw2LD" />;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,13 +109,13 @@ export function SettingsPage() {
|
|||||||
</h1>
|
</h1>
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{[Tab.Account].map(t => (
|
{[Tab.Account, Tab.Stream].map(t => (
|
||||||
<AsyncButton onClick={() => setTab(t)} className="rounded-xl px-3 py-2 bg-gray-2 hover:bg-gray-1">
|
<Layer1Button onClick={() => setTab(t)} className={t === tab ? "active" : ""}>
|
||||||
{tabName(t)}
|
{tabName(t)}
|
||||||
</AsyncButton>
|
</Layer1Button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -11,7 +11,6 @@ const LiveVideoPlayer = lazy(() => import("@/element/live-video-player"));
|
|||||||
import { extractStreamInfo, findTag, getEventFromLocationState, getHost } from "@/utils";
|
import { extractStreamInfo, findTag, getEventFromLocationState, getHost } from "@/utils";
|
||||||
import { Profile, getName } from "@/element/profile";
|
import { Profile, getName } from "@/element/profile";
|
||||||
import { LiveChat } from "@/element/live-chat";
|
import { LiveChat } from "@/element/live-chat";
|
||||||
import AsyncButton from "@/element/async-button";
|
|
||||||
import { useLogin } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
import { useZapGoal } from "@/hooks/goals";
|
import { useZapGoal } from "@/hooks/goals";
|
||||||
import { SendZapsDialog } from "@/element/send-zap";
|
import { SendZapsDialog } from "@/element/send-zap";
|
||||||
@ -29,6 +28,8 @@ import { FollowButton } from "@/element/follow-button";
|
|||||||
import { ClipButton } from "@/element/clip-button";
|
import { ClipButton } from "@/element/clip-button";
|
||||||
import { StreamState } from "@/const";
|
import { StreamState } from "@/const";
|
||||||
import { NotificationsButton } from "@/element/notifications-button";
|
import { NotificationsButton } from "@/element/notifications-button";
|
||||||
|
import { WarningButton } from "@/element/buttons";
|
||||||
|
import Pill from "@/element/pill";
|
||||||
|
|
||||||
function ProfileInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedNostrEvent }) {
|
function ProfileInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedNostrEvent }) {
|
||||||
const system = useContext(SnortContext);
|
const system = useContext(SnortContext);
|
||||||
@ -60,22 +61,22 @@ function ProfileInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedNostrEv
|
|||||||
<p>{summary}</p>
|
<p>{summary}</p>
|
||||||
<div className="tags">
|
<div className="tags">
|
||||||
<StatePill state={status as StreamState} />
|
<StatePill state={status as StreamState} />
|
||||||
<span className="pill bg-gray-1">
|
<Pill>
|
||||||
<FormattedMessage defaultMessage="{n} viewers" id="3adEeb" values={{ n: formatSats(viewers) }} />
|
<FormattedMessage defaultMessage="{n} viewers" id="3adEeb" values={{ n: formatSats(viewers) }} />
|
||||||
</span>
|
</Pill>
|
||||||
{status === StreamState.Live && (
|
{status === StreamState.Live && (
|
||||||
<span className="pill bg-gray-1">
|
<Pill>
|
||||||
<StreamTimer ev={ev} />
|
<StreamTimer ev={ev} />
|
||||||
</span>
|
</Pill>
|
||||||
)}
|
)}
|
||||||
{ev && <Tags ev={ev} />}
|
{ev && <Tags ev={ev} />}
|
||||||
</div>
|
</div>
|
||||||
{isMine && (
|
{isMine && (
|
||||||
<div className="actions">
|
<div className="actions">
|
||||||
{ev && <NewStreamDialog text="Edit" ev={ev} btnClassName="btn" />}
|
{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" />
|
<FormattedMessage defaultMessage="Delete" id="K3r6DQ" />
|
||||||
</AsyncButton>
|
</WarningButton>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -14,7 +14,7 @@ import { Views } from "./widgets/views";
|
|||||||
import { Music } from "./widgets/music";
|
import { Music } from "./widgets/music";
|
||||||
import groupBy from "lodash/groupBy";
|
import groupBy from "lodash/groupBy";
|
||||||
import { hexToBech32 } from "@snort/shared";
|
import { hexToBech32 } from "@snort/shared";
|
||||||
import AsyncButton from "@/element/async-button";
|
import { DefaultButton } from "@/element/buttons";
|
||||||
|
|
||||||
interface ZapAlertConfigurationProps {
|
interface ZapAlertConfigurationProps {
|
||||||
npub: string;
|
npub: string;
|
||||||
@ -153,9 +153,9 @@ function ZapAlertConfiguration({ npub, baseUrl }: ZapAlertConfigurationProps) {
|
|||||||
onChange={ev => setTestText(ev.target.value)}
|
onChange={ev => setTestText(ev.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<AsyncButton disabled={testText.length === 0} className="btn" onClick={testVoice}>
|
<DefaultButton disabled={testText.length === 0} onClick={testVoice}>
|
||||||
<FormattedMessage defaultMessage="Test voice" id="d5zWyh" />
|
<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" />
|
<FormattedMessage defaultMessage="Top Zappers" id="dVD/AR" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-1">
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -98,10 +98,13 @@ interface StreamInfo {
|
|||||||
starts?: string;
|
starts?: string;
|
||||||
ends?: string;
|
ends?: string;
|
||||||
service?: string;
|
service?: string;
|
||||||
|
host?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function extractStreamInfo(ev?: NostrEvent) {
|
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) => {
|
const matchTag = (tag: Array<string>, k: string, into: (v: string) => void) => {
|
||||||
if (tag[0] === k) {
|
if (tag[0] === k) {
|
||||||
into(tag[1]);
|
into(tag[1]);
|
||||||
|
@ -4,12 +4,14 @@ module.exports = {
|
|||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
"gray-1": "#171717",
|
"layer-1": "rgb(23 23 23 / <alpha-value>)",
|
||||||
"gray-2": "#222",
|
"layer-2": "rgb(34 34 34 / <alpha-value>)",
|
||||||
"gray-3": "#797979",
|
"layer-3": "rgb(50 50 50 / <alpha-value>)",
|
||||||
primary: "var(--primary)",
|
primary: "var(--primary)",
|
||||||
secondary: "var(--secondary)",
|
secondary: "var(--secondary)",
|
||||||
zap: "var(--zap)",
|
zap: "var(--zap)",
|
||||||
|
success: "rgb(0 127 0 / <alpha-value>)",
|
||||||
|
warning: "rgb(255 86 63 / <alpha-value>)"
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
"ping-once": "ping 1s cubic-bezier(0, 0, 0.2, 1);",
|
"ping-once": "ping 1s cubic-bezier(0, 0, 0.2, 1);",
|
||||||
|
769
yarn.lock
769
yarn.lock
@ -1467,7 +1467,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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
|
version: 7.22.11
|
||||||
resolution: "@babel/runtime@npm:7.22.11"
|
resolution: "@babel/runtime@npm:7.22.11"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -2052,13 +2052,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@noble/curves@npm:^1.2.0":
|
||||||
version: 1.2.0
|
version: 1.2.0
|
||||||
resolution: "@noble/curves@npm:1.2.0"
|
resolution: "@noble/curves@npm:1.2.0"
|
||||||
@ -2118,457 +2111,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@react-dnd/asap@npm:^5.0.1":
|
||||||
version: 5.0.2
|
version: 5.0.2
|
||||||
resolution: "@react-dnd/asap@npm:5.0.2"
|
resolution: "@react-dnd/asap@npm:5.0.2"
|
||||||
@ -2590,37 +2132,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"@remix-run/router@npm:1.8.0":
|
||||||
version: 1.8.0
|
version: 1.8.0
|
||||||
resolution: "@remix-run/router@npm:1.8.0"
|
resolution: "@remix-run/router@npm:1.8.0"
|
||||||
@ -2791,9 +2302,9 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@snort/shared@npm:^1.0.11":
|
"@snort/shared@npm:^1.0.14":
|
||||||
version: 1.0.11
|
version: 1.0.14
|
||||||
resolution: "@snort/shared@npm:1.0.11"
|
resolution: "@snort/shared@npm:1.0.14"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@noble/curves": ^1.2.0
|
"@noble/curves": ^1.2.0
|
||||||
"@noble/hashes": ^1.3.2
|
"@noble/hashes": ^1.3.2
|
||||||
@ -2801,32 +2312,18 @@ __metadata:
|
|||||||
debug: ^4.3.4
|
debug: ^4.3.4
|
||||||
eventemitter3: ^5.0.1
|
eventemitter3: ^5.0.1
|
||||||
light-bolt11-decoder: ^3.0.0
|
light-bolt11-decoder: ^3.0.0
|
||||||
checksum: 4a5441c9a1b2636283a9d39533821fcf4606dbbfbb39329aa814e6512282adf48768e761553e906f0c9c62c3862a0cb48b05a5a84cc871562a2d1120c7a902ac
|
checksum: 9f260aecc0f35232471259b68071183fda74cd4f0db93ad8b50d3af07f106a434896452c196db8d954caf16795fa182770f2f3c12ee7e34b5e36b83694453bb8
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@snort/shared@npm:^1.0.12":
|
"@snort/system-react@npm:^1.2.12":
|
||||||
version: 1.0.12
|
version: 1.2.12
|
||||||
resolution: "@snort/shared@npm:1.0.12"
|
resolution: "@snort/system-react@npm:1.2.12"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@noble/curves": ^1.2.0
|
"@snort/shared": ^1.0.14
|
||||||
"@noble/hashes": ^1.3.2
|
"@snort/system": ^1.2.12
|
||||||
"@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
|
|
||||||
react: ^18.2.0
|
react: ^18.2.0
|
||||||
checksum: 7f354f2251fdd4ed8e2dace0dd54c8d0b06de2d5c5eac5468850ac6416bf24d97a16fb0d6fff211a34b2fb22899f4b01c1ecc7de210066709d5b85fb436dc906
|
checksum: e0fa13c7d9efbd4cb5e226386db329f5953f9868dd1be3cf306f7fe249bf08ff32dbbaf624a6b67fa2c61309af29d783a098f3f7cfc394a18d04d33a558a668d
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -2837,44 +2334,25 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@snort/system-web@npm:^1.0.4":
|
"@snort/system-web@npm:^1.2.11":
|
||||||
version: 1.0.4
|
version: 1.2.11
|
||||||
resolution: "@snort/system-web@npm:1.0.4"
|
resolution: "@snort/system-web@npm:1.2.11"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@snort/shared": ^1.0.11
|
"@snort/shared": ^1.0.14
|
||||||
"@snort/system": ^1.2.0
|
"@snort/system": ^1.2.11
|
||||||
dexie: ^3.2.4
|
dexie: ^3.2.4
|
||||||
checksum: ee705a586009cdc35c167aaeb4e505afac4d4c5f8eb80ae3b8519e631a7e9cfae828a7e00cc589eeff509bb5bd5e9398888763e03e26e1ce888393d3f5daf11b
|
checksum: 851db0c1c830df3507297a54f0c944da42e8bc35a97fcb6fd8de1e7891b97d50e2b919ccea7c5da62ccdba3cb2c6c788207b317d404fd92a003771a6ebe070f3
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"@snort/system@npm:^1.2.0":
|
"@snort/system@npm:^1.2.11":
|
||||||
version: 1.2.0
|
version: 1.2.11
|
||||||
resolution: "@snort/system@npm:1.2.0"
|
resolution: "@snort/system@npm:1.2.11"
|
||||||
dependencies:
|
dependencies:
|
||||||
"@noble/curves": ^1.2.0
|
"@noble/curves": ^1.2.0
|
||||||
"@noble/hashes": ^1.3.2
|
"@noble/hashes": ^1.3.2
|
||||||
"@scure/base": ^1.1.2
|
"@scure/base": ^1.1.2
|
||||||
"@snort/shared": ^1.0.11
|
"@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
|
|
||||||
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
|
|
||||||
"@stablelib/xchacha20": ^1.0.1
|
"@stablelib/xchacha20": ^1.0.1
|
||||||
debug: ^4.3.4
|
debug: ^4.3.4
|
||||||
eventemitter3: ^5.0.1
|
eventemitter3: ^5.0.1
|
||||||
@ -2883,7 +2361,27 @@ __metadata:
|
|||||||
lru-cache: ^10.2.0
|
lru-cache: ^10.2.0
|
||||||
uuid: ^9.0.0
|
uuid: ^9.0.0
|
||||||
ws: ^8.14.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
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
@ -3672,15 +3170,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"aria-query@npm:5.1.3":
|
||||||
version: 5.1.3
|
version: 5.1.3
|
||||||
resolution: "aria-query@npm:5.1.3"
|
resolution: "aria-query@npm:5.1.3"
|
||||||
@ -3859,20 +3348,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"buffer-from@npm:^1.0.0":
|
||||||
version: 1.1.2
|
version: 1.1.2
|
||||||
resolution: "buffer-from@npm:1.1.2"
|
resolution: "buffer-from@npm:1.1.2"
|
||||||
@ -3955,13 +3430,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"chalk@npm:^2.4.2":
|
||||||
version: 2.4.2
|
version: 2.4.2
|
||||||
resolution: "chalk@npm:2.4.2"
|
resolution: "chalk@npm:2.4.2"
|
||||||
@ -4350,13 +3818,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"dexie@npm:^3.2.4":
|
||||||
version: 3.2.4
|
version: 3.2.4
|
||||||
resolution: "dexie@npm:3.2.4"
|
resolution: "dexie@npm:3.2.4"
|
||||||
@ -4448,13 +3909,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"emoji-mart@npm:^5.5.2":
|
||||||
version: 5.5.2
|
version: 5.5.2
|
||||||
resolution: "emoji-mart@npm:5.5.2"
|
resolution: "emoji-mart@npm:5.5.2"
|
||||||
@ -5128,13 +4582,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"get-own-enumerable-property-symbols@npm:^3.0.0":
|
||||||
version: 3.0.2
|
version: 3.0.2
|
||||||
resolution: "get-own-enumerable-property-symbols@npm:3.0.2"
|
resolution: "get-own-enumerable-property-symbols@npm:3.0.2"
|
||||||
@ -5498,15 +4945,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"ip@npm:^2.0.0":
|
||||||
version: 2.0.0
|
version: 2.0.0
|
||||||
resolution: "ip@npm:2.0.0"
|
resolution: "ip@npm:2.0.0"
|
||||||
@ -6066,7 +5504,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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
|
version: 1.4.0
|
||||||
resolution: "loose-envify@npm:1.4.0"
|
resolution: "loose-envify@npm:1.4.0"
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -6395,13 +5833,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"nopt@npm:^6.0.0":
|
||||||
version: 6.0.0
|
version: 6.0.0
|
||||||
resolution: "nopt@npm:6.0.0"
|
resolution: "nopt@npm:6.0.0"
|
||||||
@ -6953,41 +6384,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"react-resize-detector@npm:^8.0.4":
|
||||||
version: 8.1.0
|
version: 8.1.0
|
||||||
resolution: "react-resize-detector@npm:8.1.0"
|
resolution: "react-resize-detector@npm:8.1.0"
|
||||||
@ -7047,23 +6443,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"react-tag-input-component@npm:^2.0.2":
|
||||||
version: 2.0.2
|
version: 2.0.2
|
||||||
resolution: "react-tag-input-component@npm:2.0.2"
|
resolution: "react-tag-input-component@npm:2.0.2"
|
||||||
@ -7715,18 +7094,12 @@ __metadata:
|
|||||||
"@formatjs/cli": ^6.1.3
|
"@formatjs/cli": ^6.1.3
|
||||||
"@formatjs/ts-transformer": ^3.13.3
|
"@formatjs/ts-transformer": ^3.13.3
|
||||||
"@noble/curves": ^1.2.0
|
"@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
|
"@scure/base": ^1.1.3
|
||||||
"@snort/shared": ^1.0.12
|
"@snort/shared": ^1.0.14
|
||||||
"@snort/system": ^1.2.1
|
"@snort/system": ^1.2.12
|
||||||
"@snort/system-react": ^1.2.1
|
"@snort/system-react": ^1.2.12
|
||||||
"@snort/system-wasm": ^1.0.2
|
"@snort/system-wasm": ^1.0.2
|
||||||
"@snort/system-web": ^1.0.4
|
"@snort/system-web": ^1.2.11
|
||||||
"@szhsin/react-menu": ^4.0.2
|
"@szhsin/react-menu": ^4.0.2
|
||||||
"@testing-library/dom": ^9.3.1
|
"@testing-library/dom": ^9.3.1
|
||||||
"@types/lodash": ^4.14.195
|
"@types/lodash": ^4.14.195
|
||||||
@ -7743,7 +7116,6 @@ __metadata:
|
|||||||
"@webbtc/webln-types": ^1.0.12
|
"@webbtc/webln-types": ^1.0.12
|
||||||
"@webscopeio/react-textarea-autocomplete": ^4.9.2
|
"@webscopeio/react-textarea-autocomplete": ^4.9.2
|
||||||
autoprefixer: ^10.4.16
|
autoprefixer: ^10.4.16
|
||||||
browserslist: ^4.23.0
|
|
||||||
buffer: ^6.0.3
|
buffer: ^6.0.3
|
||||||
classnames: ^2.3.2
|
classnames: ^2.3.2
|
||||||
emoji-mart: ^5.5.2
|
emoji-mart: ^5.5.2
|
||||||
@ -8111,7 +7483,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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
|
version: 2.6.2
|
||||||
resolution: "tslib@npm:2.6.2"
|
resolution: "tslib@npm:2.6.2"
|
||||||
checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad
|
checksum: 329ea56123005922f39642318e3d1f0f8265d1e7fcb92c633e0809521da75eeaca28d2cf96d7248229deb40e5c19adf408259f4b9640afd20d13aecc1430f3ad
|
||||||
@ -8349,20 +7721,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"uri-js@npm:^4.2.2":
|
||||||
version: 4.4.1
|
version: 4.4.1
|
||||||
resolution: "uri-js@npm:4.4.1"
|
resolution: "uri-js@npm:4.4.1"
|
||||||
@ -8372,37 +7730,6 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"usehooks-ts@npm:^2.9.1":
|
||||||
version: 2.9.1
|
version: 2.9.1
|
||||||
resolution: "usehooks-ts@npm:2.9.1"
|
resolution: "usehooks-ts@npm:2.9.1"
|
||||||
|
Loading…
x
Reference in New Issue
Block a user