feat: style upgrades
continuous-integration/drone/push Build is failing Details

This commit is contained in:
kieran 2024-05-20 16:45:10 +01:00
parent 6250456435
commit 21919e1e3b
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
78 changed files with 1168 additions and 898 deletions

View File

@ -7,7 +7,7 @@ export const onRequest: PagesFunction<Env> = async context => {
const prefixes = ["npub1", "nprofile1", "naddr1", "nevent1", "note1"];
const isEntityPath = prefixes.some(
a => u.pathname.startsWith(`/${a}`) || u.pathname.startsWith(`/e/${a}`) || u.pathname.startsWith(`/p/${a}`)
a => u.pathname.startsWith(`/${a}`) || u.pathname.startsWith(`/e/${a}`) || u.pathname.startsWith(`/p/${a}`),
);
const nostrAddress = u.pathname.match(/^\/([a-zA-Z0-9_]+)$/i);
const next = await context.next();
@ -19,7 +19,7 @@ export const onRequest: PagesFunction<Env> = async context => {
id = `${id}@${HOST}`;
}
const fetchApi = `https://nostr.api.v0l.io/api/v1/opengraph/${id}?canonical=${encodeURIComponent(
`https://${HOST}/%s`
`https://${HOST}/%s`,
)}`;
console.log("Fetching tags from: ", fetchApi);
const reqBuf = await next.arrayBuffer();

View File

@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />

View File

@ -4,8 +4,8 @@
"dependencies": {
"@emoji-mart/data": "^1.2.1",
"@emoji-mart/react": "^1.1.1",
"@noble/curves": "^1.2.0",
"@scure/base": "^1.1.3",
"@noble/curves": "^1.4.0",
"@scure/base": "^1.1.6",
"@snort/shared": "^1.0.15",
"@snort/system": "^1.3.2",
"@snort/system-react": "^1.3.2",
@ -13,14 +13,14 @@
"@snort/wallet": "^0.1.3",
"@snort/worker-relay": "^1.0.10",
"@sqlite.org/sqlite-wasm": "^3.45.1-build1",
"@szhsin/react-menu": "^4.0.2",
"@types/webscopeio__react-textarea-autocomplete": "^4.7.2",
"@szhsin/react-menu": "^4.1.0",
"@types/webscopeio__react-textarea-autocomplete": "^4.7.5",
"@void-cat/api": "^1.0.12",
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
"buffer": "^6.0.3",
"classnames": "^2.3.2",
"emoji-mart": "^5.5.2",
"flag-icons": "^6.11.0",
"classnames": "^2.5.1",
"emoji-mart": "^5.6.0",
"flag-icons": "^7.2.1",
"hls.js": "^1.5.8",
"marked": "^12.0.2",
"qr-code-styling": "^1.6.0-rc.1",
@ -31,15 +31,17 @@
"react-dom": "^18.3.1",
"react-helmet": "^6.1.0",
"react-intersection-observer": "^9.10.2",
"react-intl": "^6.6.6",
"react-intl": "^6.6.8",
"react-router-dom": "^6.23.1",
"react-tag-input-component": "^2.0.2",
"react-textarea-autosize": "^8.5.3",
"react-use-gesture": "^9.1.3",
"react-use-pip": "^1.5.0",
"recharts": "^2.12.7",
"semantic-sdp": "^3.26.3",
"semantic-sdp": "^3.27.1",
"usehooks-ts": "^3.1.0",
"web-vitals": "^2.1.0",
"webrtc-adapter": "^8.2.3",
"web-vitals": "^4.0.0",
"webrtc-adapter": "^9.0.1",
"workbox-core": "^7.1.0",
"workbox-precaching": "^7.1.0",
"workbox-routing": "^7.1.0",
@ -75,21 +77,21 @@
"@cloudflare/workers-types": "^4.20231218.0",
"@formatjs/cli": "^6.1.3",
"@testing-library/dom": "^9.3.1",
"@types/node": "^20.10.3",
"@types/react": "^18.2.21",
"@types/react-dom": "^18.2.7",
"@types/react-helmet": "^6.1.6",
"@typescript-eslint/eslint-plugin": "^6.4.1",
"@typescript-eslint/parser": "^6.4.1",
"@vitejs/plugin-react": "^4.2.0",
"@webbtc/webln-types": "^1.0.12",
"autoprefixer": "^10.4.16",
"babel-plugin-formatjs": "^10.5.13",
"eslint": "^8.48.0",
"postcss": "^8.4.32",
"prettier": "^2.8.8",
"@types/node": "^20.12.12",
"@types/react": "^18.3.2",
"@types/react-dom": "^18.3.0",
"@types/react-helmet": "^6.1.11",
"@typescript-eslint/eslint-plugin": "^7.9.0",
"@typescript-eslint/parser": "^7.9.0",
"@vitejs/plugin-react": "^4.2.1",
"@webbtc/webln-types": "^3.0.0",
"autoprefixer": "^10.4.19",
"babel-plugin-formatjs": "^10.5.16",
"eslint": "^8.56.0",
"postcss": "^8.4.38",
"prettier": "^3.2.5",
"prop-types": "^15.8.1",
"rollup-plugin-visualizer": "^5.10.0",
"rollup-plugin-visualizer": "^5.12.0",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5",
"vite": "^5.2.11",

View File

@ -8,7 +8,7 @@ export default function AmountInput({ onChange }: { onChange: (n: number) => voi
const satsValue = useCallback(
() => (type === "usd" ? Math.round(value * 1e-6 * rates.ask) / 100 : value),
[value, type]
[value, type],
);
useEffect(() => {

View File

@ -3,62 +3,36 @@ import AsyncButton, { AsyncButtonProps } from "./async-button";
import { Icon } from "./icon";
import classNames from "classnames";
const buttonBaseClass = [
"px-3 xl:py-2 max-xl:py-[6px]",
"font-semibold rounded-full",
"disabled:opacity-20 hover:opacity-80",
"max-xl:text-sm",
"leading-none",
];
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 disabled:opacity-20"
)}
className={classNames(props.className, buttonBaseClass, "bg-neutral-800 text-white")}
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 disabled:opacity-20")}
ref={ref}
/>
);
return <AsyncButton {...props} className={classNames(props.className, buttonBaseClass, "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 disabled:opacity-20")}
ref={ref}
/>
);
return <AsyncButton {...props} className={classNames(props.className, buttonBaseClass, "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 disabled:opacity-20")}
ref={ref}
/>
);
return <AsyncButton {...props} className={classNames(props.className, buttonBaseClass, "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 disabled:opacity-20")}
ref={ref}
/>
);
return <AsyncButton {...props} className={classNames(props.className, buttonBaseClass, "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 disabled:opacity-20")}
ref={ref}
/>
);
return <AsyncButton {...props} className={classNames(props.className, buttonBaseClass, "bg-warning")} ref={ref} />;
});
export const IconButton = forwardRef<HTMLButtonElement, { iconName: string; iconSize?: number } & AsyncButtonProps>(
({ iconName, iconSize, ...props }: { iconName: string; iconSize?: number } & AsyncButtonProps, ref) => {
@ -67,14 +41,8 @@ export const IconButton = forwardRef<HTMLButtonElement, { iconName: string; icon
<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 disabled:opacity-20")}
ref={ref}
/>
);
return <AsyncButton {...props} className={classNames(props.className, buttonBaseClass, "btn-border")} ref={ref} />;
});

View File

@ -20,7 +20,7 @@ export default function CategoryLink({
key={id}
className={classNames(
"text-lg font-semibold rounded-xl border border-layer-2 border-2 hover:bg-layer-2",
className
className,
)}>
<div className="flex items-center gap-2 px-2 py-1 whitespace-nowrap">
<Icon name={icon} size={24} />

View File

@ -30,13 +30,7 @@ export function CategoryTile({
{showDetail && (
<div className="flex flex-col gap-4">
<h1>{game?.name}</h1>
{game?.genres && (
<div className="flex gap-2">
{game?.genres?.map(a => (
<Pill>{a}</Pill>
))}
</div>
)}
{game?.genres && <div className="flex gap-2">{game?.genres?.map(a => <Pill>{a}</Pill>)}</div>}
{extraDetail}
</div>
)}

View File

@ -4,20 +4,20 @@ import React, { Suspense, lazy, useContext, useMemo, useRef, useState } from "re
import { useHover, useIntersectionObserver, useOnClickOutside } from "usehooks-ts";
import { dedupe } from "@snort/shared";
const EmojiPicker = lazy(() => import("./emoji-picker"));
import { Icon } from "./icon";
import { Emoji as EmojiComponent } from "./emoji";
import { Profile } from "./profile";
import { Text } from "./text";
import { useMute } from "./mute-button";
import { SendZapsDialog } from "./send-zap";
import { CollapsibleEvent } from "./collapsible";
const EmojiPicker = lazy(() => import("../emoji-picker"));
import { Icon } from "../icon";
import { Emoji as EmojiComponent } from "../emoji";
import { Profile } from "../profile";
import { Text } from "../text";
import { useMute } from "../mute-button";
import { SendZapsDialog } from "../send-zap";
import { CollapsibleEvent } from "../collapsible";
import { useLogin } from "@/hooks/login";
import { formatSats } from "@/number";
import type { Badge, Emoji, EmojiPack } from "@/types";
import { IconButton } from "./buttons";
import Pill from "./pill";
import { IconButton } from "../buttons";
import Pill from "../pill";
import classNames from "classnames";
function emojifyReaction(reaction: string) {

View File

@ -5,13 +5,13 @@ import { useEventFeed, useEventReactions, useReactions, useUserProfile } from "@
import { unixNow, unwrap } from "@snort/shared";
import { useEffect, useMemo } from "react";
import { Icon } from "./icon";
import Spinner from "./spinner";
import { Text } from "./text";
import { Profile } from "./profile";
import { Icon } from "../icon";
import Spinner from "../spinner";
import { Text } from "../text";
import { Profile } from "../profile";
import { ChatMessage } from "./chat-message";
import { Goal } from "./goal";
import { Badge } from "./badge";
import { Goal } from "../goal";
import { Badge } from "../badge";
import { WriteMessage } from "./write-message";
import useEmoji, { packId } from "@/hooks/emoji";
import { useMutedPubkeys } from "@/hooks/lists";
@ -20,9 +20,11 @@ import { useLogin } from "@/hooks/login";
import { formatSats } from "@/number";
import { LIVE_STREAM_CHAT, LIVE_STREAM_CLIP, LIVE_STREAM_RAID, WEEK } from "@/const";
import { findTag, getHost, getTagValues, uniqBy } from "@/utils";
import { TopZappers } from "./top-zappers";
import { TopZappers } from "../top-zappers";
import { Link, useNavigate } from "react-router-dom";
import classNames from "classnames";
import { useStream } from "../stream/stream-state";
import { useLayout } from "@/pages/layout/context";
function BadgeAward({ ev }: { ev: NostrEvent }) {
const badge = findTag(ev, "a") ?? "";
@ -48,6 +50,7 @@ export function LiveChat({
goal,
canWrite,
showTopZappers,
adjustLayout,
showGoal,
showScrollbar,
height,
@ -59,6 +62,7 @@ export function LiveChat({
goal?: NostrEvent;
canWrite?: boolean;
showTopZappers?: boolean;
adjustLayout?: boolean;
showGoal?: boolean;
showScrollbar?: boolean;
height?: number;
@ -75,7 +79,7 @@ export function LiveChat({
rb.withFilter().kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP]).tag("a", [aTag]).limit(200);
}
},
true
true,
);
const login = useLogin();
const started = useMemo(() => {
@ -92,6 +96,8 @@ export function LiveChat({
const allEmojiPacks = useMemo(() => {
return uniqBy(userEmojiPacks.concat(channelEmojiPacks), packId);
}, [userEmojiPacks, channelEmojiPacks]);
const streamContext = useStream();
const layoutContext = useLayout();
const reactions = useEventReactions(link, feed);
const events = useMemo(() => {
@ -109,6 +115,35 @@ export function LiveChat({
.sort((a, b) => b.created_at - a.created_at);
}, [feed, awards]);
useEffect(() => {
const resetLayout = () => {
if (streamContext.showDetails || !adjustLayout) {
streamContext.update(c => {
c.showDetails = !adjustLayout;
return { ...c };
});
}
if (!layoutContext.showHeader) {
layoutContext.update(c => {
c.showHeader = true;
return { ...c };
});
}
};
if (adjustLayout) {
layoutContext.update(c => {
c.showHeader = false;
return { ...c };
});
return () => {
resetLayout();
};
} else {
resetLayout();
}
}, [adjustLayout]);
const filteredEvents = useMemo(() => {
return events.filter(e => !mutedPubkeys.has(e.pubkey) && !hostMutedPubkeys.has(e.pubkey));
}, [events, mutedPubkeys, hostMutedPubkeys]);
@ -126,7 +161,32 @@ export function LiveChat({
<div
className={classNames("flex flex-col-reverse grow gap-2 overflow-y-auto", {
"scrollbar-hidden": !(showScrollbar ?? true),
})}>
})}
onScroll={e => {
if (adjustLayout) {
const t = e.target as HTMLDivElement;
const atEnd = t.scrollTop >= 1;
if (atEnd) {
streamContext.update(c => {
c.showDetails = false;
return { ...c };
});
layoutContext.update(c => {
c.showHeader = false;
return { ...c };
});
} else {
streamContext.update(c => {
c.showDetails = true;
return { ...c };
});
layoutContext.update(c => {
c.showHeader = true;
return { ...c };
});
}
}
}}>
{filteredEvents.map(a => {
switch (a.kind) {
case -1:
@ -165,9 +225,10 @@ export function LiveChat({
return null;
})}
{feed.length === 0 && <Spinner />}
<div className="pt-[50dvh]"></div>
</div>
{(canWrite ?? true) && (
<div className="flex gap-2 border-t pt-2 border-layer-1">
<div className="flex gap-2 border-t py-2 border-layer-1">
{login ? (
<WriteMessage emojiPacks={allEmojiPacks} link={link} />
) : (

View File

@ -1,7 +1,9 @@
.rta__textarea {
resize: none;
.rta {
display: flex;
}
.rta__textarea {
resize: none !important;
}
.rta__list {
border: none;
}

View File

@ -1,14 +1,14 @@
import "./textarea.css";
import { useContext } from "react";
import { useContext, useEffect, useState } from "react";
import ReactTextareaAutocomplete, { TriggerType } from "@webscopeio/react-textarea-autocomplete";
import "@webscopeio/react-textarea-autocomplete/style.css";
import "./textarea.css";
import { hexToBech32 } from "@snort/shared";
import { SnortContext } from "@snort/system-react";
import { CachedMetadata, NostrPrefix, UserProfileCache } from "@snort/system";
import { CachedMetadata, NostrPrefix } from "@snort/system";
import { Emoji } from "./emoji";
import { Avatar } from "./avatar";
import { Emoji } from "../emoji";
import { Avatar } from "../avatar";
import type { EmojiTag } from "@/types";
interface EmojiItemProps {
@ -41,13 +41,19 @@ type TextareaProps = { emojis: EmojiTag[] } & React.TextareaHTMLAttributes<HTMLT
export function Textarea({ emojis, ...props }: TextareaProps) {
const system = useContext(SnortContext);
const [ref, setRef] = useState<HTMLTextAreaElement | null>(null);
const userDataProvider = async (token: string) => {
const cache = system.profileLoader.cache;
if (cache instanceof UserProfileCache) {
return await cache.search(token);
}
return await cache.search(token);
};
useEffect(() => {
if (ref) {
ref.style.height = "";
ref.style.height = `${Math.min(ref.scrollHeight, 200)}px`;
}
}, [ref, props.value]);
const emojiDataProvider = (token: string) => {
const results = emojis
.map(t => {
@ -82,6 +88,7 @@ export function Textarea({ emojis, ...props }: TextareaProps) {
autoFocus={false}
trigger={trigger}
{...props}
innerRef={r => setRef(r)}
/>
);
}

View File

@ -1,19 +1,26 @@
import { EventKind, NostrLink } from "@snort/system";
import React, { Suspense, lazy, useContext, useRef, useState } from "react";
import { FormattedMessage } from "react-intl";
import { SnortContext } from "@snort/system-react";
import { unixNowMs } from "@snort/shared";
import { unixNowMs, unwrap } from "@snort/shared";
const EmojiPicker = lazy(() => import("./emoji-picker"));
const EmojiPicker = lazy(() => import("../emoji-picker"));
import { useLogin } from "@/hooks/login";
import { Icon } from "./icon";
import { Icon } from "../icon";
import { Textarea } from "./textarea";
import type { Emoji, EmojiPack } from "@/types";
import { LIVE_STREAM_CHAT } from "@/const";
import { TimeSync } from "@/time-sync";
import { BorderButton } from "./buttons";
import AsyncButton from "../async-button";
export function WriteMessage({ link, emojiPacks }: { link: NostrLink; emojiPacks: EmojiPack[] }) {
export function WriteMessage({
link,
emojiPacks,
kind,
}: {
link: NostrLink;
emojiPacks: EmojiPack[];
kind?: EventKind;
}) {
const system = useContext(SnortContext);
const ref = useRef<HTMLDivElement | null>(null);
const emojiRef = useRef(null);
@ -39,10 +46,10 @@ export function WriteMessage({ link, emojiPacks }: { link: NostrLink; emojiPacks
const reply = await pub?.generic(eb => {
const emoji = [...emojiNames].map(name => emojis.find(e => e.at(1) === name));
eb.kind(LIVE_STREAM_CHAT as EventKind)
eb.kind(kind ?? (LIVE_STREAM_CHAT as EventKind))
.content(chat)
.createdAt(Math.floor((unixNowMs() - TimeSync) / 1000))
.tag(["a", `${link.kind}:${link.author}:${link.id}`, "", "root"])
.tag(unwrap(link.toEventTag("root")))
.processContent();
for (const e of emoji) {
if (e) {
@ -82,11 +89,21 @@ export function WriteMessage({ link, emojiPacks }: { link: NostrLink; emojiPacks
return (
<>
<div className="grow flex bg-layer-2 rounded-xl items-center" ref={ref}>
<Textarea emojis={emojis} value={chat} onKeyDown={onKeyDown} onChange={e => setChat(e.target.value)} rows={2} />
<div onClick={pickEmoji} className="p-2">
<div className="grow flex bg-layer-2 px-3 py-2 rounded-xl items-center" ref={ref}>
<Textarea
className="!p-0 !rounded-none"
emojis={emojis}
value={chat}
onKeyDown={onKeyDown}
onChange={e => setChat(e.target.value)}
rows={1}
/>
<AsyncButton onClick={pickEmoji} className="px-3 opacity-80">
<Icon name="face" />
</div>
</AsyncButton>
<AsyncButton onClick={sendChatMessage} className="px-3 opacity-80">
<Icon name="send" />
</AsyncButton>
{showEmojiPicker && (
<Suspense>
<EmojiPicker
@ -100,9 +117,6 @@ export function WriteMessage({ link, emojiPacks }: { link: NostrLink; emojiPacks
</Suspense>
)}
</div>
<BorderButton onClick={sendChatMessage}>
<FormattedMessage defaultMessage="Send" id="9WRlF4" />
</BorderButton>
</>
);
}

View File

@ -7,10 +7,10 @@ import { EmojiPack } from "./emoji-pack";
import { Badge } from "./badge";
import { EMOJI_PACK, GOAL, LIVE_STREAM_CLIP, StreamState } from "@/const";
import { useEventFeed } from "@snort/system-react";
import LiveStreamClip from "./clip";
import LiveStreamClip from "./stream/clip";
import { ExternalLink } from "./external-link";
import { extractStreamInfo } from "@/utils";
import LiveVideoPlayer from "./live-video-player";
import LiveVideoPlayer from "./stream/live-video-player";
interface EventProps {
link: NostrLink;

View File

@ -6,6 +6,7 @@ import { SnortContext } from "@snort/system-react";
import { useLogin } from "@/hooks/login";
import { Login } from "@/login";
import { DefaultButton } from "./buttons";
import { Icon } from "./icon";
export function LoggedInFollowButton({
tag,
@ -62,9 +63,12 @@ export function LoggedInFollowButton({
return (
<DefaultButton disabled={timestamp ? timestamp === 0 : true} onClick={isFollowing ? unfollow : follow}>
{isFollowing ? (
<FormattedMessage defaultMessage="Unfollow" id="izWS4J" />
<FormattedMessage defaultMessage="Unfollow" />
) : (
<FormattedMessage defaultMessage="Follow" id="ieGrWo" />
<>
<Icon name="plus" size={20} />
<FormattedMessage defaultMessage="Follow" />
</>
)}
</DefaultButton>
);

View File

@ -2,7 +2,7 @@ import { StreamState } from "@/const";
import { extractStreamInfo } from "@/utils";
import { TaggedNostrEvent } from "@snort/system";
import { Suspense } from "react";
import LiveVideoPlayer from "./live-video-player";
import LiveVideoPlayer from "./stream/live-video-player";
export default function LiveEvent({ ev }: { ev: TaggedNostrEvent }) {
const { title, image, status, stream, recording } = extractStreamInfo(ev);

View File

@ -137,7 +137,7 @@ export function LoginSignup({ close }: { close: () => void }) {
formatMessage({
defaultMessage: "Hmm, your lightning address looks wrong",
id: "4l69eO",
})
}),
);
}
}

View File

@ -69,7 +69,7 @@ export default function Modal(props: ModalProps) {
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"
"z-[42] w-screen h-screen top-0 left-0 fixed bg-black/80 flex justify-center overflow-y-auto",
)}
onMouseDown={handleBackdropClick}
onClick={e => {
@ -98,6 +98,6 @@ export default function Modal(props: ModalProps) {
{props.children}
</div>
</div>,
document.body
document.body,
);
}

View File

@ -4,7 +4,7 @@ import { NSFWStore } from "./store";
export function useContentWarning() {
const v = useSyncExternalStore(
c => NSFWStore.hook(c),
() => NSFWStore.snapshot()
() => NSFWStore.snapshot(),
);
return v;
}

View File

@ -8,7 +8,7 @@ export default function Pill({ children, selected, className, ...props }: HTMLPr
className={classNames(
className,
{ "bg-layer-3 font-bold": selected },
"px-2 py-1 font-semibold rounded-lg bg-layer-2 cursor-pointer text-sm"
"px-2 py-1 font-semibold rounded-lg bg-layer-2 cursor-pointer text-sm",
)}>
{children}
</span>

View File

@ -58,7 +58,7 @@ export function ProfileEditor({ onClose }: { onClose: () => void }) {
setInvalidLud16Message(
formatMessage({
defaultMessage: "Invalid lightning address",
})
}),
);
}
} else {
@ -76,7 +76,7 @@ export function ProfileEditor({ onClose }: { onClose: () => void }) {
setNip05AddressValid(true);
} else {
setInvalidNip05AddressMessage(
formatMessage({ defaultMessage: "Nostr address does not belong to you", id: "01iNut" })
formatMessage({ defaultMessage: "Nostr address does not belong to you", id: "01iNut" }),
);
}
} else {
@ -84,7 +84,7 @@ export function ProfileEditor({ onClose }: { onClose: () => void }) {
setInvalidNip05AddressMessage(
formatMessage({
defaultMessage: "Invalid nostr address",
})
}),
);
}
} catch (e) {
@ -92,7 +92,7 @@ export function ProfileEditor({ onClose }: { onClose: () => void }) {
setInvalidNip05AddressMessage(
formatMessage({
defaultMessage: "Invalid nostr address",
})
}),
);
}
}
@ -106,7 +106,7 @@ export function ProfileEditor({ onClose }: { onClose: () => void }) {
setInvalidNip05AddressMessage(
formatMessage({
defaultMessage: "Invalid nostr address",
})
}),
);
} else if (Nip05AddressElements.length === 2) {
nip05NostrAddressVerification(Nip05AddressElements.pop(), Nip05AddressElements.pop());
@ -192,7 +192,7 @@ export function ProfileEditor({ onClose }: { onClose: () => void }) {
setInvalidUsernameMessage(
formatMessage({
defaultMessage: "Username is too long",
})
}),
);
} else {
setUsernameValid(true);
@ -205,7 +205,7 @@ export function ProfileEditor({ onClose }: { onClose: () => void }) {
setInvalidAboutMessage(
formatMessage({
defaultMessage: "About too long",
})
}),
);
} else {
setAboutValid(true);

View File

@ -1,7 +1,7 @@
import type { ReactNode } from "react";
import { Link } from "react-router-dom";
import { useUserProfile } from "@snort/system-react";
import { UserMetadata } from "@snort/system";
import { CachedMetadata, UserMetadata } from "@snort/system";
import { hexToBech32 } from "@snort/shared";
import { useInView } from "react-intersection-observer";
import { Avatar } from "./avatar";
@ -36,6 +36,7 @@ export function Profile({
linkToProfile,
avatarSize,
gap,
profile,
}: {
pubkey: string;
icon?: ReactNode;
@ -45,9 +46,10 @@ export function Profile({
linkToProfile?: boolean;
avatarSize?: number;
gap?: number;
profile?: CachedMetadata;
}) {
const { inView, ref } = useInView({ triggerOnce: true });
const pLoaded = useUserProfile(inView ? pubkey : undefined);
const pLoaded = useUserProfile(inView && !profile ? pubkey : undefined) ?? profile;
const showAvatar = options?.showAvatar ?? true;
const showName = options?.showName ?? true;
const isAnon = pubkey === "anon";

View File

@ -41,7 +41,7 @@ export function AddForwardInputs({
};
const ingestsEurope = urls.ingests.filter(
a => a.name.toLowerCase().startsWith("europe:") && a.availability === 1
a => a.name.toLowerCase().startsWith("europe:") && a.availability === 1,
);
const random = ingestsEurope.at(ingestsEurope.length * Math.random());
return unwrap(random).url_template.replace("{stream_key}", target);
@ -71,7 +71,7 @@ export function AddForwardInputs({
formatMessage({
defaultMessage: "Stream url must start with rtmp://",
id: "7+bCC1",
})
}),
);
return;
}
@ -90,7 +90,7 @@ export function AddForwardInputs({
formatMessage({
defaultMessage: "Not a valid URL",
id: "1q4BO/",
})
}),
);
return;
}
@ -100,7 +100,7 @@ export function AddForwardInputs({
formatMessage({
defaultMessage: "Stream Key is required",
id: "50+/JW",
})
}),
);
return;
}
@ -110,7 +110,7 @@ export function AddForwardInputs({
formatMessage({
defaultMessage: "Name is required",
id: "Gvxoji",
})
}),
);
return;
}
@ -122,7 +122,7 @@ export function AddForwardInputs({
formatMessage({
defaultMessage: "Could not create stream URL",
id: "E9APoR",
})
}),
);
await provider.addForward(name, t);
} catch (e) {

View File

@ -191,11 +191,7 @@ export default function NostrProviderDialog({
<p className="pb-2">
<FormattedMessage defaultMessage="Features" id="ZXp0z1" />
</p>
<div className="flex gap-2">
{ep?.capabilities?.map(a => (
<Pill>{parseCapability(a)}</Pill>
))}
</div>
<div className="flex gap-2">{ep?.capabilities?.map(a => <Pill>{parseCapability(a)}</Pill>)}</div>
</div>
</>
);

View File

@ -13,7 +13,7 @@ import { useLogin } from "@/hooks/login";
import Copy from "./copy";
import { defaultRelays } from "@/const";
import { useRates } from "@/hooks/rates";
import { DefaultButton } from "./buttons";
import { DefaultButton, PrimaryButton } from "./buttons";
import Modal from "./modal";
import Pill from "./pill";
import { useUserProfile } from "@snort/system-react";
@ -212,12 +212,10 @@ export function SendZapsDialog(props: Omit<SendZapsProps, "onFinish">) {
{props.button ? (
<div onClick={() => setOpen(true)}>{props.button}</div>
) : (
<DefaultButton onClick={() => setOpen(true)}>
<span className="max-xl:hidden">
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
</span>
<PrimaryButton onClick={() => setOpen(true)}>
<Icon name="zap-filled" size={16} />
</DefaultButton>
<FormattedMessage defaultMessage="Zap" />
</PrimaryButton>
)}
{open && (
<Modal id="send-zaps" onClose={() => setOpen(false)}>

View File

@ -5,7 +5,7 @@ import { useContext, useState } from "react";
import { SnortContext } from "@snort/system-react";
import { Icon } from "./icon";
import { Textarea } from "./textarea";
import { Textarea } from "./chat/textarea";
import { getHost } from "@/utils";
import { useLogin } from "@/hooks/login";
import { DefaultButton } from "./buttons";
@ -27,7 +27,7 @@ export function ShareMenu({ ev }: { ev: NostrEvent }) {
},
{
link: `https://${window.location.host}/${NostrLink.fromEvent(ev).encode()}`,
}
},
);
const defaultHostMsg = formatMessage(
{
@ -37,7 +37,7 @@ export function ShareMenu({ ev }: { ev: NostrEvent }) {
{
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);
@ -61,6 +61,7 @@ export function ShareMenu({ ev }: { ev: NostrEvent }) {
menuClassName="ctx-menu"
menuButton={
<DefaultButton>
<Icon name="share" />
<FormattedMessage defaultMessage="Share" />
</DefaultButton>
}>
@ -75,7 +76,7 @@ export function ShareMenu({ ev }: { ev: NostrEvent }) {
onClick={() => {
window.open(
`https://twitter.com/intent/tweet?text=${encodeURIComponent(message)}&via=zap_stream`,
"_blank"
"_blank",
);
}}>
<Icon name="twitter" size={24} />

View File

@ -13,7 +13,7 @@ export function StatePill({ state, ...props }: StatePillProps) {
className={classNames(
"uppercase font-white",
state === StreamState.Live ? "bg-primary" : "bg-layer-1",
props.className
props.className,
)}>
{state}
</Pill>

View File

@ -42,7 +42,7 @@ export function Card({ canEdit, ev, cards }: CardProps) {
};
},
}),
[canEdit, identifier]
[canEdit, identifier],
);
function findTagByIdentifier(d: string) {
@ -93,7 +93,7 @@ export function Card({ canEdit, ev, cards }: CardProps) {
}
},
}),
[canEdit, tags, identifier]
[canEdit, tags, identifier],
);
const card = (

View File

@ -33,5 +33,5 @@ export const CardPreview = forwardRef<HTMLDivElement, CardPreviewProps>(
)}
</div>
);
}
},
);

