chore: Update translations

This commit is contained in:
Kieran 2024-02-27 19:16:50 +00:00
parent d59e0c9f36
commit ae37f361ce
42 changed files with 684 additions and 543 deletions

View File

@ -25,12 +25,7 @@ const AsyncButton = forwardRef<HTMLButtonElement, AsyncButtonProps>((props: Asyn
}
return (
<button
ref={ref}
disabled={loading || props.disabled}
{...props}
onClick={handle}
className={props.className}>
<button ref={ref} disabled={loading || props.disabled} {...props} onClick={handle} className={props.className}>
<span
style={{ visibility: loading ? "hidden" : "visible" }}
className="whitespace-nowrap flex gap-2 items-center justify-center">

View File

@ -1,4 +1,3 @@
import { HTMLProps, useState } from "react";
import classNames from "classnames";
import { getPlaceholder } from "@/utils";

View File

@ -1,31 +1,77 @@
import { forwardRef } from "react"
import AsyncButton, { AsyncButtonProps } from "./async-button"
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} />;
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} />;
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} />;
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} />;
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} />;
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} />;
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}>
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>;
});
</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} />;
});
return (
<AsyncButton
{...props}
className={classNames(props.className, "px-3 py-2 font-semibold rounded-xl btn-border")}
ref={ref}
/>
);
});

View File

@ -179,30 +179,38 @@ export function ChatMessage({
style={
isTablet
? {
display: showZapDialog || isHovering ? "flex" : "none",
}
display: showZapDialog || isHovering ? "flex" : "none",
}
: {
position: "fixed",
top: topOffset ? topOffset - 12 : 0,
left: leftOffset ? leftOffset - 32 : 0,
opacity: showZapDialog || isHovering ? 1 : 0,
pointerEvents: showZapDialog || isHovering ? "auto" : "none",
}
position: "fixed",
top: topOffset ? topOffset - 12 : 0,
left: leftOffset ? leftOffset - 32 : 0,
opacity: showZapDialog || isHovering ? 1 : 0,
pointerEvents: showZapDialog || isHovering ? "auto" : "none",
}
}>
{zapTarget && (
<SendZapsDialog
lnurl={zapTarget}
eTag={ev.id}
pubkey={ev.pubkey}
button={
<IconButton iconName="zap" iconSize={14} className="rounded-full bg-layer-2 aspect-square" />
}
button={<IconButton iconName="zap" iconSize={14} className="rounded-full bg-layer-2 aspect-square" />}
targetName={profile?.name || ev.pubkey}
/>
)}
<IconButton onClick={pickEmoji} iconName="face" iconSize={14} className="rounded-full bg-layer-2 aspect-square" />
<IconButton
onClick={pickEmoji}
iconName="face"
iconSize={14}
className="rounded-full bg-layer-2 aspect-square"
/>
{shouldShowMuteButton && (
<IconButton onClick={muteUser} iconName="user-x" iconSize={14} className="rounded-full bg-layer-2 aspect-square" />
<IconButton
onClick={muteUser}
iconName="user-x"
iconSize={14}
className="rounded-full bg-layer-2 aspect-square"
/>
)}
</div>
)}

View File

@ -86,31 +86,33 @@ export function ClipButton({ ev }: { ev: TaggedNostrEvent }) {
<FormattedMessage defaultMessage="Create Clip" id="PA0ej4" />
</span>
</DefaultButton>
{open && <Modal id="create-clip" onClose={() => setOpen(false)}>
<div className="flex flex-col">
<h1>
<FormattedMessage defaultMessage="Create Clip" id="PA0ej4" />
</h1>
{id && tempClipId && <video ref={ref} src={provider.getTempClipUrl(id, tempClipId)} controls muted />}
<TimelineBar
length={length}
offset={start}
width={300}
height={60}
setOffset={setStart}
setLength={setLength}
/>
<div className="flex flex-col gap-1">
<small>
<FormattedMessage defaultMessage="Clip title" id="YwzT/0" />
</small>
<input type="text" value={title} onChange={e => setTitle(e.target.value)} placeholder="Epic combo!" />
{open && (
<Modal id="create-clip" onClose={() => setOpen(false)}>
<div className="flex flex-col">
<h1>
<FormattedMessage defaultMessage="Create Clip" id="PA0ej4" />
</h1>
{id && tempClipId && <video ref={ref} src={provider.getTempClipUrl(id, tempClipId)} controls muted />}
<TimelineBar
length={length}
offset={start}
width={300}
height={60}
setOffset={setStart}
setLength={setLength}
/>
<div className="flex flex-col gap-1">
<small>
<FormattedMessage defaultMessage="Clip title" id="YwzT/0" />
</small>
<input type="text" value={title} onChange={e => setTitle(e.target.value)} placeholder="Epic combo!" />
</div>
<DefaultButton onClick={saveClip}>
<FormattedMessage defaultMessage="Publish Clip" id="jJLRgo" />
</DefaultButton>
</div>
<DefaultButton onClick={saveClip}>
<FormattedMessage defaultMessage="Publish Clip" id="jJLRgo" />
</DefaultButton>
</div>
</Modal>}
</Modal>
)}
</>
);
}

