fix: type errors

This commit is contained in:
2023-08-01 12:43:26 +02:00
parent 67bcfc58e0
commit e15a46192a
27 changed files with 480 additions and 380 deletions

View File

@ -4,7 +4,7 @@ import { findTag } from "utils";
export function Badge({ ev }: { ev: NostrEvent }) { export function Badge({ ev }: { ev: NostrEvent }) {
const name = findTag(ev, "name") || findTag(ev, "d"); const name = findTag(ev, "name") || findTag(ev, "d");
const description = findTag(ev, "description"); const description = findTag(ev, "description") ?? "";
const thumb = findTag(ev, "thumb"); const thumb = findTag(ev, "thumb");
const image = findTag(ev, "image"); const image = findTag(ev, "image");
return ( return (

View File

@ -8,18 +8,17 @@ import {
useIntersectionObserver, useIntersectionObserver,
} from "usehooks-ts"; } from "usehooks-ts";
import { System } from "../index"; import { EmojiPicker } from "element/emoji-picker";
import { formatSats } from "../number"; import { Icon } from "element/icon";
import { EmojiPicker } from "./emoji-picker"; import { Emoji as EmojiComponent } from "element/emoji";
import { Icon } from "./icon";
import { Emoji as EmojiComponent } from "./emoji";
import { Profile } from "./profile"; import { Profile } from "./profile";
import { Text } from "element/text"; import { Text } from "element/text";
import { SendZapsDialog } from "./send-zap"; import { SendZapsDialog } from "element/send-zap";
import { findTag } from "../utils"; import { useLogin } from "hooks/login";
import type { EmojiPack } from "../hooks/emoji"; import { formatSats } from "number";
import { useLogin } from "../hooks/login"; import { findTag } from "utils";
import type { Badge, Emoji } from "types"; import type { Badge, Emoji, EmojiPack } from "types";
import { System } from "index";
function emojifyReaction(reaction: string) { function emojifyReaction(reaction: string) {
if (reaction === "+") { if (reaction === "+") {
@ -104,7 +103,7 @@ export function ChatMessage({
if (emoji.native) { if (emoji.native) {
reply = await pub?.react(ev, emoji.native || "+1"); reply = await pub?.react(ev, emoji.native || "+1");
} else { } else {
const e = getEmojiById(emoji.id); const e = getEmojiById(emoji.id!);
if (e) { if (e) {
reply = await pub?.generic((eb) => { reply = await pub?.generic((eb) => {
return eb return eb

View File

@ -8,6 +8,7 @@ import { Mention } from "element/mention";
import { findTag } from "utils"; import { findTag } from "utils";
import { USER_EMOJIS } from "const"; import { USER_EMOJIS } from "const";
import { Login, System } from "index"; import { Login, System } from "index";
import type { EmojiPack as EmojiPackType } from "types";
export function EmojiPack({ ev }: { ev: NostrEvent }) { export function EmojiPack({ ev }: { ev: NostrEvent }) {
const login = useLogin(); const login = useLogin();
@ -18,13 +19,14 @@ export function EmojiPack({ ev }: { ev: NostrEvent }) {
const emoji = ev.tags.filter((e) => e.at(0) === "emoji"); const emoji = ev.tags.filter((e) => e.at(0) === "emoji");
async function toggleEmojiPack() { async function toggleEmojiPack() {
let newPacks = []; let newPacks = [] as EmojiPackType[];
if (isUsed) { if (isUsed) {
newPacks = login.emojis.filter( newPacks =
(e) => e.pubkey !== ev.pubkey && e.name !== name, login?.emojis.filter(
); (e) => e.author !== ev.pubkey && e.name !== name,
) ?? [];
} else { } else {
newPacks = [...login.emojis, toEmojiPack(ev)]; newPacks = [...(login?.emojis ?? []), toEmojiPack(ev)];
} }
const pub = login?.publisher(); const pub = login?.publisher();
if (pub) { if (pub) {
@ -37,7 +39,7 @@ export function EmojiPack({ ev }: { ev: NostrEvent }) {
}); });
console.debug(ev); console.debug(ev);
System.BroadcastEvent(ev); System.BroadcastEvent(ev);
Login.setEmojis(newPacks, ev.created_at); Login.setEmojis(newPacks);
} }
} }
@ -48,12 +50,14 @@ export function EmojiPack({ ev }: { ev: NostrEvent }) {
<h4>{name}</h4> <h4>{name}</h4>
<Mention pubkey={ev.pubkey} /> <Mention pubkey={ev.pubkey} />
</div> </div>
{login?.pubkey && (
<AsyncButton <AsyncButton
className={`btn btn-primary ${isUsed ? "delete-button" : ""}`} className={`btn btn-primary ${isUsed ? "delete-button" : ""}`}
onClick={toggleEmojiPack} onClick={toggleEmojiPack}
> >
{isUsed ? "Remove" : "Add"} {isUsed ? "Remove" : "Add"}
</AsyncButton> </AsyncButton>
)}
</div> </div>
<div className="emoji-pack-emojis"> <div className="emoji-pack-emojis">
{emoji.map((e) => { {emoji.map((e) => {

View File

@ -1,8 +1,7 @@
import data, { Emoji } from "@emoji-mart/data"; import data, { Emoji } from "@emoji-mart/data";
import Picker from "@emoji-mart/react"; import Picker from "@emoji-mart/react";
import { RefObject } from "react"; import { RefObject } from "react";
import { EmojiPack } from "types";
import { EmojiPack } from "../hooks/emoji";
interface EmojiPickerProps { interface EmojiPickerProps {
topOffset: number; topOffset: number;

View File

@ -1,6 +1,28 @@
import type { ReactNode } from "react";
import { Icon } from "element/icon"; import { Icon } from "element/icon";
export function ExternalIconLink({ size = 32, href, ...rest }) { interface ExternalLinkProps {
href: string;
children: ReactNode;
}
export function ExternalLink({ children, href }: ExternalLinkProps) {
return (
<a href={href} rel="noopener noreferrer" target="_blank">
{children}
</a>
);
}
interface ExternalIconLinkProps extends Omit<ExternalLinkProps, "children"> {
size?: number;
}
export function ExternalIconLink({
size = 32,
href,
...rest
}: ExternalIconLinkProps) {
return ( return (
<span style={{ cursor: "pointer" }}> <span style={{ cursor: "pointer" }}>
<Icon <Icon
@ -12,11 +34,3 @@ export function ExternalIconLink({ size = 32, href, ...rest }) {
</span> </span>
); );
} }
export function ExternalLink({ children, href }) {
return (
<a href={href} rel="noopener noreferrer" target="_blank">
{children}
</a>
);
}

View File

@ -1,4 +1,5 @@
import "./file-uploader.css"; import "./file-uploader.css";
import type { ChangeEvent } from "react";
import { VoidApi } from "@void-cat/api"; import { VoidApi } from "@void-cat/api";
import { useState } from "react"; import { useState } from "react";
@ -38,12 +39,22 @@ async function voidCatUpload(file: File | Blob): Promise<UploadResult> {
} }
} }
export function FileUploader({ defaultImage, onClear, onFileUpload }) { interface FileUploaderProps {
const [img, setImg] = useState(defaultImage); defaultImage?: string;
onClear(): void;
onFileUpload(url: string): void;
}
export function FileUploader({
defaultImage,
onClear,
onFileUpload,
}: FileUploaderProps) {
const [img, setImg] = useState<string>(defaultImage ?? "");
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
async function onFileChange(ev) { async function onFileChange(ev: ChangeEvent<HTMLInputElement>) {
const file = ev.target.files[0]; const file = ev.target.files && ev.target.files[0];
if (file) { if (file) {
try { try {
setIsUploading(true); setIsUploading(true);

View File

@ -12,7 +12,7 @@ export function LoggedInFollowButton({
value: string; value: string;
}) { }) {
const login = useLogin(); const login = useLogin();
const tags = login.follows.tags; const { tags, content, timestamp } = login!.follows;
const follows = tags.filter((t) => t.at(0) === tag); const follows = tags.filter((t) => t.at(0) === tag);
const isFollowing = follows.find((t) => t.at(1) === value); const isFollowing = follows.find((t) => t.at(1) === value);
@ -21,7 +21,7 @@ export function LoggedInFollowButton({
if (pub) { if (pub) {
const newFollows = tags.filter((t) => t.at(1) !== value); const newFollows = tags.filter((t) => t.at(1) !== value);
const ev = await pub.generic((eb) => { const ev = await pub.generic((eb) => {
eb.kind(EventKind.ContactList).content(login.follows.content); eb.kind(EventKind.ContactList).content(content ?? "");
for (const t of newFollows) { for (const t of newFollows) {
eb.tag(t); eb.tag(t);
} }
@ -29,7 +29,7 @@ export function LoggedInFollowButton({
}); });
console.debug(ev); console.debug(ev);
System.BroadcastEvent(ev); System.BroadcastEvent(ev);
Login.setFollows(newFollows, login.follows.content, ev.created_at); Login.setFollows(newFollows, content ?? "", ev.created_at);
} }
} }
@ -38,7 +38,7 @@ export function LoggedInFollowButton({
if (pub) { if (pub) {
const newFollows = [...tags, [tag, value]]; const newFollows = [...tags, [tag, value]];
const ev = await pub.generic((eb) => { const ev = await pub.generic((eb) => {
eb.kind(EventKind.ContactList).content(login.follows.content); eb.kind(EventKind.ContactList).content(content ?? "");
for (const tag of newFollows) { for (const tag of newFollows) {
eb.tag(tag); eb.tag(tag);
} }
@ -46,13 +46,13 @@ export function LoggedInFollowButton({
}); });
console.debug(ev); console.debug(ev);
System.BroadcastEvent(ev); System.BroadcastEvent(ev);
Login.setFollows(newFollows, login.follows.content, ev.created_at); Login.setFollows(newFollows, content ?? "", ev.created_at);
} }
} }
return ( return (
<AsyncButton <AsyncButton
disabled={login.follows.timestamp === 0} disabled={timestamp ? timestamp === 0 : true}
type="button" type="button"
className="btn btn-primary" className="btn btn-primary"
onClick={isFollowing ? unfollow : follow} onClick={isFollowing ? unfollow : follow}
@ -64,14 +64,12 @@ export function LoggedInFollowButton({
export function FollowTagButton({ tag }: { tag: string }) { export function FollowTagButton({ tag }: { tag: string }) {
const login = useLogin(); const login = useLogin();
return login?.pubkey ? ( return login?.pubkey ? <LoggedInFollowButton tag={"t"} value={tag} /> : null;
<LoggedInFollowButton tag={"t"} loggedIn={login.pubkey} value={tag} />
) : null;
} }
export function FollowButton({ pubkey }: { pubkey: string }) { export function FollowButton({ pubkey }: { pubkey: string }) {
const login = useLogin(); const login = useLogin();
return login?.pubkey ? ( return login?.pubkey ? (
<LoggedInFollowButton tag={"p"} loggedIn={login.pubkey} value={pubkey} /> <LoggedInFollowButton tag={"p"} value={pubkey} />
) : null; ) : null;
} }

View File

@ -1,9 +1,11 @@
import { NostrLink } from "./nostr-link"; import type { ReactNode } from "react";
import { NostrLink } from "element/nostr-link";
const FileExtensionRegex = /\.([\w]+)$/i; const FileExtensionRegex = /\.([\w]+)$/i;
interface HyperTextProps { interface HyperTextProps {
link: string; link: string;
children: ReactNode;
} }
export function HyperText({ link, children }: HyperTextProps) { export function HyperText({ link, children }: HyperTextProps) {
@ -24,7 +26,7 @@ export function HyperText({ link, children }: HyperTextProps) {
<img <img
src={url.toString()} src={url.toString()}
alt={url.toString()} alt={url.toString()}
objectFit="contain" style={{ objectFit: "contain" }}
/> />
); );
} }

View File

@ -37,7 +37,7 @@ export interface LiveChatOptions {
} }
function BadgeAward({ ev }: { ev: NostrEvent }) { function BadgeAward({ ev }: { ev: NostrEvent }) {
const badge = findTag(ev, "a"); const badge = findTag(ev, "a") ?? "";
const [k, pubkey, d] = badge.split(":"); const [k, pubkey, d] = badge.split(":");
const awardees = getTagValues(ev.tags, "p"); const awardees = getTagValues(ev.tags, "p");
const event = useAddress(Number(k), pubkey, d); const event = useAddress(Number(k), pubkey, d);

View File

@ -1,38 +1,46 @@
import "./markdown.css"; import "./markdown.css";
import { createElement } from "react";
import { useMemo } from "react"; import { useMemo } from "react";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import { HyperText } from "element/hypertext"; import { HyperText } from "element/hypertext";
import { transformText } from "element/text"; import { transformText, type Fragment } from "element/text";
import type { Tags } from "types";
interface MarkdownProps { interface MarkdownProps {
content: string; content: string;
tags?: string[]; tags?: Tags;
} }
export function Markdown({ interface LinkProps {
content, href?: string;
tags = [], children?: Array<Fragment>;
element = "div", }
}: MarkdownProps) {
interface ComponentProps {
children?: Array<Fragment>;
}
export function Markdown({ content, tags = [] }: MarkdownProps) {
const components = useMemo(() => { const components = useMemo(() => {
return { return {
li: ({ children, ...props }) => { li: ({ children, ...props }: ComponentProps) => {
return children && <li {...props}>{transformText(children, tags)}</li>; return children && <li {...props}>{transformText(children, tags)}</li>;
}, },
td: ({ children }) => td: ({ children }: ComponentProps) => {
children && <td>{transformText(children, tags)}</td>, return children && <td>{transformText(children, tags)}</td>;
p: ({ children }) => <p>{transformText(children, tags)}</p>, },
a: (props) => { p: ({ children }: ComponentProps) => {
return <HyperText link={props.href}>{props.children}</HyperText>; return children && <p>{transformText(children, tags)}</p>;
},
a: ({ href, children }: LinkProps) => {
return href && <HyperText link={href}>{children}</HyperText>;
}, },
}; };
}, [tags]); }, [tags]);
return createElement( return (
element, <div className="markdown">
{ className: "markdown" }, <ReactMarkdown components={components}>{content}</ReactMarkdown>
<ReactMarkdown components={components}>{content}</ReactMarkdown>, </div>
); );
} }

View File

@ -5,7 +5,7 @@ import { MUTED } from "const";
export function LoggedInMuteButton({ pubkey }: { pubkey: string }) { export function LoggedInMuteButton({ pubkey }: { pubkey: string }) {
const login = useLogin(); const login = useLogin();
const tags = login.muted.tags; const { tags, content, timestamp } = login!.muted;
const muted = tags.filter((t) => t.at(0) === "p"); const muted = tags.filter((t) => t.at(0) === "p");
const isMuted = muted.find((t) => t.at(1) === pubkey); const isMuted = muted.find((t) => t.at(1) === pubkey);
@ -14,7 +14,7 @@ export function LoggedInMuteButton({ pubkey }: { pubkey: string }) {
if (pub) { if (pub) {
const newMuted = tags.filter((t) => t.at(1) !== pubkey); const newMuted = tags.filter((t) => t.at(1) !== pubkey);
const ev = await pub.generic((eb) => { const ev = await pub.generic((eb) => {
eb.kind(MUTED).content(login.muted.content); eb.kind(MUTED).content(content ?? "");
for (const t of newMuted) { for (const t of newMuted) {
eb.tag(t); eb.tag(t);
} }
@ -22,7 +22,7 @@ export function LoggedInMuteButton({ pubkey }: { pubkey: string }) {
}); });
console.debug(ev); console.debug(ev);
System.BroadcastEvent(ev); System.BroadcastEvent(ev);
Login.setMuted(newMuted, login.muted.content, ev.created_at); Login.setMuted(newMuted, content ?? "", ev.created_at);
} }
} }
@ -31,7 +31,7 @@ export function LoggedInMuteButton({ pubkey }: { pubkey: string }) {
if (pub) { if (pub) {
const newMuted = [...tags, ["p", pubkey]]; const newMuted = [...tags, ["p", pubkey]];
const ev = await pub.generic((eb) => { const ev = await pub.generic((eb) => {
eb.kind(MUTED).content(login.muted.content); eb.kind(MUTED).content(content ?? "");
for (const tag of newMuted) { for (const tag of newMuted) {
eb.tag(tag); eb.tag(tag);
} }
@ -39,13 +39,13 @@ export function LoggedInMuteButton({ pubkey }: { pubkey: string }) {
}); });
console.debug(ev); console.debug(ev);
System.BroadcastEvent(ev); System.BroadcastEvent(ev);
Login.setMuted(newMuted, login.muted.content, ev.created_at); Login.setMuted(newMuted, content ?? "", ev.created_at);
} }
} }
return ( return (
<AsyncButton <AsyncButton
disabled={login.muted.timestamp === 0} disabled={timestamp ? timestamp === 0 : true}
type="button" type="button"
className="btn delete-button" className="btn delete-button"
onClick={isMuted ? unmute : mute} onClick={isMuted ? unmute : mute}
@ -57,7 +57,5 @@ export function LoggedInMuteButton({ pubkey }: { pubkey: string }) {
export function MuteButton({ pubkey }: { pubkey: string }) { export function MuteButton({ pubkey }: { pubkey: string }) {
const login = useLogin(); const login = useLogin();
return login?.pubkey ? ( return login?.pubkey ? <LoggedInMuteButton pubkey={pubkey} /> : null;
<LoggedInMuteButton loggedIn={login.pubkey} pubkey={pubkey} />
) : null;
} }

View File

@ -5,50 +5,51 @@ import * as Dialog from "@radix-ui/react-dialog";
import { DndProvider, useDrag, useDrop } from "react-dnd"; import { DndProvider, useDrag, useDrop } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend"; import { HTML5Backend } from "react-dnd-html5-backend";
import type { NostrEvent } from "@snort/system"; import type { TaggedRawEvent } from "@snort/system";
import { Toggle } from "element/toggle"; import { Toggle } from "element/toggle";
import { Icon } from "element/icon";
import { ExternalLink } from "element/external-link";
import { FileUploader } from "element/file-uploader";
import { Markdown } from "element/markdown";
import { useLogin } from "hooks/login"; import { useLogin } from "hooks/login";
import { useCards, useUserCards } from "hooks/cards"; import { useCards, useUserCards } from "hooks/cards";
import { CARD, USER_CARDS } from "const"; import { CARD, USER_CARDS } from "const";
import { toTag } from "utils"; import { toTag, findTag } from "utils";
import { Login, System } from "index"; import { Login, System } from "index";
import { findTag } from "utils"; import type { Tags } from "types";
import { Icon } from "./icon";
import { ExternalLink } from "./external-link";
import { FileUploader } from "./file-uploader";
import { Markdown } from "./markdown";
interface CardType { interface CardType {
identifier?: string; identifier: string;
content: string;
title?: string; title?: string;
image?: string; image?: string;
link?: string; link?: string;
content: string;
} }
interface CardProps { type NewCard = Omit<CardType, "identifier">;
canEdit?: boolean;
ev: NostrEvent;
cards: NostrEvent[];
}
function isEmpty(s?: string) { function isEmpty(s?: string) {
return !s || s.trim().length === 0; return !s || s.trim().length === 0;
} }
interface CardPreviewProps extends NewCard {
style: object;
}
const CardPreview = forwardRef( const CardPreview = forwardRef(
({ style, title, link, image, content }, ref) => { ({ style, title, link, image, content }: CardPreviewProps, ref) => {
const isImageOnly = !isEmpty(image) && isEmpty(content) && isEmpty(title); const isImageOnly = !isEmpty(image) && isEmpty(content) && isEmpty(title);
return ( return (
<div <div
className={`stream-card ${isImageOnly ? "image-card" : ""}`} className={`stream-card ${isImageOnly ? "image-card" : ""}`}
// @ts-expect-error: Type 'ForwardRef<unknown>'
ref={ref} ref={ref}
style={style} style={style}
> >
{title && <h1 className="card-title">{title}</h1>} {title && <h1 className="card-title">{title}</h1>}
{image && {image &&
(link?.length > 0 ? ( (link && link?.length > 0 ? (
<ExternalLink href={link}> <ExternalLink href={link}>
<img className="card-image" src={image} alt={title} /> <img className="card-image" src={image} alt={title} />
</ExternalLink> </ExternalLink>
@ -61,9 +62,19 @@ const CardPreview = forwardRef(
}, },
); );
interface CardProps {
canEdit?: boolean;
ev: TaggedRawEvent;
cards: TaggedRawEvent[];
}
interface CardItem {
identifier: string;
}
function Card({ canEdit, ev, cards }: CardProps) { function Card({ canEdit, ev, cards }: CardProps) {
const login = useLogin(); const login = useLogin();
const identifier = findTag(ev, "d"); const identifier = findTag(ev, "d") ?? "";
const title = findTag(ev, "title") || findTag(ev, "subject"); const title = findTag(ev, "title") || findTag(ev, "subject");
const image = findTag(ev, "image"); const image = findTag(ev, "image");
const link = findTag(ev, "r"); const link = findTag(ev, "r");
@ -73,9 +84,9 @@ function Card({ canEdit, ev, cards }: CardProps) {
const [style, dragRef] = useDrag( const [style, dragRef] = useDrag(
() => ({ () => ({
type: "card", type: "card",
item: { identifier }, item: { identifier } as CardItem,
canDrag: () => { canDrag: () => {
return canEdit; return Boolean(canEdit);
}, },
collect: (monitor) => { collect: (monitor) => {
const isDragging = monitor.isDragging(); const isDragging = monitor.isDragging();
@ -88,15 +99,15 @@ function Card({ canEdit, ev, cards }: CardProps) {
[canEdit, identifier], [canEdit, identifier],
); );
function findTagByIdentifier(d) { function findTagByIdentifier(d: string) {
return tags.find((t) => t.at(1).endsWith(`:${d}`)); return tags.find((t) => t.at(1)!.endsWith(`:${d}`));
} }
const [dropStyle, dropRef] = useDrop( const [dropStyle, dropRef] = useDrop(
() => ({ () => ({
accept: ["card"], accept: ["card"],
canDrop: () => { canDrop: () => {
return canEdit; return Boolean(canEdit);
}, },
collect: (monitor) => { collect: (monitor) => {
const isOvering = monitor.isOver({ shallow: true }); const isOvering = monitor.isOver({ shallow: true });
@ -106,10 +117,11 @@ function Card({ canEdit, ev, cards }: CardProps) {
}; };
}, },
async drop(item) { async drop(item) {
if (identifier === item.identifier) { const typed = item as CardItem;
if (identifier === typed.identifier) {
return; return;
} }
const newItem = findTagByIdentifier(item.identifier); const newItem = findTagByIdentifier(typed.identifier);
const oldItem = findTagByIdentifier(identifier); const oldItem = findTagByIdentifier(identifier);
const newTags = tags.map((t) => { const newTags = tags.map((t) => {
if (t === oldItem) { if (t === oldItem) {
@ -119,8 +131,9 @@ function Card({ canEdit, ev, cards }: CardProps) {
return oldItem; return oldItem;
} }
return t; return t;
}); }) as Tags;
const pub = login?.publisher(); const pub = login?.publisher();
if (pub) {
const userCardsEv = await pub.generic((eb) => { const userCardsEv = await pub.generic((eb) => {
eb.kind(USER_CARDS).content(""); eb.kind(USER_CARDS).content("");
for (const tag of newTags) { for (const tag of newTags) {
@ -131,6 +144,7 @@ function Card({ canEdit, ev, cards }: CardProps) {
console.debug(userCardsEv); console.debug(userCardsEv);
System.BroadcastEvent(userCardsEv); System.BroadcastEvent(userCardsEv);
Login.setCards(newTags, userCardsEv.created_at); Login.setCards(newTags, userCardsEv.created_at);
}
}, },
}), }),
[canEdit, tags, identifier], [canEdit, tags, identifier],
@ -166,7 +180,7 @@ interface CardDialogProps {
cta?: string; cta?: string;
cancelCta?: string; cancelCta?: string;
card?: CardType; card?: CardType;
onSave(ev: CardType): void; onSave(ev: NewCard): void;
onCancel(): void; onCancel(): void;
} }
@ -187,7 +201,7 @@ function CardDialog({
<div className="new-card"> <div className="new-card">
<h3>{header || "Add card"}</h3> <h3>{header || "Add card"}</h3>
<div className="form-control"> <div className="form-control">
<label for="card-title">Title</label> <label htmlFor="card-title">Title</label>
<input <input
id="card-title" id="card-title"
type="text" type="text"
@ -197,7 +211,7 @@ function CardDialog({
/> />
</div> </div>
<div className="form-control"> <div className="form-control">
<label for="card-image">Image</label> <label htmlFor="card-image">Image</label>
<FileUploader <FileUploader
defaultImage={image} defaultImage={image}
onFileUpload={setImage} onFileUpload={setImage}
@ -205,7 +219,7 @@ function CardDialog({
/> />
</div> </div>
<div className="form-control"> <div className="form-control">
<label for="card-image-link">Image Link</label> <label htmlFor="card-image-link">Image Link</label>
<input <input
id="card-image-link" id="card-image-link"
type="text" type="text"
@ -215,7 +229,7 @@ function CardDialog({
/> />
</div> </div>
<div className="form-control"> <div className="form-control">
<label for="card-content">Content</label> <label htmlFor="card-content">Content</label>
<textarea <textarea
placeholder="Start typing..." placeholder="Start typing..."
value={content} value={content}
@ -245,7 +259,7 @@ function CardDialog({
interface EditCardProps { interface EditCardProps {
card: CardType; card: CardType;
cards: NostrEvent[]; cards: TaggedRawEvent[];
} }
function EditCard({ card, cards }: EditCardProps) { function EditCard({ card, cards }: EditCardProps) {
@ -254,18 +268,18 @@ function EditCard({ card, cards }: EditCardProps) {
const identifier = card.identifier; const identifier = card.identifier;
const tags = cards.map(toTag); const tags = cards.map(toTag);
async function editCard({ title, image, link, content }) { async function editCard({ title, image, link, content }: CardType) {
const pub = login?.publisher(); const pub = login?.publisher();
if (pub) { if (pub) {
const ev = await pub.generic((eb) => { const ev = await pub.generic((eb) => {
eb.kind(CARD).content(content).tag(["d", card.identifier]); eb.kind(CARD).content(content).tag(["d", card.identifier]);
if (title?.length > 0) { if (title && title?.length > 0) {
eb.tag(["title", title]); eb.tag(["title", title]);
} }
if (image?.length > 0) { if (image && image?.length > 0) {
eb.tag(["image", image]); eb.tag(["image", image]);
} }
if (link?.lenght > 0) { if (link && link?.length > 0) {
eb.tag(["r", link]); eb.tag(["r", link]);
} }
return eb; return eb;
@ -279,7 +293,7 @@ function EditCard({ card, cards }: EditCardProps) {
async function onCancel() { async function onCancel() {
const pub = login?.publisher(); const pub = login?.publisher();
if (pub) { if (pub) {
const newTags = tags.filter((t) => !t.at(1).endsWith(`:${identifier}`)); const newTags = tags.filter((t) => !t.at(1)!.endsWith(`:${identifier}`));
const userCardsEv = await pub.generic((eb) => { const userCardsEv = await pub.generic((eb) => {
eb.kind(USER_CARDS).content(""); eb.kind(USER_CARDS).content("");
for (const tag of newTags) { for (const tag of newTags) {
@ -318,7 +332,7 @@ function EditCard({ card, cards }: EditCardProps) {
} }
interface AddCardProps { interface AddCardProps {
cards: NostrEvent[]; cards: TaggedRawEvent[];
} }
function AddCard({ cards }: AddCardProps) { function AddCard({ cards }: AddCardProps) {
@ -326,19 +340,19 @@ function AddCard({ cards }: AddCardProps) {
const tags = cards.map(toTag); const tags = cards.map(toTag);
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
async function createCard({ title, image, link, content }) { async function createCard({ title, image, link, content }: NewCard) {
const pub = login?.publisher(); const pub = login?.publisher();
if (pub) { if (pub) {
const ev = await pub.generic((eb) => { const ev = await pub.generic((eb) => {
const d = String(Date.now()); const d = String(Date.now());
eb.kind(CARD).content(content).tag(["d", d]); eb.kind(CARD).content(content).tag(["d", d]);
if (title?.length > 0) { if (title && title?.length > 0) {
eb.tag(["title", title]); eb.tag(["title", title]);
} }
if (image?.length > 0) { if (image && image?.length > 0) {
eb.tag(["image", image]); eb.tag(["image", image]);
} }
if (link?.length > 0) { if (link && link?.length > 0) {
eb.tag(["r", link]); eb.tag(["r", link]);
} }
return eb; return eb;
@ -382,15 +396,19 @@ function AddCard({ cards }: AddCardProps) {
); );
} }
export function StreamCardEditor() { interface StreamCardEditorProps {
const login = useLogin(); pubkey: string;
const cards = useUserCards(login.pubkey, login.cards.tags, true); tags: Tags;
}
export function StreamCardEditor({ pubkey, tags }: StreamCardEditorProps) {
const cards = useUserCards(pubkey, tags, true);
const [isEditing, setIsEditing] = useState(false); const [isEditing, setIsEditing] = useState(false);
return ( return (
<> <>
<div className="stream-cards"> <div className="stream-cards">
{cards.map((ev) => ( {cards.map((ev) => (
<Card canEdit={isEditing} cards={cards} key={ev.id} ev={ev} /> <Card canEdit={isEditing} cards={cards} key={ev.id} ev={ev!} />
))} ))}
{isEditing && <AddCard cards={cards} />} {isEditing && <AddCard cards={cards} />}
</div> </div>
@ -406,23 +424,31 @@ export function StreamCardEditor() {
); );
} }
export function ReadOnlyStreamCards({ host }) { interface StreamCardsProps {
host: string;
}
export function ReadOnlyStreamCards({ host }: StreamCardsProps) {
const cards = useCards(host); const cards = useCards(host);
return ( return (
<div className="stream-cards"> <div className="stream-cards">
{cards.map((ev) => ( {cards.map((ev) => (
<Card cards={cards} key={ev.id} ev={ev} /> <Card cards={cards} key={ev!.id} ev={ev!} />
))} ))}
</div> </div>
); );
} }
export function StreamCards({ host }) { export function StreamCards({ host }: StreamCardsProps) {
const login = useLogin(); const login = useLogin();
const canEdit = login?.pubkey === host; const canEdit = login?.pubkey === host;
return ( return (
<DndProvider backend={HTML5Backend}> <DndProvider backend={HTML5Backend}>
{canEdit ? <StreamCardEditor /> : <ReadOnlyStreamCards host={host} />} {canEdit ? (
<StreamCardEditor tags={login.cards.tags} pubkey={login.pubkey} />
) : (
<ReadOnlyStreamCards host={host} />
)}
</DndProvider> </DndProvider>
); );
} }

View File

@ -9,7 +9,7 @@ import { Emoji } from "element/emoji";
import { HyperText } from "element/hypertext"; import { HyperText } from "element/hypertext";
import { splitByUrl } from "utils"; import { splitByUrl } from "utils";
type Fragment = string | ReactNode; export type Fragment = string | ReactNode;
const NostrPrefixRegex = /^nostr:/; const NostrPrefixRegex = /^nostr:/;
const EmojiRegex = /:([\w-]+):/g; const EmojiRegex = /:([\w-]+):/g;
@ -50,7 +50,7 @@ function extractLinks(fragments: Fragment[]) {
</a> </a>
); );
} }
return <HyperText link={a} />; return <HyperText link={a}>{a}</HyperText>;
} }
return a; return a;
}); });

View File

@ -6,11 +6,14 @@ import ReactTextareaAutocomplete, {
import "@webscopeio/react-textarea-autocomplete/style.css"; import "@webscopeio/react-textarea-autocomplete/style.css";
import uniqWith from "lodash/uniqWith"; import uniqWith from "lodash/uniqWith";
import isEqual from "lodash/isEqual"; import isEqual from "lodash/isEqual";
import { MetadataCache, NostrPrefix, UserProfileCache } from "@snort/system"; import { MetadataCache, NostrPrefix, UserProfileCache } from "@snort/system";
import { System } from "index";
import { Emoji, type EmojiTag } from "./emoji"; import { Emoji } from "element/emoji";
import { Avatar } from "element/avatar"; import { Avatar } from "element/avatar";
import { hexToBech32 } from "utils"; import { hexToBech32 } from "utils";
import type { EmojiTag } from "types";
import { System } from "index";
interface EmojiItemProps { interface EmojiItemProps {
name: string; name: string;

View File

@ -4,6 +4,9 @@ import { Icon } from "element/icon";
interface ToggleProps { interface ToggleProps {
label: string; label: string;
text: string;
pressed?: boolean;
onPressedChange?: (b: boolean) => void;
} }
export function Toggle({ label, text, ...rest }: ToggleProps) { export function Toggle({ label, text, ...rest }: ToggleProps) {

View File

@ -1,14 +1,14 @@
import { NostrLink, EventKind } from "@snort/system"; import { NostrLink, EventKind } from "@snort/system";
import React, { useRef, useState } from "react"; import React, { useRef, useState } from "react";
import { LIVE_STREAM_CHAT } from "../const"; import { useLogin } from "hooks/login";
import { useLogin } from "../hooks/login"; import AsyncButton from "element/async-button";
import { System } from "../index"; import { Icon } from "element/icon";
import AsyncButton from "./async-button"; import { Textarea } from "element/textarea";
import { Icon } from "./icon"; import { EmojiPicker } from "element/emoji-picker";
import { Textarea } from "./textarea"; import type { EmojiPack, Emoji } from "types";
import { EmojiPicker } from "./emoji-picker"; import { System } from "index";
import type { EmojiPack, Emoji } from "../hooks/emoji"; import { LIVE_STREAM_CHAT } from "const";
export function WriteMessage({ export function WriteMessage({
link, link,
@ -41,7 +41,7 @@ export function WriteMessage({
const reply = await pub?.generic((eb) => { const reply = await pub?.generic((eb) => {
const emoji = [...emojiNames].map((name) => const emoji = [...emojiNames].map((name) =>
emojis.find((e) => e.at(1) === name) emojis.find((e) => e.at(1) === name),
); );
eb.kind(LIVE_STREAM_CHAT as EventKind) eb.kind(LIVE_STREAM_CHAT as EventKind)
.content(chat) .content(chat)
@ -90,7 +90,7 @@ export function WriteMessage({
emojis={emojis} emojis={emojis}
value={chat} value={chat}
onKeyDown={onKeyDown} onKeyDown={onKeyDown}
onChange={e => setChat(e.target.value)} onChange={(e) => setChat(e.target.value)}
/> />
<div onClick={pickEmoji}> <div onClick={pickEmoji}>
<Icon name="face" className="write-emoji-button" /> <Icon name="face" className="write-emoji-button" />

View File

@ -1,14 +1,23 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { EventKind, NoteCollection, RequestBuilder } from "@snort/system"; import {
TaggedRawEvent,
EventKind,
NoteCollection,
RequestBuilder,
} from "@snort/system";
import { useRequestBuilder } from "@snort/system-react"; import { useRequestBuilder } from "@snort/system-react";
import { unixNow } from "@snort/shared"; import { unixNow } from "@snort/shared";
import { findTag, toAddress, getTagValues } from "utils"; import { findTag, toAddress, getTagValues } from "utils";
import { WEEK } from "const"; import { WEEK } from "const";
import { System } from "index"; import { System } from "index";
import type { Badge } from "types";
export function useBadges(pubkey: string, leaveOpen = true) { export function useBadges(
pubkey: string,
leaveOpen = true,
): { badges: Badge[]; awards: TaggedRawEvent[] } {
const since = useMemo(() => unixNow() - WEEK, [pubkey]); const since = useMemo(() => unixNow() - WEEK, [pubkey]);
const rb = useMemo(() => { const rb = useMemo(() => {
const rb = new RequestBuilder(`badges:${pubkey.slice(0, 12)}`); const rb = new RequestBuilder(`badges:${pubkey.slice(0, 12)}`);
@ -61,7 +70,7 @@ export function useBadges(pubkey: string, leaveOpen = true) {
const badges = useMemo(() => { const badges = useMemo(() => {
return rawBadges.map((e) => { return rawBadges.map((e) => {
const name = findTag(e, "d"); const name = findTag(e, "d") ?? "";
const address = toAddress(e); const address = toAddress(e);
const awardEvents = badgeAwards.filter( const awardEvents = badgeAwards.filter(
(b) => findTag(b, "a") === address, (b) => findTag(b, "a") === address,
@ -79,7 +88,7 @@ export function useBadges(pubkey: string, leaveOpen = true) {
); );
const thumb = findTag(e, "thumb"); const thumb = findTag(e, "thumb");
const image = findTag(e, "image"); const image = findTag(e, "image");
return { name, thumb, image, awardees, accepted }; return { name, thumb, image, awardees, accepted } as Badge;
}); });
return []; return [];
}, [rawBadges]); }, [rawBadges]);

View File

@ -1,6 +1,7 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { import {
TaggedRawEvent,
ReplaceableNoteStore, ReplaceableNoteStore,
NoteCollection, NoteCollection,
RequestBuilder, RequestBuilder,
@ -15,7 +16,7 @@ export function useUserCards(
pubkey: string, pubkey: string,
userCards: Array<string[]>, userCards: Array<string[]>,
leaveOpen = false, leaveOpen = false,
) { ): TaggedRawEvent[] {
const related = useMemo(() => { const related = useMemo(() => {
// filtering to only show CARD kinds for now, but in the future we could link and render anything // filtering to only show CARD kinds for now, but in the future we could link and render anything
if (userCards?.length > 0) { if (userCards?.length > 0) {
@ -57,7 +58,7 @@ export function useUserCards(
const cards = useMemo(() => { const cards = useMemo(() => {
return related return related
.map((t) => { .map((t) => {
const [k, pubkey, identifier] = t.at(1).split(":"); const [k, pubkey, identifier] = t.at(1)!.split(":");
const kind = Number(k); const kind = Number(k);
return (data ?? []).find( return (data ?? []).find(
(e) => (e) =>
@ -66,13 +67,14 @@ export function useUserCards(
findTag(e, "d") === identifier, findTag(e, "d") === identifier,
); );
}) })
.filter((e) => e); .filter((e) => e)
.map((e) => e as TaggedRawEvent);
}, [related, data]); }, [related, data]);
return cards; return cards;
} }
export function useCards(pubkey: string, leaveOpen = false) { export function useCards(pubkey: string, leaveOpen = false): TaggedRawEvent[] {
const sub = useMemo(() => { const sub = useMemo(() => {
const b = new RequestBuilder(`user-cards:${pubkey.slice(0, 12)}`); const b = new RequestBuilder(`user-cards:${pubkey.slice(0, 12)}`);
b.withOptions({ b.withOptions({
@ -127,21 +129,23 @@ export function useCards(pubkey: string, leaveOpen = false) {
NoteCollection, NoteCollection,
subRelated, subRelated,
); );
const cardEvents = data ?? [];
const cards = useMemo(() => { const cards = useMemo(() => {
return related return related
.map((t) => { .map((t) => {
const [k, pubkey, identifier] = t.at(1).split(":"); const [k, pubkey, identifier] = t.at(1)!.split(":");
const kind = Number(k); const kind = Number(k);
return data.find( return cardEvents.find(
(e) => (e) =>
e.kind === kind && e.kind === kind &&
e.pubkey === pubkey && e.pubkey === pubkey &&
findTag(e, "d") === identifier, findTag(e, "d") === identifier,
); );
}) })
.filter((e) => e); .filter((e) => e)
}, [related, data]); .map((e) => e as TaggedRawEvent);
}, [related, cardEvents]);
return cards; return cards;
} }

View File

@ -11,7 +11,7 @@ import { useRequestBuilder } from "@snort/system-react";
import { System } from "index"; import { System } from "index";
import { findTag } from "utils"; import { findTag } from "utils";
import { EMOJI_PACK, USER_EMOJIS } from "const"; import { EMOJI_PACK, USER_EMOJIS } from "const";
import { EmojiPack } from "types"; import type { EmojiPack, Tags, EmojiTag } from "types";
function cleanShortcode(shortcode?: string) { function cleanShortcode(shortcode?: string) {
return shortcode?.replace(/\s+/g, "_").replace(/_$/, ""); return shortcode?.replace(/\s+/g, "_").replace(/_$/, "");
@ -33,10 +33,10 @@ export function packId(pack: EmojiPack): string {
return `${pack.author}:${pack.name}`; return `${pack.author}:${pack.name}`;
} }
export function useUserEmojiPacks(pubkey?: string, userEmoji: Array<string[]>) { export function useUserEmojiPacks(pubkey?: string, userEmoji?: Tags) {
const related = useMemo(() => { const related = useMemo(() => {
if (userEmoji?.length > 0) { if (userEmoji) {
return userEmoji.filter( return userEmoji?.filter(
(t) => t.at(0) === "a" && t.at(1)?.startsWith(`${EMOJI_PACK}:`), (t) => t.at(0) === "a" && t.at(1)?.startsWith(`${EMOJI_PACK}:`),
); );
} }

View File

@ -50,7 +50,7 @@ export function useStreamsFeed(tag?: string) {
); );
const ended = feedSorted.filter((a) => { const ended = feedSorted.filter((a) => {
const hasEnded = findTag(a, "status") === StreamState.Ended; const hasEnded = findTag(a, "status") === StreamState.Ended;
const recording = findTag(a, "recording"); const recording = findTag(a, "recording") ?? "";
return hasEnded && recording?.length > 0; return hasEnded && recording?.length > 0;
}); });

View File

@ -5,6 +5,7 @@ import { useRequestBuilder } from "@snort/system-react";
import { useUserEmojiPacks } from "hooks/emoji"; import { useUserEmojiPacks } from "hooks/emoji";
import { MUTED, USER_CARDS, USER_EMOJIS } from "const"; import { MUTED, USER_CARDS, USER_EMOJIS } from "const";
import type { Tags } from "types";
import { System, Login } from "index"; import { System, Login } from "index";
import { getPublisher } from "login"; import { getPublisher } from "login";
@ -23,7 +24,7 @@ export function useLogin() {
} }
export function useLoginEvents(pubkey?: string, leaveOpen = false) { export function useLoginEvents(pubkey?: string, leaveOpen = false) {
const [userEmojis, setUserEmojis] = useState([]); const [userEmojis, setUserEmojis] = useState<Tags>([]);
const session = useSyncExternalStore( const session = useSyncExternalStore(
(c) => Login.hook(c), (c) => Login.hook(c),
() => Login.snapshot(), () => Login.snapshot(),

View File

@ -2,7 +2,7 @@ import { bytesToHex } from "@noble/curves/abstract/utils";
import { schnorr } from "@noble/curves/secp256k1"; import { schnorr } from "@noble/curves/secp256k1";
import { ExternalStore } from "@snort/shared"; import { ExternalStore } from "@snort/shared";
import { EventPublisher, Nip7Signer, PrivateKeySigner } from "@snort/system"; import { EventPublisher, Nip7Signer, PrivateKeySigner } from "@snort/system";
import type { EmojiPack } from "types"; import type { EmojiPack, Tags } from "types";
export enum LoginType { export enum LoginType {
Nip7 = "nip7", Nip7 = "nip7",
@ -76,7 +76,8 @@ export class LoginStore extends ExternalStore<LoginSession | undefined> {
return this.#session ? { ...this.#session } : undefined; return this.#session ? { ...this.#session } : undefined;
} }
setFollows(follows: Array<string>, content: string, ts: number) { setFollows(follows: Tags, content: string, ts: number) {
if (!this.#session) return;
if (this.#session.follows.timestamp >= ts) { if (this.#session.follows.timestamp >= ts) {
return; return;
} }
@ -87,11 +88,13 @@ export class LoginStore extends ExternalStore<LoginSession | undefined> {
} }
setEmojis(emojis: Array<EmojiPack>) { setEmojis(emojis: Array<EmojiPack>) {
if (!this.#session) return;
this.#session.emojis = emojis; this.#session.emojis = emojis;
this.#save(); this.#save();
} }
setMuted(muted: Array<string[]>, content: string, ts: number) { setMuted(muted: Tags, content: string, ts: number) {
if (!this.#session) return;
if (this.#session.muted.timestamp >= ts) { if (this.#session.muted.timestamp >= ts) {
return; return;
} }
@ -101,7 +104,8 @@ export class LoginStore extends ExternalStore<LoginSession | undefined> {
this.#save(); this.#save();
} }
setCards(cards: Array<string[]>, ts: number) { setCards(cards: Tags, ts: number) {
if (!this.#session) return;
if (this.#session.cards.timestamp >= ts) { if (this.#session.cards.timestamp >= ts) {
return; return;
} }

View File

@ -12,7 +12,7 @@ export function TagPage() {
<div className="tag-page"> <div className="tag-page">
<div className="tag-page-header"> <div className="tag-page-header">
<h1>#{tag}</h1> <h1>#{tag}</h1>
<FollowTagButton tag={tag} /> <FollowTagButton tag={tag!} />
</div> </div>
<div className="video-grid"> <div className="video-grid">
{live.map((e) => ( {live.map((e) => (

View File

@ -1,71 +1,71 @@
import { StreamState } from "index" import { StreamState } from "index";
import { NostrEvent } from "@snort/system"; import { NostrEvent } from "@snort/system";
import { ExternalStore } from "@snort/shared"; import { ExternalStore } from "@snort/shared";
import { Nip103StreamProvider } from "./nip103"; import { Nip103StreamProvider } from "./nip103";
import { ManualProvider } from "./manual"; import { ManualProvider } from "./manual";
import { OwncastProvider } from "./owncast"; import { OwncastProvider } from "./owncast";
export interface StreamProvider { export interface StreamProvider {
get name(): string get name(): string;
get type(): StreamProviders get type(): StreamProviders;
/** /**
* Get general info about connected provider to test everything is working * Get general info about connected provider to test everything is working
*/ */
info(): Promise<StreamProviderInfo> info(): Promise<StreamProviderInfo>;
/** /**
* Create a config object to save in localStorage * Create a config object to save in localStorage
*/ */
createConfig(): unknown & { type: StreamProviders } createConfig(): unknown & { type: StreamProviders };
/** /**
* Update stream info event * Update stream info event
*/ */
updateStreamInfo(ev: NostrEvent): Promise<void> updateStreamInfo(ev: NostrEvent): Promise<void>;
/** /**
* Top-up balance with provider * Top-up balance with provider
*/ */
topup(amount: number): Promise<string> topup(amount: number): Promise<string>;
} }
export enum StreamProviders { export enum StreamProviders {
Manual = "manual", Manual = "manual",
Owncast = "owncast", Owncast = "owncast",
Cloudflare = "cloudflare", Cloudflare = "cloudflare",
NostrType = "nostr" NostrType = "nostr",
} }
export interface StreamProviderInfo { export interface StreamProviderInfo {
name: string name: string;
summary?: string summary?: string;
version?: string version?: string;
state: StreamState state: StreamState;
viewers?: number viewers?: number;
publishedEvent?: NostrEvent publishedEvent?: NostrEvent;
balance?: number balance?: number;
endpoints: Array<StreamProviderEndpoint> endpoints: Array<StreamProviderEndpoint>;
} }
export interface StreamProviderEndpoint { export interface StreamProviderEndpoint {
name: string name: string;
url: string url: string;
key: string key: string;
rate?: number rate?: number;
unit?: string unit?: string;
capabilities?: Array<string> capabilities?: Array<string>;
} }
export class ProviderStore extends ExternalStore<Array<StreamProvider>> { export class ProviderStore extends ExternalStore<Array<StreamProvider>> {
#providers: Array<StreamProvider> = [] #providers: Array<StreamProvider> = [];
constructor() { constructor() {
super(); super();
const cache = window.localStorage.getItem("providers"); const cache = window.localStorage.getItem("providers");
if (cache) { if (cache) {
const cached: Array<{ type: StreamProviders } & Record<string, unknown>> = JSON.parse(cache); const cached: Array<{ type: StreamProviders } & Record<string, unknown>> =
JSON.parse(cache);
for (const c of cached) { for (const c of cached) {
switch (c.type) { switch (c.type) {
case StreamProviders.Manual: { case StreamProviders.Manual: {
@ -77,7 +77,9 @@ export class ProviderStore extends ExternalStore<Array<StreamProvider>> {
break; break;
} }
case StreamProviders.Owncast: { case StreamProviders.Owncast: {
this.#providers.push(new OwncastProvider(c.url as string, c.token as string)); this.#providers.push(
new OwncastProvider(c.url as string, c.token as string),
);
break; break;
} }
} }
@ -92,12 +94,14 @@ export class ProviderStore extends ExternalStore<Array<StreamProvider>> {
} }
takeSnapshot() { takeSnapshot() {
const defaultProvider = new Nip103StreamProvider("https://api.zap.stream/api/nostr/"); const defaultProvider = new Nip103StreamProvider(
"https://api.zap.stream/api/nostr/",
);
return [defaultProvider, new ManualProvider(), ...this.#providers]; return [defaultProvider, new ManualProvider(), ...this.#providers];
} }
#save() { #save() {
const cfg = this.#providers.map(a => a.createConfig()); const cfg = this.#providers.map((a) => a.createConfig());
window.localStorage.setItem("providers", JSON.stringify(cfg)); window.localStorage.setItem("providers", JSON.stringify(cfg));
} }
} }

View File

@ -2,8 +2,8 @@ import { StreamState } from "index";
import { StreamProvider, StreamProviderInfo, StreamProviders } from "providers"; import { StreamProvider, StreamProviderInfo, StreamProviders } from "providers";
export class OwncastProvider implements StreamProvider { export class OwncastProvider implements StreamProvider {
#url: string #url: string;
#token: string #token: string;
constructor(url: string, token: string) { constructor(url: string, token: string) {
this.#url = url; this.#url = url;
@ -11,19 +11,19 @@ export class OwncastProvider implements StreamProvider {
} }
get name() { get name() {
return new URL(this.#url).host return new URL(this.#url).host;
} }
get type() { get type() {
return StreamProviders.Owncast return StreamProviders.Owncast;
} }
createConfig() { createConfig() {
return { return {
type: StreamProviders.Owncast, type: StreamProviders.Owncast,
url: this.#url, url: this.#url,
token: this.#token token: this.#token,
} };
} }
updateStreamInfo(): Promise<void> { updateStreamInfo(): Promise<void> {
@ -39,21 +39,26 @@ export class OwncastProvider implements StreamProvider {
summary: info.summary, summary: info.summary,
version: info.version, version: info.version,
state: status.online ? StreamState.Live : StreamState.Ended, state: status.online ? StreamState.Live : StreamState.Ended,
viewers: status.viewerCount viewers: status.viewerCount,
} as StreamProviderInfo endpoints: [],
} as StreamProviderInfo;
} }
topup(): Promise<string> { topup(): Promise<string> {
throw new Error("Method not implemented."); throw new Error("Method not implemented.");
} }
async #getJson<T>(method: "GET" | "POST", path: string, body?: unknown): Promise<T> { async #getJson<T>(
method: "GET" | "POST",
path: string,
body?: unknown,
): Promise<T> {
const rsp = await fetch(`${this.#url}${path}`, { const rsp = await fetch(`${this.#url}${path}`, {
method: method, method: method,
body: body ? JSON.stringify(body) : undefined, body: body ? JSON.stringify(body) : undefined,
headers: { headers: {
"content-type": "application/json", "content-type": "application/json",
"authorization": `Bearer ${this.#token}` authorization: `Bearer ${this.#token}`,
}, },
}); });
const json = await rsp.text(); const json = await rsp.text();
@ -62,22 +67,21 @@ export class OwncastProvider implements StreamProvider {
} }
return JSON.parse(json) as T; return JSON.parse(json) as T;
} }
} }
interface ConfigResponse { interface ConfigResponse {
name?: string, name?: string;
summary?: string, summary?: string;
logo?: string, logo?: string;
tags?: Array<string>, tags?: Array<string>;
version?: string version?: string;
} }
interface StatusResponse { interface StatusResponse {
lastConnectTime?: string lastConnectTime?: string;
lastDisconnectTime?: string lastDisconnectTime?: string;
online: boolean online: boolean;
overallMaxViewerCount: number overallMaxViewerCount: number;
sessionMaxViewerCount: number sessionMaxViewerCount: number;
viewerCount: number viewerCount: number;
} }

View File

@ -7,6 +7,10 @@ export interface Relays {
[key: string]: RelaySettings; [key: string]: RelaySettings;
} }
export type Tag = string[];
export type Tags = Tag[];
export type EmojiTag = ["emoji", string, string]; export type EmojiTag = ["emoji", string, string];
export interface Emoji { export interface Emoji {
@ -23,8 +27,8 @@ export interface EmojiPack {
export interface Badge { export interface Badge {
name: string; name: string;
thumb: string; thumb?: string;
image: string; image?: string;
awardees: Set<string>; awardees: Set<string>;
accepted: Set<string>; accepted: Set<string>;
} }

View File

@ -1,6 +1,7 @@
import { NostrEvent, NostrPrefix, encodeTLV } from "@snort/system"; import { NostrEvent, NostrPrefix, encodeTLV } from "@snort/system";
import * as utils from "@noble/curves/abstract/utils"; import * as utils from "@noble/curves/abstract/utils";
import { bech32 } from "@scure/base"; import { bech32 } from "@scure/base";
import type { Tag, Tags } from "types";
export function toAddress(e: NostrEvent): string { export function toAddress(e: NostrEvent): string {
if (e.kind && e.kind >= 30000 && e.kind <= 40000) { if (e.kind && e.kind >= 30000 && e.kind <= 40000) {
@ -16,7 +17,7 @@ export function toAddress(e: NostrEvent): string {
return e.id; return e.id;
} }
export function toTag(e: NostrEvent): string[] { export function toTag(e: NostrEvent): Tag {
if (e.kind && e.kind >= 30000 && e.kind <= 40000) { if (e.kind && e.kind >= 30000 && e.kind <= 40000) {
const dTag = findTag(e, "d"); const dTag = findTag(e, "d");
@ -105,6 +106,10 @@ export async function openFile(): Promise<File | undefined> {
}); });
} }
export function getTagValues(tags: Array<string[]>, tag: string) { export function getTagValues(tags: Tags, tag: string): Array<string> {
return tags.filter((t) => t.at(0) === tag).map((t) => t.at(1)); return tags
.filter((t) => t.at(0) === tag)
.map((t) => t.at(1))
.filter((t) => t)
.map((t) => t as string);
} }