View File

@ -7,11 +7,11 @@ import { NostrLink, TaggedNostrEvent } from "@snort/system";
import { LIVE_STREAM_CLIP, StreamState } from "@/const";
import { extractStreamInfo } from "@/utils";
import { Icon } from "./icon";
import { Icon } from "../icon";
import { unwrap } from "@snort/shared";
import { TimelineBar } from "./timeline";
import { DefaultButton } from "./buttons";
import Modal from "./modal";
import { TimelineBar } from "../timeline";
import { DefaultButton } from "../buttons";
import Modal from "../modal";
export function ClipButton({ ev }: { ev: TaggedNostrEvent }) {
const system = useContext(SnortContext);
@ -81,16 +81,14 @@ export function ClipButton({ ev }: { ev: TaggedNostrEvent }) {
return (
<>
<DefaultButton onClick={makeClip}>
<Icon name="clapperboard" />
<span className="max-lg:hidden">
<FormattedMessage defaultMessage="Create Clip" id="PA0ej4" />
</span>
<Icon name="scissor" />
<FormattedMessage defaultMessage="Clip" />
</DefaultButton>
{open && (
<Modal id="create-clip" onClose={() => setOpen(false)}>
<div className="flex flex-col">
<h1>
<FormattedMessage defaultMessage="Create Clip" id="PA0ej4" />
<FormattedMessage defaultMessage="Clip" />
</h1>
{id && tempClipId && <video ref={ref} src={provider.getTempClipUrl(id, tempClipId)} controls muted />}
<TimelineBar

View File

@ -3,7 +3,7 @@ import { NostrEvent } from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { FormattedMessage } from "react-intl";
import { Link } from "react-router-dom";
import { getName } from "./profile";
import { getName } from "../profile";
export function ClipTile({ ev }: { ev: NostrEvent }) {
const profile = useUserProfile(ev.pubkey);

View File

@ -1,9 +1,9 @@
import { NostrLink, TaggedNostrEvent } from "@snort/system";
import { Profile } from "./profile";
import { Profile } from "../profile";
import { FormattedMessage } from "react-intl";
import { extractStreamInfo, findTag } from "@/utils";
import { useEventFeed } from "@snort/system-react";
import EventReactions from "./event-reactions";
import EventReactions from "../event-reactions";
import { Link } from "react-router-dom";
export default function LiveStreamClip({ ev }: { ev: TaggedNostrEvent }) {

View File

@ -2,8 +2,8 @@
import Hls from "hls.js";
import { HTMLProps, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { FormattedMessage } from "react-intl";
import { Icon } from "./icon";
import { ProgressBar } from "./progress-bar";
import { Icon } from "../icon";
import { ProgressBar } from "../progress-bar";
import { Menu, MenuItem } from "@szhsin/react-menu";
import { StreamState } from "@/const";
import classNames from "classnames";
@ -140,7 +140,7 @@ export default function LiveVideoPlayer({
}, [video, volume, muted]);
const { isPictureInPictureActive, isPictureInPictureAvailable, togglePictureInPicture } = usePictureInPicture(
video as VideoRefType
video as VideoRefType,
);
const handlePIPClick = useCallback(async () => {
@ -222,7 +222,7 @@ export default function LiveVideoPlayer({
/>
)}
</div>
<div className="flex gap-1 items-center h-full py-2">
<div className="flex gap-1 items-center h-full py-2 max-sm:hidden">
<Icon name={muted ? "volume-muted" : "volume"} onClick={toggleMute} />
<ProgressBar value={volume} setValue={v => setVolume(v)} style={{ width: "100px", height: "100%" }} />
</div>
@ -244,7 +244,9 @@ export default function LiveVideoPlayer({
</Menu>
</div>
{isPictureInPictureAvailable && (
<div className="pl-3 py-2 cursor-pointer tracking-wide font-bold text-sm" onClick={handlePIPClick}>
<div
className="pl-3 py-2 cursor-pointer tracking-wide font-bold text-sm max-xl:hidden"
onClick={handlePIPClick}>
PIP
</div>
)}

View File

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

View File

@ -7,19 +7,21 @@ import { SnortContext, useUserProfile } from "@snort/system-react";
import { useContext, useState } from "react";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import { WarningButton } from "./buttons";
import { WarningButton } from "../buttons";
import { ClipButton } from "./clip-button";
import { FollowButton } from "./follow-button";
import GameInfoCard from "./game-info";
import { NewStreamDialog } from "./new-stream";
import { FollowButton } from "../follow-button";
import GameInfoCard from "../game-info";
import { NewStreamDialog } from "../new-stream";
import { NotificationsButton } from "./notifications-button";
import Pill from "./pill";
import { Profile, getName } from "./profile";
import { SendZapsDialog } from "./send-zap";
import { ShareMenu } from "./share-menu";
import { StatePill } from "./state-pill";
import Pill from "../pill";
import { Profile, getName } from "../profile";
import { SendZapsDialog } from "../send-zap";
import { ShareMenu } from "../share-menu";
import { StatePill } from "../state-pill";
import { StreamTimer } from "./stream-time";
import { Tags } from "./tags";
import { Tags } from "../tags";
import { useStream } from "./stream-state";
import { StreamSummary } from "./summary";
export function StreamInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedNostrEvent }) {
const system = useContext(SnortContext);
@ -28,6 +30,7 @@ export function StreamInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedN
const host = getHost(ev);
const profile = useUserProfile(host);
const zapTarget = profile?.lud16 ?? profile?.lud06;
const streamContext = useStream();
const { status, participants, title, summary, service, gameId, gameInfo } = extractStreamInfo(ev);
const isMine = ev?.pubkey === login?.pubkey;
@ -42,18 +45,41 @@ export function StreamInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedN
}
}
if (!streamContext.showDetails) return;
const viewers = Number(participants ?? "0");
return (
<>
<div className="flex gap-2 max-xl:flex-col max-xl:px-2">
<div className="grow flex flex-col gap-2 max-xl:hidden">
<h1>{title}</h1>
{summary && <StreamSummary text={summary} />}
<div className="flex gap-2 flex-wrap">
<div className="flex gap-2 max-xl:flex-col">
<div className="grow flex flex-col gap-2">
<div className="text-3xl font-semibold">{title}</div>
<div className="flex max-xl:flex-col xl:justify-between max-xl:gap-2">
<div className="flex gap-4">
<Profile pubkey={host ?? ""} avatarSize={40} />
<FollowButton pubkey={host} hideWhenFollowing={true} />
</div>
<div className="flex gap-2">
{ev && (
<>
<ClipButton ev={ev} />
<ShareMenu ev={ev} />
{service && <NotificationsButton host={host} service={service} />}
{zapTarget && (
<SendZapsDialog
lnurl={zapTarget}
pubkey={host}
aTag={`${ev.kind}:${ev.pubkey}:${findTag(ev, "d")}`}
eTag={goal?.id}
targetName={getName(ev.pubkey, profile)}
/>
)}