View File

@ -17,13 +17,16 @@ interface MediaURLProps {
export function MediaURL({ url, children }: MediaURLProps) {
const [open, setOpen] = useState(false);
return (<>
<span onClick={() => setOpen(true)}>{url.toString()}</span>
{open && <Modal id="media-preview" onClose={() => setOpen(false)}>
<ExternalLink href={url.toString()}>{url.toString()}</ExternalLink>
{children}
</Modal>}
</>
return (
<>
<span onClick={() => setOpen(true)}>{url.toString()}</span>
{open && (
<Modal id="media-preview" onClose={() => setOpen(false)}>
<ExternalLink href={url.toString()}>{url.toString()}</ExternalLink>
{children}
</Modal>
)}
</>
);
}
@ -37,12 +40,20 @@ export function CollapsibleEvent({ link }: { link: NostrLink }) {
<div className="flex justify-between">
<div className="flex gap-2">
<EventIcon kind={event?.kind} />
<FormattedMessage defaultMessage="Note by {name}" id="ALdW69" values={{
name: <Mention pubkey={author ?? ""} />
}} />
<FormattedMessage
defaultMessage="Note by {name}"
id="ALdW69"
values={{
name: <Mention pubkey={author ?? ""} />,
}}
/>
</div>
<DefaultButton onClick={() => setOpen(s => !s)}>
{open ? <FormattedMessage defaultMessage="Hide" id="VA/Z1S" /> : <FormattedMessage defaultMessage="Show" id="K7AkdL" />}
{open ? (
<FormattedMessage defaultMessage="Hide" id="VA/Z1S" />
) : (
<FormattedMessage defaultMessage="Show" id="K7AkdL" />
)}
</DefaultButton>
</div>
{open && event && <NostrEvent ev={event} />}

View File

@ -45,14 +45,16 @@ export function EmojiPack({ ev }: { ev: NostrEvent }) {
<div className="outline emoji-pack">
<div className="emoji-pack-title">
<h4>{name}</h4>
{login?.pubkey && (isUsed ?
<WarningButton onClick={toggleEmojiPack}>
<FormattedMessage defaultMessage="Remove" id="G/yZLu" />
</WarningButton> :
<DefaultButton onClick={toggleEmojiPack}>
<FormattedMessage defaultMessage="Add" id="2/2yg+" />
</DefaultButton>
)}
{login?.pubkey &&
(isUsed ? (
<WarningButton onClick={toggleEmojiPack}>
<FormattedMessage defaultMessage="Remove" id="G/yZLu" />
</WarningButton>
) : (
<DefaultButton onClick={toggleEmojiPack}>
<FormattedMessage defaultMessage="Add" id="2/2yg+" />
</DefaultButton>
))}
</div>
<div className="emoji-pack-emojis">
{emoji.map(e => {

View File

@ -60,9 +60,7 @@ export function LoggedInFollowButton({
if (isFollowing && hideWhenFollowing) return;
return (
<DefaultButton
disabled={timestamp ? timestamp === 0 : true}
onClick={isFollowing ? unfollow : follow}>
<DefaultButton disabled={timestamp ? timestamp === 0 : true} onClick={isFollowing ? unfollow : follow}>
{isFollowing ? (
<FormattedMessage defaultMessage="Unfollow" id="izWS4J" />
) : (

View File

@ -14,7 +14,7 @@ import { Icon } from "./icon";
import { useZaps } from "@/hooks/zaps";
import classNames from "classnames";
export function Goal({ ev, confetti }: { ev: NostrEvent, confetti?: boolean }) {
export function Goal({ ev, confetti }: { ev: NostrEvent; confetti?: boolean }) {
const profile = useUserProfile(ev.pubkey);
const zapTarget = profile?.lud16 ?? profile?.lud06;
const link = NostrLink.fromEvent(ev);
@ -43,22 +43,28 @@ export function Goal({ ev, confetti }: { ev: NostrEvent, confetti?: boolean }) {
{ev.content.length > 0 && <p>{ev.content}</p>}
<div className="relative h-10">
<div className="absolute bg-layer-2 h-3 rounded-full my-4 w-full"></div>
<div className="absolute bg-zap h-3 rounded-full text-xs font-medium my-4 leading-3 pl-2" style={{
width: `${progress}%`
}}>
<div
className="absolute bg-zap h-3 rounded-full text-xs font-medium my-4 leading-3 pl-2"
style={{
width: `${progress}%`,
}}>
{soFar > 0 ? formatSats(soFar) : ""}
</div>
<div className="absolute text-right text-xs right-10 font-medium my-4 leading-3">
<FormattedMessage defaultMessage="Goal: {amount}" id="QceMQZ" values={{ amount: formatSats(goalAmount) }} />
</div>
<div className={classNames("absolute right-0 rounded-full p-2 my-1",
{ "bg-zap": isFinished, "bg-layer-2": !isFinished })}>
<div
className={classNames("absolute right-0 rounded-full p-2 my-1", {
"bg-zap": isFinished,
"bg-layer-2": !isFinished,
})}>
<Icon name="zap-filled" />
</div>
</div>
{isFinished && previousValue === false && (confetti ?? true) &&
<Confetti numberOfPieces={2100} recycle={false} />}
</div >
{isFinished && previousValue === false && (confetti ?? true) && (
<Confetti numberOfPieces={2100} recycle={false} />
)}
</div>
);
return zapTarget ? (

View File

@ -5,20 +5,16 @@ 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);
const { title, image, status, stream, recording } = extractStreamInfo(ev);
return <Suspense>
<LiveVideoPlayer
title={title}
stream={status === StreamState.Live ? stream : recording}
poster={image}
status={status}
/>
return (
<Suspense>
<LiveVideoPlayer
title={title}
stream={status === StreamState.Live ? stream : recording}
poster={image}
status={status}
/>
</Suspense>
}
);
}

View File

@ -255,12 +255,7 @@ export function LoginSignup({ close }: { close: () => void }) {
</div>
</div>
<div className="username">
<input
type="text"
placeholder="Username"
value={username}
onChange={e => setUsername(e.target.value)}
/>
<input type="text" placeholder="Username" value={username} onChange={e => setUsername(e.target.value)} />
<small>
<FormattedMessage defaultMessage="You can change this later" id="ZmqxZs" />
</small>

View File

@ -1,7 +1,12 @@
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" />
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>
}
);
}

View File

@ -4,95 +4,97 @@ 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;
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;
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);
}
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);
};
}, []);
useEffect(() => {
document.body.classList.add("scroll-lock");
document.body.style.paddingRight = `${getScrollbarWidth()}px`;
const handleBackdropClick = (e: React.MouseEvent) => {
e.stopPropagation();
props.onClose?.(e);
};
document.addEventListener("keydown", handleKeyDown);
return () => {
document.body.classList.remove("scroll-lock");
document.body.style.paddingRight = "";
document.removeEventListener("keydown", handleKeyDown);
};
}, []);
const handleBackdropClick = (e: React.MouseEvent) => {
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();
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}
}}>
<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();
}}>
<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,
);
e.stopPropagation();
props.onClose?.(e);
}}
className="rounded-full aspect-square"
iconSize={10}
/>
</div>
{props.children}
</div>
</div>,
document.body
);
}

