feat: style upgrades
This commit is contained in:
parent
6250456435
commit
21919e1e3b
@ -7,7 +7,7 @@ export const onRequest: PagesFunction<Env> = async context => {
|
|||||||
|
|
||||||
const prefixes = ["npub1", "nprofile1", "naddr1", "nevent1", "note1"];
|
const prefixes = ["npub1", "nprofile1", "naddr1", "nevent1", "note1"];
|
||||||
const isEntityPath = prefixes.some(
|
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 nostrAddress = u.pathname.match(/^\/([a-zA-Z0-9_]+)$/i);
|
||||||
const next = await context.next();
|
const next = await context.next();
|
||||||
@ -19,7 +19,7 @@ export const onRequest: PagesFunction<Env> = async context => {
|
|||||||
id = `${id}@${HOST}`;
|
id = `${id}@${HOST}`;
|
||||||
}
|
}
|
||||||
const fetchApi = `https://nostr.api.v0l.io/api/v1/opengraph/${id}?canonical=${encodeURIComponent(
|
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);
|
console.log("Fetching tags from: ", fetchApi);
|
||||||
const reqBuf = await next.arrayBuffer();
|
const reqBuf = await next.arrayBuffer();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
|
52
package.json
52
package.json
@ -4,8 +4,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emoji-mart/data": "^1.2.1",
|
"@emoji-mart/data": "^1.2.1",
|
||||||
"@emoji-mart/react": "^1.1.1",
|
"@emoji-mart/react": "^1.1.1",
|
||||||
"@noble/curves": "^1.2.0",
|
"@noble/curves": "^1.4.0",
|
||||||
"@scure/base": "^1.1.3",
|
"@scure/base": "^1.1.6",
|
||||||
"@snort/shared": "^1.0.15",
|
"@snort/shared": "^1.0.15",
|
||||||
"@snort/system": "^1.3.2",
|
"@snort/system": "^1.3.2",
|
||||||
"@snort/system-react": "^1.3.2",
|
"@snort/system-react": "^1.3.2",
|
||||||
@ -13,14 +13,14 @@
|
|||||||
"@snort/wallet": "^0.1.3",
|
"@snort/wallet": "^0.1.3",
|
||||||
"@snort/worker-relay": "^1.0.10",
|
"@snort/worker-relay": "^1.0.10",
|
||||||
"@sqlite.org/sqlite-wasm": "^3.45.1-build1",
|
"@sqlite.org/sqlite-wasm": "^3.45.1-build1",
|
||||||
"@szhsin/react-menu": "^4.0.2",
|
"@szhsin/react-menu": "^4.1.0",
|
||||||
"@types/webscopeio__react-textarea-autocomplete": "^4.7.2",
|
"@types/webscopeio__react-textarea-autocomplete": "^4.7.5",
|
||||||
"@void-cat/api": "^1.0.12",
|
"@void-cat/api": "^1.0.12",
|
||||||
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
|
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
|
||||||
"buffer": "^6.0.3",
|
"buffer": "^6.0.3",
|
||||||
"classnames": "^2.3.2",
|
"classnames": "^2.5.1",
|
||||||
"emoji-mart": "^5.5.2",
|
"emoji-mart": "^5.6.0",
|
||||||
"flag-icons": "^6.11.0",
|
"flag-icons": "^7.2.1",
|
||||||
"hls.js": "^1.5.8",
|
"hls.js": "^1.5.8",
|
||||||
"marked": "^12.0.2",
|
"marked": "^12.0.2",
|
||||||
"qr-code-styling": "^1.6.0-rc.1",
|
"qr-code-styling": "^1.6.0-rc.1",
|
||||||
@ -31,15 +31,17 @@
|
|||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-helmet": "^6.1.0",
|
"react-helmet": "^6.1.0",
|
||||||
"react-intersection-observer": "^9.10.2",
|
"react-intersection-observer": "^9.10.2",
|
||||||
"react-intl": "^6.6.6",
|
"react-intl": "^6.6.8",
|
||||||
"react-router-dom": "^6.23.1",
|
"react-router-dom": "^6.23.1",
|
||||||
"react-tag-input-component": "^2.0.2",
|
"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",
|
"react-use-pip": "^1.5.0",
|
||||||
"recharts": "^2.12.7",
|
"recharts": "^2.12.7",
|
||||||
"semantic-sdp": "^3.26.3",
|
"semantic-sdp": "^3.27.1",
|
||||||
"usehooks-ts": "^3.1.0",
|
"usehooks-ts": "^3.1.0",
|
||||||
"web-vitals": "^2.1.0",
|
"web-vitals": "^4.0.0",
|
||||||
"webrtc-adapter": "^8.2.3",
|
"webrtc-adapter": "^9.0.1",
|
||||||
"workbox-core": "^7.1.0",
|
"workbox-core": "^7.1.0",
|
||||||
"workbox-precaching": "^7.1.0",
|
"workbox-precaching": "^7.1.0",
|
||||||
"workbox-routing": "^7.1.0",
|
"workbox-routing": "^7.1.0",
|
||||||
@ -75,21 +77,21 @@
|
|||||||
"@cloudflare/workers-types": "^4.20231218.0",
|
"@cloudflare/workers-types": "^4.20231218.0",
|
||||||
"@formatjs/cli": "^6.1.3",
|
"@formatjs/cli": "^6.1.3",
|
||||||
"@testing-library/dom": "^9.3.1",
|
"@testing-library/dom": "^9.3.1",
|
||||||
"@types/node": "^20.10.3",
|
"@types/node": "^20.12.12",
|
||||||
"@types/react": "^18.2.21",
|
"@types/react": "^18.3.2",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.3.0",
|
||||||
"@types/react-helmet": "^6.1.6",
|
"@types/react-helmet": "^6.1.11",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
"@typescript-eslint/eslint-plugin": "^7.9.0",
|
||||||
"@typescript-eslint/parser": "^6.4.1",
|
"@typescript-eslint/parser": "^7.9.0",
|
||||||
"@vitejs/plugin-react": "^4.2.0",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"@webbtc/webln-types": "^1.0.12",
|
"@webbtc/webln-types": "^3.0.0",
|
||||||
"autoprefixer": "^10.4.16",
|
"autoprefixer": "^10.4.19",
|
||||||
"babel-plugin-formatjs": "^10.5.13",
|
"babel-plugin-formatjs": "^10.5.16",
|
||||||
"eslint": "^8.48.0",
|
"eslint": "^8.56.0",
|
||||||
"postcss": "^8.4.32",
|
"postcss": "^8.4.38",
|
||||||
"prettier": "^2.8.8",
|
"prettier": "^3.2.5",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"rollup-plugin-visualizer": "^5.10.0",
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
"tailwindcss": "^3.4.3",
|
"tailwindcss": "^3.4.3",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"vite": "^5.2.11",
|
"vite": "^5.2.11",
|
||||||
|
@ -8,7 +8,7 @@ export default function AmountInput({ onChange }: { onChange: (n: number) => voi
|
|||||||
|
|
||||||
const satsValue = useCallback(
|
const satsValue = useCallback(
|
||||||
() => (type === "usd" ? Math.round(value * 1e-6 * rates.ask) / 100 : value),
|
() => (type === "usd" ? Math.round(value * 1e-6 * rates.ask) / 100 : value),
|
||||||
[value, type]
|
[value, type],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -3,62 +3,36 @@ import AsyncButton, { AsyncButtonProps } from "./async-button";
|
|||||||
import { Icon } from "./icon";
|
import { Icon } from "./icon";
|
||||||
import classNames from "classnames";
|
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) => {
|
export const DefaultButton = forwardRef<HTMLButtonElement, AsyncButtonProps>((props: AsyncButtonProps, ref) => {
|
||||||
return (
|
return (
|
||||||
<AsyncButton
|
<AsyncButton
|
||||||
{...props}
|
{...props}
|
||||||
className={classNames(
|
className={classNames(props.className, buttonBaseClass, "bg-neutral-800 text-white")}
|
||||||
props.className,
|
|
||||||
"px-3 py-2 font-semibold rounded-xl bg-white text-black disabled:opacity-20"
|
|
||||||
)}
|
|
||||||
ref={ref}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
export const PrimaryButton = forwardRef<HTMLButtonElement, AsyncButtonProps>((props: AsyncButtonProps, ref) => {
|
export const PrimaryButton = forwardRef<HTMLButtonElement, AsyncButtonProps>((props: AsyncButtonProps, ref) => {
|
||||||
return (
|
return <AsyncButton {...props} className={classNames(props.className, buttonBaseClass, "bg-primary")} ref={ref} />;
|
||||||
<AsyncButton
|
|
||||||
{...props}
|
|
||||||
className={classNames(props.className, "px-3 py-2 font-semibold rounded-xl bg-primary disabled:opacity-20")}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
export const Layer1Button = forwardRef<HTMLButtonElement, AsyncButtonProps>((props: AsyncButtonProps, ref) => {
|
export const Layer1Button = forwardRef<HTMLButtonElement, AsyncButtonProps>((props: AsyncButtonProps, ref) => {
|
||||||
return (
|
return <AsyncButton {...props} className={classNames(props.className, buttonBaseClass, "bg-layer-1")} ref={ref} />;
|
||||||
<AsyncButton
|
|
||||||
{...props}
|
|
||||||
className={classNames(props.className, "px-3 py-2 font-semibold rounded-xl bg-layer-1 disabled:opacity-20")}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
export const Layer2Button = forwardRef<HTMLButtonElement, AsyncButtonProps>((props: AsyncButtonProps, ref) => {
|
export const Layer2Button = forwardRef<HTMLButtonElement, AsyncButtonProps>((props: AsyncButtonProps, ref) => {
|
||||||
return (
|
return <AsyncButton {...props} className={classNames(props.className, buttonBaseClass, "bg-layer-2")} ref={ref} />;
|
||||||
<AsyncButton
|
|
||||||
{...props}
|
|
||||||
className={classNames(props.className, "px-3 py-2 font-semibold rounded-xl bg-layer-2 disabled:opacity-20")}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
export const Layer3Button = forwardRef<HTMLButtonElement, AsyncButtonProps>((props: AsyncButtonProps, ref) => {
|
export const Layer3Button = forwardRef<HTMLButtonElement, AsyncButtonProps>((props: AsyncButtonProps, ref) => {
|
||||||
return (
|
return <AsyncButton {...props} className={classNames(props.className, buttonBaseClass, "bg-layer-3")} ref={ref} />;
|
||||||
<AsyncButton
|
|
||||||
{...props}
|
|
||||||
className={classNames(props.className, "px-3 py-2 font-semibold rounded-xl bg-layer-3 disabled:opacity-20")}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
export const WarningButton = forwardRef<HTMLButtonElement, AsyncButtonProps>((props: AsyncButtonProps, ref) => {
|
export const WarningButton = forwardRef<HTMLButtonElement, AsyncButtonProps>((props: AsyncButtonProps, ref) => {
|
||||||
return (
|
return <AsyncButton {...props} className={classNames(props.className, buttonBaseClass, "bg-warning")} ref={ref} />;
|
||||||
<AsyncButton
|
|
||||||
{...props}
|
|
||||||
className={classNames(props.className, "px-3 py-2 font-semibold rounded-xl bg-warning disabled:opacity-20")}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
export const IconButton = forwardRef<HTMLButtonElement, { iconName: string; iconSize?: number } & AsyncButtonProps>(
|
export const IconButton = forwardRef<HTMLButtonElement, { iconName: string; iconSize?: number } & AsyncButtonProps>(
|
||||||
({ iconName, iconSize, ...props }: { iconName: string; iconSize?: number } & AsyncButtonProps, ref) => {
|
({ 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} />
|
<Icon name={iconName} size={iconSize} />
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
export const BorderButton = forwardRef<HTMLButtonElement, AsyncButtonProps>((props: AsyncButtonProps, ref) => {
|
export const BorderButton = forwardRef<HTMLButtonElement, AsyncButtonProps>((props: AsyncButtonProps, ref) => {
|
||||||
return (
|
return <AsyncButton {...props} className={classNames(props.className, buttonBaseClass, "btn-border")} ref={ref} />;
|
||||||
<AsyncButton
|
|
||||||
{...props}
|
|
||||||
className={classNames(props.className, "px-3 py-2 font-semibold rounded-xl btn-border disabled:opacity-20")}
|
|
||||||
ref={ref}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
@ -20,7 +20,7 @@ export default function CategoryLink({
|
|||||||
key={id}
|
key={id}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"text-lg font-semibold rounded-xl border border-layer-2 border-2 hover:bg-layer-2",
|
"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">
|
<div className="flex items-center gap-2 px-2 py-1 whitespace-nowrap">
|
||||||
<Icon name={icon} size={24} />
|
<Icon name={icon} size={24} />
|
||||||
|
@ -30,13 +30,7 @@ export function CategoryTile({
|
|||||||
{showDetail && (
|
{showDetail && (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<h1>{game?.name}</h1>
|
<h1>{game?.name}</h1>
|
||||||
{game?.genres && (
|
{game?.genres && <div className="flex gap-2">{game?.genres?.map(a => <Pill>{a}</Pill>)}</div>}
|
||||||
<div className="flex gap-2">
|
|
||||||
{game?.genres?.map(a => (
|
|
||||||
<Pill>{a}</Pill>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{extraDetail}
|
{extraDetail}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -4,20 +4,20 @@ import React, { Suspense, lazy, useContext, useMemo, useRef, useState } from "re
|
|||||||
import { useHover, useIntersectionObserver, useOnClickOutside } from "usehooks-ts";
|
import { useHover, useIntersectionObserver, useOnClickOutside } from "usehooks-ts";
|
||||||
import { dedupe } from "@snort/shared";
|
import { dedupe } from "@snort/shared";
|
||||||
|
|
||||||
const EmojiPicker = lazy(() => import("./emoji-picker"));
|
const EmojiPicker = lazy(() => import("../emoji-picker"));
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "../icon";
|
||||||
import { Emoji as EmojiComponent } from "./emoji";
|
import { Emoji as EmojiComponent } from "../emoji";
|
||||||
import { Profile } from "./profile";
|
import { Profile } from "../profile";
|
||||||
import { Text } from "./text";
|
import { Text } from "../text";
|
||||||
import { useMute } from "./mute-button";
|
import { useMute } from "../mute-button";
|
||||||
import { SendZapsDialog } from "./send-zap";
|
import { SendZapsDialog } from "../send-zap";
|
||||||
import { CollapsibleEvent } from "./collapsible";
|
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 { IconButton } from "./buttons";
|
import { IconButton } from "../buttons";
|
||||||
import Pill from "./pill";
|
import Pill from "../pill";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
function emojifyReaction(reaction: string) {
|
function emojifyReaction(reaction: string) {
|
@ -5,13 +5,13 @@ import { useEventFeed, useEventReactions, useReactions, useUserProfile } from "@
|
|||||||
import { unixNow, unwrap } from "@snort/shared";
|
import { unixNow, unwrap } from "@snort/shared";
|
||||||
import { useEffect, useMemo } from "react";
|
import { useEffect, useMemo } from "react";
|
||||||
|
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "../icon";
|
||||||
import Spinner from "./spinner";
|
import Spinner from "../spinner";
|
||||||
import { Text } from "./text";
|
import { Text } from "../text";
|
||||||
import { Profile } from "./profile";
|
import { Profile } from "../profile";
|
||||||
import { ChatMessage } from "./chat-message";
|
import { ChatMessage } from "./chat-message";
|
||||||
import { Goal } from "./goal";
|
import { Goal } from "../goal";
|
||||||
import { Badge } from "./badge";
|
import { Badge } from "../badge";
|
||||||
import { WriteMessage } from "./write-message";
|
import { WriteMessage } from "./write-message";
|
||||||
import useEmoji, { packId } from "@/hooks/emoji";
|
import useEmoji, { packId } from "@/hooks/emoji";
|
||||||
import { useMutedPubkeys } from "@/hooks/lists";
|
import { useMutedPubkeys } from "@/hooks/lists";
|
||||||
@ -20,9 +20,11 @@ import { useLogin } from "@/hooks/login";
|
|||||||
import { formatSats } from "@/number";
|
import { formatSats } from "@/number";
|
||||||
import { LIVE_STREAM_CHAT, LIVE_STREAM_CLIP, LIVE_STREAM_RAID, WEEK } from "@/const";
|
import { LIVE_STREAM_CHAT, LIVE_STREAM_CLIP, LIVE_STREAM_RAID, WEEK } from "@/const";
|
||||||
import { findTag, getHost, getTagValues, uniqBy } from "@/utils";
|
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 { Link, useNavigate } from "react-router-dom";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
import { useStream } from "../stream/stream-state";
|
||||||
|
import { useLayout } from "@/pages/layout/context";
|
||||||
|
|
||||||
function BadgeAward({ ev }: { ev: NostrEvent }) {
|
function BadgeAward({ ev }: { ev: NostrEvent }) {
|
||||||
const badge = findTag(ev, "a") ?? "";
|
const badge = findTag(ev, "a") ?? "";
|
||||||
@ -48,6 +50,7 @@ export function LiveChat({
|
|||||||
goal,
|
goal,
|
||||||
canWrite,
|
canWrite,
|
||||||
showTopZappers,
|
showTopZappers,
|
||||||
|
adjustLayout,
|
||||||
showGoal,
|
showGoal,
|
||||||
showScrollbar,
|
showScrollbar,
|
||||||
height,
|
height,
|
||||||
@ -59,6 +62,7 @@ export function LiveChat({
|
|||||||
goal?: NostrEvent;
|
goal?: NostrEvent;
|
||||||
canWrite?: boolean;
|
canWrite?: boolean;
|
||||||
showTopZappers?: boolean;
|
showTopZappers?: boolean;
|
||||||
|
adjustLayout?: boolean;
|
||||||
showGoal?: boolean;
|
showGoal?: boolean;
|
||||||
showScrollbar?: boolean;
|
showScrollbar?: boolean;
|
||||||
height?: number;
|
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);
|
rb.withFilter().kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP]).tag("a", [aTag]).limit(200);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
true
|
true,
|
||||||
);
|
);
|
||||||
const login = useLogin();
|
const login = useLogin();
|
||||||
const started = useMemo(() => {
|
const started = useMemo(() => {
|
||||||
@ -92,6 +96,8 @@ export function LiveChat({
|
|||||||
const allEmojiPacks = useMemo(() => {
|
const allEmojiPacks = useMemo(() => {
|
||||||
return uniqBy(userEmojiPacks.concat(channelEmojiPacks), packId);
|
return uniqBy(userEmojiPacks.concat(channelEmojiPacks), packId);
|
||||||
}, [userEmojiPacks, channelEmojiPacks]);
|
}, [userEmojiPacks, channelEmojiPacks]);
|
||||||
|
const streamContext = useStream();
|
||||||
|
const layoutContext = useLayout();
|
||||||
|
|
||||||
const reactions = useEventReactions(link, feed);
|
const reactions = useEventReactions(link, feed);
|
||||||
const events = useMemo(() => {
|
const events = useMemo(() => {
|
||||||
@ -109,6 +115,35 @@ export function LiveChat({
|
|||||||
.sort((a, b) => b.created_at - a.created_at);
|
.sort((a, b) => b.created_at - a.created_at);
|
||||||
}, [feed, awards]);
|
}, [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(() => {
|
const filteredEvents = useMemo(() => {
|
||||||
return events.filter(e => !mutedPubkeys.has(e.pubkey) && !hostMutedPubkeys.has(e.pubkey));
|
return events.filter(e => !mutedPubkeys.has(e.pubkey) && !hostMutedPubkeys.has(e.pubkey));
|
||||||
}, [events, mutedPubkeys, hostMutedPubkeys]);
|
}, [events, mutedPubkeys, hostMutedPubkeys]);
|
||||||
@ -126,7 +161,32 @@ export function LiveChat({
|
|||||||
<div
|
<div
|
||||||
className={classNames("flex flex-col-reverse grow gap-2 overflow-y-auto", {
|
className={classNames("flex flex-col-reverse grow gap-2 overflow-y-auto", {
|
||||||
"scrollbar-hidden": !(showScrollbar ?? true),
|
"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 => {
|
{filteredEvents.map(a => {
|
||||||
switch (a.kind) {
|
switch (a.kind) {
|
||||||
case -1:
|
case -1:
|
||||||
@ -165,9 +225,10 @@ export function LiveChat({
|
|||||||
return null;
|
return null;
|
||||||
})}
|
})}
|
||||||
{feed.length === 0 && <Spinner />}
|
{feed.length === 0 && <Spinner />}
|
||||||
|
<div className="pt-[50dvh]"></div>
|
||||||
</div>
|
</div>
|
||||||
{(canWrite ?? true) && (
|
{(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 ? (
|
{login ? (
|
||||||
<WriteMessage emojiPacks={allEmojiPacks} link={link} />
|
<WriteMessage emojiPacks={allEmojiPacks} link={link} />
|
||||||
) : (
|
) : (
|
@ -1,7 +1,9 @@
|
|||||||
.rta__textarea {
|
.rta {
|
||||||
resize: none;
|
display: flex;
|
||||||
|
}
|
||||||
|
.rta__textarea {
|
||||||
|
resize: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rta__list {
|
.rta__list {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
@ -1,14 +1,14 @@
|
|||||||
import "./textarea.css";
|
import { useContext, useEffect, useState } from "react";
|
||||||
import { useContext } from "react";
|
|
||||||
import ReactTextareaAutocomplete, { TriggerType } from "@webscopeio/react-textarea-autocomplete";
|
import ReactTextareaAutocomplete, { TriggerType } from "@webscopeio/react-textarea-autocomplete";
|
||||||
import "@webscopeio/react-textarea-autocomplete/style.css";
|
import "@webscopeio/react-textarea-autocomplete/style.css";
|
||||||
|
import "./textarea.css";
|
||||||
|
|
||||||
import { hexToBech32 } from "@snort/shared";
|
import { hexToBech32 } from "@snort/shared";
|
||||||
import { SnortContext } from "@snort/system-react";
|
import { SnortContext } from "@snort/system-react";
|
||||||
import { CachedMetadata, NostrPrefix, UserProfileCache } from "@snort/system";
|
import { CachedMetadata, NostrPrefix } from "@snort/system";
|
||||||
|
|
||||||
import { Emoji } from "./emoji";
|
import { Emoji } from "../emoji";
|
||||||
import { Avatar } from "./avatar";
|
import { Avatar } from "../avatar";
|
||||||
import type { EmojiTag } from "@/types";
|
import type { EmojiTag } from "@/types";
|
||||||
|
|
||||||
interface EmojiItemProps {
|
interface EmojiItemProps {
|
||||||
@ -41,13 +41,19 @@ type TextareaProps = { emojis: EmojiTag[] } & React.TextareaHTMLAttributes<HTMLT
|
|||||||
|
|
||||||
export function Textarea({ emojis, ...props }: TextareaProps) {
|
export function Textarea({ emojis, ...props }: TextareaProps) {
|
||||||
const system = useContext(SnortContext);
|
const system = useContext(SnortContext);
|
||||||
|
const [ref, setRef] = useState<HTMLTextAreaElement | null>(null);
|
||||||
const userDataProvider = async (token: string) => {
|
const userDataProvider = async (token: string) => {
|
||||||
const cache = system.profileLoader.cache;
|
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 emojiDataProvider = (token: string) => {
|
||||||
const results = emojis
|
const results = emojis
|
||||||
.map(t => {
|
.map(t => {
|
||||||
@ -82,6 +88,7 @@ export function Textarea({ emojis, ...props }: TextareaProps) {
|
|||||||
autoFocus={false}
|
autoFocus={false}
|
||||||
trigger={trigger}
|
trigger={trigger}
|
||||||
{...props}
|
{...props}
|
||||||
|
innerRef={r => setRef(r)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -1,19 +1,26 @@
|
|||||||
import { EventKind, NostrLink } from "@snort/system";
|
import { EventKind, NostrLink } from "@snort/system";
|
||||||
import React, { Suspense, lazy, useContext, useRef, useState } from "react";
|
import React, { Suspense, lazy, useContext, useRef, useState } from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
|
||||||
import { SnortContext } from "@snort/system-react";
|
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 { useLogin } from "@/hooks/login";
|
||||||
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 "@/time-sync";
|
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 system = useContext(SnortContext);
|
||||||
const ref = useRef<HTMLDivElement | null>(null);
|
const ref = useRef<HTMLDivElement | null>(null);
|
||||||
const emojiRef = useRef(null);
|
const emojiRef = useRef(null);
|
||||||
@ -39,10 +46,10 @@ export function WriteMessage({ link, emojiPacks }: { link: NostrLink; emojiPacks
|
|||||||
|
|
||||||
const reply = await pub?.generic(eb => {
|
const reply = await pub?.generic(eb => {
|
||||||
const emoji = [...emojiNames].map(name => emojis.find(e => e.at(1) === name));
|
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)
|
.content(chat)
|
||||||
.createdAt(Math.floor((unixNowMs() - TimeSync) / 1000))
|
.createdAt(Math.floor((unixNowMs() - TimeSync) / 1000))
|
||||||
.tag(["a", `${link.kind}:${link.author}:${link.id}`, "", "root"])
|
.tag(unwrap(link.toEventTag("root")))
|
||||||
.processContent();
|
.processContent();
|
||||||
for (const e of emoji) {
|
for (const e of emoji) {
|
||||||
if (e) {
|
if (e) {
|
||||||
@ -82,11 +89,21 @@ export function WriteMessage({ link, emojiPacks }: { link: NostrLink; emojiPacks
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="grow flex bg-layer-2 rounded-xl items-center" ref={ref}>
|
<div className="grow flex bg-layer-2 px-3 py-2 rounded-xl items-center" ref={ref}>
|
||||||
<Textarea emojis={emojis} value={chat} onKeyDown={onKeyDown} onChange={e => setChat(e.target.value)} rows={2} />
|
<Textarea
|
||||||
<div onClick={pickEmoji} className="p-2">
|
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" />
|
<Icon name="face" />
|
||||||
</div>
|
</AsyncButton>
|
||||||
|
<AsyncButton onClick={sendChatMessage} className="px-3 opacity-80">
|
||||||
|
<Icon name="send" />
|
||||||
|
</AsyncButton>
|
||||||
{showEmojiPicker && (
|
{showEmojiPicker && (
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<EmojiPicker
|
<EmojiPicker
|
||||||
@ -100,9 +117,6 @@ export function WriteMessage({ link, emojiPacks }: { link: NostrLink; emojiPacks
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<BorderButton onClick={sendChatMessage}>
|
|
||||||
<FormattedMessage defaultMessage="Send" id="9WRlF4" />
|
|
||||||
</BorderButton>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -7,10 +7,10 @@ import { EmojiPack } from "./emoji-pack";
|
|||||||
import { Badge } from "./badge";
|
import { Badge } from "./badge";
|
||||||
import { EMOJI_PACK, GOAL, LIVE_STREAM_CLIP, StreamState } from "@/const";
|
import { EMOJI_PACK, GOAL, LIVE_STREAM_CLIP, StreamState } from "@/const";
|
||||||
import { useEventFeed } from "@snort/system-react";
|
import { useEventFeed } from "@snort/system-react";
|
||||||
import LiveStreamClip from "./clip";
|
import LiveStreamClip from "./stream/clip";
|
||||||
import { ExternalLink } from "./external-link";
|
import { ExternalLink } from "./external-link";
|
||||||
import { extractStreamInfo } from "@/utils";
|
import { extractStreamInfo } from "@/utils";
|
||||||
import LiveVideoPlayer from "./live-video-player";
|
import LiveVideoPlayer from "./stream/live-video-player";
|
||||||
|
|
||||||
interface EventProps {
|
interface EventProps {
|
||||||
link: NostrLink;
|
link: NostrLink;
|
||||||
|
@ -6,6 +6,7 @@ import { SnortContext } from "@snort/system-react";
|
|||||||
import { useLogin } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
import { Login } from "@/login";
|
import { Login } from "@/login";
|
||||||
import { DefaultButton } from "./buttons";
|
import { DefaultButton } from "./buttons";
|
||||||
|
import { Icon } from "./icon";
|
||||||
|
|
||||||
export function LoggedInFollowButton({
|
export function LoggedInFollowButton({
|
||||||
tag,
|
tag,
|
||||||
@ -62,9 +63,12 @@ export function LoggedInFollowButton({
|
|||||||
return (
|
return (
|
||||||
<DefaultButton disabled={timestamp ? timestamp === 0 : true} onClick={isFollowing ? unfollow : follow}>
|
<DefaultButton disabled={timestamp ? timestamp === 0 : true} onClick={isFollowing ? unfollow : follow}>
|
||||||
{isFollowing ? (
|
{isFollowing ? (
|
||||||
<FormattedMessage defaultMessage="Unfollow" id="izWS4J" />
|
<FormattedMessage defaultMessage="Unfollow" />
|
||||||
) : (
|
) : (
|
||||||
<FormattedMessage defaultMessage="Follow" id="ieGrWo" />
|
<>
|
||||||
|
<Icon name="plus" size={20} />
|
||||||
|
<FormattedMessage defaultMessage="Follow" />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</DefaultButton>
|
</DefaultButton>
|
||||||
);
|
);
|
||||||
|
@ -2,7 +2,7 @@ import { StreamState } from "@/const";
|
|||||||
import { extractStreamInfo } from "@/utils";
|
import { extractStreamInfo } from "@/utils";
|
||||||
import { TaggedNostrEvent } from "@snort/system";
|
import { TaggedNostrEvent } from "@snort/system";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import LiveVideoPlayer from "./live-video-player";
|
import LiveVideoPlayer from "./stream/live-video-player";
|
||||||
|
|
||||||
export default function LiveEvent({ ev }: { ev: TaggedNostrEvent }) {
|
export default function LiveEvent({ ev }: { ev: TaggedNostrEvent }) {
|
||||||
const { title, image, status, stream, recording } = extractStreamInfo(ev);
|
const { title, image, status, stream, recording } = extractStreamInfo(ev);
|
||||||
|
@ -137,7 +137,7 @@ export function LoginSignup({ close }: { close: () => void }) {
|
|||||||
formatMessage({
|
formatMessage({
|
||||||
defaultMessage: "Hmm, your lightning address looks wrong",
|
defaultMessage: "Hmm, your lightning address looks wrong",
|
||||||
id: "4l69eO",
|
id: "4l69eO",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,7 @@ export default function Modal(props: ModalProps) {
|
|||||||
return createPortal(
|
return createPortal(
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
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}
|
onMouseDown={handleBackdropClick}
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
@ -98,6 +98,6 @@ export default function Modal(props: ModalProps) {
|
|||||||
{props.children}
|
{props.children}
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
document.body
|
document.body,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,7 @@ import { NSFWStore } from "./store";
|
|||||||
export function useContentWarning() {
|
export function useContentWarning() {
|
||||||
const v = useSyncExternalStore(
|
const v = useSyncExternalStore(
|
||||||
c => NSFWStore.hook(c),
|
c => NSFWStore.hook(c),
|
||||||
() => NSFWStore.snapshot()
|
() => NSFWStore.snapshot(),
|
||||||
);
|
);
|
||||||
return v;
|
return v;
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ export default function Pill({ children, selected, className, ...props }: HTMLPr
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
className,
|
className,
|
||||||
{ "bg-layer-3 font-bold": selected },
|
{ "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}
|
{children}
|
||||||
</span>
|
</span>
|
||||||
|
@ -58,7 +58,7 @@ export function ProfileEditor({ onClose }: { onClose: () => void }) {
|
|||||||
setInvalidLud16Message(
|
setInvalidLud16Message(
|
||||||
formatMessage({
|
formatMessage({
|
||||||
defaultMessage: "Invalid lightning address",
|
defaultMessage: "Invalid lightning address",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -76,7 +76,7 @@ export function ProfileEditor({ onClose }: { onClose: () => void }) {
|
|||||||
setNip05AddressValid(true);
|
setNip05AddressValid(true);
|
||||||
} else {
|
} else {
|
||||||
setInvalidNip05AddressMessage(
|
setInvalidNip05AddressMessage(
|
||||||
formatMessage({ defaultMessage: "Nostr address does not belong to you", id: "01iNut" })
|
formatMessage({ defaultMessage: "Nostr address does not belong to you", id: "01iNut" }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -84,7 +84,7 @@ export function ProfileEditor({ onClose }: { onClose: () => void }) {
|
|||||||
setInvalidNip05AddressMessage(
|
setInvalidNip05AddressMessage(
|
||||||
formatMessage({
|
formatMessage({
|
||||||
defaultMessage: "Invalid nostr address",
|
defaultMessage: "Invalid nostr address",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@ -92,7 +92,7 @@ export function ProfileEditor({ onClose }: { onClose: () => void }) {
|
|||||||
setInvalidNip05AddressMessage(
|
setInvalidNip05AddressMessage(
|
||||||
formatMessage({
|
formatMessage({
|
||||||
defaultMessage: "Invalid nostr address",
|
defaultMessage: "Invalid nostr address",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -106,7 +106,7 @@ export function ProfileEditor({ onClose }: { onClose: () => void }) {
|
|||||||
setInvalidNip05AddressMessage(
|
setInvalidNip05AddressMessage(
|
||||||
formatMessage({
|
formatMessage({
|
||||||
defaultMessage: "Invalid nostr address",
|
defaultMessage: "Invalid nostr address",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
} else if (Nip05AddressElements.length === 2) {
|
} else if (Nip05AddressElements.length === 2) {
|
||||||
nip05NostrAddressVerification(Nip05AddressElements.pop(), Nip05AddressElements.pop());
|
nip05NostrAddressVerification(Nip05AddressElements.pop(), Nip05AddressElements.pop());
|
||||||
@ -192,7 +192,7 @@ export function ProfileEditor({ onClose }: { onClose: () => void }) {
|
|||||||
setInvalidUsernameMessage(
|
setInvalidUsernameMessage(
|
||||||
formatMessage({
|
formatMessage({
|
||||||
defaultMessage: "Username is too long",
|
defaultMessage: "Username is too long",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setUsernameValid(true);
|
setUsernameValid(true);
|
||||||
@ -205,7 +205,7 @@ export function ProfileEditor({ onClose }: { onClose: () => void }) {
|
|||||||
setInvalidAboutMessage(
|
setInvalidAboutMessage(
|
||||||
formatMessage({
|
formatMessage({
|
||||||
defaultMessage: "About too long",
|
defaultMessage: "About too long",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
setAboutValid(true);
|
setAboutValid(true);
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
import { UserMetadata } from "@snort/system";
|
import { CachedMetadata, UserMetadata } from "@snort/system";
|
||||||
import { hexToBech32 } from "@snort/shared";
|
import { hexToBech32 } from "@snort/shared";
|
||||||
import { useInView } from "react-intersection-observer";
|
import { useInView } from "react-intersection-observer";
|
||||||
import { Avatar } from "./avatar";
|
import { Avatar } from "./avatar";
|
||||||
@ -36,6 +36,7 @@ export function Profile({
|
|||||||
linkToProfile,
|
linkToProfile,
|
||||||
avatarSize,
|
avatarSize,
|
||||||
gap,
|
gap,
|
||||||
|
profile,
|
||||||
}: {
|
}: {
|
||||||
pubkey: string;
|
pubkey: string;
|
||||||
icon?: ReactNode;
|
icon?: ReactNode;
|
||||||
@ -45,9 +46,10 @@ export function Profile({
|
|||||||
linkToProfile?: boolean;
|
linkToProfile?: boolean;
|
||||||
avatarSize?: number;
|
avatarSize?: number;
|
||||||
gap?: number;
|
gap?: number;
|
||||||
|
profile?: CachedMetadata;
|
||||||
}) {
|
}) {
|
||||||
const { inView, ref } = useInView({ triggerOnce: true });
|
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 showAvatar = options?.showAvatar ?? true;
|
||||||
const showName = options?.showName ?? true;
|
const showName = options?.showName ?? true;
|
||||||
const isAnon = pubkey === "anon";
|
const isAnon = pubkey === "anon";
|
||||||
|
@ -41,7 +41,7 @@ export function AddForwardInputs({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ingestsEurope = urls.ingests.filter(
|
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());
|
const random = ingestsEurope.at(ingestsEurope.length * Math.random());
|
||||||
return unwrap(random).url_template.replace("{stream_key}", target);
|
return unwrap(random).url_template.replace("{stream_key}", target);
|
||||||
@ -71,7 +71,7 @@ export function AddForwardInputs({
|
|||||||
formatMessage({
|
formatMessage({
|
||||||
defaultMessage: "Stream url must start with rtmp://",
|
defaultMessage: "Stream url must start with rtmp://",
|
||||||
id: "7+bCC1",
|
id: "7+bCC1",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -90,7 +90,7 @@ export function AddForwardInputs({
|
|||||||
formatMessage({
|
formatMessage({
|
||||||
defaultMessage: "Not a valid URL",
|
defaultMessage: "Not a valid URL",
|
||||||
id: "1q4BO/",
|
id: "1q4BO/",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -100,7 +100,7 @@ export function AddForwardInputs({
|
|||||||
formatMessage({
|
formatMessage({
|
||||||
defaultMessage: "Stream Key is required",
|
defaultMessage: "Stream Key is required",
|
||||||
id: "50+/JW",
|
id: "50+/JW",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -110,7 +110,7 @@ export function AddForwardInputs({
|
|||||||
formatMessage({
|
formatMessage({
|
||||||
defaultMessage: "Name is required",
|
defaultMessage: "Name is required",
|
||||||
id: "Gvxoji",
|
id: "Gvxoji",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -122,7 +122,7 @@ export function AddForwardInputs({
|
|||||||
formatMessage({
|
formatMessage({
|
||||||
defaultMessage: "Could not create stream URL",
|
defaultMessage: "Could not create stream URL",
|
||||||
id: "E9APoR",
|
id: "E9APoR",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
await provider.addForward(name, t);
|
await provider.addForward(name, t);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
@ -191,11 +191,7 @@ export default function NostrProviderDialog({
|
|||||||
<p className="pb-2">
|
<p className="pb-2">
|
||||||
<FormattedMessage defaultMessage="Features" id="ZXp0z1" />
|
<FormattedMessage defaultMessage="Features" id="ZXp0z1" />
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">{ep?.capabilities?.map(a => <Pill>{parseCapability(a)}</Pill>)}</div>
|
||||||
{ep?.capabilities?.map(a => (
|
|
||||||
<Pill>{parseCapability(a)}</Pill>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -13,7 +13,7 @@ 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 { DefaultButton, PrimaryButton } from "./buttons";
|
||||||
import Modal from "./modal";
|
import Modal from "./modal";
|
||||||
import Pill from "./pill";
|
import Pill from "./pill";
|
||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
@ -212,12 +212,10 @@ export function SendZapsDialog(props: Omit<SendZapsProps, "onFinish">) {
|
|||||||
{props.button ? (
|
{props.button ? (
|
||||||
<div onClick={() => setOpen(true)}>{props.button}</div>
|
<div onClick={() => setOpen(true)}>{props.button}</div>
|
||||||
) : (
|
) : (
|
||||||
<DefaultButton onClick={() => setOpen(true)}>
|
<PrimaryButton onClick={() => setOpen(true)}>
|
||||||
<span className="max-xl:hidden">
|
|
||||||
<FormattedMessage defaultMessage="Zap" id="fBI91o" />
|
|
||||||
</span>
|
|
||||||
<Icon name="zap-filled" size={16} />
|
<Icon name="zap-filled" size={16} />
|
||||||
</DefaultButton>
|
<FormattedMessage defaultMessage="Zap" />
|
||||||
|
</PrimaryButton>
|
||||||
)}
|
)}
|
||||||
{open && (
|
{open && (
|
||||||
<Modal id="send-zaps" onClose={() => setOpen(false)}>
|
<Modal id="send-zaps" onClose={() => setOpen(false)}>
|
||||||
|
@ -5,7 +5,7 @@ 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 "./chat/textarea";
|
||||||
import { getHost } from "@/utils";
|
import { getHost } from "@/utils";
|
||||||
import { useLogin } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
import { DefaultButton } from "./buttons";
|
import { DefaultButton } from "./buttons";
|
||||||
@ -27,7 +27,7 @@ export function ShareMenu({ ev }: { ev: NostrEvent }) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
link: `https://${window.location.host}/${NostrLink.fromEvent(ev).encode()}`,
|
link: `https://${window.location.host}/${NostrLink.fromEvent(ev).encode()}`,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
const defaultHostMsg = formatMessage(
|
const defaultHostMsg = formatMessage(
|
||||||
{
|
{
|
||||||
@ -37,7 +37,7 @@ export function ShareMenu({ ev }: { ev: NostrEvent }) {
|
|||||||
{
|
{
|
||||||
name: `nostr:${new NostrLink(NostrPrefix.PublicKey, host ?? ev.pubkey).encode()}`,
|
name: `nostr:${new NostrLink(NostrPrefix.PublicKey, host ?? ev.pubkey).encode()}`,
|
||||||
link: `https://${window.location.host}/${NostrLink.fromEvent(ev).encode()}`,
|
link: `https://${window.location.host}/${NostrLink.fromEvent(ev).encode()}`,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
const [message, setMessage] = useState(login?.pubkey === host ? defaultMyMsg : defaultHostMsg);
|
const [message, setMessage] = useState(login?.pubkey === host ? defaultMyMsg : defaultHostMsg);
|
||||||
|
|
||||||
@ -61,6 +61,7 @@ export function ShareMenu({ ev }: { ev: NostrEvent }) {
|
|||||||
menuClassName="ctx-menu"
|
menuClassName="ctx-menu"
|
||||||
menuButton={
|
menuButton={
|
||||||
<DefaultButton>
|
<DefaultButton>
|
||||||
|
<Icon name="share" />
|
||||||
<FormattedMessage defaultMessage="Share" />
|
<FormattedMessage defaultMessage="Share" />
|
||||||
</DefaultButton>
|
</DefaultButton>
|
||||||
}>
|
}>
|
||||||
@ -75,7 +76,7 @@ export function ShareMenu({ ev }: { ev: NostrEvent }) {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
window.open(
|
window.open(
|
||||||
`https://twitter.com/intent/tweet?text=${encodeURIComponent(message)}&via=zap_stream`,
|
`https://twitter.com/intent/tweet?text=${encodeURIComponent(message)}&via=zap_stream`,
|
||||||
"_blank"
|
"_blank",
|
||||||
);
|
);
|
||||||
}}>
|
}}>
|
||||||
<Icon name="twitter" size={24} />
|
<Icon name="twitter" size={24} />
|
||||||
|
@ -13,7 +13,7 @@ export function StatePill({ state, ...props }: StatePillProps) {
|
|||||||
className={classNames(
|
className={classNames(
|
||||||
"uppercase font-white",
|
"uppercase font-white",
|
||||||
state === StreamState.Live ? "bg-primary" : "bg-layer-1",
|
state === StreamState.Live ? "bg-primary" : "bg-layer-1",
|
||||||
props.className
|
props.className,
|
||||||
)}>
|
)}>
|
||||||
{state}
|
{state}
|
||||||
</Pill>
|
</Pill>
|
||||||
|
@ -42,7 +42,7 @@ export function Card({ canEdit, ev, cards }: CardProps) {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[canEdit, identifier]
|
[canEdit, identifier],
|
||||||
);
|
);
|
||||||
|
|
||||||
function findTagByIdentifier(d: string) {
|
function findTagByIdentifier(d: string) {
|
||||||
@ -93,7 +93,7 @@ export function Card({ canEdit, ev, cards }: CardProps) {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
[canEdit, tags, identifier]
|
[canEdit, tags, identifier],
|
||||||
);
|
);
|
||||||
|
|
||||||
const card = (
|
const card = (
|
||||||
|
@ -33,5 +33,5 @@ export const CardPreview = forwardRef<HTMLDivElement, CardPreviewProps>(
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
@ -7,11 +7,11 @@ import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
|||||||
|
|
||||||
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 { DefaultButton } from "../buttons";
|
||||||
import Modal from "./modal";
|
import Modal from "../modal";
|
||||||
|
|
||||||
export function ClipButton({ ev }: { ev: TaggedNostrEvent }) {
|
export function ClipButton({ ev }: { ev: TaggedNostrEvent }) {
|
||||||
const system = useContext(SnortContext);
|
const system = useContext(SnortContext);
|
||||||
@ -81,16 +81,14 @@ export function ClipButton({ ev }: { ev: TaggedNostrEvent }) {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DefaultButton onClick={makeClip}>
|
<DefaultButton onClick={makeClip}>
|
||||||
<Icon name="clapperboard" />
|
<Icon name="scissor" />
|
||||||
<span className="max-lg:hidden">
|
<FormattedMessage defaultMessage="Clip" />
|
||||||
<FormattedMessage defaultMessage="Create Clip" id="PA0ej4" />
|
|
||||||
</span>
|
|
||||||
</DefaultButton>
|
</DefaultButton>
|
||||||
{open && (
|
{open && (
|
||||||
<Modal id="create-clip" onClose={() => setOpen(false)}>
|
<Modal id="create-clip" onClose={() => setOpen(false)}>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<h1>
|
<h1>
|
||||||
<FormattedMessage defaultMessage="Create Clip" id="PA0ej4" />
|
<FormattedMessage defaultMessage="Clip" />
|
||||||
</h1>
|
</h1>
|
||||||
{id && tempClipId && <video ref={ref} src={provider.getTempClipUrl(id, tempClipId)} controls muted />}
|
{id && tempClipId && <video ref={ref} src={provider.getTempClipUrl(id, tempClipId)} controls muted />}
|
||||||
<TimelineBar
|
<TimelineBar
|
@ -3,7 +3,7 @@ import { NostrEvent } from "@snort/system";
|
|||||||
import { useUserProfile } from "@snort/system-react";
|
import { useUserProfile } from "@snort/system-react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { getName } from "./profile";
|
import { getName } from "../profile";
|
||||||
|
|
||||||
export function ClipTile({ ev }: { ev: NostrEvent }) {
|
export function ClipTile({ ev }: { ev: NostrEvent }) {
|
||||||
const profile = useUserProfile(ev.pubkey);
|
const profile = useUserProfile(ev.pubkey);
|
@ -1,9 +1,9 @@
|
|||||||
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||||
import { Profile } from "./profile";
|
import { Profile } from "../profile";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { extractStreamInfo, findTag } from "@/utils";
|
import { extractStreamInfo, findTag } from "@/utils";
|
||||||
import { useEventFeed } from "@snort/system-react";
|
import { useEventFeed } from "@snort/system-react";
|
||||||
import EventReactions from "./event-reactions";
|
import EventReactions from "../event-reactions";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
export default function LiveStreamClip({ ev }: { ev: TaggedNostrEvent }) {
|
export default function LiveStreamClip({ ev }: { ev: TaggedNostrEvent }) {
|
@ -2,8 +2,8 @@
|
|||||||
import Hls from "hls.js";
|
import Hls from "hls.js";
|
||||||
import { HTMLProps, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { HTMLProps, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
import { Icon } from "./icon";
|
import { Icon } from "../icon";
|
||||||
import { ProgressBar } from "./progress-bar";
|
import { ProgressBar } from "../progress-bar";
|
||||||
import { Menu, MenuItem } from "@szhsin/react-menu";
|
import { Menu, MenuItem } from "@szhsin/react-menu";
|
||||||
import { StreamState } from "@/const";
|
import { StreamState } from "@/const";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
@ -140,7 +140,7 @@ export default function LiveVideoPlayer({
|
|||||||
}, [video, volume, muted]);
|
}, [video, volume, muted]);
|
||||||
|
|
||||||
const { isPictureInPictureActive, isPictureInPictureAvailable, togglePictureInPicture } = usePictureInPicture(
|
const { isPictureInPictureActive, isPictureInPictureAvailable, togglePictureInPicture } = usePictureInPicture(
|
||||||
video as VideoRefType
|
video as VideoRefType,
|
||||||
);
|
);
|
||||||
|
|
||||||
const handlePIPClick = useCallback(async () => {
|
const handlePIPClick = useCallback(async () => {
|
||||||
@ -222,7 +222,7 @@ export default function LiveVideoPlayer({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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} />
|
<Icon name={muted ? "volume-muted" : "volume"} onClick={toggleMute} />
|
||||||
<ProgressBar value={volume} setValue={v => setVolume(v)} style={{ width: "100px", height: "100%" }} />
|
<ProgressBar value={volume} setValue={v => setVolume(v)} style={{ width: "100px", height: "100%" }} />
|
||||||
</div>
|
</div>
|
||||||
@ -244,7 +244,9 @@ export default function LiveVideoPlayer({
|
|||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
{isPictureInPictureAvailable && (
|
{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
|
PIP
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
@ -3,8 +3,8 @@ 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";
|
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();
|
||||||
@ -81,7 +81,7 @@ export function NotificationsButton({ host, service }: { host: string; service:
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DefaultButton onClick={subscribed ? unsubscribe : subscribe}>
|
<DefaultButton onClick={subscribed ? unsubscribe : subscribe}>
|
||||||
<Icon name={subscribed ? "bell-off" : "bell-ringing"} />
|
<Icon name={subscribed ? "bell-off" : "bell-plus"} />
|
||||||
</DefaultButton>
|
</DefaultButton>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -7,19 +7,21 @@ import { SnortContext, useUserProfile } from "@snort/system-react";
|
|||||||
import { useContext, useState } from "react";
|
import { useContext, 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 { WarningButton } from "./buttons";
|
import { WarningButton } from "../buttons";
|
||||||
import { ClipButton } from "./clip-button";
|
import { ClipButton } from "./clip-button";
|
||||||
import { FollowButton } from "./follow-button";
|
import { FollowButton } from "../follow-button";
|
||||||
import GameInfoCard from "./game-info";
|
import GameInfoCard from "../game-info";
|
||||||
import { NewStreamDialog } from "./new-stream";
|
import { NewStreamDialog } from "../new-stream";
|
||||||
import { NotificationsButton } from "./notifications-button";
|
import { NotificationsButton } from "./notifications-button";
|
||||||
import Pill from "./pill";
|
import Pill from "../pill";
|
||||||
import { Profile, getName } from "./profile";
|
import { Profile, getName } from "../profile";
|
||||||
import { SendZapsDialog } from "./send-zap";
|
import { SendZapsDialog } from "../send-zap";
|
||||||
import { ShareMenu } from "./share-menu";
|
import { ShareMenu } from "../share-menu";
|
||||||
import { StatePill } from "./state-pill";
|
import { StatePill } from "../state-pill";
|
||||||
import { StreamTimer } from "./stream-time";
|
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 }) {
|
export function StreamInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedNostrEvent }) {
|
||||||
const system = useContext(SnortContext);
|
const system = useContext(SnortContext);
|
||||||
@ -28,6 +30,7 @@ export function StreamInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedN
|
|||||||
const host = getHost(ev);
|
const host = getHost(ev);
|
||||||
const profile = useUserProfile(host);
|
const profile = useUserProfile(host);
|
||||||
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
||||||
|
const streamContext = useStream();
|
||||||
|
|
||||||
const { status, participants, title, summary, service, gameId, gameInfo } = extractStreamInfo(ev);
|
const { status, participants, title, summary, service, gameId, gameInfo } = extractStreamInfo(ev);
|
||||||
const isMine = ev?.pubkey === login?.pubkey;
|
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");
|
const viewers = Number(participants ?? "0");
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex gap-2 max-xl:flex-col max-xl:px-2">
|
<div className="flex gap-2 max-xl:flex-col">
|
||||||
<div className="grow flex flex-col gap-2 max-xl:hidden">
|
<div className="grow flex flex-col gap-2">
|
||||||
<h1>{title}</h1>
|
<div className="text-3xl font-semibold">{title}</div>
|
||||||
{summary && <StreamSummary text={summary} />}
|
<div className="flex max-xl:flex-col xl:justify-between max-xl:gap-2">
|
||||||
|
<div className="flex gap-4">
|
||||||
<div className="flex gap-2 flex-wrap">
|
<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)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 flex-wrap max-xl:hidden">
|
||||||
<StatePill state={status as StreamState} />
|
<StatePill state={status as StreamState} />
|
||||||
<Pill>
|
<Pill>
|
||||||
<FormattedMessage defaultMessage="{n} viewers" id="3adEeb" values={{ n: formatSats(viewers) }} />
|
<FormattedMessage defaultMessage="{n} viewers" values={{ n: formatSats(viewers) }} />
|
||||||
</Pill>
|
</Pill>
|
||||||
{status === StreamState.Live && (
|
{status === StreamState.Live && (
|
||||||
<Pill>
|
<Pill>
|
||||||
@ -67,6 +93,7 @@ export function StreamInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedN
|
|||||||
)}
|
)}
|
||||||
{ev && <Tags ev={ev} />}
|
{ev && <Tags ev={ev} />}
|
||||||
</div>
|
</div>
|
||||||
|
{summary && <StreamSummary text={summary} />}
|
||||||
{isMine && (
|
{isMine && (
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
{ev && <NewStreamDialog text={<FormattedMessage defaultMessage="Edit" />} ev={ev} />}
|
{ev && <NewStreamDialog text={<FormattedMessage defaultMessage="Edit" />} ev={ev} />}
|
||||||
@ -76,52 +103,7 @@ export function StreamInfo({ ev, goal }: { ev?: TaggedNostrEvent; goal?: TaggedN
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between sm:gap-4 max-sm:gap-2 flex-wrap max-md:flex-col lg:items-center">
|
|
||||||
<Profile pubkey={host ?? ""} />
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<FollowButton pubkey={host} hideWhenFollowing={true} />
|
|
||||||
{ev && (
|
|
||||||
<>
|
|
||||||
<ShareMenu ev={ev} />
|
|
||||||
<ClipButton 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)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function StreamSummary({ text }: { text: string }) {
|
|
||||||
const [expand, setExpand] = useState(false);
|
|
||||||
|
|
||||||
const cutOff = 100;
|
|
||||||
const shouldExpand = text.length > cutOff;
|
|
||||||
return (
|
|
||||||
<div className="whitespace-pre text-pretty">
|
|
||||||
{shouldExpand && !expand ? text.slice(0, cutOff) : text}
|
|
||||||
{shouldExpand && "... "}
|
|
||||||
{shouldExpand && (
|
|
||||||
<span
|
|
||||||
className="text-primary text-bold cursor-pointer"
|
|
||||||
onClick={() => {
|
|
||||||
setExpand(x => !x);
|
|
||||||
}}>
|
|
||||||
{expand && <FormattedMessage defaultMessage="Show Less" />}
|
|
||||||
{!expand && <FormattedMessage defaultMessage="Show More" />}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
38
src/element/stream/stream-state.tsx
Normal file
38
src/element/stream/stream-state.tsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { NostrLink, NostrPrefix } from "@snort/system";
|
||||||
|
import { ReactNode, createContext, useContext, useState } from "react";
|
||||||
|
|
||||||
|
interface StreamState {
|
||||||
|
link: NostrLink;
|
||||||
|
showDetails: boolean;
|
||||||
|
update: (fn: (c: StreamState) => StreamState) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
link: new NostrLink(NostrPrefix.Address, ""),
|
||||||
|
showDetails: false,
|
||||||
|
update: c => c,
|
||||||
|
} as StreamState;
|
||||||
|
|
||||||
|
const StreamContext = createContext<StreamState>(initialState);
|
||||||
|
|
||||||
|
export function StreamContextProvider({ children, link }: { children?: ReactNode; link: NostrLink }) {
|
||||||
|
const [state, setState] = useState<StreamState>({
|
||||||
|
...initialState,
|
||||||
|
link,
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<StreamContext.Provider
|
||||||
|
value={{
|
||||||
|
...state,
|
||||||
|
update: (fn: (c: StreamState) => StreamState) => {
|
||||||
|
setState(fn);
|
||||||
|
},
|
||||||
|
}}>
|
||||||
|
{children}
|
||||||
|
</StreamContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useStream() {
|
||||||
|
return useContext(StreamContext);
|
||||||
|
}
|
@ -16,7 +16,7 @@ export function StreamTimer({ ev }: { ev?: NostrEvent }) {
|
|||||||
const mins = Math.floor((diff % hour) / min);
|
const mins = Math.floor((diff % hour) / min);
|
||||||
const secs = Math.floor(diff % min);
|
const secs = Math.floor(diff % min);
|
||||||
setTime(
|
setTime(
|
||||||
`${hours.toFixed(0).padStart(2, "0")}:${mins.toFixed(0).padStart(2, "0")}:${secs.toFixed(0).padStart(2, "0")}`
|
`${hours.toFixed(0).padStart(2, "0")}:${mins.toFixed(0).padStart(2, "0")}:${secs.toFixed(0).padStart(2, "0")}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
24
src/element/stream/summary.tsx
Normal file
24
src/element/stream/summary.tsx
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
|
export function StreamSummary({ text }: { text: string }) {
|
||||||
|
const [expand, setExpand] = useState(false);
|
||||||
|
|
||||||
|
const cutOff = 100;
|
||||||
|
const shouldExpand = text.length > cutOff;
|
||||||
|
return (
|
||||||
|
<div className="whitespace-pre text-pretty">
|
||||||
|
{shouldExpand && !expand ? text.slice(0, cutOff) : text}
|
||||||
|
{shouldExpand && (
|
||||||
|
<span
|
||||||
|
className="text-primary text-bold cursor-pointer"
|
||||||
|
onClick={() => {
|
||||||
|
setExpand(x => !x);
|
||||||
|
}}>
|
||||||
|
{expand && <FormattedMessage defaultMessage="Hide" />}
|
||||||
|
{!expand && <FormattedMessage defaultMessage="...more" />}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -35,7 +35,7 @@ export default function StreamSummary({ link, preload }: { link: NostrLink; prel
|
|||||||
rb.withFilter().kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP]).replyToLink([thisLink]);
|
rb.withFilter().kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP]).replyToLink([thisLink]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
true
|
true,
|
||||||
);
|
);
|
||||||
const reactions = useEventReactions(thisLink ?? link, data);
|
const reactions = useEventReactions(thisLink ?? link, data);
|
||||||
|
|
||||||
@ -43,11 +43,14 @@ export default function StreamSummary({ link, preload }: { link: NostrLink; prel
|
|||||||
return Object.entries(
|
return Object.entries(
|
||||||
data
|
data
|
||||||
.filter(a => a.kind === LIVE_STREAM_CHAT)
|
.filter(a => a.kind === LIVE_STREAM_CHAT)
|
||||||
.reduce((acc, v) => {
|
.reduce(
|
||||||
acc[v.pubkey] ??= [];
|
(acc, v) => {
|
||||||
acc[v.pubkey].push(v);
|
acc[v.pubkey] ??= [];
|
||||||
return acc;
|
acc[v.pubkey].push(v);
|
||||||
}, {} as Record<string, Array<NostrEvent>>)
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, Array<NostrEvent>>,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.map(([k, v]) => ({
|
.map(([k, v]) => ({
|
||||||
pubkey: k,
|
pubkey: k,
|
||||||
@ -58,12 +61,15 @@ export default function StreamSummary({ link, preload }: { link: NostrLink; prel
|
|||||||
|
|
||||||
const zapsSummary = useMemo(() => {
|
const zapsSummary = useMemo(() => {
|
||||||
return Object.entries(
|
return Object.entries(
|
||||||
reactions.zaps.reduce((acc, v) => {
|
reactions.zaps.reduce(
|
||||||
if (!v.sender) return acc;
|
(acc, v) => {
|
||||||
acc[v.sender] ??= [];
|
if (!v.sender) return acc;
|
||||||
acc[v.sender].push(v);
|
acc[v.sender] ??= [];
|
||||||
return acc;
|
acc[v.sender].push(v);
|
||||||
}, {} as Record<string, Array<ParsedZap>>)
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, Array<ParsedZap>>,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
.map(([k, v]) => ({
|
.map(([k, v]) => ({
|
||||||
pubkey: k,
|
pubkey: k,
|
||||||
@ -94,42 +100,45 @@ export default function StreamSummary({ link, preload }: { link: NostrLink; prel
|
|||||||
const ret = data
|
const ret = data
|
||||||
.sort((a, b) => (a.created_at > b.created_at ? -1 : 1))
|
.sort((a, b) => (a.created_at > b.created_at ? -1 : 1))
|
||||||
.filter(a => a.created_at >= startTime && a.created_at < endTime)
|
.filter(a => a.created_at >= startTime && a.created_at < endTime)
|
||||||
.reduce((acc, v) => {
|
.reduce(
|
||||||
const time = Math.floor(v.created_at - (v.created_at % windowSize));
|
(acc, v) => {
|
||||||
if (time < min) {
|
const time = Math.floor(v.created_at - (v.created_at % windowSize));
|
||||||
min = time;
|
if (time < min) {
|
||||||
}
|
min = time;
|
||||||
if (time > max) {
|
}
|
||||||
max = time;
|
if (time > max) {
|
||||||
}
|
max = time;
|
||||||
const key = time.toString();
|
}
|
||||||
acc[key] ??= {
|
const key = time.toString();
|
||||||
time,
|
acc[key] ??= {
|
||||||
zaps: 0,
|
time,
|
||||||
messages: 0,
|
zaps: 0,
|
||||||
reactions: 0,
|
messages: 0,
|
||||||
clips: 0,
|
reactions: 0,
|
||||||
raids: 0,
|
clips: 0,
|
||||||
shares: 0,
|
raids: 0,
|
||||||
};
|
shares: 0,
|
||||||
|
};
|
||||||
|
|
||||||
if (v.kind === LIVE_STREAM_CHAT) {
|
if (v.kind === LIVE_STREAM_CHAT) {
|
||||||
acc[key].messages++;
|
acc[key].messages++;
|
||||||
} else if (v.kind === EventKind.ZapReceipt) {
|
} else if (v.kind === EventKind.ZapReceipt) {
|
||||||
acc[key].zaps++;
|
acc[key].zaps++;
|
||||||
} else if (v.kind === EventKind.Reaction) {
|
} else if (v.kind === EventKind.Reaction) {
|
||||||
acc[key].reactions++;
|
acc[key].reactions++;
|
||||||
} else if (v.kind === EventKind.TextNote) {
|
} else if (v.kind === EventKind.TextNote) {
|
||||||
acc[key].shares++;
|
acc[key].shares++;
|
||||||
} else if (v.kind === LIVE_STREAM_CLIP) {
|
} else if (v.kind === LIVE_STREAM_CLIP) {
|
||||||
acc[key].clips++;
|
acc[key].clips++;
|
||||||
} else if (v.kind === LIVE_STREAM_RAID) {
|
} else if (v.kind === LIVE_STREAM_RAID) {
|
||||||
acc[key].raids++;
|
acc[key].raids++;
|
||||||
} else {
|
} else {
|
||||||
console.debug("Uncounted stat", v);
|
console.debug("Uncounted stat", v);
|
||||||
}
|
}
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, StatSlot>);
|
},
|
||||||
|
{} as Record<string, StatSlot>,
|
||||||
|
);
|
||||||
|
|
||||||
// fill empty time slots
|
// fill empty time slots
|
||||||
for (let x = min; x < max; x += windowSize) {
|
for (let x = min; x < max; x += windowSize) {
|
||||||
|
@ -12,7 +12,7 @@ import Pill from "./pill";
|
|||||||
import { CategoryZaps } from "./category/zaps";
|
import { CategoryZaps } from "./category/zaps";
|
||||||
import { StreamState } from "@/const";
|
import { StreamState } from "@/const";
|
||||||
import { useRecentClips } from "@/hooks/clips";
|
import { useRecentClips } from "@/hooks/clips";
|
||||||
import { ClipTile } from "./clip-tile";
|
import { ClipTile } from "./stream/clip-tile";
|
||||||
|
|
||||||
interface VideoGridSortedProps {
|
interface VideoGridSortedProps {
|
||||||
evs: Array<TaggedNostrEvent>;
|
evs: Array<TaggedNostrEvent>;
|
||||||
@ -38,7 +38,7 @@ export default function VideoGridSorted({
|
|||||||
(ev: NostrEvent) => {
|
(ev: NostrEvent) => {
|
||||||
return tags.find(t => t.at(1) === getHost(ev));
|
return tags.find(t => t.at(1) === getHost(ev));
|
||||||
},
|
},
|
||||||
[tags]
|
[tags],
|
||||||
);
|
);
|
||||||
const { live, planned, ended } = useSortedStreams(evs, showAll ? 0 : undefined);
|
const { live, planned, ended } = useSortedStreams(evs, showAll ? 0 : undefined);
|
||||||
const hashtags = getTagValues(tags, "t");
|
const hashtags = getTagValues(tags, "t");
|
||||||
@ -138,7 +138,7 @@ function PopularCategories({ items }: { items: Array<TaggedNostrEvent> }) {
|
|||||||
zaps: number;
|
zaps: number;
|
||||||
streams: number;
|
streams: number;
|
||||||
}
|
}
|
||||||
>
|
>,
|
||||||
);
|
);
|
||||||
|
|
||||||
return Object.values(grouped)
|
return Object.values(grouped)
|
||||||
|
@ -40,7 +40,7 @@ export function VideoTile({
|
|||||||
"blur transition": contentWarning,
|
"blur transition": contentWarning,
|
||||||
"hover:blur-none": isGrownUp,
|
"hover:blur-none": isGrownUp,
|
||||||
},
|
},
|
||||||
"h-full"
|
"h-full",
|
||||||
)}
|
)}
|
||||||
state={ev}>
|
state={ev}>
|
||||||
<div className="relative mb-2 aspect-video">
|
<div className="relative mb-2 aspect-video">
|
||||||
|
26
src/element/video/comment.tsx
Normal file
26
src/element/video/comment.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { TaggedNostrEvent } from "@snort/system";
|
||||||
|
import { Profile, getName } from "../profile";
|
||||||
|
import { Text } from "@/element/text";
|
||||||
|
import { useUserProfile } from "@snort/system-react";
|
||||||
|
import EventReactions from "../event-reactions";
|
||||||
|
|
||||||
|
export default function VideoComment({ ev }: { ev: TaggedNostrEvent }) {
|
||||||
|
const profile = useUserProfile(ev.pubkey);
|
||||||
|
return (
|
||||||
|
<div className="grid gap-4 grid-cols-[min-content_auto]">
|
||||||
|
<Profile
|
||||||
|
pubkey={ev.pubkey}
|
||||||
|
profile={profile}
|
||||||
|
avatarSize={40}
|
||||||
|
options={{
|
||||||
|
showName: false,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="text-medium">{getName(ev.pubkey, profile)}</div>
|
||||||
|
<Text content={ev.content} tags={ev.tags} />
|
||||||
|
<EventReactions ev={ev} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
23
src/element/video/comments.tsx
Normal file
23
src/element/video/comments.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { NostrLink, RequestBuilder } from "@snort/system";
|
||||||
|
import { useRequestBuilder } from "@snort/system-react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import VideoComment from "./comment";
|
||||||
|
|
||||||
|
export default function VideoComments({ link }: { link: NostrLink }) {
|
||||||
|
const sub = useMemo(() => {
|
||||||
|
const rb = new RequestBuilder(`video-comments:${link.id}`);
|
||||||
|
rb.withFilter().kinds([1]).replyToLink([link]);
|
||||||
|
|
||||||
|
return rb;
|
||||||
|
}, [link.id]);
|
||||||
|
|
||||||
|
const comments = useRequestBuilder(sub);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
{comments.map(a => (
|
||||||
|
<VideoComment key={a.id} ev={a} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -9,7 +9,7 @@ import type { Badge } from "@/types";
|
|||||||
export function useBadges(
|
export function useBadges(
|
||||||
pubkey: string,
|
pubkey: string,
|
||||||
since: number,
|
since: number,
|
||||||
leaveOpen = true
|
leaveOpen = true,
|
||||||
): { badges: Badge[]; awards: TaggedNostrEvent[] } {
|
): { badges: Badge[]; awards: TaggedNostrEvent[] } {
|
||||||
const rb = useMemo(() => {
|
const rb = useMemo(() => {
|
||||||
if (!pubkey) return null;
|
if (!pubkey) return null;
|
||||||
@ -54,7 +54,7 @@ export function useBadges(
|
|||||||
acceptedEvents
|
acceptedEvents
|
||||||
.filter(pb => awardees.has(pb.pubkey))
|
.filter(pb => awardees.has(pb.pubkey))
|
||||||
.filter(pb => pb.tags.find(t => t.at(0) === "a" && t.at(1) === address))
|
.filter(pb => pb.tags.find(t => t.at(0) === "a" && t.at(1) === address))
|
||||||
.map(pb => pb.pubkey)
|
.map(pb => pb.pubkey),
|
||||||
);
|
);
|
||||||
const thumb = findTag(e, "thumb");
|
const thumb = findTag(e, "thumb");
|
||||||
const image = findTag(e, "image");
|
const image = findTag(e, "image");
|
||||||
|
@ -31,7 +31,7 @@ export function useCurrentStreamFeed(link: NostrLink, leaveOpen = false, evPrelo
|
|||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const hosting = [...q, ...(evPreload ? [evPreload] : [])].filter(
|
const hosting = [...q, ...(evPreload ? [evPreload] : [])].filter(
|
||||||
a => a.pubkey === author || a.tags.some(b => b[0] === "p" && b[1] === author && b[3] === "host")
|
a => a.pubkey === author || a.tags.some(b => b[0] === "p" && b[1] === author && b[3] === "host"),
|
||||||
);
|
);
|
||||||
return [...(hosting ?? [])].sort((a, b) => (b.created_at > a.created_at ? 1 : -1)).at(0);
|
return [...(hosting ?? [])].sort((a, b) => (b.created_at > a.created_at ? 1 : -1)).at(0);
|
||||||
}, [q]);
|
}, [q]);
|
||||||
|
@ -19,7 +19,7 @@ const LangSelector = new LangStore();
|
|||||||
export function useLang() {
|
export function useLang() {
|
||||||
const store = useSyncExternalStore(
|
const store = useSyncExternalStore(
|
||||||
c => LangSelector.hook(c),
|
c => LangSelector.hook(c),
|
||||||
() => LangSelector.snapshot()
|
() => LangSelector.snapshot(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -12,7 +12,7 @@ export function useLiveChatFeed(link?: NostrLink, limit?: number) {
|
|||||||
rb.withFilter().kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP]).tag("a", [aTag]).limit(limit);
|
rb.withFilter().kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP]).tag("a", [aTag]).limit(limit);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
true
|
true,
|
||||||
);
|
);
|
||||||
return { messages, reactions: reactions ?? [] };
|
return { messages, reactions: reactions ?? [] };
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,7 @@ import { getPublisher, getSigner, Login, LoginSession } from "@/login";
|
|||||||
export function useLogin() {
|
export function useLogin() {
|
||||||
const session = useSyncExternalStore(
|
const session = useSyncExternalStore(
|
||||||
c => Login.hook(c),
|
c => Login.hook(c),
|
||||||
() => Login.snapshot()
|
() => Login.snapshot(),
|
||||||
);
|
);
|
||||||
if (!session) return;
|
if (!session) return;
|
||||||
return {
|
return {
|
||||||
@ -32,7 +32,7 @@ export function useLoginEvents(pubkey?: string, leaveOpen = false) {
|
|||||||
const [userEmojis, setUserEmojis] = useState<Tags>([]);
|
const [userEmojis, setUserEmojis] = useState<Tags>([]);
|
||||||
const session = useSyncExternalStore(
|
const session = useSyncExternalStore(
|
||||||
c => Login.hook(c),
|
c => Login.hook(c),
|
||||||
() => Login.snapshot()
|
() => Login.snapshot(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const sub = useMemo(() => {
|
const sub = useMemo(() => {
|
||||||
|
@ -7,7 +7,7 @@ import { useSyncExternalStore } from "react";
|
|||||||
export function useStreamProvider() {
|
export function useStreamProvider() {
|
||||||
return useSyncExternalStore(
|
return useSyncExternalStore(
|
||||||
c => StreamProviderStore.hook(c),
|
c => StreamProviderStore.hook(c),
|
||||||
() => StreamProviderStore.snapshot()
|
() => StreamProviderStore.snapshot(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,7 +14,7 @@ export function useWallet() {
|
|||||||
if (s.wallet && d) {
|
if (s.wallet && d) {
|
||||||
s.wallet.data = d;
|
s.wallet.data = d;
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,6 @@ export function useZaps(link?: NostrLink, leaveOpen = false) {
|
|||||||
.sort((a, b) => (b.created_at > a.created_at ? 1 : -1))
|
.sort((a, b) => (b.created_at > a.created_at ? 1 : -1))
|
||||||
.map(ev => parseZap(ev))
|
.map(ev => parseZap(ev))
|
||||||
.filter(z => z && z.valid) ?? [],
|
.filter(z => z && z.valid) ?? [],
|
||||||
[zaps.length]
|
[zaps.length],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -122,6 +122,9 @@
|
|||||||
<symbol id="bell-ringing" viewBox="0 0 22 22" fill="none">
|
<symbol id="bell-ringing" viewBox="0 0 22 22" fill="none">
|
||||||
<path d="M8.35442 20C9.05956 20.6224 9.9858 21 11.0002 21C12.0147 21 12.9409 20.6224 13.6461 20M1.29414 4.81989C1.27979 3.36854 2.06227 2.01325 3.32635 1.3M20.7024 4.8199C20.7167 3.36855 19.9342 2.01325 18.6702 1.3M17.0002 7C17.0002 5.4087 16.3681 3.88258 15.2429 2.75736C14.1177 1.63214 12.5915 1 11.0002 1C9.40895 1 7.88283 1.63214 6.75761 2.75736C5.63239 3.88258 5.00025 5.4087 5.00025 7C5.00025 10.0902 4.22072 12.206 3.34991 13.6054C2.61538 14.7859 2.24811 15.3761 2.26157 15.5408C2.27649 15.7231 2.31511 15.7926 2.46203 15.9016C2.59471 16 3.19284 16 4.3891 16H17.6114C18.8077 16 19.4058 16 19.5385 15.9016C19.6854 15.7926 19.724 15.7231 19.7389 15.5408C19.7524 15.3761 19.3851 14.7859 18.6506 13.6054C17.7798 12.206 17.0002 10.0902 17.0002 7Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M8.35442 20C9.05956 20.6224 9.9858 21 11.0002 21C12.0147 21 12.9409 20.6224 13.6461 20M1.29414 4.81989C1.27979 3.36854 2.06227 2.01325 3.32635 1.3M20.7024 4.8199C20.7167 3.36855 19.9342 2.01325 18.6702 1.3M17.0002 7C17.0002 5.4087 16.3681 3.88258 15.2429 2.75736C14.1177 1.63214 12.5915 1 11.0002 1C9.40895 1 7.88283 1.63214 6.75761 2.75736C5.63239 3.88258 5.00025 5.4087 5.00025 7C5.00025 10.0902 4.22072 12.206 3.34991 13.6054C2.61538 14.7859 2.24811 15.3761 2.26157 15.5408C2.27649 15.7231 2.31511 15.7926 2.46203 15.9016C2.59471 16 3.19284 16 4.3891 16H17.6114C18.8077 16 19.4058 16 19.5385 15.9016C19.6854 15.7926 19.724 15.7231 19.7389 15.5408C19.7524 15.3761 19.3851 14.7859 18.6506 13.6054C17.7798 12.206 17.0002 10.0902 17.0002 7Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
<symbol id="bell-plus" viewBox="0 0 18 20" fill="none">
|
||||||
|
<path d="M6.7952 17.5C7.38281 18.0187 8.15468 18.3334 9.00006 18.3334C9.84543 18.3334 10.6173 18.0187 11.2049 17.5M14.0001 6.66669V1.66669M11.5001 4.16669H16.5001M9.83339 1.7366C9.55983 1.69036 9.28116 1.66669 9.00006 1.66669C7.67397 1.66669 6.4022 2.19347 5.46452 3.13115C4.52684 4.06883 4.00006 5.3406 4.00006 6.66669C4.00006 9.24184 3.35045 11.005 2.62478 12.1712C2.01267 13.1549 1.7066 13.6468 1.71783 13.784C1.73025 13.9359 1.76244 13.9939 1.88487 14.0847C1.99544 14.1667 2.49388 14.1667 3.49077 14.1667H14.5093C15.5062 14.1667 16.0046 14.1667 16.1152 14.0847C16.2376 13.9939 16.2698 13.9359 16.2822 13.784C16.2935 13.6468 15.9874 13.1548 15.3752 12.171C14.9652 11.512 14.5794 10.6624 14.3215 9.58335" stroke="currentColor" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</symbol>
|
||||||
<symbol id="bell-off" viewBox="0 0 24 24" fill="none">
|
<symbol id="bell-off" viewBox="0 0 24 24" fill="none">
|
||||||
<path d="M8.63306 3.03371C9.61959 2.3649 10.791 2 12 2C13.5913 2 15.1174 2.63214 16.2426 3.75736C17.3679 4.88258 18 6.4087 18 8C18 10.1008 18.2702 11.7512 18.6484 13.0324M6.25867 6.25724C6.08866 6.81726 6 7.40406 6 8C6 11.0902 5.22047 13.206 4.34966 14.6054C3.61513 15.7859 3.24786 16.3761 3.26132 16.5408C3.27624 16.7231 3.31486 16.7926 3.46178 16.9016C3.59446 17 4.19259 17 5.38885 17H17M9.35418 21C10.0593 21.6224 10.9856 22 12 22C13.0144 22 13.9407 21.6224 14.6458 21M21 21L3 3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M8.63306 3.03371C9.61959 2.3649 10.791 2 12 2C13.5913 2 15.1174 2.63214 16.2426 3.75736C17.3679 4.88258 18 6.4087 18 8C18 10.1008 18.2702 11.7512 18.6484 13.0324M6.25867 6.25724C6.08866 6.81726 6 7.40406 6 8C6 11.0902 5.22047 13.206 4.34966 14.6054C3.61513 15.7859 3.24786 16.3761 3.26132 16.5408C3.27624 16.7231 3.31486 16.7926 3.46178 16.9016C3.59446 17 4.19259 17 5.38885 17H17M9.35418 21C10.0593 21.6224 10.9856 22 12 22C13.0144 22 13.9407 21.6224 14.6458 21M21 21L3 3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
@ -172,6 +175,21 @@
|
|||||||
<path d="M17.4 12H13.6C13.0399 12 12.7599 12 12.546 12.109C12.3578 12.2049 12.2049 12.3578 12.109 12.546C12 12.7599 12 13.0399 12 13.6V17.4C12 17.9601 12 18.2401 12.109 18.454C12.2049 18.6422 12.3578 18.7951 12.546 18.891C12.7599 19 13.0399 19 13.6 19H17.4C17.9601 19 18.2401 19 18.454 18.891C18.6422 18.7951 18.7951 18.6422 18.891 18.454C19 18.2401 19 17.9601 19 17.4V13.6C19 13.0399 19 12.7599 18.891 12.546C18.7951 12.3578 18.6422 12.2049 18.454 12.109C18.2401 12 17.9601 12 17.4 12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M17.4 12H13.6C13.0399 12 12.7599 12 12.546 12.109C12.3578 12.2049 12.2049 12.3578 12.109 12.546C12 12.7599 12 13.0399 12 13.6V17.4C12 17.9601 12 18.2401 12.109 18.454C12.2049 18.6422 12.3578 18.7951 12.546 18.891C12.7599 19 13.0399 19 13.6 19H17.4C17.9601 19 18.2401 19 18.454 18.891C18.6422 18.7951 18.7951 18.6422 18.891 18.454C19 18.2401 19 17.9601 19 17.4V13.6C19 13.0399 19 12.7599 18.891 12.546C18.7951 12.3578 18.6422 12.2049 18.454 12.109C18.2401 12 17.9601 12 17.4 12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path d="M6.4 12H2.6C2.03995 12 1.75992 12 1.54601 12.109C1.35785 12.2049 1.20487 12.3578 1.10899 12.546C1 12.7599 1 13.0399 1 13.6V17.4C1 17.9601 1 18.2401 1.10899 18.454C1.20487 18.6422 1.35785 18.7951 1.54601 18.891C1.75992 19 2.03995 19 2.6 19H6.4C6.96005 19 7.24008 19 7.45399 18.891C7.64215 18.7951 7.79513 18.6422 7.89101 18.454C8 18.2401 8 17.9601 8 17.4V13.6C8 13.0399 8 12.7599 7.89101 12.546C7.79513 12.3578 7.64215 12.2049 7.45399 12.109C7.24008 12 6.96005 12 6.4 12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M6.4 12H2.6C2.03995 12 1.75992 12 1.54601 12.109C1.35785 12.2049 1.20487 12.3578 1.10899 12.546C1 12.7599 1 13.0399 1 13.6V17.4C1 17.9601 1 18.2401 1.10899 18.454C1.20487 18.6422 1.35785 18.7951 1.54601 18.891C1.75992 19 2.03995 19 2.6 19H6.4C6.96005 19 7.24008 19 7.45399 18.891C7.64215 18.7951 7.79513 18.6422 7.89101 18.454C8 18.2401 8 17.9601 8 17.4V13.6C8 13.0399 8 12.7599 7.89101 12.546C7.79513 12.3578 7.64215 12.2049 7.45399 12.109C7.24008 12 6.96005 12 6.4 12Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
</symbol>
|
</symbol>
|
||||||
|
<symbol id="scissor" viewBox="0 0 19 18" fill="none">
|
||||||
|
<path fill-rule="evenodd" clip-rule="evenodd" d="M2.2976 6.86647C1.32098 6.28522 0.666626 5.21903 0.666626 4.00002C0.666626 2.15907 2.15901 0.666687 3.99996 0.666687C5.84091 0.666687 7.33329 2.15907 7.33329 4.00002C7.33329 5.16986 6.73066 6.19895 5.8189 6.79379L8.31576 8.06491L16.1219 4.0909C16.532 3.88209 17.0338 4.04532 17.2426 4.45546C17.4514 4.86561 17.2882 5.36737 16.878 5.57617L10.1526 9.00002L16.8781 12.4239C17.2882 12.6327 17.4514 13.1345 17.2426 13.5446C17.0338 13.9548 16.5321 14.118 16.1219 13.9092L8.31576 9.93513L5.8189 11.2063C6.73066 11.8011 7.33329 12.8302 7.33329 14C7.33329 15.841 5.84091 17.3334 3.99996 17.3334C2.15901 17.3334 0.666626 15.841 0.666626 14C0.666626 12.781 1.32095 11.7149 2.29754 11.1336C2.31982 11.1192 2.34297 11.1058 2.36698 11.0934C2.44754 11.048 2.53017 11.0059 2.6147 10.9673L6.47894 9.00002L2.61463 7.03274C2.53015 6.99409 2.44757 6.95201 2.36706 6.90668C2.34304 6.89426 2.31988 6.88083 2.2976 6.86647ZM2.33329 4.00002C2.33329 3.07955 3.07948 2.33335 3.99996 2.33335C4.92043 2.33335 5.66663 3.07955 5.66663 4.00002C5.66663 4.9205 4.92043 5.66669 3.99996 5.66669C3.76081 5.66669 3.53342 5.61632 3.32783 5.52561L3.16412 5.44226C2.66733 5.15373 2.33329 4.61589 2.33329 4.00002ZM3.16407 12.5578C2.66731 12.8463 2.33329 13.3842 2.33329 14C2.33329 14.9205 3.07949 15.6667 3.99996 15.6667C4.92043 15.6667 5.66663 14.9205 5.66663 14C5.66663 13.0795 4.92043 12.3334 3.99996 12.3334C3.76083 12.3334 3.53346 12.3837 3.32788 12.4744L3.16407 12.5578Z" fill="currentColor"/>
|
||||||
|
<path d="M12.75 9.00002C12.75 8.53978 13.1231 8.16669 13.5833 8.16669H13.5916C14.0519 8.16669 14.425 8.53978 14.425 9.00002C14.425 9.46026 14.0519 9.83335 13.5916 9.83335H13.5833C13.1231 9.83335 12.75 9.46026 12.75 9.00002Z" fill="currentColor"/>
|
||||||
|
<path d="M17.3333 8.16669C16.8731 8.16669 16.5 8.53978 16.5 9.00002C16.5 9.46026 16.8731 9.83335 17.3333 9.83335H17.3416C17.8019 9.83335 18.175 9.46026 18.175 9.00002C18.175 8.53978 17.8019 8.16669 17.3416 8.16669H17.3333Z" fill="currentColor"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="share" viewBox="0 0 18 18" fill="none">
|
||||||
|
<path d="M10.6666 1.50002C10.6666 1.03978 11.0397 0.666687 11.5 0.666687H16.5C16.9602 0.666687 17.3333 1.03978 17.3333 1.50002L17.3333 6.50002C17.3333 6.96026 16.9602 7.33335 16.5 7.33335C16.0397 7.33335 15.6666 6.96026 15.6666 6.50002L15.6666 3.51187L10.4225 8.75594C10.0971 9.08138 9.56947 9.08138 9.24404 8.75594C8.9186 8.43051 8.9186 7.90287 9.24404 7.57743L14.4881 2.33335H11.5C11.0397 2.33335 10.6666 1.96026 10.6666 1.50002Z" fill="currentColor"/>
|
||||||
|
<path d="M5.46554 2.33335L7.33329 2.33335C7.79353 2.33335 8.16663 2.70645 8.16663 3.16669C8.16663 3.62693 7.79353 4.00002 7.33329 4.00002H5.49996C4.78614 4.00002 4.3009 4.00067 3.92583 4.03131C3.56048 4.06116 3.37364 4.11527 3.24331 4.18168C2.92971 4.34147 2.67474 4.59643 2.51495 4.91004C2.44854 5.04037 2.39444 5.22721 2.36459 5.59255C2.33394 5.96763 2.33329 6.45287 2.33329 7.16669V12.5C2.33329 13.2138 2.33394 13.6991 2.36459 14.0742C2.39444 14.4395 2.44854 14.6263 2.51495 14.7567C2.67474 15.0703 2.92971 15.3252 3.24331 15.485C3.37364 15.5514 3.56048 15.6055 3.92583 15.6354C4.3009 15.666 4.78614 15.6667 5.49996 15.6667H10.8333C11.5471 15.6667 12.0324 15.666 12.4074 15.6354C12.7728 15.6055 12.9596 15.5514 13.0899 15.485C13.4035 15.3252 13.6585 15.0703 13.8183 14.7567C13.8847 14.6263 13.9388 14.4395 13.9687 14.0742C13.9993 13.6991 14 13.2138 14 12.5V10.6667C14 10.2065 14.3731 9.83335 14.8333 9.83335C15.2935 9.83335 15.6666 10.2065 15.6666 10.6667V12.5345C15.6666 13.2053 15.6666 13.7589 15.6298 14.2099C15.5915 14.6783 15.5094 15.1089 15.3033 15.5133C14.9837 16.1405 14.4738 16.6505 13.8466 16.97C13.4421 17.1761 13.0116 17.2583 12.5431 17.2965C12.0922 17.3334 11.5385 17.3334 10.8677 17.3334H5.46552C4.79472 17.3334 4.2411 17.3334 3.79011 17.2965C3.32169 17.2583 2.89111 17.1761 2.48666 16.97C1.85945 16.6505 1.34952 16.1405 1.02994 15.5133C0.823863 15.1089 0.741726 14.6783 0.703455 14.2099C0.666608 13.7589 0.666616 13.2053 0.666626 12.5344V7.13227C0.666616 6.46147 0.666608 5.90783 0.703455 5.45683C0.741726 4.98842 0.823863 4.55783 1.02994 4.15339C1.34952 3.52618 1.85945 3.01624 2.48666 2.69667C2.89111 2.49059 3.32169 2.40845 3.79011 2.37018C4.2411 2.33333 4.79474 2.33334 5.46554 2.33335Z" fill="currentColor"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="send" viewBox="0 0 22 20" fill="none">
|
||||||
|
<path d="M9.50043 10H4.00043M3.91577 10.2915L1.58085 17.2662C1.39742 17.8142 1.3057 18.0881 1.37152 18.2569C1.42868 18.4034 1.55144 18.5145 1.70292 18.5567C1.87736 18.6054 2.14083 18.4869 2.66776 18.2497L19.3792 10.7296C19.8936 10.4981 20.1507 10.3824 20.2302 10.2216C20.2993 10.082 20.2993 9.9181 20.2302 9.77843C20.1507 9.61767 19.8936 9.50195 19.3792 9.2705L2.66193 1.74776C2.13659 1.51135 1.87392 1.39315 1.69966 1.44164C1.54832 1.48375 1.42556 1.59454 1.36821 1.74078C1.30216 1.90917 1.3929 2.18255 1.57437 2.72931L3.91642 9.78556C3.94759 9.87947 3.96317 9.92642 3.96933 9.97444C3.97479 10.0171 3.97473 10.0602 3.96916 10.1028C3.96289 10.1508 3.94718 10.1977 3.91577 10.2915Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="plus-circle" viewBox="0 0 22 22" fill="none" >
|
||||||
|
<path d="M11 7V15M7 11H15M21 11C21 16.5228 16.5228 21 11 21C5.47715 21 1 16.5228 1 11C1 5.47715 5.47715 1 11 1C16.5228 1 21 5.47715 21 11Z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</symbol>
|
||||||
|
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 58 KiB |
@ -8,7 +8,9 @@ body {
|
|||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
color: white;
|
color: white;
|
||||||
|
overscroll-behavior: contain;
|
||||||
@apply bg-layer-0;
|
@apply bg-layer-0;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
@ -36,12 +38,16 @@ body {
|
|||||||
.btn-border {
|
.btn-border {
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
background: linear-gradient(black, black) padding-box, linear-gradient(94.73deg, #2bd9ff 0%, #f838d9 100%) border-box;
|
background:
|
||||||
|
linear-gradient(black, black) padding-box,
|
||||||
|
linear-gradient(94.73deg, #2bd9ff 0%, #f838d9 100%) border-box;
|
||||||
transition: 0.3s;
|
transition: 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-border:hover {
|
.btn-border:hover {
|
||||||
background: linear-gradient(black, black) padding-box, linear-gradient(94.73deg, #14b4d8 0%, #ba179f 100%) border-box;
|
background:
|
||||||
|
linear-gradient(black, black) padding-box,
|
||||||
|
linear-gradient(94.73deg, #14b4d8 0%, #ba179f 100%) border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
|
@ -22,7 +22,6 @@ import { StreamSummaryPage } from "@/pages/summary";
|
|||||||
import { EmbededPage } from "./pages/embed";
|
import { EmbededPage } from "./pages/embed";
|
||||||
import { WasmOptimizer, WasmPath, wasmInit } from "./wasm";
|
import { WasmOptimizer, WasmPath, wasmInit } from "./wasm";
|
||||||
const DashboardPage = lazy(() => import("./pages/dashboard"));
|
const DashboardPage = lazy(() => import("./pages/dashboard"));
|
||||||
import MockPage from "./pages/mock";
|
|
||||||
import { syncClock } from "./time-sync";
|
import { syncClock } from "./time-sync";
|
||||||
import SettingsPage from "./pages/settings";
|
import SettingsPage from "./pages/settings";
|
||||||
import AccountSettingsTab from "./pages/settings/account";
|
import AccountSettingsTab from "./pages/settings/account";
|
||||||
@ -45,7 +44,7 @@ import { UploadPage } from "./pages/upload";
|
|||||||
|
|
||||||
const hasWasm = "WebAssembly" in globalThis;
|
const hasWasm = "WebAssembly" in globalThis;
|
||||||
const workerRelay = new WorkerRelayInterface(
|
const workerRelay = new WorkerRelayInterface(
|
||||||
import.meta.env.DEV ? new URL("@snort/worker-relay/dist/esm/worker.mjs", import.meta.url) : new WorkerVite()
|
import.meta.env.DEV ? new URL("@snort/worker-relay/dist/esm/worker.mjs", import.meta.url) : new WorkerVite(),
|
||||||
);
|
);
|
||||||
const System = new NostrSystem({
|
const System = new NostrSystem({
|
||||||
optimizer: hasWasm ? WasmOptimizer : undefined,
|
optimizer: hasWasm ? WasmOptimizer : undefined,
|
||||||
@ -88,10 +87,6 @@ const router = createBrowserRouter([
|
|||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
|
||||||
path: "/mock",
|
|
||||||
element: <MockPage />,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/",
|
path: "/",
|
||||||
element: <RootPage />,
|
element: <RootPage />,
|
||||||
@ -227,5 +222,5 @@ root.render(
|
|||||||
</LayoutContextProvider>
|
</LayoutContextProvider>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
</SnortContext.Provider>
|
</SnortContext.Provider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>,
|
||||||
);
|
);
|
||||||
|
@ -65,6 +65,9 @@
|
|||||||
"2/2yg+": {
|
"2/2yg+": {
|
||||||
"defaultMessage": "Add"
|
"defaultMessage": "Add"
|
||||||
},
|
},
|
||||||
|
"2lVQYF": {
|
||||||
|
"defaultMessage": "...more"
|
||||||
|
},
|
||||||
"2ukA4d": {
|
"2ukA4d": {
|
||||||
"defaultMessage": "{n} hours"
|
"defaultMessage": "{n} hours"
|
||||||
},
|
},
|
||||||
@ -92,6 +95,9 @@
|
|||||||
"4RhY4O": {
|
"4RhY4O": {
|
||||||
"defaultMessage": "Example settings in OBS (Apple M1 Mac)"
|
"defaultMessage": "Example settings in OBS (Apple M1 Mac)"
|
||||||
},
|
},
|
||||||
|
"4XfMux": {
|
||||||
|
"defaultMessage": "Videos"
|
||||||
|
},
|
||||||
"4iBdw1": {
|
"4iBdw1": {
|
||||||
"defaultMessage": "Raid"
|
"defaultMessage": "Raid"
|
||||||
},
|
},
|
||||||
@ -197,9 +203,6 @@
|
|||||||
"BD0vyn": {
|
"BD0vyn": {
|
||||||
"defaultMessage": "{name} created a clip"
|
"defaultMessage": "{name} created a clip"
|
||||||
},
|
},
|
||||||
"BGxpTN": {
|
|
||||||
"defaultMessage": "Stream Chat"
|
|
||||||
},
|
|
||||||
"Bd1yEX": {
|
"Bd1yEX": {
|
||||||
"defaultMessage": "New Stream Goal"
|
"defaultMessage": "New Stream Goal"
|
||||||
},
|
},
|
||||||
@ -263,6 +266,9 @@
|
|||||||
"GGaJMU": {
|
"GGaJMU": {
|
||||||
"defaultMessage": "Top Chatters"
|
"defaultMessage": "Top Chatters"
|
||||||
},
|
},
|
||||||
|
"GSuQPh": {
|
||||||
|
"defaultMessage": "Unknown event link"
|
||||||
|
},
|
||||||
"GSye7T": {
|
"GSye7T": {
|
||||||
"defaultMessage": "Lightning Address"
|
"defaultMessage": "Lightning Address"
|
||||||
},
|
},
|
||||||
@ -275,6 +281,9 @@
|
|||||||
"Gvxoji": {
|
"Gvxoji": {
|
||||||
"defaultMessage": "Name is required"
|
"defaultMessage": "Name is required"
|
||||||
},
|
},
|
||||||
|
"GwbTAz": {
|
||||||
|
"defaultMessage": "Streams"
|
||||||
|
},
|
||||||
"H/bNs9": {
|
"H/bNs9": {
|
||||||
"defaultMessage": "Save this and keep it safe! If you lose this key, you won't be able to access your account ever again. Yep, it's that serious!"
|
"defaultMessage": "Save this and keep it safe! If you lose this key, you won't be able to access your account ever again. Yep, it's that serious!"
|
||||||
},
|
},
|
||||||
@ -386,9 +395,6 @@
|
|||||||
"Oxqtyf": {
|
"Oxqtyf": {
|
||||||
"defaultMessage": "We hooked you up with a lightning wallet so you can get paid by viewers right away!"
|
"defaultMessage": "We hooked you up with a lightning wallet so you can get paid by viewers right away!"
|
||||||
},
|
},
|
||||||
"PA0ej4": {
|
|
||||||
"defaultMessage": "Create Clip"
|
|
||||||
},
|
|
||||||
"PHE60k": {
|
"PHE60k": {
|
||||||
"defaultMessage": "Leave blank if you do not wish to set up any goals."
|
"defaultMessage": "Leave blank if you do not wish to set up any goals."
|
||||||
},
|
},
|
||||||
@ -485,9 +491,6 @@
|
|||||||
"W7DNWx": {
|
"W7DNWx": {
|
||||||
"defaultMessage": "Stream Forwarding"
|
"defaultMessage": "Stream Forwarding"
|
||||||
},
|
},
|
||||||
"W8nHSd": {
|
|
||||||
"defaultMessage": "FAQ"
|
|
||||||
},
|
|
||||||
"W9355R": {
|
"W9355R": {
|
||||||
"defaultMessage": "Unmute"
|
"defaultMessage": "Unmute"
|
||||||
},
|
},
|
||||||
@ -635,6 +638,9 @@
|
|||||||
"hpl4BP": {
|
"hpl4BP": {
|
||||||
"defaultMessage": "Chat Widget"
|
"defaultMessage": "Chat Widget"
|
||||||
},
|
},
|
||||||
|
"hzSNj4": {
|
||||||
|
"defaultMessage": "Dashboard"
|
||||||
|
},
|
||||||
"ieGrWo": {
|
"ieGrWo": {
|
||||||
"defaultMessage": "Follow"
|
"defaultMessage": "Follow"
|
||||||
},
|
},
|
||||||
@ -804,6 +810,9 @@
|
|||||||
"wCIL7o": {
|
"wCIL7o": {
|
||||||
"defaultMessage": "Broadcast on Nostr"
|
"defaultMessage": "Broadcast on Nostr"
|
||||||
},
|
},
|
||||||
|
"wCgTu5": {
|
||||||
|
"defaultMessage": "Comments"
|
||||||
|
},
|
||||||
"wEQDC6": {
|
"wEQDC6": {
|
||||||
"defaultMessage": "Edit"
|
"defaultMessage": "Edit"
|
||||||
},
|
},
|
||||||
@ -819,9 +828,6 @@
|
|||||||
"we4Lby": {
|
"we4Lby": {
|
||||||
"defaultMessage": "Info"
|
"defaultMessage": "Info"
|
||||||
},
|
},
|
||||||
"wzWWzV": {
|
|
||||||
"defaultMessage": "Top zappers"
|
|
||||||
},
|
|
||||||
"x82IOl": {
|
"x82IOl": {
|
||||||
"defaultMessage": "Mute"
|
"defaultMessage": "Mute"
|
||||||
},
|
},
|
||||||
@ -840,6 +846,9 @@
|
|||||||
"yR0V+W": {
|
"yR0V+W": {
|
||||||
"defaultMessage": "To start streaming on zap.stream, follow these steps:"
|
"defaultMessage": "To start streaming on zap.stream, follow these steps:"
|
||||||
},
|
},
|
||||||
|
"yj8NrY": {
|
||||||
|
"defaultMessage": "Clip"
|
||||||
|
},
|
||||||
"yzKwBQ": {
|
"yzKwBQ": {
|
||||||
"defaultMessage": "eg. nsec1xyz"
|
"defaultMessage": "eg. nsec1xyz"
|
||||||
},
|
},
|
||||||
|
@ -82,11 +82,13 @@ export default function Category() {
|
|||||||
|
|
||||||
const results = useRequestBuilder(sub);
|
const results = useRequestBuilder(sub);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="px-2 p-4">
|
||||||
<div className="flex gap-4 overflow-x-scroll scrollbar-hidden">
|
<div className="px-2 min-w-0">
|
||||||
{AllCategories.map(a => (
|
<div className="flex gap-4 overflow-x-scroll scrollbar-hidden">
|
||||||
<CategoryLink key={a.id} id={a.id} name={a.name} icon={a.icon} />
|
{AllCategories.map(a => (
|
||||||
))}
|
<CategoryLink key={a.id} id={a.id} name={a.name} icon={a.icon} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{id && (
|
{id && (
|
||||||
<div className="flex gap-4 py-8">
|
<div className="flex gap-4 py-8">
|
||||||
|
@ -2,7 +2,7 @@ import { useParams } from "react-router-dom";
|
|||||||
import { NostrPrefix, encodeTLV, parseNostrLink } from "@snort/system";
|
import { NostrPrefix, encodeTLV, parseNostrLink } from "@snort/system";
|
||||||
import { unwrap } from "@snort/shared";
|
import { unwrap } from "@snort/shared";
|
||||||
|
|
||||||
import { LiveChat } from "@/element/live-chat";
|
import { LiveChat } from "@/element/chat/live-chat";
|
||||||
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
|
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
|
||||||
import { findTag } from "@/utils";
|
import { findTag } from "@/utils";
|
||||||
import { useZapGoal } from "@/hooks/goals";
|
import { useZapGoal } from "@/hooks/goals";
|
||||||
@ -21,7 +21,6 @@ export function ChatPopout() {
|
|||||||
ev={ev}
|
ev={ev}
|
||||||
link={lnk}
|
link={lnk}
|
||||||
canWrite={chat}
|
canWrite={chat}
|
||||||
showHeader={false}
|
|
||||||
showScrollbar={false}
|
showScrollbar={false}
|
||||||
goal={goal}
|
goal={goal}
|
||||||
className="h-inherit"
|
className="h-inherit"
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { ChatZap } from "@/element/live-chat";
|
import { ChatZap } from "@/element/chat/live-chat";
|
||||||
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||||
import { useEventReactions } from "@snort/system-react";
|
import { useEventReactions } from "@snort/system-react";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
@ -20,7 +20,7 @@ export function DashboardZapColumn({
|
|||||||
const reactions = useEventReactions(link, feed);
|
const reactions = useEventReactions(link, feed);
|
||||||
const sortedZaps = useMemo(
|
const sortedZaps = useMemo(
|
||||||
() => reactions.zaps.sort((a, b) => (b.created_at > a.created_at ? 1 : -1)),
|
() => reactions.zaps.sort((a, b) => (b.created_at > a.created_at ? 1 : -1)),
|
||||||
[reactions.zaps]
|
[reactions.zaps],
|
||||||
);
|
);
|
||||||
const latestZap = sortedZaps.at(0);
|
const latestZap = sortedZaps.at(0);
|
||||||
const zapSum = sortedZaps.reduce((acc, v) => acc + v.amount, 0);
|
const zapSum = sortedZaps.reduce((acc, v) => acc + v.amount, 0);
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
import { LiveChat } from "@/element/live-chat";
|
import { LiveChat } from "@/element/chat/live-chat";
|
||||||
import LiveVideoPlayer from "@/element/live-video-player";
|
import LiveVideoPlayer from "@/element/stream/live-video-player";
|
||||||
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
|
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
|
||||||
import { extractStreamInfo } from "@/utils";
|
import { extractStreamInfo } from "@/utils";
|
||||||
import { EventExt, NostrEvent, NostrLink } from "@snort/system";
|
import { EventExt, NostrEvent, NostrLink } from "@snort/system";
|
||||||
import { SnortContext, useReactions } from "@snort/system-react";
|
import { SnortContext, useReactions } from "@snort/system-react";
|
||||||
import { Suspense, lazy, useContext, useEffect, useMemo, useState } from "react";
|
import { Suspense, lazy, useContext, useEffect, useMemo, useState } from "react";
|
||||||
import { FormattedMessage, FormattedNumber } from "react-intl";
|
import { FormattedMessage, FormattedNumber } from "react-intl";
|
||||||
import { StreamTimer } from "@/element/stream-time";
|
import { StreamTimer } from "@/element/stream/stream-time";
|
||||||
import { LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP, StreamState } from "@/const";
|
import { LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP, StreamState } from "@/const";
|
||||||
import { DashboardRaidButton } from "./button-raid";
|
import { DashboardRaidButton } from "./button-raid";
|
||||||
import { DashboardZapColumn } from "./column-zaps";
|
import { DashboardZapColumn } from "./column-zaps";
|
||||||
@ -71,7 +71,7 @@ export function DashboardForLink({ link }: { link: NostrLink }) {
|
|||||||
rb.withFilter().kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP]).replyToLink([streamLink]);
|
rb.withFilter().kinds([LIVE_STREAM_CHAT, LIVE_STREAM_RAID, LIVE_STREAM_CLIP]).replyToLink([streamLink]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
true
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!streamLink && !location.search.includes("setupComplete=true")) return <DashboardIntro />;
|
if (!streamLink && !location.search.includes("setupComplete=true")) return <DashboardIntro />;
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { StreamState } from "@/const";
|
import { StreamState } from "@/const";
|
||||||
import LiveVideoPlayer from "@/element/live-video-player";
|
import LiveVideoPlayer from "@/element/stream/live-video-player";
|
||||||
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
|
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
|
||||||
import { useStreamLink } from "@/hooks/stream-link";
|
import { useStreamLink } from "@/hooks/stream-link";
|
||||||
import { extractStreamInfo, trackEvent } from "@/utils";
|
import { extractStreamInfo, trackEvent } from "@/utils";
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
import { ReactNode, createContext, useState } from "react";
|
import { ReactNode, createContext, useContext, useState } from "react";
|
||||||
|
|
||||||
interface LayoutContextType {
|
interface LayoutContextType {
|
||||||
leftNav: boolean;
|
leftNav: boolean;
|
||||||
|
leftNavExpand: boolean;
|
||||||
|
showHeader: boolean;
|
||||||
theme: string;
|
theme: string;
|
||||||
update: (fn: (c: LayoutContextType) => LayoutContextType) => void;
|
update: (fn: (c: LayoutContextType) => LayoutContextType) => void;
|
||||||
}
|
}
|
||||||
const defaultLayoutContext: LayoutContextType = {
|
const defaultLayoutContext: LayoutContextType = {
|
||||||
leftNav: true,
|
leftNav: true,
|
||||||
|
leftNavExpand: false,
|
||||||
|
showHeader: true,
|
||||||
theme: "",
|
theme: "",
|
||||||
update: c => c,
|
update: c => c,
|
||||||
};
|
};
|
||||||
export const LayoutContext = createContext<LayoutContextType>(defaultLayoutContext);
|
const LayoutContext = createContext<LayoutContextType>(defaultLayoutContext);
|
||||||
|
|
||||||
export function LayoutContextProvider({ children }: { children: ReactNode }) {
|
export function LayoutContextProvider({ children }: { children: ReactNode }) {
|
||||||
const [value, setValue] = useState<LayoutContextType>(defaultLayoutContext);
|
const [value, setValue] = useState<LayoutContextType>(defaultLayoutContext);
|
||||||
@ -26,3 +30,7 @@ export function LayoutContextProvider({ children }: { children: ReactNode }) {
|
|||||||
</LayoutContext.Provider>
|
</LayoutContext.Provider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useLayout() {
|
||||||
|
return useContext(LayoutContext);
|
||||||
|
}
|
||||||
|
@ -11,11 +11,11 @@ import { FormattedMessage } from "react-intl";
|
|||||||
import { Link, useNavigate } from "react-router-dom";
|
import { Link, useNavigate } from "react-router-dom";
|
||||||
import { useLang } from "@/hooks/lang";
|
import { useLang } from "@/hooks/lang";
|
||||||
import { useLogin } from "@/hooks/login";
|
import { useLogin } from "@/hooks/login";
|
||||||
import { useContext, useState } from "react";
|
import { useState } from "react";
|
||||||
import { Profile } from "@/element/profile";
|
import { Profile } from "@/element/profile";
|
||||||
import { SearchBar } from "./search";
|
import { SearchBar } from "./search";
|
||||||
import { NavLinkIcon } from "./nav-icon";
|
import { NavLinkIcon } from "./nav-icon";
|
||||||
import { LayoutContext } from "./context";
|
import { useLayout } from "./context";
|
||||||
|
|
||||||
export function HeaderNav() {
|
export function HeaderNav() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -23,7 +23,7 @@ export function HeaderNav() {
|
|||||||
const [showLogin, setShowLogin] = useState(false);
|
const [showLogin, setShowLogin] = useState(false);
|
||||||
const { lang, setLang } = useLang();
|
const { lang, setLang } = useLang();
|
||||||
const country = lang.split(/[-_]/i)[1]?.toLowerCase();
|
const country = lang.split(/[-_]/i)[1]?.toLowerCase();
|
||||||
const layoutState = useContext(LayoutContext);
|
const layoutState = useLayout();
|
||||||
|
|
||||||
function langSelector() {
|
function langSelector() {
|
||||||
return (
|
return (
|
||||||
@ -54,16 +54,24 @@ export function HeaderNav() {
|
|||||||
if (!login) return;
|
if (!login) return;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-3 items-center pr-4 py-1">
|
<div className="flex gap-2 items-center pr-4 py-1">
|
||||||
{(!import.meta.env.VITE_SINGLE_PUBLISHER || import.meta.env.VITE_SINGLE_PUBLISHER === login.pubkey) && (
|
{(!import.meta.env.VITE_SINGLE_PUBLISHER || import.meta.env.VITE_SINGLE_PUBLISHER === login.pubkey) && (
|
||||||
<>
|
<Menu
|
||||||
<Link to="/upload">
|
menuClassName="ctx-menu"
|
||||||
<IconButton iconName="upload" iconSize={20} className="px-3 py-2 hover:bg-layer-1 rounded-xl" />
|
menuButton={
|
||||||
</Link>
|
<IconButton iconName="plus-circle" iconSize={20} className="px-3 py-2 hover:bg-layer-1 rounded-xl" />
|
||||||
<Link to="/dashboard">
|
}
|
||||||
<IconButton iconName="signal" iconSize={20} className="px-3 py-2 hover:bg-layer-1 rounded-xl" />
|
align="end"
|
||||||
</Link>
|
gap={5}>
|
||||||
</>
|
<MenuItem onClick={() => navigate("/upload")}>
|
||||||
|
<Icon name="upload" size={24} />
|
||||||
|
<FormattedMessage defaultMessage="Upload" />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => navigate("/dashboard")}>
|
||||||
|
<Icon name="signal" size={24} />
|
||||||
|
<FormattedMessage defaultMessage="Dashboard" />
|
||||||
|
</MenuItem>
|
||||||
|
</Menu>
|
||||||
)}
|
)}
|
||||||
<Menu
|
<Menu
|
||||||
menuClassName="ctx-menu"
|
menuClassName="ctx-menu"
|
||||||
@ -83,19 +91,23 @@ export function HeaderNav() {
|
|||||||
gap={5}>
|
gap={5}>
|
||||||
<MenuItem onClick={() => navigate(profileLink(undefined, login.pubkey))}>
|
<MenuItem onClick={() => navigate(profileLink(undefined, login.pubkey))}>
|
||||||
<Icon name="user" size={24} />
|
<Icon name="user" size={24} />
|
||||||
<FormattedMessage defaultMessage="Profile" id="itPgxd" />
|
<FormattedMessage defaultMessage="Profile" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={() => navigate("/settings")}>
|
<MenuItem onClick={() => navigate("/settings")}>
|
||||||
<Icon name="settings" size={24} />
|
<Icon name="settings" size={24} />
|
||||||
<FormattedMessage defaultMessage="Settings" id="D3idYv" />
|
<FormattedMessage defaultMessage="Settings" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={() => navigate("/widgets")}>
|
<MenuItem onClick={() => navigate("/widgets")}>
|
||||||
<Icon name="widget" size={24} />
|
<Icon name="widget" size={24} />
|
||||||
<FormattedMessage defaultMessage="Widgets" id="jgOqxt" />
|
<FormattedMessage defaultMessage="Widgets" />
|
||||||
|
</MenuItem>
|
||||||
|
<MenuItem onClick={() => window.open("https://discord.gg/Wtg6NVDdbT")}>
|
||||||
|
<Icon name="link" size={24} />
|
||||||
|
Discord
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem onClick={() => Login.logout()}>
|
<MenuItem onClick={() => Login.logout()}>
|
||||||
<Icon name="logout" size={24} />
|
<Icon name="logout" size={24} />
|
||||||
<FormattedMessage defaultMessage="Logout" id="C81/uG" />
|
<FormattedMessage defaultMessage="Logout" />
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
</Menu>
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
@ -123,32 +135,28 @@ export function HeaderNav() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!layoutState.showHeader) return;
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center gap-4">
|
||||||
<div className="flex gap-4 items-center m-2">
|
<div className="flex gap-4 items-center m-2">
|
||||||
<NavLinkIcon
|
{layoutState.leftNav && (
|
||||||
name="hamburger"
|
<NavLinkIcon
|
||||||
className="!opacity-100 max-xl:hidden"
|
name="hamburger"
|
||||||
onClick={() => {
|
className="!opacity-100 max-xl:hidden"
|
||||||
layoutState.update(c => {
|
onClick={() => {
|
||||||
c.leftNav = !c.leftNav;
|
layoutState.update(c => {
|
||||||
return { ...c };
|
c.leftNavExpand = !c.leftNavExpand;
|
||||||
});
|
return { ...c };
|
||||||
}}
|
});
|
||||||
/>
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Link to="/">
|
<Link to="/">
|
||||||
<Logo width={33} />
|
<Logo width={33} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<SearchBar />
|
<SearchBar />
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<Link
|
|
||||||
to="https://discord.gg/Wtg6NVDdbT"
|
|
||||||
target="_blank"
|
|
||||||
className="flex items-center max-md:hidden gap-1 bg-layer-1 hover:bg-layer-2 font-bold p-2 rounded-xl">
|
|
||||||
<Icon name="link" />
|
|
||||||
Discord
|
|
||||||
</Link>
|
|
||||||
{langSelector()}
|
{langSelector()}
|
||||||
{loggedIn()}
|
{loggedIn()}
|
||||||
{loggedOut()}
|
{loggedOut()}
|
||||||
|
@ -1,29 +1,29 @@
|
|||||||
import { useContext } from "react";
|
import { useLayout } from "./context";
|
||||||
import { LayoutContext } from "./context";
|
|
||||||
import { NavLinkIcon } from "./nav-icon";
|
import { NavLinkIcon } from "./nav-icon";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
|
||||||
export function LeftNav() {
|
export function LeftNav() {
|
||||||
const layout = useContext(LayoutContext);
|
const layout = useLayout();
|
||||||
|
|
||||||
|
if (layout.leftNav === false) return;
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 p-2 max-xl:hidden">
|
<div className="flex flex-col gap-4 p-2 max-xl:hidden">
|
||||||
<NavLinkIcon name="signal" route="/streams" className="flex gap-2 items-center">
|
<NavLinkIcon name="signal" route="/streams" className="flex gap-2 items-center">
|
||||||
{layout.leftNav && (
|
{layout.leftNavExpand && (
|
||||||
<span className="pr-3">
|
<span className="pr-3">
|
||||||
<FormattedMessage defaultMessage="Streams" />
|
<FormattedMessage defaultMessage="Streams" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</NavLinkIcon>
|
</NavLinkIcon>
|
||||||
<NavLinkIcon name="play-circle" route="/videos" className="flex gap-2 items-center">
|
<NavLinkIcon name="play-circle" route="/videos" className="flex gap-2 items-center">
|
||||||
{layout.leftNav && (
|
{layout.leftNavExpand && (
|
||||||
<span className="pr-3">
|
<span className="pr-3">
|
||||||
<FormattedMessage defaultMessage="Videos" />
|
<FormattedMessage defaultMessage="Videos" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</NavLinkIcon>
|
</NavLinkIcon>
|
||||||
<NavLinkIcon name="grid" route="/category" className="flex gap-2 items-center">
|
<NavLinkIcon name="grid" route="/category" className="flex gap-2 items-center">
|
||||||
{layout.leftNav && (
|
{layout.leftNavExpand && (
|
||||||
<span className="pr-3">
|
<span className="pr-3">
|
||||||
<FormattedMessage defaultMessage="Categories" />
|
<FormattedMessage defaultMessage="Categories" />
|
||||||
</span>
|
</span>
|
||||||
|
@ -10,13 +10,12 @@ export function SearchBar() {
|
|||||||
const [search, setSearch] = useState(term ?? "");
|
const [search, setSearch] = useState(term ?? "");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="pr-4 h-fit flex items-center rounded-full px-3 py-1 lg:border lg:border-layer-2">
|
<div className="pr-4 h-fit flex items-center rounded-full px-3 py-1 border border-layer-2 max-xl:min-w-0">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="reset max-lg:hidden bg-transparent"
|
className="reset bg-transparent"
|
||||||
placeholder={formatMessage({
|
placeholder={formatMessage({
|
||||||
defaultMessage: "Search",
|
defaultMessage: "Search",
|
||||||
id: "xmcVZ0",
|
|
||||||
})}
|
})}
|
||||||
value={search}
|
value={search}
|
||||||
onChange={e => setSearch(e.target.value)}
|
onChange={e => setSearch(e.target.value)}
|
||||||
|
@ -7,11 +7,14 @@ import { StreamPage } from "./stream-page";
|
|||||||
import { VideoPage } from "./video";
|
import { VideoPage } from "./video";
|
||||||
import { EventEmbed as NostrEventElement } from "@/element/event-embed";
|
import { EventEmbed as NostrEventElement } from "@/element/event-embed";
|
||||||
import { FormattedMessage } from "react-intl";
|
import { FormattedMessage } from "react-intl";
|
||||||
|
import { useLayout } from "./layout/context";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
export function LinkHandler() {
|
export function LinkHandler() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const evPreload = getEventFromLocationState(location.state);
|
const evPreload = getEventFromLocationState(location.state);
|
||||||
const link = useStreamLink();
|
const link = useStreamLink();
|
||||||
|
const layoutContext = useLayout();
|
||||||
|
|
||||||
if (!link) return;
|
if (!link) return;
|
||||||
|
|
||||||
@ -23,7 +26,7 @@ export function LinkHandler() {
|
|||||||
);
|
);
|
||||||
} else if (link.kind === EventKind.LiveEvent) {
|
} else if (link.kind === EventKind.LiveEvent) {
|
||||||
return (
|
return (
|
||||||
<div className="h-[calc(100dvh-52px)] w-full">
|
<div className={classNames(layoutContext.showHeader ? "h-[calc(100dvh-44px)]" : "h-[calc(100dvh)]", "w-full")}>
|
||||||
<StreamPage link={link} evPreload={evPreload} />
|
<StreamPage link={link} evPreload={evPreload} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,27 +0,0 @@
|
|||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
@ -23,7 +23,7 @@ import { Goal } from "@/element/goal";
|
|||||||
import { TopZappers } from "@/element/top-zappers";
|
import { TopZappers } from "@/element/top-zappers";
|
||||||
import { useProfileClips } from "@/hooks/clips";
|
import { useProfileClips } from "@/hooks/clips";
|
||||||
import VideoGrid from "@/element/video-grid";
|
import VideoGrid from "@/element/video-grid";
|
||||||
import { ClipTile } from "@/element/clip-tile";
|
import { ClipTile } from "@/element/stream/clip-tile";
|
||||||
|
|
||||||
const defaultBanner = "https://void.cat/d/Hn1AdN5UKmceuDkgDW847q.webp";
|
const defaultBanner = "https://void.cat/d/Hn1AdN5UKmceuDkgDW847q.webp";
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ export function RootPage() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<VideoGridSorted evs={streams} showEnded={false} showPopular={true} showRecentClips={true} />
|
<VideoGridSorted evs={streams} showEnded={false} showPopular={true} showRecentClips={false} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,19 @@
|
|||||||
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||||
import { Helmet } from "react-helmet";
|
import { Helmet } from "react-helmet";
|
||||||
import { Suspense, lazy } from "react";
|
import { Suspense, lazy, useEffect } from "react";
|
||||||
import { useMediaQuery } from "usehooks-ts";
|
import { useMediaQuery } from "usehooks-ts";
|
||||||
|
|
||||||
const LiveVideoPlayer = lazy(() => import("@/element/live-video-player"));
|
const LiveVideoPlayer = lazy(() => import("@/element/stream/live-video-player"));
|
||||||
import { extractStreamInfo, getHost } from "@/utils";
|
import { extractStreamInfo, getHost } from "@/utils";
|
||||||
import { LiveChat } from "@/element/live-chat";
|
import { LiveChat } from "@/element/chat/live-chat";
|
||||||
import { useZapGoal } from "@/hooks/goals";
|
import { useZapGoal } from "@/hooks/goals";
|
||||||
import { StreamCards } from "@/element/stream-cards";
|
import { StreamCards } from "@/element/stream-cards";
|
||||||
import { ContentWarningOverlay, useContentWarning } from "@/element/nsfw";
|
import { ContentWarningOverlay, useContentWarning } from "@/element/nsfw";
|
||||||
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
|
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
|
||||||
import { StreamState } from "@/const";
|
import { StreamState } from "@/const";
|
||||||
import { StreamInfo } from "@/element/stream-info";
|
import { StreamInfo } from "@/element/stream/stream-info";
|
||||||
|
import { useLayout } from "./layout/context";
|
||||||
|
import { StreamContextProvider } from "@/element/stream/stream-state";
|
||||||
|
|
||||||
export function StreamPage({ link, evPreload }: { evPreload?: TaggedNostrEvent; link: NostrLink }) {
|
export function StreamPage({ link, evPreload }: { evPreload?: TaggedNostrEvent; link: NostrLink }) {
|
||||||
const ev = useCurrentStreamFeed(link, true, evPreload);
|
const ev = useCurrentStreamFeed(link, true, evPreload);
|
||||||
@ -31,6 +33,25 @@ export function StreamPage({ link, evPreload }: { evPreload?: TaggedNostrEvent;
|
|||||||
const goal = useZapGoal(goalTag);
|
const goal = useZapGoal(goalTag);
|
||||||
const isDesktop = useMediaQuery("(min-width: 1280px)");
|
const isDesktop = useMediaQuery("(min-width: 1280px)");
|
||||||
const isGrownUp = useContentWarning();
|
const isGrownUp = useContentWarning();
|
||||||
|
const layout = useLayout();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (layout.leftNav) {
|
||||||
|
layout.update(c => {
|
||||||
|
c.leftNav = false;
|
||||||
|
return { ...c };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [layout]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
layout.update(c => {
|
||||||
|
c.leftNav = true;
|
||||||
|
return { ...c };
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (contentWarning && !isGrownUp) {
|
if (contentWarning && !isGrownUp) {
|
||||||
return <ContentWarningOverlay />;
|
return <ContentWarningOverlay />;
|
||||||
@ -42,38 +63,42 @@ export function StreamPage({ link, evPreload }: { evPreload?: TaggedNostrEvent;
|
|||||||
...(tags ?? []),
|
...(tags ?? []),
|
||||||
].join(", ");
|
].join(", ");
|
||||||
return (
|
return (
|
||||||
<div className="xl:grid xl:grid-cols-[auto_450px] 2xl:xl:grid-cols-[auto_500px] max-xl:flex max-xl:flex-col xl:gap-4 max-xl:gap-1 h-full">
|
<StreamContextProvider link={link}>
|
||||||
<Helmet>
|
<div className="xl:grid xl:grid-cols-[auto_450px] 2xl:xl:grid-cols-[auto_500px] max-xl:flex max-xl:flex-col xl:gap-4 max-xl:gap-1 h-full">
|
||||||
<title>{`${title} - zap.stream`}</title>
|
<Helmet>
|
||||||
<meta name="description" content={descriptionContent} />
|
<title>{`${title} - zap.stream`}</title>
|
||||||
<meta property="og:url" content={`https://${window.location.host}/${link.encode()}`} />
|
<meta name="description" content={descriptionContent} />
|
||||||
<meta property="og:type" content="video" />
|
<meta property="og:url" content={`https://${window.location.host}/${link.encode()}`} />
|
||||||
<meta property="og:title" content={title} />
|
<meta property="og:type" content="video" />
|
||||||
<meta property="og:description" content={descriptionContent} />
|
<meta property="og:title" content={title} />
|
||||||
<meta property="og:image" content={image ?? ""} />
|
<meta property="og:description" content={descriptionContent} />
|
||||||
</Helmet>
|
<meta property="og:image" content={image ?? ""} />
|
||||||
<div className="flex flex-col gap-2 xl:overflow-y-auto scrollbar-hidden">
|
</Helmet>
|
||||||
<Suspense>
|
<div className="flex flex-col gap-2 xl:overflow-y-auto scrollbar-hidden">
|
||||||
<LiveVideoPlayer
|
<Suspense>
|
||||||
title={title}
|
<LiveVideoPlayer
|
||||||
stream={status === StreamState.Live ? stream : recording}
|
title={title}
|
||||||
poster={image}
|
stream={status === StreamState.Live ? stream : recording}
|
||||||
status={status}
|
poster={image}
|
||||||
className="max-xl:max-h-[30vh] xl:w-full mx-auto"
|
status={status}
|
||||||
/>
|
className="max-xl:max-h-[30vh] xl:w-full mx-auto"
|
||||||
</Suspense>
|
/>
|
||||||
<StreamInfo ev={ev as TaggedNostrEvent} goal={goal} />
|
</Suspense>
|
||||||
{isDesktop && <StreamCards host={host} />}
|
<div className="lg:px-5 max-lg:px-2">
|
||||||
|
<StreamInfo ev={ev as TaggedNostrEvent} goal={goal} />
|
||||||
|
{isDesktop && <StreamCards host={host} />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<LiveChat
|
||||||
|
link={evLink ?? link}
|
||||||
|
ev={ev}
|
||||||
|
goal={goal}
|
||||||
|
canWrite={status === StreamState.Live}
|
||||||
|
adjustLayout={!isDesktop}
|
||||||
|
showGoal={true}
|
||||||
|
className="min-h-0 xl:border xl:border-layer-2 xl:rounded-xl xl:p-3 max-xl:px-2 h-inherit"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<LiveChat
|
</StreamContextProvider>
|
||||||
link={evLink ?? link}
|
|
||||||
ev={ev}
|
|
||||||
goal={goal}
|
|
||||||
canWrite={status === StreamState.Live}
|
|
||||||
showTopZappers={isDesktop}
|
|
||||||
showGoal={true}
|
|
||||||
className="min-h-0 xl:border xl:border-layer-2 xl:rounded-xl xl:p-3 max-xl:px-2 h-inherit"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1 +1,8 @@
|
|||||||
export function UploadPage() {}
|
export function UploadPage() {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Upload</h1>
|
||||||
|
<b>Coming Soon..</b>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -1,28 +1,66 @@
|
|||||||
import { StreamInfo } from "@/element/stream-info";
|
import { Textarea } from "@/element/chat/textarea";
|
||||||
|
import { WriteMessage } from "@/element/chat/write-message";
|
||||||
|
import { FollowButton } from "@/element/follow-button";
|
||||||
|
import { Profile, getName } from "@/element/profile";
|
||||||
|
import { SendZapsDialog } from "@/element/send-zap";
|
||||||
|
import { ShareMenu } from "@/element/share-menu";
|
||||||
|
import { StreamSummary } from "@/element/stream/summary";
|
||||||
|
import VideoComments from "@/element/video/comments";
|
||||||
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
|
import { useCurrentStreamFeed } from "@/hooks/current-stream-feed";
|
||||||
import { getHost, extractStreamInfo } from "@/utils";
|
import { getHost, extractStreamInfo } from "@/utils";
|
||||||
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||||
|
import { useUserProfile } from "@snort/system-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
|
||||||
export function VideoPage({ link, evPreload }: { link: NostrLink; evPreload?: TaggedNostrEvent }) {
|
export function VideoPage({ link, evPreload }: { link: NostrLink; evPreload?: TaggedNostrEvent }) {
|
||||||
|
const { formatMessage } = useIntl();
|
||||||
const ev = useCurrentStreamFeed(link, true, evPreload);
|
const ev = useCurrentStreamFeed(link, true, evPreload);
|
||||||
|
const [newComment, setNewComment] = useState("");
|
||||||
const host = getHost(ev);
|
const host = getHost(ev);
|
||||||
const evLink = ev ? NostrLink.fromEvent(ev) : undefined;
|
const { title, summary, image, contentWarning, recording } = extractStreamInfo(ev);
|
||||||
const {
|
const profile = useUserProfile(host);
|
||||||
title,
|
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
||||||
summary,
|
|
||||||
image,
|
|
||||||
status,
|
|
||||||
tags,
|
|
||||||
contentWarning,
|
|
||||||
stream,
|
|
||||||
recording,
|
|
||||||
goal: goalTag,
|
|
||||||
} = extractStreamInfo(ev);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 w-[80dvw] mx-auto">
|
<div className="p-4 w-[80dvw] mx-auto">
|
||||||
<video src={recording} controls className="w-full aspect-video" />
|
<video src={recording} controls className="w-full aspect-video" poster={image} />
|
||||||
<StreamInfo ev={ev as TaggedNostrEvent} />
|
<div className="grid grid-cols-[auto_450px]">
|
||||||
|
<div className="flex flex-col gap-4">
|
||||||
|
<div className="font-medium text-xl">{title}</div>
|
||||||
|
<div className="flex justify-between">
|
||||||
|
{/* PROFILE SECTION */}
|
||||||
|
<div className="flex gap-2 items-center">
|
||||||
|
<Profile pubkey={host} />
|
||||||
|
<FollowButton pubkey={host} />
|
||||||
|
</div>
|
||||||
|
{/* ACTIONS */}
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{ev && (
|
||||||
|
<>
|
||||||
|
<ShareMenu ev={ev} />
|
||||||
|
{zapTarget && (
|
||||||
|
<SendZapsDialog
|
||||||
|
lnurl={zapTarget}
|
||||||
|
pubkey={host}
|
||||||
|
aTag={link.tagKey}
|
||||||
|
targetName={getName(ev.pubkey, profile)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{summary && <StreamSummary text={summary} />}
|
||||||
|
<h3>
|
||||||
|
<FormattedMessage defaultMessage="Comments" />
|
||||||
|
</h3>
|
||||||
|
<div>
|
||||||
|
<WriteMessage link={link} emojiPacks={[]} kind={1} />
|
||||||
|
</div>
|
||||||
|
<VideoComments link={link} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,11 @@ import { appendDedupe } from "@snort/shared";
|
|||||||
export class NostrStreamProvider implements StreamProvider {
|
export class NostrStreamProvider implements StreamProvider {
|
||||||
#publisher?: EventPublisher;
|
#publisher?: EventPublisher;
|
||||||
|
|
||||||
constructor(readonly name: string, readonly url: string, pub?: EventPublisher) {
|
constructor(
|
||||||
|
readonly name: string,
|
||||||
|
readonly url: string,
|
||||||
|
pub?: EventPublisher,
|
||||||
|
) {
|
||||||
if (!url.endsWith("/")) {
|
if (!url.endsWith("/")) {
|
||||||
this.url = `${url}/`;
|
this.url = `${url}/`;
|
||||||
}
|
}
|
||||||
|
@ -23,9 +23,9 @@ self.addEventListener("install", event => {
|
|||||||
cacheNames.map(cacheName => {
|
cacheNames.map(cacheName => {
|
||||||
console.debug("Deleting cache: ", cacheName);
|
console.debug("Deleting cache: ", cacheName);
|
||||||
return caches.delete(cacheName);
|
return caches.delete(cacheName);
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
// always skip waiting
|
// always skip waiting
|
||||||
self.skipWaiting();
|
self.skipWaiting();
|
||||||
@ -56,7 +56,7 @@ self.addEventListener("notificationclick", event => {
|
|||||||
if (client.url === url() && "focus" in client) return client.focus();
|
if (client.url === url() && "focus" in client) return client.focus();
|
||||||
}
|
}
|
||||||
if (self.clients.openWindow) return self.clients.openWindow(url());
|
if (self.clients.openWindow) return self.clients.openWindow(url());
|
||||||
})()
|
})(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -75,7 +75,7 @@ self.addEventListener("push", async e => {
|
|||||||
console.debug(ret);
|
console.debug(ret);
|
||||||
await self.registration.showNotification(
|
await self.registration.showNotification(
|
||||||
`${data.name ?? hexToBech32("npub", data.pubkey).slice(0, 12)} went live`,
|
`${data.name ?? hexToBech32("npub", data.pubkey).slice(0, 12)} went live`,
|
||||||
ret
|
ret,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -21,6 +21,7 @@
|
|||||||
"1q4BO/": "Not a valid URL",
|
"1q4BO/": "Not a valid URL",
|
||||||
"1qsXCO": "eg. name@wallet.com",
|
"1qsXCO": "eg. name@wallet.com",
|
||||||
"2/2yg+": "Add",
|
"2/2yg+": "Add",
|
||||||
|
"2lVQYF": "...more",
|
||||||
"2ukA4d": "{n} hours",
|
"2ukA4d": "{n} hours",
|
||||||
"2wdFaB": "End Stream",
|
"2wdFaB": "End Stream",
|
||||||
"37mth/": "Viewers",
|
"37mth/": "Viewers",
|
||||||
@ -30,6 +31,7 @@
|
|||||||
"3yk8fB": "Wallet",
|
"3yk8fB": "Wallet",
|
||||||
"47FYwb": "Cancel",
|
"47FYwb": "Cancel",
|
||||||
"4RhY4O": "Example settings in OBS (Apple M1 Mac)",
|
"4RhY4O": "Example settings in OBS (Apple M1 Mac)",
|
||||||
|
"4XfMux": "Videos",
|
||||||
"4iBdw1": "Raid",
|
"4iBdw1": "Raid",
|
||||||
"4l69eO": "Hmm, your lightning address looks wrong",
|
"4l69eO": "Hmm, your lightning address looks wrong",
|
||||||
"4l6vz1": "Copy",
|
"4l6vz1": "Copy",
|
||||||
@ -65,7 +67,6 @@
|
|||||||
"Axo/o5": "Science & Technology",
|
"Axo/o5": "Science & Technology",
|
||||||
"AyGauy": "Login",
|
"AyGauy": "Login",
|
||||||
"BD0vyn": "{name} created a clip",
|
"BD0vyn": "{name} created a clip",
|
||||||
"BGxpTN": "Stream Chat",
|
|
||||||
"Bd1yEX": "New Stream Goal",
|
"Bd1yEX": "New Stream Goal",
|
||||||
"Bep/gA": "Private key",
|
"Bep/gA": "Private key",
|
||||||
"BzQPM+": "Destination",
|
"BzQPM+": "Destination",
|
||||||
@ -87,10 +88,12 @@
|
|||||||
"G/yZLu": "Remove",
|
"G/yZLu": "Remove",
|
||||||
"G857ni": "LNURL or invoice",
|
"G857ni": "LNURL or invoice",
|
||||||
"GGaJMU": "Top Chatters",
|
"GGaJMU": "Top Chatters",
|
||||||
|
"GSuQPh": "Unknown event link",
|
||||||
"GSye7T": "Lightning Address",
|
"GSye7T": "Lightning Address",
|
||||||
"GcozGF": "Invalid lightning address",
|
"GcozGF": "Invalid lightning address",
|
||||||
"Gq6x9o": "Cover Image",
|
"Gq6x9o": "Cover Image",
|
||||||
"Gvxoji": "Name is required",
|
"Gvxoji": "Name is required",
|
||||||
|
"GwbTAz": "Streams",
|
||||||
"H/bNs9": "Save this and keep it safe! If you lose this key, you won't be able to access your account ever again. Yep, it's that serious!",
|
"H/bNs9": "Save this and keep it safe! If you lose this key, you won't be able to access your account ever again. Yep, it's that serious!",
|
||||||
"H4hJvF": "Choose a category",
|
"H4hJvF": "Choose a category",
|
||||||
"H5+NAX": "Balance",
|
"H5+NAX": "Balance",
|
||||||
@ -128,7 +131,6 @@
|
|||||||
"ObZZEz": "No clips yet",
|
"ObZZEz": "No clips yet",
|
||||||
"OkXMLE": "Max Audio Bitrate",
|
"OkXMLE": "Max Audio Bitrate",
|
||||||
"Oxqtyf": "We hooked you up with a lightning wallet so you can get paid by viewers right away!",
|
"Oxqtyf": "We hooked you up with a lightning wallet so you can get paid by viewers right away!",
|
||||||
"PA0ej4": "Create Clip",
|
|
||||||
"PHE60k": "Leave blank if you do not wish to set up any goals.",
|
"PHE60k": "Leave blank if you do not wish to set up any goals.",
|
||||||
"PUymyQ": "Come check out {name} stream on zap.stream! {link}",
|
"PUymyQ": "Come check out {name} stream on zap.stream! {link}",
|
||||||
"PXAur5": "Withdraw",
|
"PXAur5": "Withdraw",
|
||||||
@ -161,7 +163,6 @@
|
|||||||
"VDOpia": "What are zaps?",
|
"VDOpia": "What are zaps?",
|
||||||
"VKb1MS": "Categories",
|
"VKb1MS": "Categories",
|
||||||
"W7DNWx": "Stream Forwarding",
|
"W7DNWx": "Stream Forwarding",
|
||||||
"W8nHSd": "FAQ",
|
|
||||||
"W9355R": "Unmute",
|
"W9355R": "Unmute",
|
||||||
"WVJZ0U": "Value",
|
"WVJZ0U": "Value",
|
||||||
"WsjXrZ": "Click on Log In",
|
"WsjXrZ": "Click on Log In",
|
||||||
@ -210,6 +211,7 @@
|
|||||||
"hMzcSq": "Messages",
|
"hMzcSq": "Messages",
|
||||||
"heyxZL": "Enable text to speech",
|
"heyxZL": "Enable text to speech",
|
||||||
"hpl4BP": "Chat Widget",
|
"hpl4BP": "Chat Widget",
|
||||||
|
"hzSNj4": "Dashboard",
|
||||||
"ieGrWo": "Follow",
|
"ieGrWo": "Follow",
|
||||||
"ieKb+k": "What does it cost to stream?",
|
"ieKb+k": "What does it cost to stream?",
|
||||||
"itPgxd": "Profile",
|
"itPgxd": "Profile",
|
||||||
@ -266,18 +268,19 @@
|
|||||||
"w0Xm2F": "Start typing",
|
"w0Xm2F": "Start typing",
|
||||||
"w3btjR": "Gambling",
|
"w3btjR": "Gambling",
|
||||||
"wCIL7o": "Broadcast on Nostr",
|
"wCIL7o": "Broadcast on Nostr",
|
||||||
|
"wCgTu5": "Comments",
|
||||||
"wEQDC6": "Edit",
|
"wEQDC6": "Edit",
|
||||||
"wMKVFz": "Select voice...",
|
"wMKVFz": "Select voice...",
|
||||||
"wRGjPp": "A nostr extension simply saves your keys so you can safely log in without having to re-enter them every time. ZapStream uses the extension to authorize actions on your behalf without ever seeing your key information. This has a significant advantage over having to trust that websites handle your credentials safely.",
|
"wRGjPp": "A nostr extension simply saves your keys so you can safely log in without having to re-enter them every time. ZapStream uses the extension to authorize actions on your behalf without ever seeing your key information. This has a significant advantage over having to trust that websites handle your credentials safely.",
|
||||||
"wTwfnv": "Invalid nostr address",
|
"wTwfnv": "Invalid nostr address",
|
||||||
"we4Lby": "Info",
|
"we4Lby": "Info",
|
||||||
"wzWWzV": "Top zappers",
|
|
||||||
"x82IOl": "Mute",
|
"x82IOl": "Mute",
|
||||||
"xi3sgh": "How do i get more sats?",
|
"xi3sgh": "How do i get more sats?",
|
||||||
"xmcVZ0": "Search",
|
"xmcVZ0": "Search",
|
||||||
"y867Vs": "Volume",
|
"y867Vs": "Volume",
|
||||||
"yLxIgl": "Clips",
|
"yLxIgl": "Clips",
|
||||||
"yR0V+W": "To start streaming on zap.stream, follow these steps:",
|
"yR0V+W": "To start streaming on zap.stream, follow these steps:",
|
||||||
|
"yj8NrY": "Clip",
|
||||||
"yzKwBQ": "eg. nsec1xyz",
|
"yzKwBQ": "eg. nsec1xyz",
|
||||||
"z2qCcJ": "Max Video Bitrate",
|
"z2qCcJ": "Max Video Bitrate",
|
||||||
"zEYkgY": "Talk",
|
"zEYkgY": "Talk",
|
||||||
|
30
src/utils.ts
30
src/utils.ts
@ -75,11 +75,14 @@ export function getEventFromLocationState(state: unknown | undefined | null) {
|
|||||||
|
|
||||||
export function uniqBy<T>(vals: Array<T>, key: (x: T) => string) {
|
export function uniqBy<T>(vals: Array<T>, key: (x: T) => string) {
|
||||||
return Object.values(
|
return Object.values(
|
||||||
vals.reduce((acc, v) => {
|
vals.reduce(
|
||||||
const k = key(v);
|
(acc, v) => {
|
||||||
acc[k] ??= v;
|
const k = key(v);
|
||||||
return acc;
|
acc[k] ??= v;
|
||||||
}, {} as Record<string, T>)
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, T>,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -195,7 +198,7 @@ export function extractGameTag(tags: Array<string>) {
|
|||||||
export function trackEvent(
|
export function trackEvent(
|
||||||
event: string,
|
event: string,
|
||||||
props?: Record<string, string | boolean>,
|
props?: Record<string, string | boolean>,
|
||||||
e?: { destination?: { url: string } }
|
e?: { destination?: { url: string } },
|
||||||
) {
|
) {
|
||||||
if (!import.meta.env.DEV) {
|
if (!import.meta.env.DEV) {
|
||||||
fetch("https://pa.v0l.io/api/event", {
|
fetch("https://pa.v0l.io/api/event", {
|
||||||
@ -215,10 +218,13 @@ export function trackEvent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function groupBy<T>(val: Array<T>, selector: (a: T) => string | number): Record<string, Array<T>> {
|
export function groupBy<T>(val: Array<T>, selector: (a: T) => string | number): Record<string, Array<T>> {
|
||||||
return val.reduce((acc, v) => {
|
return val.reduce(
|
||||||
const key = selector(v);
|
(acc, v) => {
|
||||||
acc[key] ??= [];
|
const key = selector(v);
|
||||||
acc[key].push(v);
|
acc[key] ??= [];
|
||||||
return acc;
|
acc[key].push(v);
|
||||||
}, {} as Record<string, Array<T>>);
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, Array<T>>,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -7,12 +7,12 @@ interface StateEventTarget extends EventTarget {
|
|||||||
addEventListener<K extends keyof StateEventMap>(
|
addEventListener<K extends keyof StateEventMap>(
|
||||||
type: K,
|
type: K,
|
||||||
listener: (ev: StateEventMap[K]) => void,
|
listener: (ev: StateEventMap[K]) => void,
|
||||||
options?: boolean | AddEventListenerOptions
|
options?: boolean | AddEventListenerOptions,
|
||||||
): void;
|
): void;
|
||||||
addEventListener(
|
addEventListener(
|
||||||
type: string,
|
type: string,
|
||||||
callback: EventListenerOrEventListenerObject | null,
|
callback: EventListenerOrEventListenerObject | null,
|
||||||
options?: EventListenerOptions | boolean
|
options?: EventListenerOptions | boolean,
|
||||||
): void;
|
): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,7 +58,7 @@ export class WISH extends TypedEventTarget {
|
|||||||
detail: {
|
detail: {
|
||||||
message: str,
|
message: str,
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,7 +139,7 @@ export class WISH extends TypedEventTarget {
|
|||||||
}
|
}
|
||||||
if (pair.local && pair.remote) {
|
if (pair.local && pair.remote) {
|
||||||
this.logMessage(
|
this.logMessage(
|
||||||
`[${track.kind}] Selected Candidate: (local ${pair.local.address})-(remote ${pair.remote.candidate})`
|
`[${track.kind}] Selected Candidate: (local ${pair.local.address})-(remote ${pair.remote.candidate})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -178,7 +178,7 @@ export class WISH extends TypedEventTarget {
|
|||||||
detail: {
|
detail: {
|
||||||
status: "disconnected",
|
status: "disconnected",
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -299,7 +299,7 @@ export class WISH extends TypedEventTarget {
|
|||||||
detail: {
|
detail: {
|
||||||
status: "connected",
|
status: "connected",
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
this.connecting = false;
|
this.connecting = false;
|
||||||
this.connectedResolver();
|
this.connectedResolver();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user