refactor: upgrade snort pkgs

This commit is contained in:
2023-10-16 23:05:37 +01:00
parent d9bcea518b
commit b36c305a53
57 changed files with 389 additions and 1094 deletions

View File

@ -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);

View File

@ -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>
);

View File

@ -1,4 +1,4 @@
.emoji {
.custom-emoji {
width: 21px;
height: 21px;
display: inline-block;

View File

@ -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[] }) {

View File

@ -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);
}
}

View File

@ -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));

View File

@ -6,7 +6,7 @@ const FileExtensionRegex = /\.([\w]+)$/i;
interface HyperTextProps {
link: string;
children: ReactNode;
children?: ReactNode;
}
export function HyperText({ link, children }: HyperTextProps) {

View File

@ -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}
/>
);
}

View File

@ -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) {

View File

@ -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;
}

View File

@ -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>
);
}
});

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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("");

View File

@ -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)}`;

View File

@ -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={

View File

@ -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 (

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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("");
}

View File

@ -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(() => {

View File

@ -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) ?? []
);
}

View File

@ -341,7 +341,7 @@ div.paper {
background-color: unset;
}
.emoji {
.custom-emoji {
width: 15px;
height: 15px;
margin-bottom: -2px;

View File

@ -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;
},

View File

@ -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");

View File

@ -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}`;

View File

@ -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>}

View File

@ -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>

View File

@ -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);

View File

@ -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

View File

@ -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> {

View File

@ -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");

View File

@ -418,4 +418,3 @@
"defaultMessage": "Zap Alert"
}
}

View File

@ -418,4 +418,3 @@
"defaultMessage": "Зап Тревога"
}
}

View File

@ -418,4 +418,3 @@
"defaultMessage": "জ্যাপ অ্যালার্ট"
}
}

View File

@ -418,4 +418,3 @@
"defaultMessage": "Zap Alert"
}
}

View File

@ -418,4 +418,3 @@
"defaultMessage": "Zap Alert"
}
}

View File

@ -418,4 +418,3 @@
"defaultMessage": "Alerta de Zap"
}
}

View File

@ -418,4 +418,3 @@
"defaultMessage": "هشدار زپ"
}
}

View File

@ -418,4 +418,3 @@
"defaultMessage": "Zap-hälytys"
}
}

View File

@ -418,4 +418,3 @@
"defaultMessage": "Alerte Zap"
}
}

View File

@ -418,4 +418,3 @@
"defaultMessage": "Zap Riadó"
}
}

View File

@ -418,4 +418,3 @@
"defaultMessage": "Zap Alert"
}
}

View File

@ -418,4 +418,3 @@
"defaultMessage": "ザップアラート"
}
}

View File

@ -418,4 +418,3 @@
"defaultMessage": "Zap Alert"
}
}

View File

@ -418,4 +418,3 @@
"defaultMessage": "Alerta Zap"
}
}

View File

@ -418,4 +418,3 @@
"defaultMessage": "Оповещение о запе"
}
}

View File

@ -418,4 +418,3 @@
"defaultMessage": "Zap Alert"
}
}

View File

@ -418,4 +418,3 @@
"defaultMessage": "Tahadhari ya Zap"
}
}

View File

@ -418,4 +418,3 @@
"defaultMessage": "Zap Alert"
}
}

View File

@ -418,4 +418,3 @@
"defaultMessage": "打闪提示"
}
}

View File

@ -418,4 +418,3 @@
"defaultMessage": "打閃提示"
}
}

View File

@ -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);
}