View File

@ -37,52 +37,55 @@ export function NewGoalDialog() {
}
const isValid = goalName.length && Number(goalAmount) > 0;
return (<>
<DefaultButton onClick={() => setOpen(true)}>
<Icon name="zap-filled" size={12} />
<span>
<FormattedMessage defaultMessage="Add stream goal" id="wOy57k" />
</span>
</DefaultButton>
{open && <Modal id="new-goal" onClose={() => setOpen(false)}>
<div className="new-goal content-inner">
<div className="zap-goals">
<Icon name="zap-filled" className="stream-zap-goals-icon" size={16} />
<h3>
<FormattedMessage defaultMessage="Stream Zap Goals" id="0GfNiL" />
</h3>
</div>
<div>
<p>
<FormattedMessage defaultMessage="Name" id="HAlOn1" />
</p>
<input
type="text"
value={goalName}
placeholder="e.g. New Laptop"
onChange={e => setGoalName(e.target.value)}
/>
</div>
<div>
<p>
<FormattedMessage defaultMessage="Amount" id="/0TOL5" />
</p>
<input
type="number"
placeholder="21"
min="1"
max="2100000000000000"
value={goalAmount}
onChange={e => setGoalAmount(e.target.value)}
/>
</div>
<div className="create-goal">
<DefaultButton disabled={!isValid} onClick={publishGoal}>
<FormattedMessage defaultMessage="Create Goal" id="X2PZ7D" />
</DefaultButton>
</div>
</div>
</Modal>}
</>
return (
<>
<DefaultButton onClick={() => setOpen(true)}>
<Icon name="zap-filled" size={12} />
<span>
<FormattedMessage defaultMessage="Add stream goal" id="wOy57k" />
</span>
</DefaultButton>
{open && (
<Modal id="new-goal" onClose={() => setOpen(false)}>
<div className="new-goal content-inner">
<div className="zap-goals">
<Icon name="zap-filled" className="stream-zap-goals-icon" size={16} />
<h3>
<FormattedMessage defaultMessage="Stream Zap Goals" id="0GfNiL" />
</h3>
</div>
<div>
<p>
<FormattedMessage defaultMessage="Name" id="HAlOn1" />
</p>
<input
type="text"
value={goalName}
placeholder="e.g. New Laptop"
onChange={e => setGoalName(e.target.value)}
/>
</div>
<div>
<p>
<FormattedMessage defaultMessage="Amount" id="/0TOL5" />
</p>
<input
type="number"
placeholder="21"
min="1"
max="2100000000000000"
value={goalAmount}
onChange={e => setGoalAmount(e.target.value)}
/>
</div>
<div className="create-goal">
<DefaultButton disabled={!isValid} onClick={publishGoal}>
<FormattedMessage defaultMessage="Create Goal" id="X2PZ7D" />
</DefaultButton>
</div>
</div>
</Modal>
)}
</>
);
}

