refactor: upgrade snort pkgs
This commit is contained in:
@ -1,7 +1,8 @@
|
||||
import { useUserProfile, SnortContext } from "@snort/system-react";
|
||||
import { NostrEvent, parseZap, EventKind } from "@snort/system";
|
||||
import { useUserProfile, SnortContext, useEventReactions } from "@snort/system-react";
|
||||
import { EventKind, NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||
import React, { useRef, useState, useMemo, useContext } from "react";
|
||||
import { useMediaQuery, useHover, useOnClickOutside, useIntersectionObserver } from "usehooks-ts";
|
||||
import { dedupe } from "@snort/shared";
|
||||
|
||||
import { EmojiPicker } from "element/emoji-picker";
|
||||
import { Icon } from "element/icon";
|
||||
@ -13,7 +14,6 @@ import { SendZapsDialog } from "element/send-zap";
|
||||
import { CollapsibleEvent } from "element/collapsible";
|
||||
import { useLogin } from "hooks/login";
|
||||
import { formatSats } from "number";
|
||||
import { findTag } from "utils";
|
||||
import type { Badge, Emoji, EmojiPack } from "types";
|
||||
|
||||
function emojifyReaction(reaction: string) {
|
||||
@ -26,28 +26,26 @@ function emojifyReaction(reaction: string) {
|
||||
return reaction;
|
||||
}
|
||||
|
||||
const customComponents = {
|
||||
Event: CollapsibleEvent,
|
||||
};
|
||||
|
||||
export function ChatMessage({
|
||||
streamer,
|
||||
ev,
|
||||
reactions,
|
||||
related,
|
||||
emojiPacks,
|
||||
badges,
|
||||
}: {
|
||||
ev: NostrEvent;
|
||||
ev: TaggedNostrEvent;
|
||||
streamer: string;
|
||||
reactions: readonly NostrEvent[];
|
||||
related: ReadonlyArray<TaggedNostrEvent>;
|
||||
emojiPacks: EmojiPack[];
|
||||
badges: Badge[];
|
||||
}) {
|
||||
const system = useContext(SnortContext);
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const inView = useIntersectionObserver(ref, {
|
||||
freezeOnceVisible: true,
|
||||
});
|
||||
const emojiRef = useRef(null);
|
||||
const link = NostrLink.fromEvent(ev);
|
||||
const isTablet = useMediaQuery("(max-width: 1020px)");
|
||||
const isHovering = useHover(ref);
|
||||
const { mute } = useMute(ev.pubkey);
|
||||
@ -57,25 +55,16 @@ export function ChatMessage({
|
||||
const profile = useUserProfile(inView?.isIntersecting ? ev.pubkey : undefined);
|
||||
const shouldShowMuteButton = ev.pubkey !== streamer && ev.pubkey !== login?.pubkey;
|
||||
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
||||
const system = useContext(SnortContext);
|
||||
const zaps = useMemo(() => {
|
||||
return reactions
|
||||
.filter(a => a.kind === EventKind.ZapReceipt)
|
||||
.map(a => parseZap(a, system.ProfileLoader.Cache))
|
||||
.filter(a => a && a.valid);
|
||||
}, [reactions]);
|
||||
const emojiReactions = useMemo(() => {
|
||||
const emojified = reactions
|
||||
.filter(e => e.kind === EventKind.Reaction && findTag(e, "e") === ev.id)
|
||||
.map(ev => emojifyReaction(ev.content));
|
||||
return [...new Set(emojified)];
|
||||
}, [ev, reactions]);
|
||||
const { zaps, reactions } = useEventReactions(ev, related);
|
||||
const emojiNames = emojiPacks.map(p => p.emojis).flat();
|
||||
|
||||
const hasReactions = emojiReactions.length > 0;
|
||||
const filteredReactions = useMemo(() => {
|
||||
return reactions.all.filter(a => link.isReplyToThis(a));
|
||||
}, [ev, reactions.all]);
|
||||
|
||||
const hasReactions = filteredReactions.length > 0;
|
||||
const totalZaps = useMemo(() => {
|
||||
const messageZaps = zaps.filter(z => z.event === ev.id);
|
||||
return messageZaps.reduce((acc, z) => acc + z.amount, 0);
|
||||
return zaps.filter(a => a.event?.id === ev.id).reduce((acc, z) => acc + z.amount, 0);
|
||||
}, [zaps, ev]);
|
||||
const hasZaps = totalZaps > 0;
|
||||
const awardedBadges = badges.filter(b => b.awardees.has(ev.pubkey) && b.accepted.has(ev.pubkey));
|
||||
@ -153,7 +142,7 @@ export function ChatMessage({
|
||||
pubkey={ev.pubkey}
|
||||
profile={profile}
|
||||
/>
|
||||
<Text tags={ev.tags} content={ev.content} customComponents={customComponents} />
|
||||
<Text tags={ev.tags} content={ev.content} eventComponent={CollapsibleEvent} />
|
||||
{(hasReactions || hasZaps) && (
|
||||
<div className="message-reactions">
|
||||
{hasZaps && (
|
||||
@ -162,7 +151,7 @@ export function ChatMessage({
|
||||
<span className="zap-pill-amount">{formatSats(totalZaps)}</span>
|
||||
</div>
|
||||
)}
|
||||
{emojiReactions.map(e => {
|
||||
{dedupe(filteredReactions.map(v => emojifyReaction(v.content))).map(e => {
|
||||
const isCustomEmojiReaction = e.length > 1 && e.startsWith(":") && e.endsWith(":");
|
||||
const emojiName = e.replace(/:/g, "");
|
||||
const emoji = isCustomEmojiReaction && getEmojiById(emojiName);
|
||||
|
@ -6,11 +6,14 @@ import { toEmojiPack } from "hooks/emoji";
|
||||
import AsyncButton from "element/async-button";
|
||||
import { findTag } from "utils";
|
||||
import { USER_EMOJIS } from "const";
|
||||
import { Login, System } from "index";
|
||||
import { Login } from "index";
|
||||
import type { EmojiPack as EmojiPackType } from "types";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useContext } from "react";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
|
||||
export function EmojiPack({ ev }: { ev: NostrEvent }) {
|
||||
const system = useContext(SnortContext);
|
||||
const login = useLogin();
|
||||
const name = findTag(ev, "d");
|
||||
const isUsed = login?.emojis.find(e => e.author === ev.pubkey && e.name === name);
|
||||
@ -33,7 +36,7 @@ export function EmojiPack({ ev }: { ev: NostrEvent }) {
|
||||
return eb;
|
||||
});
|
||||
console.debug(ev);
|
||||
System.BroadcastEvent(ev);
|
||||
await system.BroadcastEvent(ev);
|
||||
Login.setEmojis(newPacks);
|
||||
}
|
||||
}
|
||||
@ -55,7 +58,7 @@ export function EmojiPack({ ev }: { ev: NostrEvent }) {
|
||||
const [, name, image] = e;
|
||||
return (
|
||||
<div className="emoji-definition">
|
||||
<img alt={name} className="emoji" src={image} />
|
||||
<img alt={name} className="custom-emoji" src={image} />
|
||||
<span className="emoji-name">{name}</span>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,4 +1,4 @@
|
||||
.emoji {
|
||||
.custom-emoji {
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
display: inline-block;
|
||||
|
@ -8,7 +8,7 @@ export type EmojiProps = {
|
||||
};
|
||||
|
||||
export function Emoji({ name, url }: EmojiProps) {
|
||||
return <img alt={name} src={url} className="emoji" />;
|
||||
return <img alt={name} src={url} className="custom-emoji" />;
|
||||
}
|
||||
|
||||
export function Emojify({ content, emoji }: { content: string; emoji: EmojiTag[] }) {
|
||||
|
@ -2,10 +2,13 @@ import { EventKind } from "@snort/system";
|
||||
|
||||
import { useLogin } from "hooks/login";
|
||||
import AsyncButton from "element/async-button";
|
||||
import { Login, System } from "index";
|
||||
import { Login } from "index";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useContext } from "react";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
|
||||
export function LoggedInFollowButton({ tag, value }: { tag: "p" | "t"; value: string }) {
|
||||
const system = useContext(SnortContext);
|
||||
const login = useLogin();
|
||||
if (!login) return;
|
||||
|
||||
@ -25,7 +28,7 @@ export function LoggedInFollowButton({ tag, value }: { tag: "p" | "t"; value: st
|
||||
return eb;
|
||||
});
|
||||
console.debug(ev);
|
||||
System.BroadcastEvent(ev);
|
||||
await system.BroadcastEvent(ev);
|
||||
Login.setFollows(newFollows, content ?? "", ev.created_at);
|
||||
}
|
||||
}
|
||||
@ -42,7 +45,7 @@ export function LoggedInFollowButton({ tag, value }: { tag: "p" | "t"; value: st
|
||||
return eb;
|
||||
});
|
||||
console.debug(ev);
|
||||
System.BroadcastEvent(ev);
|
||||
await system.BroadcastEvent(ev);
|
||||
Login.setFollows(newFollows, content ?? "", ev.created_at);
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,10 @@ import { useMemo } from "react";
|
||||
import * as Progress from "@radix-ui/react-progress";
|
||||
import Confetti from "react-confetti";
|
||||
|
||||
import { type NostrEvent } from "@snort/system";
|
||||
import { NostrLink, type NostrEvent } from "@snort/system";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
|
||||
import { eventToLink, findTag } from "utils";
|
||||
import { findTag } from "utils";
|
||||
import { formatSats } from "number";
|
||||
import usePreviousValue from "hooks/usePreviousValue";
|
||||
import { SendZapsDialog } from "element/send-zap";
|
||||
@ -18,7 +18,7 @@ import { useZaps } from "hooks/zaps";
|
||||
export function Goal({ ev }: { ev: NostrEvent }) {
|
||||
const profile = useUserProfile(ev.pubkey);
|
||||
const zapTarget = profile?.lud16 ?? profile?.lud06;
|
||||
const link = eventToLink(ev);
|
||||
const link = NostrLink.fromEvent(ev);
|
||||
const zaps = useZaps(link, true);
|
||||
const goalAmount = useMemo(() => {
|
||||
const amount = findTag(ev, "amount");
|
||||
@ -30,7 +30,7 @@ export function Goal({ ev }: { ev: NostrEvent }) {
|
||||
}
|
||||
|
||||
const soFar = useMemo(() => {
|
||||
return zaps.filter(z => z.receiver === ev.pubkey && z.event === ev.id).reduce((acc, z) => acc + z.amount, 0);
|
||||
return zaps.filter(z => z.receiver === ev.pubkey && z.event?.id === ev.id).reduce((acc, z) => acc + z.amount, 0);
|
||||
}, [zaps]);
|
||||
|
||||
const progress = Math.max(0, Math.min(100, (soFar / goalAmount) * 100));
|
||||
|
@ -6,7 +6,7 @@ const FileExtensionRegex = /\.([\w]+)$/i;
|
||||
|
||||
interface HyperTextProps {
|
||||
link: string;
|
||||
children: ReactNode;
|
||||
children?: ReactNode;
|
||||
}
|
||||
|
||||
export function HyperText({ link, children }: HyperTextProps) {
|
||||
|
@ -2,7 +2,7 @@ import "./live-chat.css";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { EventKind, NostrPrefix, NostrLink, ParsedZap, NostrEvent, parseZap, encodeTLV } from "@snort/system";
|
||||
import { unixNow } from "@snort/shared";
|
||||
import { useMemo } from "react";
|
||||
import { useContext, useMemo } from "react";
|
||||
import uniqBy from "lodash.uniqby";
|
||||
|
||||
import { Icon } from "element/icon";
|
||||
@ -22,8 +22,8 @@ import { useAddress } from "hooks/event";
|
||||
import { formatSats } from "number";
|
||||
import { WEEK, LIVE_STREAM_CHAT } from "const";
|
||||
import { findTag, getTagValues, getHost } from "utils";
|
||||
import { System } from "index";
|
||||
import { TopZappers } from "element/top-zappers";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
|
||||
export interface LiveChatOptions {
|
||||
canWrite?: boolean;
|
||||
@ -61,6 +61,7 @@ export function LiveChat({
|
||||
options?: LiveChatOptions;
|
||||
height?: number;
|
||||
}) {
|
||||
const system = useContext(SnortContext);
|
||||
const host = getHost(ev);
|
||||
const feed = useLiveChatFeed(link, goal ? [goal.id] : undefined);
|
||||
const login = useLogin();
|
||||
@ -79,7 +80,7 @@ export function LiveChat({
|
||||
return uniqBy(userEmojiPacks.concat(channelEmojiPacks), packId);
|
||||
}, [userEmojiPacks, channelEmojiPacks]);
|
||||
|
||||
const zaps = feed.zaps.map(ev => parseZap(ev, System.ProfileLoader.Cache)).filter(z => z && z.valid);
|
||||
const zaps = feed.zaps.map(ev => parseZap(ev, system.ProfileLoader.Cache)).filter(z => z && z.valid);
|
||||
const events = useMemo(() => {
|
||||
return [...feed.messages, ...feed.zaps, ...awards].sort((a, b) => b.created_at - a.created_at);
|
||||
}, [feed.messages, feed.zaps, awards]);
|
||||
@ -132,7 +133,7 @@ export function LiveChat({
|
||||
streamer={host}
|
||||
ev={a}
|
||||
key={a.id}
|
||||
reactions={feed.reactions}
|
||||
related={feed.reactions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -10,19 +10,20 @@ import LoginKey2x from "../login-key@2x.jpg";
|
||||
import LoginWallet from "../login-wallet.jpg";
|
||||
import LoginWallet2x from "../login-wallet@2x.jpg";
|
||||
|
||||
import { CSSProperties, useState } from "react";
|
||||
import { CSSProperties, useContext, useState } from "react";
|
||||
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
|
||||
import { EventPublisher, UserMetadata } from "@snort/system";
|
||||
import { schnorr } from "@noble/curves/secp256k1";
|
||||
import { bytesToHex } from "@noble/curves/abstract/utils";
|
||||
import { LNURL, bech32ToHex, getPublicKey } from "@snort/shared";
|
||||
import { LNURL, bech32ToHex, getPublicKey, hexToBech32 } from "@snort/shared";
|
||||
import { VoidApi } from "@void-cat/api";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
|
||||
import AsyncButton from "./async-button";
|
||||
import { Login, System } from "index";
|
||||
import { Login } from "index";
|
||||
import { Icon } from "./icon";
|
||||
import Copy from "./copy";
|
||||
import { hexToBech32, openFile } from "utils";
|
||||
import { openFile } from "utils";
|
||||
import { LoginType } from "login";
|
||||
import { DefaultProvider, StreamProviderInfo } from "providers";
|
||||
import { Nip103StreamProvider } from "providers/zsz";
|
||||
@ -36,6 +37,7 @@ enum Stage {
|
||||
}
|
||||
|
||||
export function LoginSignup({ close }: { close: () => void }) {
|
||||
const system = useContext(SnortContext);
|
||||
const [error, setError] = useState("");
|
||||
const [stage, setStage] = useState(Stage.Login);
|
||||
const [username, setUsername] = useState("");
|
||||
@ -136,7 +138,7 @@ export function LoginSignup({ close }: { close: () => void }) {
|
||||
|
||||
const ev = await pub.metadata(profile);
|
||||
console.debug(ev);
|
||||
System.BroadcastEvent(ev);
|
||||
system.BroadcastEvent(ev);
|
||||
|
||||
setStage(Stage.SaveKey);
|
||||
} catch (e) {
|
||||
|
@ -1,20 +1,37 @@
|
||||
.markdown a {
|
||||
color: var(--text-link);
|
||||
}
|
||||
|
||||
.markdown > ul,
|
||||
.markdown > ol {
|
||||
margin: 0;
|
||||
padding: 0 12px;
|
||||
.markdown {
|
||||
font-size: 18px;
|
||||
font-weight: 400;
|
||||
line-height: 29px;
|
||||
}
|
||||
|
||||
.markdown > p {
|
||||
font-size: 18px;
|
||||
font-style: normal;
|
||||
overflow-wrap: break-word;
|
||||
font-weight: 400;
|
||||
line-height: 29px; /* 161.111% */
|
||||
.markdown a {
|
||||
color: var(--text-link);
|
||||
}
|
||||
|
||||
.markdown blockquote {
|
||||
margin: 0;
|
||||
color: var(--font-secondary-color);
|
||||
border-left: 2px solid var(--font-secondary-color);
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.markdown hr {
|
||||
border: 0;
|
||||
height: 1px;
|
||||
background-image: var(--gray-gradient);
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.markdown img:not(.custom-emoji),
|
||||
.markdown video,
|
||||
.markdown iframe,
|
||||
.markdown audio {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.markdown iframe,
|
||||
.markdown video {
|
||||
width: -webkit-fill-available;
|
||||
aspect-ratio: 16 / 9;
|
||||
}
|
||||
|
@ -1,49 +1,95 @@
|
||||
import "./markdown.css";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
|
||||
import { HyperText } from "element/hypertext";
|
||||
import { transformText, type Fragment } from "element/text";
|
||||
import type { Tags } from "types";
|
||||
import { ReactNode, forwardRef, useMemo } from "react";
|
||||
import { marked, Token } from "marked";
|
||||
import { HyperText } from "./hypertext";
|
||||
import { Text } from "./text";
|
||||
|
||||
interface MarkdownProps {
|
||||
content: string;
|
||||
tags?: Tags;
|
||||
tags?: Array<Array<string>>;
|
||||
}
|
||||
|
||||
interface LinkProps {
|
||||
href?: string;
|
||||
children?: Array<Fragment>;
|
||||
function renderToken(t: Token): ReactNode {
|
||||
try {
|
||||
switch (t.type) {
|
||||
case "paragraph": {
|
||||
return <p>{t.tokens ? t.tokens.map(renderToken) : t.raw}</p>;
|
||||
}
|
||||
case "image": {
|
||||
return <img src={t.href} />;
|
||||
}
|
||||
case "heading": {
|
||||
switch (t.depth) {
|
||||
case 1:
|
||||
return <h1>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h1>;
|
||||
case 2:
|
||||
return <h2>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h2>;
|
||||
case 3:
|
||||
return <h3>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h3>;
|
||||
case 4:
|
||||
return <h4>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h4>;
|
||||
case 5:
|
||||
return <h5>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h5>;
|
||||
case 6:
|
||||
return <h6>{t.tokens ? t.tokens.map(renderToken) : t.raw}</h6>;
|
||||
}
|
||||
throw new Error("Invalid heading");
|
||||
}
|
||||
case "codespan": {
|
||||
return <code>{t.raw}</code>;
|
||||
}
|
||||
case "code": {
|
||||
return <pre>{t.raw}</pre>;
|
||||
}
|
||||
case "br": {
|
||||
return <br />;
|
||||
}
|
||||
case "hr": {
|
||||
return <hr />;
|
||||
}
|
||||
case "blockquote": {
|
||||
return <blockquote>{t.tokens ? t.tokens.map(renderToken) : t.raw}</blockquote>;
|
||||
}
|
||||
case "link": {
|
||||
return <HyperText link={t.href}>{t.tokens ? t.tokens.map(renderToken) : t.raw}</HyperText>;
|
||||
}
|
||||
case "list": {
|
||||
if (t.ordered) {
|
||||
return <ol>{t.items.map(renderToken)}</ol>;
|
||||
} else {
|
||||
return <ul>{t.items.map(renderToken)}</ul>;
|
||||
}
|
||||
}
|
||||
case "list_item": {
|
||||
return <li>{t.tokens ? t.tokens.map(renderToken) : t.raw}</li>;
|
||||
}
|
||||
case "em": {
|
||||
return <em>{t.tokens ? t.tokens.map(renderToken) : t.raw}</em>;
|
||||
}
|
||||
case "del": {
|
||||
return <s>{t.tokens ? t.tokens.map(renderToken) : t.raw}</s>;
|
||||
}
|
||||
default: {
|
||||
if ("tokens" in t) {
|
||||
return (t.tokens as Array<Token>).map(renderToken);
|
||||
}
|
||||
return <Text content={t.raw} tags={[]} />;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
interface ComponentProps {
|
||||
children?: Array<Fragment>;
|
||||
}
|
||||
export const Markdown = forwardRef<HTMLDivElement, MarkdownProps>((props: MarkdownProps, ref) => {
|
||||
const parsed = useMemo(() => {
|
||||
return marked.lexer(props.content);
|
||||
}, [props.content, props.tags]);
|
||||
|
||||
export function Markdown({ content, tags = [] }: MarkdownProps) {
|
||||
const components = useMemo(() => {
|
||||
return {
|
||||
li: ({ children, ...props }: ComponentProps) => {
|
||||
return children && <li {...props}>{transformText(children, tags)}</li>;
|
||||
},
|
||||
td: ({ children }: ComponentProps) => {
|
||||
return children && <td>{transformText(children, tags)}</td>;
|
||||
},
|
||||
th: ({ children }: ComponentProps) => {
|
||||
return children && <th>{transformText(children, tags)}</th>;
|
||||
},
|
||||
p: ({ children }: ComponentProps) => {
|
||||
return children && <p>{transformText(children, tags)}</p>;
|
||||
},
|
||||
a: ({ href, children }: LinkProps) => {
|
||||
return href && <HyperText link={href}>{children}</HyperText>;
|
||||
},
|
||||
};
|
||||
}, [tags]);
|
||||
return (
|
||||
<div className="markdown">
|
||||
<ReactMarkdown components={components}>{content}</ReactMarkdown>
|
||||
<div className="markdown" ref={ref}>
|
||||
{parsed.filter(a => a.type !== "footnote" && a.type !== "footnotes").map(a => renderToken(a))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Link } from "react-router-dom";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
import { hexToBech32 } from "utils";
|
||||
import { hexToBech32 } from "@snort/shared";
|
||||
|
||||
interface MentionProps {
|
||||
pubkey: string;
|
||||
|
@ -1,11 +1,13 @@
|
||||
import { useMemo } from "react";
|
||||
import { useContext, useMemo } from "react";
|
||||
import { useLogin } from "hooks/login";
|
||||
import AsyncButton from "element/async-button";
|
||||
import { Login, System } from "index";
|
||||
import { Login } from "index";
|
||||
import { MUTED } from "const";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
|
||||
export function useMute(pubkey: string) {
|
||||
const system = useContext(SnortContext);
|
||||
const login = useLogin();
|
||||
const { tags, content } = login?.muted ?? { tags: [] };
|
||||
const muted = useMemo(() => tags.filter(t => t.at(0) === "p"), [tags]);
|
||||
@ -23,7 +25,7 @@ export function useMute(pubkey: string) {
|
||||
return eb;
|
||||
});
|
||||
console.debug(ev);
|
||||
System.BroadcastEvent(ev);
|
||||
await system.BroadcastEvent(ev);
|
||||
Login.setMuted(newMuted, content ?? "", ev.created_at);
|
||||
}
|
||||
}
|
||||
@ -40,7 +42,7 @@ export function useMute(pubkey: string) {
|
||||
return eb;
|
||||
});
|
||||
console.debug(ev);
|
||||
System.BroadcastEvent(ev);
|
||||
await system.BroadcastEvent(ev);
|
||||
Login.setMuted(newMuted, content ?? "", ev.created_at);
|
||||
}
|
||||
}
|
||||
|
@ -3,14 +3,15 @@ import * as Dialog from "@radix-ui/react-dialog";
|
||||
|
||||
import AsyncButton from "./async-button";
|
||||
import { Icon } from "element/icon";
|
||||
import { useState } from "react";
|
||||
import { System } from "index";
|
||||
import { useContext, useState } from "react";
|
||||
import { GOAL } from "const";
|
||||
import { useLogin } from "hooks/login";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { defaultRelays } from "const";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
|
||||
export function NewGoalDialog() {
|
||||
const system = useContext(SnortContext);
|
||||
const [open, setOpen] = useState(false);
|
||||
const login = useLogin();
|
||||
|
||||
@ -28,7 +29,7 @@ export function NewGoalDialog() {
|
||||
return eb;
|
||||
});
|
||||
console.debug(evNew);
|
||||
System.BroadcastEvent(evNew);
|
||||
await system.BroadcastEvent(evNew);
|
||||
setOpen(false);
|
||||
setGoalName("");
|
||||
setGoalAmount("");
|
||||
|
@ -4,15 +4,17 @@ import * as Dialog from "@radix-ui/react-dialog";
|
||||
import { Icon } from "element/icon";
|
||||
import { useStreamProvider } from "hooks/stream-provider";
|
||||
import { StreamProvider, StreamProviders } from "providers";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { StreamEditor, StreamEditorProps } from "./stream-editor";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { eventLink, findTag } from "utils";
|
||||
import { NostrProviderDialog } from "./nostr-provider-dialog";
|
||||
import { unwrap } from "@snort/shared";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
|
||||
function NewStream({ ev, onFinish }: StreamEditorProps) {
|
||||
const system = useContext(SnortContext);
|
||||
const providers = useStreamProvider();
|
||||
const [currentProvider, setCurrentProvider] = useState<StreamProvider>();
|
||||
const navigate = useNavigate();
|
||||
@ -33,7 +35,7 @@ function NewStream({ ev, onFinish }: StreamEditorProps) {
|
||||
return (
|
||||
<StreamEditor
|
||||
onFinish={ex => {
|
||||
currentProvider.updateStreamInfo(ex);
|
||||
currentProvider.updateStreamInfo(system, ex);
|
||||
if (!ev) {
|
||||
if (findTag(ex, "content-warning") && __XXX_HOST && __XXX === false) {
|
||||
location.href = `${__XXX_HOST}/${eventLink(ex)}`;
|
||||
|
@ -1,13 +1,15 @@
|
||||
import { NostrEvent } from "@snort/system";
|
||||
import { StreamProvider, StreamProviderEndpoint, StreamProviderInfo } from "providers";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
import { SendZaps } from "./send-zap";
|
||||
import { StreamEditor, StreamEditorProps } from "./stream-editor";
|
||||
import Spinner from "./spinner";
|
||||
import AsyncButton from "./async-button";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
|
||||
export function NostrProviderDialog({ provider, ...others }: { provider: StreamProvider } & StreamEditorProps) {
|
||||
const system = useContext(SnortContext);
|
||||
const [topup, setTopup] = useState(false);
|
||||
const [info, setInfo] = useState<StreamProviderInfo>();
|
||||
const [ep, setEndpoint] = useState<StreamProviderEndpoint>();
|
||||
@ -181,7 +183,7 @@ export function NostrProviderDialog({ provider, ...others }: { provider: StreamP
|
||||
) : (
|
||||
<StreamEditor
|
||||
onFinish={ex => {
|
||||
provider.updateStreamInfo(ex);
|
||||
provider.updateStreamInfo(system, ex);
|
||||
others.onFinish?.(ex);
|
||||
}}
|
||||
ev={
|
||||
|
@ -4,7 +4,7 @@ import { type NostrEvent, NostrPrefix } from "@snort/system";
|
||||
import { Markdown } from "element/markdown";
|
||||
import { ExternalIconLink } from "element/external-link";
|
||||
import { Profile } from "element/profile";
|
||||
import { hexToBech32 } from "utils";
|
||||
import { hexToBech32 } from "@snort/shared";
|
||||
|
||||
export function Note({ ev }: { ev: NostrEvent }) {
|
||||
return (
|
||||
|
@ -4,17 +4,18 @@ import { unwrap } from "@snort/shared";
|
||||
import { NostrEvent, NostrPrefix, encodeTLV } from "@snort/system";
|
||||
|
||||
import { Icon } from "./icon";
|
||||
import { useState } from "react";
|
||||
import { useContext, useState } from "react";
|
||||
import { Textarea } from "./textarea";
|
||||
import { findTag } from "utils";
|
||||
import AsyncButton from "./async-button";
|
||||
import { useLogin } from "hooks/login";
|
||||
import { System } from "index";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
|
||||
type ShareOn = "nostr" | "twitter";
|
||||
|
||||
export function ShareMenu({ ev }: { ev: NostrEvent }) {
|
||||
const system = useContext(SnortContext);
|
||||
const [share, setShare] = useState<ShareOn>();
|
||||
const [message, setMessage] = useState("");
|
||||
const login = useLogin();
|
||||
@ -27,7 +28,7 @@ export function ShareMenu({ ev }: { ev: NostrEvent }) {
|
||||
if (pub) {
|
||||
const ev = await pub.note(message);
|
||||
console.debug(ev);
|
||||
System.BroadcastEvent(ev);
|
||||
await system.BroadcastEvent(ev);
|
||||
setShare(undefined);
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,13 @@
|
||||
import "./stream-cards.css";
|
||||
|
||||
import { useState, forwardRef } from "react";
|
||||
import { useState, forwardRef, useContext } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import * as Dialog from "@radix-ui/react-dialog";
|
||||
import { DndProvider, useDrag, useDrop } from "react-dnd";
|
||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||
|
||||
import { TaggedNostrEvent } from "@snort/system";
|
||||
import { removeUndefined, unwrap } from "@snort/shared";
|
||||
import { NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||
|
||||
import { Toggle } from "element/toggle";
|
||||
import { Icon } from "element/icon";
|
||||
@ -16,9 +17,10 @@ import { Markdown } from "element/markdown";
|
||||
import { useLogin } from "hooks/login";
|
||||
import { useCards, useUserCards } from "hooks/cards";
|
||||
import { CARD, USER_CARDS } from "const";
|
||||
import { toTag, findTag } from "utils";
|
||||
import { Login, System } from "index";
|
||||
import { findTag } from "utils";
|
||||
import { Login } from "index";
|
||||
import type { Tags } from "types";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
|
||||
interface CardType {
|
||||
identifier: string;
|
||||
@ -71,6 +73,7 @@ interface CardItem {
|
||||
}
|
||||
|
||||
function Card({ canEdit, ev, cards }: CardProps) {
|
||||
const system = useContext(SnortContext);
|
||||
const login = useLogin();
|
||||
const identifier = findTag(ev, "d") ?? "";
|
||||
const title = findTag(ev, "title") || findTag(ev, "subject");
|
||||
@ -78,7 +81,7 @@ function Card({ canEdit, ev, cards }: CardProps) {
|
||||
const link = findTag(ev, "r");
|
||||
const content = ev.content;
|
||||
const evCard = { title, image, link, content, identifier };
|
||||
const tags = cards.map(toTag);
|
||||
const tags = removeUndefined(cards.map(a => NostrLink.fromEvent(a).toEventTag()));
|
||||
const [style, dragRef] = useDrag(
|
||||
() => ({
|
||||
type: "card",
|
||||
@ -140,7 +143,7 @@ function Card({ canEdit, ev, cards }: CardProps) {
|
||||
return eb;
|
||||
});
|
||||
console.debug(userCardsEv);
|
||||
System.BroadcastEvent(userCardsEv);
|
||||
await system.BroadcastEvent(userCardsEv);
|
||||
Login.setCards(newTags, userCardsEv.created_at);
|
||||
}
|
||||
},
|
||||
@ -255,10 +258,11 @@ interface EditCardProps {
|
||||
}
|
||||
|
||||
function EditCard({ card, cards }: EditCardProps) {
|
||||
const system = useContext(SnortContext);
|
||||
const login = useLogin();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const identifier = card.identifier;
|
||||
const tags = cards.map(toTag);
|
||||
const tags = removeUndefined(cards.map(a => NostrLink.fromEvent(a).toEventTag()));
|
||||
const { formatMessage } = useIntl();
|
||||
|
||||
async function editCard({ title, image, link, content }: CardType) {
|
||||
@ -278,7 +282,7 @@ function EditCard({ card, cards }: EditCardProps) {
|
||||
return eb;
|
||||
});
|
||||
console.debug(ev);
|
||||
System.BroadcastEvent(ev);
|
||||
await system.BroadcastEvent(ev);
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
@ -296,7 +300,7 @@ function EditCard({ card, cards }: EditCardProps) {
|
||||
});
|
||||
|
||||
console.debug(userCardsEv);
|
||||
System.BroadcastEvent(userCardsEv);
|
||||
await system.BroadcastEvent(userCardsEv);
|
||||
Login.setCards(newTags, userCardsEv.created_at);
|
||||
setIsOpen(false);
|
||||
}
|
||||
@ -333,8 +337,9 @@ interface AddCardProps {
|
||||
}
|
||||
|
||||
function AddCard({ cards }: AddCardProps) {
|
||||
const system = useContext(SnortContext);
|
||||
const login = useLogin();
|
||||
const tags = cards.map(toTag);
|
||||
const tags = removeUndefined(cards.map(a => NostrLink.fromEvent(a).toEventTag()));
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
async function createCard({ title, image, link, content }: NewCard) {
|
||||
@ -356,18 +361,16 @@ function AddCard({ cards }: AddCardProps) {
|
||||
});
|
||||
const userCardsEv = await pub.generic(eb => {
|
||||
eb.kind(USER_CARDS).content("");
|
||||
for (const tag of tags) {
|
||||
eb.tag(tag);
|
||||
}
|
||||
eb.tag(toTag(ev));
|
||||
tags.forEach(a => eb.tag(a));
|
||||
eb.tag(unwrap(NostrLink.fromEvent(ev).toEventTag()));
|
||||
return eb;
|
||||
});
|
||||
|
||||
console.debug(ev);
|
||||
console.debug(userCardsEv);
|
||||
|
||||
System.BroadcastEvent(ev);
|
||||
System.BroadcastEvent(userCardsEv);
|
||||
await system.BroadcastEvent(ev);
|
||||
await system.BroadcastEvent(userCardsEv);
|
||||
setIsOpen(false);
|
||||
}
|
||||
}
|
||||
|
@ -1,207 +1,52 @@
|
||||
import { useMemo, type ReactNode, type FunctionComponent } from "react";
|
||||
import { NostrLink, NostrPrefix, ParsedFragment, transformText, tryParseNostrLink } from "@snort/system";
|
||||
import { FunctionComponent, useMemo } from "react";
|
||||
|
||||
import { type NostrLink, parseNostrLink, validateNostrLink } from "@snort/system";
|
||||
import { Emoji } from "./emoji";
|
||||
import { Mention } from "./mention";
|
||||
import { HyperText } from "./hypertext";
|
||||
import { Event } from "./Event";
|
||||
|
||||
import { Event } from "element/Event";
|
||||
import { Mention } from "element/mention";
|
||||
import { Emoji } from "element/emoji";
|
||||
import { HyperText } from "element/hypertext";
|
||||
import { splitByUrl } from "utils";
|
||||
import type { Tags } from "types";
|
||||
|
||||
export type Fragment = string | ReactNode;
|
||||
|
||||
const NostrPrefixRegex = /^nostr:/;
|
||||
const EmojiRegex = /:([\w-]+):/g;
|
||||
|
||||
function extractLinks(fragments: Fragment[]) {
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return splitByUrl(f).map(a => {
|
||||
const validateLink = () => {
|
||||
const normalizedStr = a.toLowerCase();
|
||||
|
||||
if (normalizedStr.startsWith("web+nostr:") || normalizedStr.startsWith("nostr:")) {
|
||||
return validateNostrLink(normalizedStr);
|
||||
}
|
||||
|
||||
return normalizedStr.startsWith("http:") || normalizedStr.startsWith("https:");
|
||||
};
|
||||
|
||||
if (validateLink()) {
|
||||
return <HyperText link={a}>{a}</HyperText>;
|
||||
}
|
||||
return a;
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
function extractEmoji(fragments: Fragment[], tags: string[][]) {
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(EmojiRegex).map(i => {
|
||||
const t = tags.find(a => a[0] === "emoji" && a[1] === i);
|
||||
if (t) {
|
||||
return <Emoji name={t[1]} url={t[2]} />;
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
function extractNprofiles(fragments: Fragment[]) {
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(/(nostr:nprofile1[a-z0-9]+)/g).map(i => {
|
||||
if (i.startsWith("nostr:nprofile1")) {
|
||||
try {
|
||||
const link = parseNostrLink(i.replace(NostrPrefixRegex, ""));
|
||||
return <Mention key={link.id} pubkey={link.id} />;
|
||||
} catch (error) {
|
||||
return i;
|
||||
}
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
function extractNpubs(fragments: Fragment[]) {
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(/(nostr:npub1[a-z0-9]+)/g).map(i => {
|
||||
if (i.startsWith("nostr:npub1")) {
|
||||
try {
|
||||
const link = parseNostrLink(i.replace(NostrPrefixRegex, ""));
|
||||
return <Mention key={link.id} pubkey={link.id} />;
|
||||
} catch (error) {
|
||||
return i;
|
||||
}
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
function extractNevents(fragments: Fragment[], Event: NostrComponent) {
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(/(nostr:nevent1[a-z0-9]+)/g).map(i => {
|
||||
if (i.startsWith("nostr:nevent1")) {
|
||||
try {
|
||||
const link = parseNostrLink(i.replace(NostrPrefixRegex, ""));
|
||||
return <Event link={link} />;
|
||||
} catch (error) {
|
||||
return i;
|
||||
}
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
function extractNaddrs(fragments: Fragment[], Address: NostrComponent) {
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(/(nostr:naddr1[a-z0-9]+)/g).map(i => {
|
||||
if (i.startsWith("nostr:naddr1")) {
|
||||
try {
|
||||
const link = parseNostrLink(i.replace(NostrPrefixRegex, ""));
|
||||
return <Address key={i} link={link} />;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return i;
|
||||
}
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
function extractNoteIds(fragments: Fragment[], Event: NostrComponent) {
|
||||
return fragments
|
||||
.map(f => {
|
||||
if (typeof f === "string") {
|
||||
return f.split(/(nostr:note1[a-z0-9]+)/g).map(i => {
|
||||
if (i.startsWith("nostr:note1")) {
|
||||
try {
|
||||
const link = parseNostrLink(i.replace(NostrPrefixRegex, ""));
|
||||
return <Event link={link} />;
|
||||
} catch (error) {
|
||||
return i;
|
||||
}
|
||||
} else {
|
||||
return i;
|
||||
}
|
||||
});
|
||||
}
|
||||
return f;
|
||||
})
|
||||
.flat();
|
||||
}
|
||||
|
||||
export type NostrComponent = FunctionComponent<{ link: NostrLink }>;
|
||||
|
||||
export interface NostrComponents {
|
||||
Event: NostrComponent;
|
||||
}
|
||||
|
||||
const components: NostrComponents = {
|
||||
Event,
|
||||
};
|
||||
|
||||
export function transformText(ps: Fragment[], tags: Array<string[]>, customComponents = components) {
|
||||
let fragments = extractEmoji(ps, tags);
|
||||
fragments = extractNprofiles(fragments);
|
||||
fragments = extractNevents(fragments, customComponents.Event);
|
||||
fragments = extractNaddrs(fragments, customComponents.Event);
|
||||
fragments = extractNoteIds(fragments, customComponents.Event);
|
||||
fragments = extractNpubs(fragments);
|
||||
fragments = extractLinks(fragments);
|
||||
|
||||
return fragments;
|
||||
}
|
||||
export type EventComponent = FunctionComponent<{ link: NostrLink }>;
|
||||
|
||||
interface TextProps {
|
||||
content: string;
|
||||
tags: Tags;
|
||||
customComponents?: NostrComponents;
|
||||
tags: Array<Array<string>>;
|
||||
eventComponent?: EventComponent;
|
||||
}
|
||||
|
||||
export function Text({ content, tags, customComponents }: TextProps) {
|
||||
// todo: RTL langugage support
|
||||
const element = useMemo(() => {
|
||||
return <span className="text">{transformText([content], tags, customComponents)}</span>;
|
||||
export function Text({ content, tags, eventComponent }: TextProps) {
|
||||
const frags = useMemo(() => {
|
||||
return transformText(content, tags);
|
||||
}, [content, tags]);
|
||||
|
||||
return <>{element}</>;
|
||||
function renderFrag(f: ParsedFragment) {
|
||||
switch (f.type) {
|
||||
case "custom_emoji":
|
||||
return <Emoji name="" url={f.content} />;
|
||||
case "media":
|
||||
case "link": {
|
||||
if (f.content.startsWith("nostr:")) {
|
||||
const link = tryParseNostrLink(f.content);
|
||||
if (link) {
|
||||
if (
|
||||
link.type === NostrPrefix.Event ||
|
||||
link?.type === NostrPrefix.Address ||
|
||||
link?.type === NostrPrefix.Note
|
||||
) {
|
||||
return eventComponent?.({ link }) ?? <Event link={link} />;
|
||||
} else {
|
||||
return <Mention pubkey={link.id} />;
|
||||
}
|
||||
}
|
||||
}
|
||||
return <HyperText link={f.content} />;
|
||||
}
|
||||
case "mention":
|
||||
return <Mention pubkey={f.content} />;
|
||||
default:
|
||||
return <span className="text">{f.content}</span>;
|
||||
}
|
||||
}
|
||||
|
||||
return frags.map(renderFrag);
|
||||
}
|
||||
|
@ -1,17 +1,17 @@
|
||||
import "./textarea.css";
|
||||
import type { KeyboardEvent, ChangeEvent } from "react";
|
||||
import { type KeyboardEvent, type ChangeEvent, useContext } from "react";
|
||||
import ReactTextareaAutocomplete, { TriggerType } from "@webscopeio/react-textarea-autocomplete";
|
||||
import "@webscopeio/react-textarea-autocomplete/style.css";
|
||||
import uniqWith from "lodash/uniqWith";
|
||||
import isEqual from "lodash/isEqual";
|
||||
|
||||
import { hexToBech32 } from "@snort/shared";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
import { MetadataCache, NostrPrefix, UserProfileCache } from "@snort/system";
|
||||
|
||||
import { Emoji } from "element/emoji";
|
||||
import { Avatar } from "element/avatar";
|
||||
import { hexToBech32 } from "utils";
|
||||
import type { EmojiTag } from "types";
|
||||
import { System } from "index";
|
||||
|
||||
interface EmojiItemProps {
|
||||
name: string;
|
||||
@ -48,8 +48,9 @@ interface TextareaProps {
|
||||
}
|
||||
|
||||
export function Textarea({ emojis, ...props }: TextareaProps) {
|
||||
const system = useContext(SnortContext);
|
||||
const userDataProvider = async (token: string) => {
|
||||
const cache = System.ProfileLoader.Cache;
|
||||
const cache = system.ProfileLoader.Cache;
|
||||
if (cache instanceof UserProfileCache) {
|
||||
return await cache.search(token);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { NostrLink, EventKind } from "@snort/system";
|
||||
import React, { useRef, useState } from "react";
|
||||
import React, { useContext, useRef, useState } from "react";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
import { useLogin } from "hooks/login";
|
||||
@ -8,10 +8,11 @@ import { Icon } from "element/icon";
|
||||
import { Textarea } from "element/textarea";
|
||||
import { EmojiPicker } from "element/emoji-picker";
|
||||
import type { EmojiPack, Emoji } from "types";
|
||||
import { System } from "index";
|
||||
import { LIVE_STREAM_CHAT } from "const";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
|
||||
export function WriteMessage({ link, emojiPacks }: { link: NostrLink; emojiPacks: EmojiPack[] }) {
|
||||
const system = useContext(SnortContext);
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const emojiRef = useRef(null);
|
||||
const [chat, setChat] = useState("");
|
||||
@ -49,7 +50,7 @@ export function WriteMessage({ link, emojiPacks }: { link: NostrLink; emojiPacks
|
||||
});
|
||||
if (reply) {
|
||||
console.debug(reply);
|
||||
System.BroadcastEvent(reply);
|
||||
system.BroadcastEvent(reply);
|
||||
}
|
||||
setChat("");
|
||||
}
|
||||
|
@ -1,11 +1,11 @@
|
||||
import { useMemo } from "react";
|
||||
import { useContext, useMemo } from "react";
|
||||
import { RequestBuilder, NoteCollection, NostrLink, EventKind, parseZap } from "@snort/system";
|
||||
import { useRequestBuilder } from "@snort/system-react";
|
||||
import { SnortContext, useRequestBuilder } from "@snort/system-react";
|
||||
import { LIVE_STREAM } from "const";
|
||||
import { findTag } from "utils";
|
||||
import { System } from "index";
|
||||
|
||||
export function useProfile(link: NostrLink, leaveOpen = false) {
|
||||
const system = useContext(SnortContext);
|
||||
const sub = useMemo(() => {
|
||||
const b = new RequestBuilder(`profile:${link.id.slice(0, 12)}`);
|
||||
b.withOptions({
|
||||
@ -43,7 +43,7 @@ export function useProfile(link: NostrLink, leaveOpen = false) {
|
||||
|
||||
const { data: zapsData } = useRequestBuilder(NoteCollection, zapsSub);
|
||||
const zaps = (zapsData ?? [])
|
||||
.map(ev => parseZap(ev, System.ProfileLoader.Cache))
|
||||
.map(ev => parseZap(ev, system.ProfileLoader.Cache))
|
||||
.filter(z => z && z.valid && z.receiver === link.id);
|
||||
|
||||
const sortedStreams = useMemo(() => {
|
||||
|
@ -1,8 +1,7 @@
|
||||
import { useContext, useMemo, useEffect } from "react";
|
||||
import { unwrap } from "@snort/shared";
|
||||
import { NostrLink, RequestBuilder, NostrPrefix, EventKind, NoteCollection, parseZap } from "@snort/system";
|
||||
import { SnortContext, useRequestBuilder } from "@snort/system-react";
|
||||
import { System } from "index";
|
||||
import { useContext, useMemo, useEffect } from "react";
|
||||
import { findTag } from "utils";
|
||||
|
||||
export function useZaps(link?: NostrLink, leaveOpen = false) {
|
||||
@ -34,7 +33,7 @@ export function useZaps(link?: NostrLink, leaveOpen = false) {
|
||||
return (
|
||||
[...(zaps ?? [])]
|
||||
.sort((a, b) => (b.created_at > a.created_at ? 1 : -1))
|
||||
.map(ev => parseZap(ev, System.ProfileLoader.Cache))
|
||||
.map(ev => parseZap(ev, system.ProfileLoader.Cache))
|
||||
.filter(z => z && z.valid) ?? []
|
||||
);
|
||||
}
|
||||
|
@ -341,7 +341,7 @@ div.paper {
|
||||
background-color: unset;
|
||||
}
|
||||
|
||||
.emoji {
|
||||
.custom-emoji {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
margin-bottom: -2px;
|
||||
|
@ -6,6 +6,7 @@ import React from "react";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { NostrSystem } from "@snort/system";
|
||||
import { SnortContext } from "@snort/system-react";
|
||||
import { SnortSystemDb } from "@snort/system-web";
|
||||
import { RouterProvider, createBrowserRouter } from "react-router-dom";
|
||||
|
||||
import { RootPage } from "pages/root";
|
||||
@ -30,7 +31,11 @@ export enum StreamState {
|
||||
Planned = "planned",
|
||||
}
|
||||
|
||||
export const System = new NostrSystem({});
|
||||
const db = new SnortSystemDb();
|
||||
const System = new NostrSystem({
|
||||
db,
|
||||
checkSigs: false,
|
||||
});
|
||||
export const Login = new LoginStore();
|
||||
|
||||
register();
|
||||
@ -44,6 +49,7 @@ const router = createBrowserRouter([
|
||||
{
|
||||
element: <LayoutPage />,
|
||||
loader: async () => {
|
||||
db.ready = await db.isAvailable();
|
||||
await System.Init();
|
||||
return null;
|
||||
},
|
||||
@ -86,6 +92,7 @@ const router = createBrowserRouter([
|
||||
path: "/chat/:id",
|
||||
element: <ChatPopout />,
|
||||
loader: async () => {
|
||||
db.ready = await db.isAvailable();
|
||||
await System.Init();
|
||||
return null;
|
||||
},
|
||||
@ -94,6 +101,7 @@ const router = createBrowserRouter([
|
||||
path: "/alert/:id/:type",
|
||||
element: <AlertsPage />,
|
||||
loader: async () => {
|
||||
db.ready = await db.isAvailable();
|
||||
await System.Init();
|
||||
return null;
|
||||
},
|
||||
|
@ -4,16 +4,16 @@ import { useLocation, useNavigate } from "react-router-dom";
|
||||
import { Helmet } from "react-helmet";
|
||||
|
||||
import { LiveVideoPlayer } from "element/live-video-player";
|
||||
import { eventToLink, findTag, getEventFromLocationState, getHost } from "utils";
|
||||
import { findTag, getEventFromLocationState, getHost } from "utils";
|
||||
import { Profile, getName } from "element/profile";
|
||||
import { LiveChat } from "element/live-chat";
|
||||
import AsyncButton from "element/async-button";
|
||||
import { useLogin } from "hooks/login";
|
||||
import { useZapGoal } from "hooks/goals";
|
||||
import { StreamState, System } from "index";
|
||||
import { StreamState } from "index";
|
||||
import { SendZapsDialog } from "element/send-zap";
|
||||
import { NostrEvent } from "@snort/system";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
import { SnortContext, useUserProfile } from "@snort/system-react";
|
||||
import { NewStreamDialog } from "element/new-stream";
|
||||
import { Tags } from "element/tags";
|
||||
import { StatePill } from "element/state-pill";
|
||||
@ -25,8 +25,10 @@ import { ContentWarningOverlay, isContentWarningAccepted } from "element/content
|
||||
import { useCurrentStreamFeed } from "hooks/current-stream-feed";
|
||||
import { useStreamLink } from "hooks/stream-link";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { useContext } from "react";
|
||||
|
||||
function ProfileInfo({ ev, goal }: { ev?: NostrEvent; goal?: TaggedNostrEvent }) {
|
||||
const system = useContext(SnortContext);
|
||||
const login = useLogin();
|
||||
const navigate = useNavigate();
|
||||
const host = getHost(ev);
|
||||
@ -41,7 +43,7 @@ function ProfileInfo({ ev, goal }: { ev?: NostrEvent; goal?: TaggedNostrEvent })
|
||||
if (pub && ev) {
|
||||
const evDelete = await pub.delete(ev.id);
|
||||
console.debug(evDelete);
|
||||
System.BroadcastEvent(evDelete);
|
||||
await system.BroadcastEvent(evDelete);
|
||||
navigate("/");
|
||||
}
|
||||
}
|
||||
@ -113,7 +115,7 @@ export function StreamPageHandler() {
|
||||
export function StreamPage({ link, evPreload }: { evPreload?: NostrEvent; link: NostrLink }) {
|
||||
const ev = useCurrentStreamFeed(link, true, evPreload);
|
||||
const host = getHost(ev);
|
||||
const evLink = ev ? eventToLink(ev) : undefined;
|
||||
const evLink = ev ? NostrLink.fromEvent(ev) : undefined;
|
||||
const goal = useZapGoal(findTag(ev, "goal"));
|
||||
|
||||
const title = findTag(ev, "title");
|
||||
|
@ -2,18 +2,18 @@
|
||||
import "./widgets.css";
|
||||
import { useState, useMemo } from "react";
|
||||
import { useIntl, FormattedMessage } from "react-intl";
|
||||
import { NostrLink, NostrPrefix } from "@snort/system";
|
||||
|
||||
import { NostrPrefix, createNostrLink } from "@snort/system";
|
||||
import Copy from "element/copy";
|
||||
import { useCurrentStreamFeed } from "hooks/current-stream-feed";
|
||||
import { getVoices, speak, toTextToSpeechParams } from "text2speech";
|
||||
import { useLogin } from "hooks/login";
|
||||
import { eventToLink, hexToBech32 } from "utils";
|
||||
import { ZapAlertItem } from "./widgets/zaps";
|
||||
import { TopZappersWidget } from "./widgets/top-zappers";
|
||||
import { Views } from "./widgets/views";
|
||||
import { Music } from "./widgets/music";
|
||||
import groupBy from "lodash/groupBy";
|
||||
import { hexToBech32 } from "@snort/shared";
|
||||
|
||||
interface ZapAlertConfigurationProps {
|
||||
npub: string;
|
||||
@ -164,9 +164,9 @@ function ZapAlertConfiguration({ npub, baseUrl }: ZapAlertConfigurationProps) {
|
||||
|
||||
export function WidgetsPage() {
|
||||
const login = useLogin();
|
||||
const profileLink = createNostrLink(NostrPrefix.PublicKey, login?.pubkey ?? "");
|
||||
const profileLink = new NostrLink(NostrPrefix.PublicKey, login?.pubkey ?? "");
|
||||
const current = useCurrentStreamFeed(profileLink);
|
||||
const currentLink = current ? eventToLink(current) : undefined;
|
||||
const currentLink = current ? NostrLink.fromEvent(current) : undefined;
|
||||
const npub = hexToBech32("npub", login?.pubkey);
|
||||
|
||||
const baseUrl = `${window.location.protocol}//${window.location.host}`;
|
||||
|
@ -12,7 +12,8 @@ export function Music({ link }: { link: NostrLink }) {
|
||||
const expiry = nowPlaying && findTag(nowPlaying, "expiration");
|
||||
const isExpired = expiry && Number(expiry) < unixNow();
|
||||
return (
|
||||
(nowPlaying && !isExpired) && (
|
||||
nowPlaying &&
|
||||
!isExpired && (
|
||||
<div className="music">
|
||||
{cover && <img className="cover" src={cover} alt={nowPlaying.content} />}
|
||||
{nowPlaying && <p className="track">🎵 {nowPlaying.content}</p>}
|
||||
|
@ -3,11 +3,10 @@ import { TopZappers } from "element/top-zappers";
|
||||
import { useCurrentStreamFeed } from "hooks/current-stream-feed";
|
||||
import { useZaps } from "hooks/zaps";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { eventToLink } from "utils";
|
||||
|
||||
export function TopZappersWidget({ link }: { link: NostrLink }) {
|
||||
const currentEvent = useCurrentStreamFeed(link, true);
|
||||
const zaps = useZaps(currentEvent ? eventToLink(currentEvent) : undefined, true);
|
||||
const zaps = useZaps(currentEvent ? NostrLink.fromEvent(currentEvent) : undefined, true);
|
||||
return (
|
||||
<div className="top-zappers-widget">
|
||||
<div>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { useMemo, useState, useEffect } from "react";
|
||||
|
||||
import { hexToBech32 } from "@snort/shared";
|
||||
import type { NostrLink, ParsedZap } from "@snort/system";
|
||||
import { NostrLink, ParsedZap } from "@snort/system";
|
||||
import { useUserProfile } from "@snort/system-react";
|
||||
|
||||
import { useCurrentStreamFeed } from "hooks/current-stream-feed";
|
||||
@ -10,7 +10,7 @@ import { useMutedPubkeys } from "hooks/lists";
|
||||
import { formatSats } from "number";
|
||||
import { useTextToSpeechParams, getVoices, speak } from "text2speech";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
import { getHost, eventToLink } from "utils";
|
||||
import { getHost } from "utils";
|
||||
|
||||
function useZapQueue(zapStream: ParsedZap[], zapTime = 10_000) {
|
||||
const zaps = useMemo(() => {
|
||||
@ -34,7 +34,7 @@ function useZapQueue(zapStream: ParsedZap[], zapTime = 10_000) {
|
||||
|
||||
export function ZapAlerts({ link }: { link: NostrLink }) {
|
||||
const currentEvent = useCurrentStreamFeed(link, true);
|
||||
const currentLink = currentEvent ? eventToLink(currentEvent) : undefined;
|
||||
const currentLink = currentEvent ? NostrLink.fromEvent(currentEvent) : undefined;
|
||||
const host = getHost(currentEvent);
|
||||
const zaps = useZaps(currentLink, true);
|
||||
const zap = useZapQueue(zaps);
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { StreamState } from "index";
|
||||
import { NostrEvent } from "@snort/system";
|
||||
import { NostrEvent, SystemInterface } from "@snort/system";
|
||||
import { ExternalStore } from "@snort/shared";
|
||||
import { Nip103StreamProvider } from "./zsz";
|
||||
import { ManualProvider } from "./manual";
|
||||
@ -22,7 +22,7 @@ export interface StreamProvider {
|
||||
/**
|
||||
* Update stream info event
|
||||
*/
|
||||
updateStreamInfo(ev: NostrEvent): Promise<void>;
|
||||
updateStreamInfo(system: SystemInterface, ev: NostrEvent): Promise<void>;
|
||||
|
||||
/**
|
||||
* Top-up balance with provider
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { NostrEvent } from "@snort/system";
|
||||
import { System } from "index";
|
||||
import { NostrEvent, SystemInterface } from "@snort/system";
|
||||
import { StreamProvider, StreamProviderInfo, StreamProviders } from "providers";
|
||||
|
||||
export class ManualProvider implements StreamProvider {
|
||||
@ -23,9 +22,8 @@ export class ManualProvider implements StreamProvider {
|
||||
};
|
||||
}
|
||||
|
||||
updateStreamInfo(ev: NostrEvent): Promise<void> {
|
||||
System.BroadcastEvent(ev);
|
||||
return Promise.resolve();
|
||||
async updateStreamInfo(system: SystemInterface, ev: NostrEvent): Promise<void> {
|
||||
await system.BroadcastEvent(ev);
|
||||
}
|
||||
|
||||
topup(): Promise<string> {
|
||||
|
@ -6,7 +6,7 @@ import {
|
||||
StreamProviderStreamInfo,
|
||||
StreamProviders,
|
||||
} from ".";
|
||||
import { EventKind, EventPublisher, NostrEvent } from "@snort/system";
|
||||
import { EventKind, EventPublisher, NostrEvent, SystemInterface } from "@snort/system";
|
||||
import { Login, StreamState } from "index";
|
||||
import { getPublisher } from "login";
|
||||
import { findTag } from "utils";
|
||||
@ -53,7 +53,7 @@ export class Nip103StreamProvider implements StreamProvider {
|
||||
};
|
||||
}
|
||||
|
||||
async updateStreamInfo(ev: NostrEvent): Promise<void> {
|
||||
async updateStreamInfo(system: SystemInterface, ev: NostrEvent): Promise<void> {
|
||||
const title = findTag(ev, "title");
|
||||
const summary = findTag(ev, "summary");
|
||||
const image = findTag(ev, "image");
|
||||
|
@ -418,4 +418,3 @@
|
||||
"defaultMessage": "Zap Alert"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,4 +418,3 @@
|
||||
"defaultMessage": "Зап Тревога"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,4 +418,3 @@
|
||||
"defaultMessage": "জ্যাপ অ্যালার্ট"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,4 +418,3 @@
|
||||
"defaultMessage": "Zap Alert"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,4 +418,3 @@
|
||||
"defaultMessage": "Zap Alert"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,4 +418,3 @@
|
||||
"defaultMessage": "Alerta de Zap"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,4 +418,3 @@
|
||||
"defaultMessage": "هشدار زپ"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,4 +418,3 @@
|
||||
"defaultMessage": "Zap-hälytys"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,4 +418,3 @@
|
||||
"defaultMessage": "Alerte Zap"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,4 +418,3 @@
|
||||
"defaultMessage": "Zap Riadó"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,4 +418,3 @@
|
||||
"defaultMessage": "Zap Alert"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,4 +418,3 @@
|
||||
"defaultMessage": "ザップアラート"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,4 +418,3 @@
|
||||
"defaultMessage": "Zap Alert"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,4 +418,3 @@
|
||||
"defaultMessage": "Alerta Zap"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,4 +418,3 @@
|
||||
"defaultMessage": "Оповещение о запе"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,4 +418,3 @@
|
||||
"defaultMessage": "Zap Alert"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,4 +418,3 @@
|
||||
"defaultMessage": "Tahadhari ya Zap"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,4 +418,3 @@
|
||||
"defaultMessage": "Zap Alert"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,4 +418,3 @@
|
||||
"defaultMessage": "打闪提示"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -418,4 +418,3 @@
|
||||
"defaultMessage": "打閃提示"
|
||||
}
|
||||
}
|
||||
|
||||
|
58
src/utils.ts
58
src/utils.ts
@ -1,9 +1,7 @@
|
||||
import { NostrEvent, NostrPrefix, TaggedNostrEvent, createNostrLink, encodeTLV } from "@snort/system";
|
||||
import * as utils from "@noble/curves/abstract/utils";
|
||||
import { bech32 } from "@scure/base";
|
||||
import type { Tag, Tags } from "types";
|
||||
import { NostrEvent, NostrLink, TaggedNostrEvent } from "@snort/system";
|
||||
|
||||
import type { Tags } from "types";
|
||||
import { LIVE_STREAM } from "const";
|
||||
import { unwrap } from "@snort/shared";
|
||||
|
||||
export function toAddress(e: NostrEvent): string {
|
||||
if (e.kind && e.kind >= 30000 && e.kind <= 40000) {
|
||||
@ -19,20 +17,6 @@ export function toAddress(e: NostrEvent): string {
|
||||
return e.id;
|
||||
}
|
||||
|
||||
export function toTag(e: NostrEvent): Tag {
|
||||
if (e.kind && e.kind >= 30000 && e.kind <= 40000) {
|
||||
const dTag = findTag(e, "d");
|
||||
|
||||
return ["a", `${e.kind}:${e.pubkey}:${dTag}`];
|
||||
}
|
||||
|
||||
if (e.kind === 0 || e.kind === 3) {
|
||||
return ["p", e.pubkey];
|
||||
}
|
||||
|
||||
return ["e", e.id];
|
||||
}
|
||||
|
||||
export function findTag(e: NostrEvent | undefined, tag: string) {
|
||||
const maybeTag = e?.tags.find(evTag => {
|
||||
return evTag[0] === tag;
|
||||
@ -40,27 +24,6 @@ export function findTag(e: NostrEvent | undefined, tag: string) {
|
||||
return maybeTag && maybeTag[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert hex to bech32
|
||||
*/
|
||||
export function hexToBech32(hrp: string, hex?: string) {
|
||||
if (typeof hex !== "string" || hex.length === 0 || hex.length % 2 !== 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
if (hrp === NostrPrefix.Note || hrp === NostrPrefix.PrivateKey || hrp === NostrPrefix.PublicKey) {
|
||||
const buf = utils.hexToBytes(hex);
|
||||
return bech32.encode(hrp, bech32.toWords(buf));
|
||||
} else {
|
||||
return encodeTLV(hrp as NostrPrefix, hex);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Invalid hex", hex, e);
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export function splitByUrl(str: string) {
|
||||
const urlRegex =
|
||||
/((?:http|ftp|https|nostr|web\+nostr|magnet):\/?\/?(?:[\w+?.\w+])+(?:[a-zA-Z0-9~!@#$%^&*()_\-=+\\/?.:;',]*)?(?:[-A-Za-z0-9+&@#/%=~()_|]))/i;
|
||||
@ -69,12 +32,7 @@ export function splitByUrl(str: string) {
|
||||
}
|
||||
|
||||
export function eventLink(ev: NostrEvent | TaggedNostrEvent) {
|
||||
if (ev.kind && ev.kind >= 30000 && ev.kind <= 40000) {
|
||||
const d = findTag(ev, "d") ?? "";
|
||||
return encodeTLV(NostrPrefix.Address, d, "relays" in ev ? ev.relays : undefined, ev.kind, ev.pubkey);
|
||||
} else {
|
||||
return encodeTLV(NostrPrefix.Event, ev.id, "relays" in ev ? ev.relays : undefined);
|
||||
}
|
||||
return NostrLink.fromEvent(ev).encode();
|
||||
}
|
||||
|
||||
export function getHost(ev?: NostrEvent) {
|
||||
@ -110,11 +68,3 @@ export function getEventFromLocationState(state: unknown | undefined | null) {
|
||||
? (state as NostrEvent)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function eventToLink(ev: NostrEvent) {
|
||||
if (ev.kind >= 30_000 && ev.kind < 40_000) {
|
||||
const dTag = unwrap(findTag(ev, "d"));
|
||||
return createNostrLink(NostrPrefix.Address, dTag, undefined, ev.kind, ev.pubkey);
|
||||
}
|
||||
return createNostrLink(NostrPrefix.Event, ev.id, undefined, ev.kind, ev.pubkey);
|
||||
}
|
||||
|
Reference in New Issue
Block a user