View File

@ -89,9 +89,7 @@ function NewStream({ ev, onFinish }: Omit<StreamEditorProps, "onFinish"> & { onF
</Pill>
))}
</div>
<div className="flex flex-col gap-4">
{providerDialog()}
</div>
<div className="flex flex-col gap-4">{providerDialog()}</div>
</>
);
}
@ -116,11 +114,13 @@ export function NewStreamDialog(props: NewStreamDialogProps & StreamEditorProps)
</>
)}
</DefaultButton>
{open && <Modal id="new-stream" onClose={() => setOpen(false)}>
<div className="new-stream">
<NewStream {...props} onFinish={() => setOpen(false)} />
</div>
</Modal>}
{open && (
<Modal id="new-stream" onClose={() => setOpen(false)}>
<div className="new-stream">
<NewStream {...props} onFinish={() => setOpen(false)} />
</div>
</Modal>
)}
</>
);
}

View File

@ -157,9 +157,7 @@ export function NostrProviderDialog({
</p>
<div className="flex gap-2">
{sortEndpoints(info.endpoints).map(a => (
<Pill
selected={ep?.name === a.name}
onClick={() => setEndpoint(a)}>
<Pill selected={ep?.name === a.name} onClick={() => setEndpoint(a)}>
{a.name}
</Pill>
))}
@ -178,8 +176,7 @@ export function NostrProviderDialog({
</p>
<div className="flex gap-2">
<input type="password" value={ep?.key} disabled />
<DefaultButton
onClick={() => window.navigator.clipboard.writeText(ep?.key ?? "")}>
<DefaultButton onClick={() => window.navigator.clipboard.writeText(ep?.key ?? "")}>
<FormattedMessage defaultMessage="Copy" id="4l6vz1" />
</DefaultButton>
</div>

View File

@ -2,5 +2,15 @@ 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>
}
return (
<span
{...props}
className={classNames(
className,
{ "bg-layer-3 font-bold": selected },
"px-2 py-1 font-semibold rounded-lg bg-layer-2 cursor-pointer text-sm"
)}>
{children}
</span>
);
}

View File

@ -207,20 +207,23 @@ export function SendZaps({ lnurl, pubkey, aTag, eTag, targetName, onFinish }: Se
export function SendZapsDialog(props: Omit<SendZapsProps, "onFinish">) {
const [open, setOpen] = useState(false);
return (<>
{props.button ? (
props.button
) : (
<DefaultButton onClick={() => setOpen(true)}>
<span className="max-xl:hidden">
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
</span>
<Icon name="zap-filled" size={16} />
</DefaultButton>
)}
{open && <Modal id="send-zaps" onClose={() => setOpen(false)}>
<SendZaps {...props} onFinish={() => setOpen(false)} />
</Modal>}
</>
return (
<>
{props.button ? (
props.button
) : (
<DefaultButton onClick={() => setOpen(true)}>
<span className="max-xl:hidden">
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
</span>
<Icon name="zap-filled" size={16} />
</DefaultButton>
)}
{open && (
<Modal id="send-zaps" onClose={() => setOpen(false)}>
<SendZaps {...props} onFinish={() => setOpen(false)} />
</Modal>
)}
</>
);
}

View File

@ -20,17 +20,25 @@ export function ShareMenu({ ev }: { ev: NostrEvent }) {
const { formatMessage } = useIntl();
const host = getHost(ev);
const defaultMyMsg = formatMessage({
defaultMessage: "Come check out my stream on zap.stream!\n\n{link}\n\n", id: 'HsgeUk'
}, {
link: `https://${window.location.host}/${NostrLink.fromEvent(ev).encode()}`
});
const defaultHostMsg = formatMessage({
defaultMessage: "Come check out {name} stream on zap.stream!\n\n{link}", id: 'PUymyQ'
}, {
name: `nostr:${new NostrLink(NostrPrefix.PublicKey, host ?? ev.pubkey).encode()}`,
link: `https://${window.location.host}/${NostrLink.fromEvent(ev).encode()}`
});
const defaultMyMsg = formatMessage(
{
defaultMessage: "Come check out my stream on zap.stream!\n\n{link}\n\n",
id: "HsgeUk",
},
{
link: `https://${window.location.host}/${NostrLink.fromEvent(ev).encode()}`,
}
);
const defaultHostMsg = formatMessage(
{
defaultMessage: "Come check out {name} stream on zap.stream!\n\n{link}",
id: "PUymyQ",
},
{
name: `nostr:${new NostrLink(NostrPrefix.PublicKey, host ?? ev.pubkey).encode()}`,
link: `https://${window.location.host}/${NostrLink.fromEvent(ev).encode()}`,
}
);
const [message, setMessage] = useState(login?.pubkey === host ? defaultMyMsg : defaultHostMsg);
async function sendMessage() {
@ -62,25 +70,27 @@ export function ShareMenu({ ev }: { ev: NostrEvent }) {
<FormattedMessage defaultMessage="Broadcast on Nostr" id="wCIL7o" />
</MenuItem>
</Menu>
{share && <Modal id="share" onClose={() => setShare(undefined)}>
<div className="flex flex-col gap-4">
<h2>
<FormattedMessage defaultMessage="Share" id="OKhRC6" />
</h2>
<Textarea
emojis={[]}
value={message}
onChange={e => setMessage(e.target.value)}
onKeyDown={() => {
//noop
}}
rows={15}
/>
<DefaultButton onClick={sendMessage}>
<FormattedMessage defaultMessage="Send" id="9WRlF4" />
</DefaultButton>
</div>
</Modal>}
{share && (
<Modal id="share" onClose={() => setShare(undefined)}>
<div className="flex flex-col gap-4">
<h2>
<FormattedMessage defaultMessage="Share" id="OKhRC6" />
</h2>
<Textarea
emojis={[]}
value={message}
onChange={e => setMessage(e.target.value)}
onKeyDown={() => {
//noop
}}
rows={15}
/>
<DefaultButton onClick={sendMessage}>
<FormattedMessage defaultMessage="Send" id="9WRlF4" />
</DefaultButton>
</div>
</Modal>
)}
</>
);
}

View File

@ -9,7 +9,6 @@ import Modal from "../modal";
import { NewCard } from ".";
import { CardDialog } from "./new-card";
interface AddCardProps {
cards: TaggedNostrEvent[];
}
@ -54,11 +53,15 @@ export function AddCard({ cards }: AddCardProps) {
}
return (
<div className="flex flex-col items-center justify-center bg-layer-1 rounded-xl gap-4 p-2 cursor-pointer" onClick={() => setOpen(true)}>
<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>}
{open && (
<Modal id="add-card" onClose={() => setOpen(false)}>
<CardDialog onSave={createCard} onCancel={() => setOpen(false)} />
</Modal>
)}
</div>
);
}

View File

@ -97,20 +97,15 @@ export function Card({ canEdit, ev, cards }: CardProps) {
);
const card = (
<CardPreview
ref={dropRef}
title={title}
link={link}
image={image}
content={content}
style={dropStyle}
/>
<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 (
<div ref={dragRef} style={style}>
{card}
<EditCard card={evCard} cards={cards} />
</div>
);
}
return card;
}

View File

@ -12,73 +12,76 @@ import { CardDialog } from "./new-card";
import { CardType } from ".";
interface EditCardProps {
card: CardType;
cards: TaggedNostrEvent[];
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();
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 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]);
}
}
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);
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);
}
}
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>}
async function onCancel() {
const pub = login?.publisher();
if (pub) {
const newTags = tags.filter(t => !t[1].endsWith(`:${identifier}`));
const userCardsEv = await pub.generic(eb => {
eb.kind(USER_CARDS).content("");
for (const tag of newTags) {
eb.tag(tag);
}
return eb;
});
console.debug(userCardsEv);
await system.BroadcastEvent(userCardsEv);
Login.setCards(newTags, userCardsEv.created_at);
setOpen(false);
}
}
return (
<>
<DefaultButton onClick={() => setOpen(true)}>
<FormattedMessage defaultMessage="Edit" id="wEQDC6" />
</DefaultButton>
{open && (
<Modal id="edit-stream-card" onClose={() => setOpen(false)}>
<CardDialog
header={formatMessage({ defaultMessage: "Edit", id: "wEQDC6" })}
cta={formatMessage({ defaultMessage: "Save", id: "jvo0vs" })}
cancelCta={formatMessage({ defaultMessage: "Delete", id: "K3r6DQ" })}
card={card}
onSave={editCard}
onCancel={onCancel}
/>
</Modal>
)}
</>
);
);
}

View File

@ -5,7 +5,6 @@ import { FileUploader } from "../file-uploader";
import { DefaultButton, WarningButton } from "../buttons";
import { CardType, NewCard } from ".";
interface CardDialogProps {
header?: string;
cta?: string;
@ -34,24 +33,28 @@ export function CardDialog({ header, cta, cancelCta, card, onSave, onCancel }: C
type="text"
value={title}
onChange={e => setTitle(e.target.value)}
placeholder={formatMessage({ defaultMessage: "e.g. about me", id: "k21gTS" })} />
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)} />
</>}
{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" />
@ -60,7 +63,8 @@ export function CardDialog({ header, cta, cancelCta, card, onSave, onCancel }: C
placeholder={formatMessage({ defaultMessage: "Start typing", id: "w0Xm2F" })}
value={content}
rows={5}
onChange={e => setContent(e.target.value)} />
onChange={e => setContent(e.target.value)}
/>
<span className="help-text">
<FormattedMessage
defaultMessage="Supports {markdown}"
@ -71,7 +75,8 @@ export function CardDialog({ header, cta, cancelCta, card, onSave, onCancel }: C
<FormattedMessage defaultMessage="Markdown" id="jr4+vD" />
</ExternalLink>
),
}} />
}}
/>
</span>
<div className="flex justify-between">
<WarningButton onClick={onCancel}>

View File

@ -5,34 +5,33 @@ import classNames from "classnames";
const Markdown = lazy(() => import("../markdown"));
interface CardPreviewProps extends NewCard {
style: object;
style: object;
}
function isEmpty(s: string | undefined) {
return s === undefined || s.trim().length > 0;
return s === undefined || s.trim().length > 0;
}
export const CardPreview = forwardRef<HTMLDivElement, CardPreviewProps>(({ style, title, link, image, content }: CardPreviewProps, ref) => {
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>
<div className={classNames("flex flex-col gap-4", { "": isImageOnly })} ref={ref} style={style}>
{title && <h2>{title}</h2>}
{image &&
(link && link?.length > 0 ? (
<ExternalLink href={link}>
<img src={image} alt={title} />
</ExternalLink>
) : (
<img src={image} alt={title} />
))}
{content && (
<Suspense>
<Markdown content={content} />
</Suspense>
)}
</div>
);
});
}
);

View File

@ -11,4 +11,4 @@
padding: 4px 10px !important;
border-radius: 12px !important;
display: unset !important;
}
}

View File

@ -15,9 +15,7 @@ export function Tags({ children, max, ev }: { children?: ReactNode; max?: number
<>
{children}
{status === StreamState.Planned && (
<Pill>
{status === StreamState.Planned ? <FormattedMessage defaultMessage="Starts " id="0hNxBy" /> : ""}
</Pill>
<Pill>{status === StreamState.Planned ? <FormattedMessage defaultMessage="Starts " id="0hNxBy" /> : ""}</Pill>
)}
{tags.map(a => (
<a href={`/t/${encodeURIComponent(a)}`} key={a}>

View File

@ -9,8 +9,7 @@ export function Toggle({ size, className, checked, ...props }: HTMLProps<SVGSVGE
width={size}
height={props.height ?? size}
className={className}
onClick={props.onClick}
>
onClick={props.onClick}>
<path
fillRule="evenodd"
clipRule="evenodd"
@ -24,7 +23,7 @@ export function Toggle({ size, className, checked, ...props }: HTMLProps<SVGSVGE
fill="#ffffff"
className="transition-transform"
style={{
transform: checked ? "translateX(12px)" : "translateX(0px)"
transform: checked ? "translateX(12px)" : "translateX(0px)",
}}
/>
</svg>

View File

@ -2,9 +2,30 @@ import { ParsedZap } from "@snort/system";
import useTopZappers from "@/hooks/top-zappers";
import { ZapperRow } from "./zapper-row";
export function TopZappers({ zaps, limit, avatarSize, showName, className }: { zaps: ParsedZap[]; limit?: number, avatarSize?: number, showName?: boolean, className?: string }) {
export function TopZappers({
zaps,
limit,
avatarSize,
showName,
className,
}: {
zaps: ParsedZap[];
limit?: number;
avatarSize?: number;
showName?: boolean;
className?: string;
}) {
const zappers = useTopZappers(zaps);
return zappers.slice(0, limit ?? 10).map(({ pubkey, total }) => (
<ZapperRow pubkey={pubkey} total={total} key={pubkey} showName={showName ?? false} avatarSize={avatarSize} className={className} />
));
return zappers
.slice(0, limit ?? 10)
.map(({ pubkey, total }) => (
<ZapperRow
pubkey={pubkey}
total={total}
key={pubkey}
showName={showName ?? false}
avatarSize={avatarSize}
className={className}
/>
));
}

View File

@ -28,13 +28,13 @@ export function VideoTile({
const hasImg = (image?.length ?? 0) > 0;
return (
<div className="flex flex-col gap-2">
<Link
to={`/${link.encode()}`}
className={classNames({ "blur": contentWarning }, "h-full")}
state={ev}>
<Link to={`/${link.encode()}`} className={classNames({ blur: contentWarning }, "h-full")} state={ev}>
<div className="relative mb-2 aspect-video">
{hasImg ? <img loading="lazy" className="aspect-video object-cover rounded-xl" src={image} /> :
<Logo className="text-white aspect-video" />}
{hasImg ? (
<img loading="lazy" className="aspect-video object-cover rounded-xl" src={image} />
) : (
<Logo className="text-white aspect-video" />
)}
<span className="flex flex-col justify-between absolute top-0 h-full right-4 items-end py-2">
{showStatus && <StatePill state={status as StreamState} />}
{participants && (

View File

@ -4,7 +4,19 @@ import { Profile } from "./profile";
import { FormattedMessage } from "react-intl";
import classNames from "classnames";
export function ZapperRow({ pubkey, total, showName, avatarSize, className }: { pubkey: string; total: number; showName?: boolean, avatarSize?: number, className?: string }) {
export function ZapperRow({
pubkey,
total,
showName,
avatarSize,
className,
}: {
pubkey: string;
total: number;
showName?: boolean;
avatarSize?: number;
className?: string;
}) {
return (
<div className={classNames(className, "flex gap-1 justify-between items-center")}>
{pubkey === "anon" ? (

View File

@ -4,12 +4,12 @@ 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]);
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);
}
return useRequestBuilder(sub);
}

View File

@ -60,7 +60,7 @@ async function doInit() {
await System.Init();
try {
const req = await fetch("https://api.zap.stream/api/time", {
signal: AbortSignal.timeout(1000)
signal: AbortSignal.timeout(1000),
});
const nowAtServer = (await req.json()).time as number;
const now = unixNowMs();
@ -81,7 +81,7 @@ const router = createBrowserRouter([
children: [
{
path: "/mock",
element: <MockPage />
element: <MockPage />,
},
{
path: "/",

View File

@ -110,7 +110,7 @@ function DashboardChatList({ link }: { link: NostrLink }) {
<Profile pubkey={a} avatarSize={32} gap={4} />
<div className="flex gap-2">
<MuteButton pubkey={a} />
<DefaultButton onClick={() => { }} className="font-bold">
<DefaultButton onClick={() => {}} className="font-bold">
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
</DefaultButton>
</div>
@ -173,13 +173,16 @@ function DashboardHighlightZap({ zap }: { zap: ParsedZap }) {
function DashboardRaidButton({ link }: { link: NostrLink }) {
const [show, setShow] = useState(false);
return (<>
<DefaultButton onClick={() => setShow(true)}>
<FormattedMessage defaultMessage="Raid" id="4iBdw1" />
</DefaultButton>
{show && <Modal id="raid-menu" onClose={() => setShow(false)}>
<DashboardRaidMenu link={link} onClose={() => setShow(false)} />
</Modal>}
</>
return (
<>
<DefaultButton onClick={() => setShow(true)}>
<FormattedMessage defaultMessage="Raid" id="4iBdw1" />
</DefaultButton>
{show && (
<Modal id="raid-menu" onClose={() => setShow(false)}>
<DashboardRaidMenu link={link} onClose={() => setShow(false)} />
</Modal>
)}
</>
);
}

View File

@ -110,15 +110,18 @@ export function LayoutPage() {
function loggedOut() {
if (login) return;
return (<>
<BorderButton onClick={() => setShowLogin(true)}>
<FormattedMessage defaultMessage="Login" id="AyGauy" />
<Icon name="login" />
</BorderButton>
{showLogin && <Modal id="login">
<LoginSignup close={() => setShowLogin(false)} />
</Modal>}
</>
return (
<>
<BorderButton onClick={() => setShowLogin(true)}>
<FormattedMessage defaultMessage="Login" id="AyGauy" />
<Icon name="login" />
</BorderButton>
{showLogin && (
<Modal id="login">
<LoginSignup close={() => setShowLogin(false)} />
</Modal>
)}
</>
);
}

View File

@ -4,22 +4,24 @@ 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);
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} />
return (
<div className="">
<LiveChat link={fakeStreamLink} ev={fakeStream} height={600} />
<SendZapsDialog lnurl="donate@snort.social" aTag={fakeStreamLink.toEventTag()![1]} pubkey={pubkey} />
</div>
}
);
}

View File

@ -75,7 +75,15 @@ export function ProfilePage() {
);
}
function ProfileHeader({ profile, link, streams }: { profile?: CachedMetadata, link: NostrLink, streams: Array<NostrEvent> }) {
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);
@ -90,44 +98,46 @@ function ProfileHeader({ profile, link, streams }: { profile?: CachedMetadata, l
}
}
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" />}
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 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 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>
<div className="flex gap-2 items-center">
{zapTarget && (
<SendZapsDialog
aTag={liveEvent ? `${liveEvent.kind}:${liveEvent.pubkey}:${findTag(liveEvent, "d")}` : undefined}
lnurl={zapTarget}
button={
<DefaultButton>
<Icon name="zap-filled" className="zap-button-icon" />
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
</DefaultButton>
}
targetName={profile?.name || link.id}
/>
)}
<FollowButton pubkey={link.id} />
<MuteButton pubkey={link.id} />
</div>
</div>
);
}
function ProfileStreamList({ streams }: { streams: Array<TaggedNostrEvent> }) {
if (streams.length === 0) {
return <FormattedMessage defaultMessage="No streams yet" id="0rVLjV" />
return <FormattedMessage defaultMessage="No streams yet" id="0rVLjV" />;
}
return (
<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">
@ -153,23 +163,25 @@ 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 <FormattedMessage defaultMessage="No goals yet" id="ZaNcK4" />;
}
return goals
.sort((a, b) => a.created_at > b.created_at ? -1 : 1)
.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>);
.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 <FormattedMessage defaultMessage="No clips yet" id="ObZZEz" />;
}
return clips.map(a => {
const r = findTag(a, "r");
return <video src={r} />
})
}
return <video src={r} />;
});
}

View File

@ -38,9 +38,7 @@ export function StreamProvidersPage() {
<div className="paper">
<h3>{mapName(p)}</h3>
{mapLogo(p)}
<DefaultButton onClick={() => navigate(p)}>
+ Configure
</DefaultButton>
<DefaultButton onClick={() => navigate(p)}>+ Configure</DefaultButton>
</div>
);
}

View File

@ -82,9 +82,7 @@ export function ConfigureOwncast() {
<input type="password" value={token} onChange={e => setToken(e.target.value)} />
</div>
</div>
<DefaultButton onClick={tryConnect}>
Connect
</DefaultButton>
<DefaultButton onClick={tryConnect}>Connect</DefaultButton>
</div>
<div>{status()}</div>
</div>

View File

@ -75,19 +75,21 @@ export function SettingsPage() {
);
}
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>
</>
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>
</>
);
}
}
}

View File

@ -297,7 +297,7 @@
"defaultMessage": "Скрий"
},
"Vn2WiP": {
"defaultMessage": "Get Stream Key"
"defaultMessage": "Получи Стрийм Ключ"
},
"W7DNWx": {
"defaultMessage": "Препращане на потоци"
@ -312,7 +312,7 @@
"defaultMessage": "Реакции"
},
"Y0DXJb": {
"defaultMessage": "Recording URL"
"defaultMessage": "Записване URL"
},
"YPh5Nq": {
"defaultMessage": "@ {курс}",
@ -322,7 +322,7 @@
"defaultMessage": "{n}p"
},
"YwzT/0": {
"defaultMessage": "Clip title"
"defaultMessage": "Заглавие на клипа"
},
"Z8ZOEY": {
"defaultMessage": "Този метод не е сигурен. Препоръчваме ви да използвате {ностърлинк}"
@ -406,7 +406,7 @@
"defaultMessage": "Набези {name}"
},
"jJLRgo": {
"defaultMessage": "Publish Clip"
"defaultMessage": "Публикувай Клип"
},
"jctiUc": {
"defaultMessage": "Най-висока гледаемост"

View File

@ -103,7 +103,7 @@ interface StreamInfo {
export function extractStreamInfo(ev?: NostrEvent) {
const ret = {
host: getHost(ev)
host: getHost(ev),
} as StreamInfo;
const matchTag = (tag: Array<string>, k: string, into: (v: string) => void) => {
if (tag[0] === k) {
@ -161,5 +161,5 @@ export function groupBy<T>(val: Array<T>, selector: (a: T) => string | number):
acc[key] ??= [];
acc[key].push(v);
return acc;
}, {} as Record<string, Array<T>>)
}
}, {} as Record<string, Array<T>>);
}

View File

@ -11,7 +11,7 @@ module.exports = {
secondary: "var(--secondary)",
zap: "var(--zap)",
success: "rgb(0 127 0 / <alpha-value>)",
warning: "rgb(255 86 63 / <alpha-value>)"
warning: "rgb(255 86 63 / <alpha-value>)",
},
animation: {
"ping-once": "ping 1s cubic-bezier(0, 0, 0.2, 1);",