forked from Kieran/zap.stream
fix: type errors
This commit is contained in:
parent
67bcfc58e0
commit
e15a46192a
@ -4,7 +4,7 @@ import { findTag } from "utils";
|
||||
|
||||
export function Badge({ ev }: { ev: NostrEvent }) {
|
||||
const name = findTag(ev, "name") || findTag(ev, "d");
|
||||
const description = findTag(ev, "description");
|
||||
const description = findTag(ev, "description") ?? "";
|
||||
const thumb = findTag(ev, "thumb");
|
||||
const image = findTag(ev, "image");
|
||||
return (
|
||||
|
@ -8,18 +8,17 @@ import {
|
||||
useIntersectionObserver,
|
||||
} from "usehooks-ts";
|
||||
|
||||
import { System } from "../index";
|
||||
import { formatSats } from "../number";
|
||||
import { EmojiPicker } from "./emoji-picker";
|
||||
import { Icon } from "./icon";
|
||||
import { Emoji as EmojiComponent } from "./emoji";
|
||||
import { EmojiPicker } from "element/emoji-picker";
|
||||
import { Icon } from "element/icon";
|
||||
import { Emoji as EmojiComponent } from "element/emoji";
|
||||
import { Profile } from "./profile";
|
||||
import { Text } from "element/text";
|
||||
import { SendZapsDialog } from "./send-zap";
|
||||
import { findTag } from "../utils";
|
||||
import type { EmojiPack } from "../hooks/emoji";
|
||||
import { useLogin } from "../hooks/login";
|
||||
import type { Badge, Emoji } from "types";
|
||||
import { SendZapsDialog } from "element/send-zap";
|
||||
import { useLogin } from "hooks/login";
|
||||
import { formatSats } from "number";
|
||||
import { findTag } from "utils";
|
||||
import type { Badge, Emoji, EmojiPack } from "types";
|
||||
import { System } from "index";
|
||||
|
||||
function emojifyReaction(reaction: string) {
|
||||
if (reaction === "+") {
|
||||
@ -104,7 +103,7 @@ export function ChatMessage({
|
||||
if (emoji.native) {
|
||||
reply = await pub?.react(ev, emoji.native || "+1");
|
||||
} else {
|
||||
const e = getEmojiById(emoji.id);
|
||||
const e = getEmojiById(emoji.id!);
|
||||
if (e) {
|
||||
reply = await pub?.generic((eb) => {
|
||||
return eb
|
||||
|
@ -8,6 +8,7 @@ import { Mention } from "element/mention";
|
||||
import { findTag } from "utils";
|
||||
import { USER_EMOJIS } from "const";
|
||||
import { Login, System } from "index";
|
||||
import type { EmojiPack as EmojiPackType } from "types";
|
||||
|
||||
export function EmojiPack({ ev }: { ev: NostrEvent }) {
|
||||
const login = useLogin();
|
||||
@ -18,13 +19,14 @@ export function EmojiPack({ ev }: { ev: NostrEvent }) {
|
||||
const emoji = ev.tags.filter((e) => e.at(0) === "emoji");
|
||||
|
||||
async function toggleEmojiPack() {
|
||||
let newPacks = [];
|
||||
let newPacks = [] as EmojiPackType[];
|
||||
if (isUsed) {
|
||||
newPacks = login.emojis.filter(
|
||||
(e) => e.pubkey !== ev.pubkey && e.name !== name,
|
||||
);
|
||||
newPacks =
|
||||
login?.emojis.filter(
|
||||
(e) => e.author !== ev.pubkey && e.name !== name,
|
||||
) ?? [];
|
||||
} else {
|
||||
newPacks = [...login.emojis, toEmojiPack(ev)];
|
||||
newPacks = [...(login?.emojis ?? []), toEmojiPack(ev)];
|
||||
}
|
||||
const pub = login?.publisher();
|
||||
if (pub) {
|
||||
@ -37,7 +39,7 @@ export function EmojiPack({ ev }: { ev: NostrEvent }) {
|
||||
});
|
||||
console.debug(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>
|
||||
<Mention pubkey={ev.pubkey} />
|
||||
</div>
|
||||
<AsyncButton
|
||||
className={`btn btn-primary ${isUsed ? "delete-button" : ""}`}
|
||||
onClick={toggleEmojiPack}
|
||||
>
|
||||
{isUsed ? "Remove" : "Add"}
|
||||
</AsyncButton>
|
||||
{login?.pubkey && (
|
||||
<AsyncButton
|
||||
className={`btn btn-primary ${isUsed ? "delete-button" : ""}`}
|
||||
onClick={toggleEmojiPack}
|
||||
>
|
||||
{isUsed ? "Remove" : "Add"}
|
||||
</AsyncButton>
|
||||
)}
|
||||
</div>
|
||||
<div className="emoji-pack-emojis">
|
||||
{emoji.map((e) => {
|
||||
|
@ -1,71 +1,70 @@
|
||||
import data, { Emoji } from "@emoji-mart/data";
|
||||
import Picker from "@emoji-mart/react";
|
||||
import { RefObject } from "react";
|
||||
|
||||
import { EmojiPack } from "../hooks/emoji";
|
||||
import { EmojiPack } from "types";
|
||||
|
||||
interface EmojiPickerProps {
|
||||
topOffset: number;
|
||||
leftOffset: number;
|
||||
emojiPacks?: EmojiPack[];
|
||||
onEmojiSelect: (e: Emoji) => void;
|
||||
onClickOutside: () => void;
|
||||
height?: number;
|
||||
ref: RefObject<HTMLDivElement>;
|
||||
topOffset: number;
|
||||
leftOffset: number;
|
||||
emojiPacks?: EmojiPack[];
|
||||
onEmojiSelect: (e: Emoji) => void;
|
||||
onClickOutside: () => void;
|
||||
height?: number;
|
||||
ref: RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export function EmojiPicker({
|
||||
topOffset,
|
||||
leftOffset,
|
||||
onEmojiSelect,
|
||||
onClickOutside,
|
||||
emojiPacks = [],
|
||||
height = 300,
|
||||
ref,
|
||||
topOffset,
|
||||
leftOffset,
|
||||
onEmojiSelect,
|
||||
onClickOutside,
|
||||
emojiPacks = [],
|
||||
height = 300,
|
||||
ref,
|
||||
}: EmojiPickerProps) {
|
||||
const customEmojiList = emojiPacks.map((pack) => {
|
||||
const customEmojiList = emojiPacks.map((pack) => {
|
||||
return {
|
||||
id: pack.address,
|
||||
name: pack.name,
|
||||
emojis: pack.emojis.map((e) => {
|
||||
const [, name, url] = e;
|
||||
return {
|
||||
id: pack.address,
|
||||
name: pack.name,
|
||||
emojis: pack.emojis.map((e) => {
|
||||
const [, name, url] = e;
|
||||
return {
|
||||
id: name,
|
||||
name,
|
||||
skins: [{ src: url }],
|
||||
};
|
||||
}),
|
||||
id: name,
|
||||
name,
|
||||
skins: [{ src: url }],
|
||||
};
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: topOffset - height - 10,
|
||||
left: leftOffset,
|
||||
zIndex: 1,
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<style>
|
||||
{`
|
||||
}),
|
||||
};
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: topOffset - height - 10,
|
||||
left: leftOffset,
|
||||
zIndex: 1,
|
||||
}}
|
||||
ref={ref}
|
||||
>
|
||||
<style>
|
||||
{`
|
||||
em-emoji-picker { max-height: ${height}px; }
|
||||
`}
|
||||
</style>
|
||||
<Picker
|
||||
autoFocus
|
||||
data={data}
|
||||
custom={customEmojiList}
|
||||
perLine={7}
|
||||
previewPosition="none"
|
||||
skinTonePosition="search"
|
||||
theme="dark"
|
||||
onEmojiSelect={onEmojiSelect}
|
||||
onClickOutside={onClickOutside}
|
||||
maxFrequentRows={0}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
</style>
|
||||
<Picker
|
||||
autoFocus
|
||||
data={data}
|
||||
custom={customEmojiList}
|
||||
perLine={7}
|
||||
previewPosition="none"
|
||||
skinTonePosition="search"
|
||||
theme="dark"
|
||||
onEmojiSelect={onEmojiSelect}
|
||||
onClickOutside={onClickOutside}
|
||||
maxFrequentRows={0}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,6 +1,28 @@
|
||||
import type { ReactNode } from "react";
|
||||
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 (
|
||||
<span style={{ cursor: "pointer" }}>
|
||||
<Icon
|
||||
@ -12,11 +34,3 @@ export function ExternalIconLink({ size = 32, href, ...rest }) {
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExternalLink({ children, href }) {
|
||||
return (
|
||||
<a href={href} rel="noopener noreferrer" target="_blank">
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import "./file-uploader.css";
|
||||
import type { ChangeEvent } from "react";
|
||||
import { VoidApi } from "@void-cat/api";
|
||||
import { useState } from "react";
|
||||
|
||||
@ -38,12 +39,22 @@ async function voidCatUpload(file: File | Blob): Promise<UploadResult> {
|
||||
}
|
||||
}
|
||||
|
||||
export function FileUploader({ defaultImage, onClear, onFileUpload }) {
|
||||
const [img, setImg] = useState(defaultImage);
|
||||
interface FileUploaderProps {
|
||||
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);
|
||||
|
||||
async function onFileChange(ev) {
|
||||
const file = ev.target.files[0];
|
||||
async function onFileChange(ev: ChangeEvent<HTMLInputElement>) {
|
||||
const file = ev.target.files && ev.target.files[0];
|
||||
if (file) {
|
||||
try {
|
||||
setIsUploading(true);
|
||||
|
@ -12,7 +12,7 @@ export function LoggedInFollowButton({
|
||||
value: string;
|
||||
}) {
|
||||
const login = useLogin();
|
||||
const tags = login.follows.tags;
|
||||
const { tags, content, timestamp } = login!.follows;
|
||||
const follows = tags.filter((t) => t.at(0) === tag);
|
||||
const isFollowing = follows.find((t) => t.at(1) === value);
|
||||
|
||||
@ -21,7 +21,7 @@ export function LoggedInFollowButton({
|
||||
if (pub) {
|
||||
const newFollows = tags.filter((t) => t.at(1) !== value);
|
||||
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) {
|
||||
eb.tag(t);
|
||||
}
|
||||
@ -29,7 +29,7 @@ export function LoggedInFollowButton({
|
||||
});
|
||||
console.debug(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) {
|
||||
const newFollows = [...tags, [tag, value]];
|
||||
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) {
|
||||
eb.tag(tag);
|
||||
}
|
||||
@ -46,13 +46,13 @@ export function LoggedInFollowButton({
|
||||
});
|
||||
console.debug(ev);
|
||||
System.BroadcastEvent(ev);
|
||||
Login.setFollows(newFollows, login.follows.content, ev.created_at);
|
||||
Login.setFollows(newFollows, content ?? "", ev.created_at);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AsyncButton
|
||||
disabled={login.follows.timestamp === 0}
|
||||
disabled={timestamp ? timestamp === 0 : true}
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={isFollowing ? unfollow : follow}
|
||||
@ -64,14 +64,12 @@ export function LoggedInFollowButton({
|
||||
|
||||
export function FollowTagButton({ tag }: { tag: string }) {
|
||||
const login = useLogin();
|
||||
return login?.pubkey ? (
|
||||
<LoggedInFollowButton tag={"t"} loggedIn={login.pubkey} value={tag} />
|
||||
) : null;
|
||||
return login?.pubkey ? <LoggedInFollowButton tag={"t"} value={tag} /> : null;
|
||||
}
|
||||
|
||||
export function FollowButton({ pubkey }: { pubkey: string }) {
|
||||
const login = useLogin();
|
||||
return login?.pubkey ? (
|
||||
<LoggedInFollowButton tag={"p"} loggedIn={login.pubkey} value={pubkey} />
|
||||
<LoggedInFollowButton tag={"p"} value={pubkey} />
|
||||
) : null;
|
||||
}
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { NostrLink } from "./nostr-link";
|
||||
import type { ReactNode } from "react";
|
||||
import { NostrLink } from "element/nostr-link";
|
||||
|
||||
const FileExtensionRegex = /\.([\w]+)$/i;
|
||||
|
||||
interface HyperTextProps {
|
||||
link: string;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
export function HyperText({ link, children }: HyperTextProps) {
|
||||
@ -24,7 +26,7 @@ export function HyperText({ link, children }: HyperTextProps) {
|
||||
<img
|
||||
src={url.toString()}
|
||||
alt={url.toString()}
|
||||
objectFit="contain"
|
||||
style={{ objectFit: "contain" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ export interface LiveChatOptions {
|
||||
}
|
||||
|
||||
function BadgeAward({ ev }: { ev: NostrEvent }) {
|
||||
const badge = findTag(ev, "a");
|
||||
const badge = findTag(ev, "a") ?? "";
|
||||
const [k, pubkey, d] = badge.split(":");
|
||||
const awardees = getTagValues(ev.tags, "p");
|
||||
const event = useAddress(Number(k), pubkey, d);
|
||||
|
@ -1,38 +1,46 @@
|
||||
import "./markdown.css";
|
||||
|
||||
import { createElement } from "react";
|
||||
import { useMemo } from "react";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
|
||||
import { HyperText } from "element/hypertext";
|
||||
import { transformText } from "element/text";
|
||||
import { transformText, type Fragment } from "element/text";
|
||||
import type { Tags } from "types";
|
||||
|
||||
interface MarkdownProps {
|
||||
content: string;
|
||||
tags?: string[];
|
||||
tags?: Tags;
|
||||
}
|
||||
|
||||
export function Markdown({
|
||||
content,
|
||||
tags = [],
|
||||
element = "div",
|
||||
}: MarkdownProps) {
|
||||
interface LinkProps {
|
||||
href?: string;
|
||||
children?: Array<Fragment>;
|
||||
}
|
||||
|
||||
interface ComponentProps {
|
||||
children?: Array<Fragment>;
|
||||
}
|
||||
|
||||
export function Markdown({ content, tags = [] }: MarkdownProps) {
|
||||
const components = useMemo(() => {
|
||||
return {
|
||||
li: ({ children, ...props }) => {
|
||||
li: ({ children, ...props }: ComponentProps) => {
|
||||
return children && <li {...props}>{transformText(children, tags)}</li>;
|
||||
},
|
||||
td: ({ children }) =>
|
||||
children && <td>{transformText(children, tags)}</td>,
|
||||
p: ({ children }) => <p>{transformText(children, tags)}</p>,
|
||||
a: (props) => {
|
||||
return <HyperText link={props.href}>{props.children}</HyperText>;
|
||||
td: ({ children }: ComponentProps) => {
|
||||
return children && <td>{transformText(children, tags)}</td>;
|
||||
},
|
||||
p: ({ children }: ComponentProps) => {
|
||||
return children && <p>{transformText(children, tags)}</p>;
|
||||
},
|
||||
a: ({ href, children }: LinkProps) => {
|
||||
return href && <HyperText link={href}>{children}</HyperText>;
|
||||
},
|
||||
};
|
||||
}, [tags]);
|
||||
return createElement(
|
||||
element,
|
||||
{ className: "markdown" },
|
||||
<ReactMarkdown components={components}>{content}</ReactMarkdown>,
|
||||
return (
|
||||
<div className="markdown">
|
||||
<ReactMarkdown components={components}>{content}</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -5,7 +5,7 @@ import { MUTED } from "const";
|
||||
|
||||
export function LoggedInMuteButton({ pubkey }: { pubkey: string }) {
|
||||
const login = useLogin();
|
||||
const tags = login.muted.tags;
|
||||
const { tags, content, timestamp } = login!.muted;
|
||||
const muted = tags.filter((t) => t.at(0) === "p");
|
||||
const isMuted = muted.find((t) => t.at(1) === pubkey);
|
||||
|
||||
@ -14,7 +14,7 @@ export function LoggedInMuteButton({ pubkey }: { pubkey: string }) {
|
||||
if (pub) {
|
||||
const newMuted = tags.filter((t) => t.at(1) !== pubkey);
|
||||
const ev = await pub.generic((eb) => {
|
||||
eb.kind(MUTED).content(login.muted.content);
|
||||
eb.kind(MUTED).content(content ?? "");
|
||||
for (const t of newMuted) {
|
||||
eb.tag(t);
|
||||
}
|
||||
@ -22,7 +22,7 @@ export function LoggedInMuteButton({ pubkey }: { pubkey: string }) {
|
||||
});
|
||||
console.debug(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) {
|
||||
const newMuted = [...tags, ["p", pubkey]];
|
||||
const ev = await pub.generic((eb) => {
|
||||
eb.kind(MUTED).content(login.muted.content);
|
||||
eb.kind(MUTED).content(content ?? "");
|
||||
for (const tag of newMuted) {
|
||||
eb.tag(tag);
|
||||
}
|
||||
@ -39,13 +39,13 @@ export function LoggedInMuteButton({ pubkey }: { pubkey: string }) {
|
||||
});
|
||||
console.debug(ev);
|
||||
System.BroadcastEvent(ev);
|
||||
Login.setMuted(newMuted, login.muted.content, ev.created_at);
|
||||
Login.setMuted(newMuted, content ?? "", ev.created_at);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<AsyncButton
|
||||
disabled={login.muted.timestamp === 0}
|
||||
disabled={timestamp ? timestamp === 0 : true}
|
||||
type="button"
|
||||
className="btn delete-button"
|
||||
onClick={isMuted ? unmute : mute}
|
||||
@ -57,7 +57,5 @@ export function LoggedInMuteButton({ pubkey }: { pubkey: string }) {
|
||||
|
||||
export function MuteButton({ pubkey }: { pubkey: string }) {
|
||||
const login = useLogin();
|
||||
return login?.pubkey ? (
|
||||
<LoggedInMuteButton loggedIn={login.pubkey} pubkey={pubkey} />
|
||||
) : null;
|
||||
return login?.pubkey ? <LoggedInMuteButton pubkey={pubkey} /> : null;
|
||||
}
|
||||
|
@ -5,50 +5,51 @@ import * as Dialog from "@radix-ui/react-dialog";
|
||||
import { DndProvider, useDrag, useDrop } from "react-dnd";
|
||||
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 { 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 { useCards, useUserCards } from "hooks/cards";
|
||||
import { CARD, USER_CARDS } from "const";
|
||||
import { toTag } from "utils";
|
||||
import { toTag, findTag } from "utils";
|
||||
import { Login, System } from "index";
|
||||
import { findTag } from "utils";
|
||||
import { Icon } from "./icon";
|
||||
import { ExternalLink } from "./external-link";
|
||||
import { FileUploader } from "./file-uploader";
|
||||
import { Markdown } from "./markdown";
|
||||
import type { Tags } from "types";
|
||||
|
||||
interface CardType {
|
||||
identifier?: string;
|
||||
identifier: string;
|
||||
content: string;
|
||||
title?: string;
|
||||
image?: string;
|
||||
link?: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
interface CardProps {
|
||||
canEdit?: boolean;
|
||||
ev: NostrEvent;
|
||||
cards: NostrEvent[];
|
||||
}
|
||||
type NewCard = Omit<CardType, "identifier">;
|
||||
|
||||
function isEmpty(s?: string) {
|
||||
return !s || s.trim().length === 0;
|
||||
}
|
||||
|
||||
interface CardPreviewProps extends NewCard {
|
||||
style: object;
|
||||
}
|
||||
|
||||
const CardPreview = forwardRef(
|
||||
({ style, title, link, image, content }, ref) => {
|
||||
({ style, title, link, image, content }: CardPreviewProps, ref) => {
|
||||
const isImageOnly = !isEmpty(image) && isEmpty(content) && isEmpty(title);
|
||||
return (
|
||||
<div
|
||||
className={`stream-card ${isImageOnly ? "image-card" : ""}`}
|
||||
// @ts-expect-error: Type 'ForwardRef<unknown>'
|
||||
ref={ref}
|
||||
style={style}
|
||||
>
|
||||
{title && <h1 className="card-title">{title}</h1>}
|
||||
{image &&
|
||||
(link?.length > 0 ? (
|
||||
(link && link?.length > 0 ? (
|
||||
<ExternalLink href={link}>
|
||||
<img className="card-image" src={image} alt={title} />
|
||||
</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) {
|
||||
const login = useLogin();
|
||||
const identifier = findTag(ev, "d");
|
||||
const identifier = findTag(ev, "d") ?? "";
|
||||
const title = findTag(ev, "title") || findTag(ev, "subject");
|
||||
const image = findTag(ev, "image");
|
||||
const link = findTag(ev, "r");
|
||||
@ -73,9 +84,9 @@ function Card({ canEdit, ev, cards }: CardProps) {
|
||||
const [style, dragRef] = useDrag(
|
||||
() => ({
|
||||
type: "card",
|
||||
item: { identifier },
|
||||
item: { identifier } as CardItem,
|
||||
canDrag: () => {
|
||||
return canEdit;
|
||||
return Boolean(canEdit);
|
||||
},
|
||||
collect: (monitor) => {
|
||||
const isDragging = monitor.isDragging();
|
||||
@ -88,15 +99,15 @@ function Card({ canEdit, ev, cards }: CardProps) {
|
||||
[canEdit, identifier],
|
||||
);
|
||||
|
||||
function findTagByIdentifier(d) {
|
||||
return tags.find((t) => t.at(1).endsWith(`:${d}`));
|
||||
function findTagByIdentifier(d: string) {
|
||||
return tags.find((t) => t.at(1)!.endsWith(`:${d}`));
|
||||
}
|
||||
|
||||
const [dropStyle, dropRef] = useDrop(
|
||||
() => ({
|
||||
accept: ["card"],
|
||||
canDrop: () => {
|
||||
return canEdit;
|
||||
return Boolean(canEdit);
|
||||
},
|
||||
collect: (monitor) => {
|
||||
const isOvering = monitor.isOver({ shallow: true });
|
||||
@ -106,10 +117,11 @@ function Card({ canEdit, ev, cards }: CardProps) {
|
||||
};
|
||||
},
|
||||
async drop(item) {
|
||||
if (identifier === item.identifier) {
|
||||
const typed = item as CardItem;
|
||||
if (identifier === typed.identifier) {
|
||||
return;
|
||||
}
|
||||
const newItem = findTagByIdentifier(item.identifier);
|
||||
const newItem = findTagByIdentifier(typed.identifier);
|
||||
const oldItem = findTagByIdentifier(identifier);
|
||||
const newTags = tags.map((t) => {
|
||||
if (t === oldItem) {
|
||||
@ -119,18 +131,20 @@ function Card({ canEdit, ev, cards }: CardProps) {
|
||||
return oldItem;
|
||||
}
|
||||
return t;
|
||||
});
|
||||
}) as Tags;
|
||||
const pub = login?.publisher();
|
||||
const userCardsEv = await pub.generic((eb) => {
|
||||
eb.kind(USER_CARDS).content("");
|
||||
for (const tag of newTags) {
|
||||
eb.tag(tag);
|
||||
}
|
||||
return eb;
|
||||
});
|
||||
console.debug(userCardsEv);
|
||||
System.BroadcastEvent(userCardsEv);
|
||||
Login.setCards(newTags, userCardsEv.created_at);
|
||||
if (pub) {
|
||||
const userCardsEv = await pub.generic((eb) => {
|
||||
eb.kind(USER_CARDS).content("");
|
||||
for (const tag of newTags) {
|
||||
eb.tag(tag);
|
||||
}
|
||||
return eb;
|
||||
});
|
||||
console.debug(userCardsEv);
|
||||
System.BroadcastEvent(userCardsEv);
|
||||
Login.setCards(newTags, userCardsEv.created_at);
|
||||
}
|
||||
},
|
||||
}),
|
||||
[canEdit, tags, identifier],
|
||||
@ -166,7 +180,7 @@ interface CardDialogProps {
|
||||
cta?: string;
|
||||
cancelCta?: string;
|
||||
card?: CardType;
|
||||
onSave(ev: CardType): void;
|
||||
onSave(ev: NewCard): void;
|
||||
onCancel(): void;
|
||||
}
|
||||
|
||||
@ -187,7 +201,7 @@ function CardDialog({
|
||||
<div className="new-card">
|
||||
<h3>{header || "Add card"}</h3>
|
||||
<div className="form-control">
|
||||
<label for="card-title">Title</label>
|
||||
<label htmlFor="card-title">Title</label>
|
||||
<input
|
||||
id="card-title"
|
||||
type="text"
|
||||
@ -197,7 +211,7 @@ function CardDialog({
|
||||
/>
|
||||
</div>
|
||||
<div className="form-control">
|
||||
<label for="card-image">Image</label>
|
||||
<label htmlFor="card-image">Image</label>
|
||||
<FileUploader
|
||||
defaultImage={image}
|
||||
onFileUpload={setImage}
|
||||
@ -205,7 +219,7 @@ function CardDialog({
|
||||
/>
|
||||
</div>
|
||||
<div className="form-control">
|
||||
<label for="card-image-link">Image Link</label>
|
||||
<label htmlFor="card-image-link">Image Link</label>
|
||||
<input
|
||||
id="card-image-link"
|
||||
type="text"
|
||||
@ -215,7 +229,7 @@ function CardDialog({
|
||||
/>
|
||||
</div>
|
||||
<div className="form-control">
|
||||
<label for="card-content">Content</label>
|
||||
<label htmlFor="card-content">Content</label>
|
||||
<textarea
|
||||
placeholder="Start typing..."
|
||||
value={content}
|
||||
@ -245,7 +259,7 @@ function CardDialog({
|
||||
|
||||
interface EditCardProps {
|
||||
card: CardType;
|
||||
cards: NostrEvent[];
|
||||
cards: TaggedRawEvent[];
|
||||
}
|
||||
|
||||
function EditCard({ card, cards }: EditCardProps) {
|
||||
@ -254,18 +268,18 @@ function EditCard({ card, cards }: EditCardProps) {
|
||||
const identifier = card.identifier;
|
||||
const tags = cards.map(toTag);
|
||||
|
||||
async function editCard({ title, image, link, content }) {
|
||||
async function editCard({ title, image, link, content }: CardType) {
|
||||
const pub = login?.publisher();
|
||||
if (pub) {
|
||||
const ev = await pub.generic((eb) => {
|
||||
eb.kind(CARD).content(content).tag(["d", card.identifier]);
|
||||
if (title?.length > 0) {
|
||||
if (title && title?.length > 0) {
|
||||
eb.tag(["title", title]);
|
||||
}
|
||||
if (image?.length > 0) {
|
||||
if (image && image?.length > 0) {
|
||||
eb.tag(["image", image]);
|
||||
}
|
||||
if (link?.lenght > 0) {
|
||||
if (link && link?.length > 0) {
|
||||
eb.tag(["r", link]);
|
||||
}
|
||||
return eb;
|
||||
@ -279,7 +293,7 @@ function EditCard({ card, cards }: EditCardProps) {
|
||||
async function onCancel() {
|
||||
const pub = login?.publisher();
|
||||
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) => {
|
||||
eb.kind(USER_CARDS).content("");
|
||||
for (const tag of newTags) {
|
||||
@ -318,7 +332,7 @@ function EditCard({ card, cards }: EditCardProps) {
|
||||
}
|
||||
|
||||
interface AddCardProps {
|
||||
cards: NostrEvent[];
|
||||
cards: TaggedRawEvent[];
|
||||
}
|
||||
|
||||
function AddCard({ cards }: AddCardProps) {
|
||||
@ -326,19 +340,19 @@ function AddCard({ cards }: AddCardProps) {
|
||||
const tags = cards.map(toTag);
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
async function createCard({ title, image, link, content }) {
|
||||
async function createCard({ title, image, link, content }: NewCard) {
|
||||
const pub = login?.publisher();
|
||||
if (pub) {
|
||||
const ev = await pub.generic((eb) => {
|
||||
const d = String(Date.now());
|
||||
eb.kind(CARD).content(content).tag(["d", d]);
|
||||
if (title?.length > 0) {
|
||||
if (title && title?.length > 0) {
|
||||
eb.tag(["title", title]);
|
||||
}
|
||||
if (image?.length > 0) {
|
||||
if (image && image?.length > 0) {
|
||||
eb.tag(["image", image]);
|
||||
}
|
||||
if (link?.length > 0) {
|
||||
if (link && link?.length > 0) {
|
||||
eb.tag(["r", link]);
|
||||
}
|
||||
return eb;
|
||||
@ -382,15 +396,19 @@ function AddCard({ cards }: AddCardProps) {
|
||||
);
|
||||
}
|
||||
|
||||
export function StreamCardEditor() {
|
||||
const login = useLogin();
|
||||
const cards = useUserCards(login.pubkey, login.cards.tags, true);
|
||||
interface StreamCardEditorProps {
|
||||
pubkey: string;
|
||||
tags: Tags;
|
||||
}
|
||||
|
||||
export function StreamCardEditor({ pubkey, tags }: StreamCardEditorProps) {
|
||||
const cards = useUserCards(pubkey, tags, true);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<div className="stream-cards">
|
||||
{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} />}
|
||||
</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);
|
||||
return (
|
||||
<div className="stream-cards">
|
||||
{cards.map((ev) => (
|
||||
<Card cards={cards} key={ev.id} ev={ev} />
|
||||
<Card cards={cards} key={ev!.id} ev={ev!} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function StreamCards({ host }) {
|
||||
export function StreamCards({ host }: StreamCardsProps) {
|
||||
const login = useLogin();
|
||||
const canEdit = login?.pubkey === host;
|
||||
return (
|
||||
<DndProvider backend={HTML5Backend}>
|
||||
{canEdit ? <StreamCardEditor /> : <ReadOnlyStreamCards host={host} />}
|
||||
{canEdit ? (
|
||||
<StreamCardEditor tags={login.cards.tags} pubkey={login.pubkey} />
|
||||
) : (
|
||||
<ReadOnlyStreamCards host={host} />
|
||||
)}
|
||||
</DndProvider>
|
||||
);
|
||||
}
|
||||
|
@ -9,7 +9,7 @@ import { Emoji } from "element/emoji";
|
||||
import { HyperText } from "element/hypertext";
|
||||
import { splitByUrl } from "utils";
|
||||
|
||||
type Fragment = string | ReactNode;
|
||||
export type Fragment = string | ReactNode;
|
||||
|
||||
const NostrPrefixRegex = /^nostr:/;
|
||||
const EmojiRegex = /:([\w-]+):/g;
|
||||
@ -50,7 +50,7 @@ function extractLinks(fragments: Fragment[]) {
|
||||
</a>
|
||||
);
|
||||
}
|
||||
return <HyperText link={a} />;
|
||||
return <HyperText link={a}>{a}</HyperText>;
|
||||
}
|
||||
return a;
|
||||
});
|
||||
|
@ -6,11 +6,14 @@ import ReactTextareaAutocomplete, {
|
||||
import "@webscopeio/react-textarea-autocomplete/style.css";
|
||||
import uniqWith from "lodash/uniqWith";
|
||||
import isEqual from "lodash/isEqual";
|
||||
|
||||
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 { hexToBech32 } from "utils";
|
||||
import type { EmojiTag } from "types";
|
||||
import { System } from "index";
|
||||
|
||||
interface EmojiItemProps {
|
||||
name: string;
|
||||
|
@ -4,6 +4,9 @@ import { Icon } from "element/icon";
|
||||
|
||||
interface ToggleProps {
|
||||
label: string;
|
||||
text: string;
|
||||
pressed?: boolean;
|
||||
onPressedChange?: (b: boolean) => void;
|
||||
}
|
||||
|
||||
export function Toggle({ label, text, ...rest }: ToggleProps) {
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { NostrLink, EventKind } from "@snort/system";
|
||||
import React, { useRef, useState } from "react";
|
||||
|
||||
import { LIVE_STREAM_CHAT } from "../const";
|
||||
import { useLogin } from "../hooks/login";
|
||||
import { System } from "../index";
|
||||
import AsyncButton from "./async-button";
|
||||
import { Icon } from "./icon";
|
||||
import { Textarea } from "./textarea";
|
||||
import { EmojiPicker } from "./emoji-picker";
|
||||
import type { EmojiPack, Emoji } from "../hooks/emoji";
|
||||
import { useLogin } from "hooks/login";
|
||||
import AsyncButton from "element/async-button";
|
||||
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";
|
||||
|
||||
export function WriteMessage({
|
||||
link,
|
||||
@ -41,7 +41,7 @@ export function WriteMessage({
|
||||
|
||||
const reply = await pub?.generic((eb) => {
|
||||
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)
|
||||
.content(chat)
|
||||
@ -90,7 +90,7 @@ export function WriteMessage({
|
||||
emojis={emojis}
|
||||
value={chat}
|
||||
onKeyDown={onKeyDown}
|
||||
onChange={e => setChat(e.target.value)}
|
||||
onChange={(e) => setChat(e.target.value)}
|
||||
/>
|
||||
<div onClick={pickEmoji}>
|
||||
<Icon name="face" className="write-emoji-button" />
|
||||
|
@ -1,14 +1,23 @@
|
||||
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 { unixNow } from "@snort/shared";
|
||||
|
||||
import { findTag, toAddress, getTagValues } from "utils";
|
||||
import { WEEK } from "const";
|
||||
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 rb = useMemo(() => {
|
||||
const rb = new RequestBuilder(`badges:${pubkey.slice(0, 12)}`);
|
||||
@ -61,7 +70,7 @@ export function useBadges(pubkey: string, leaveOpen = true) {
|
||||
|
||||
const badges = useMemo(() => {
|
||||
return rawBadges.map((e) => {
|
||||
const name = findTag(e, "d");
|
||||
const name = findTag(e, "d") ?? "";
|
||||
const address = toAddress(e);
|
||||
const awardEvents = badgeAwards.filter(
|
||||
(b) => findTag(b, "a") === address,
|
||||
@ -79,7 +88,7 @@ export function useBadges(pubkey: string, leaveOpen = true) {
|
||||
);
|
||||
const thumb = findTag(e, "thumb");
|
||||
const image = findTag(e, "image");
|
||||
return { name, thumb, image, awardees, accepted };
|
||||
return { name, thumb, image, awardees, accepted } as Badge;
|
||||
});
|
||||
return [];
|
||||
}, [rawBadges]);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
import {
|
||||
TaggedRawEvent,
|
||||
ReplaceableNoteStore,
|
||||
NoteCollection,
|
||||
RequestBuilder,
|
||||
@ -15,7 +16,7 @@ export function useUserCards(
|
||||
pubkey: string,
|
||||
userCards: Array<string[]>,
|
||||
leaveOpen = false,
|
||||
) {
|
||||
): TaggedRawEvent[] {
|
||||
const related = useMemo(() => {
|
||||
// filtering to only show CARD kinds for now, but in the future we could link and render anything
|
||||
if (userCards?.length > 0) {
|
||||
@ -57,7 +58,7 @@ export function useUserCards(
|
||||
const cards = useMemo(() => {
|
||||
return related
|
||||
.map((t) => {
|
||||
const [k, pubkey, identifier] = t.at(1).split(":");
|
||||
const [k, pubkey, identifier] = t.at(1)!.split(":");
|
||||
const kind = Number(k);
|
||||
return (data ?? []).find(
|
||||
(e) =>
|
||||
@ -66,13 +67,14 @@ export function useUserCards(
|
||||
findTag(e, "d") === identifier,
|
||||
);
|
||||
})
|
||||
.filter((e) => e);
|
||||
.filter((e) => e)
|
||||
.map((e) => e as TaggedRawEvent);
|
||||
}, [related, data]);
|
||||
|
||||
return cards;
|
||||
}
|
||||
|
||||
export function useCards(pubkey: string, leaveOpen = false) {
|
||||
export function useCards(pubkey: string, leaveOpen = false): TaggedRawEvent[] {
|
||||
const sub = useMemo(() => {
|
||||
const b = new RequestBuilder(`user-cards:${pubkey.slice(0, 12)}`);
|
||||
b.withOptions({
|
||||
@ -127,21 +129,23 @@ export function useCards(pubkey: string, leaveOpen = false) {
|
||||
NoteCollection,
|
||||
subRelated,
|
||||
);
|
||||
const cardEvents = data ?? [];
|
||||
|
||||
const cards = useMemo(() => {
|
||||
return related
|
||||
.map((t) => {
|
||||
const [k, pubkey, identifier] = t.at(1).split(":");
|
||||
const [k, pubkey, identifier] = t.at(1)!.split(":");
|
||||
const kind = Number(k);
|
||||
return data.find(
|
||||
return cardEvents.find(
|
||||
(e) =>
|
||||
e.kind === kind &&
|
||||
e.pubkey === pubkey &&
|
||||
findTag(e, "d") === identifier,
|
||||
);
|
||||
})
|
||||
.filter((e) => e);
|
||||
}, [related, data]);
|
||||
.filter((e) => e)
|
||||
.map((e) => e as TaggedRawEvent);
|
||||
}, [related, cardEvents]);
|
||||
|
||||
return cards;
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ import { useRequestBuilder } from "@snort/system-react";
|
||||
import { System } from "index";
|
||||
import { findTag } from "utils";
|
||||
import { EMOJI_PACK, USER_EMOJIS } from "const";
|
||||
import { EmojiPack } from "types";
|
||||
import type { EmojiPack, Tags, EmojiTag } from "types";
|
||||
|
||||
function cleanShortcode(shortcode?: string) {
|
||||
return shortcode?.replace(/\s+/g, "_").replace(/_$/, "");
|
||||
@ -33,10 +33,10 @@ export function packId(pack: EmojiPack): string {
|
||||
return `${pack.author}:${pack.name}`;
|
||||
}
|
||||
|
||||
export function useUserEmojiPacks(pubkey?: string, userEmoji: Array<string[]>) {
|
||||
export function useUserEmojiPacks(pubkey?: string, userEmoji?: Tags) {
|
||||
const related = useMemo(() => {
|
||||
if (userEmoji?.length > 0) {
|
||||
return userEmoji.filter(
|
||||
if (userEmoji) {
|
||||
return userEmoji?.filter(
|
||||
(t) => t.at(0) === "a" && t.at(1)?.startsWith(`${EMOJI_PACK}:`),
|
||||
);
|
||||
}
|
||||
|
@ -50,7 +50,7 @@ export function useStreamsFeed(tag?: string) {
|
||||
);
|
||||
const ended = feedSorted.filter((a) => {
|
||||
const hasEnded = findTag(a, "status") === StreamState.Ended;
|
||||
const recording = findTag(a, "recording");
|
||||
const recording = findTag(a, "recording") ?? "";
|
||||
return hasEnded && recording?.length > 0;
|
||||
});
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { useRequestBuilder } from "@snort/system-react";
|
||||
|
||||
import { useUserEmojiPacks } from "hooks/emoji";
|
||||
import { MUTED, USER_CARDS, USER_EMOJIS } from "const";
|
||||
import type { Tags } from "types";
|
||||
import { System, Login } from "index";
|
||||
import { getPublisher } from "login";
|
||||
|
||||
@ -23,7 +24,7 @@ export function useLogin() {
|
||||
}
|
||||
|
||||
export function useLoginEvents(pubkey?: string, leaveOpen = false) {
|
||||
const [userEmojis, setUserEmojis] = useState([]);
|
||||
const [userEmojis, setUserEmojis] = useState<Tags>([]);
|
||||
const session = useSyncExternalStore(
|
||||
(c) => Login.hook(c),
|
||||
() => Login.snapshot(),
|
||||
|
12
src/login.ts
12
src/login.ts
@ -2,7 +2,7 @@ import { bytesToHex } from "@noble/curves/abstract/utils";
|
||||
import { schnorr } from "@noble/curves/secp256k1";
|
||||
import { ExternalStore } from "@snort/shared";
|
||||
import { EventPublisher, Nip7Signer, PrivateKeySigner } from "@snort/system";
|
||||
import type { EmojiPack } from "types";
|
||||
import type { EmojiPack, Tags } from "types";
|
||||
|
||||
export enum LoginType {
|
||||
Nip7 = "nip7",
|
||||
@ -76,7 +76,8 @@ export class LoginStore extends ExternalStore<LoginSession | 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) {
|
||||
return;
|
||||
}
|
||||
@ -87,11 +88,13 @@ export class LoginStore extends ExternalStore<LoginSession | undefined> {
|
||||
}
|
||||
|
||||
setEmojis(emojis: Array<EmojiPack>) {
|
||||
if (!this.#session) return;
|
||||
this.#session.emojis = emojis;
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
@ -101,7 +104,8 @@ export class LoginStore extends ExternalStore<LoginSession | undefined> {
|
||||
this.#save();
|
||||
}
|
||||
|
||||
setCards(cards: Array<string[]>, ts: number) {
|
||||
setCards(cards: Tags, ts: number) {
|
||||
if (!this.#session) return;
|
||||
if (this.#session.cards.timestamp >= ts) {
|
||||
return;
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ export function TagPage() {
|
||||
<div className="tag-page">
|
||||
<div className="tag-page-header">
|
||||
<h1>#{tag}</h1>
|
||||
<FollowTagButton tag={tag} />
|
||||
<FollowTagButton tag={tag!} />
|
||||
</div>
|
||||
<div className="video-grid">
|
||||
{live.map((e) => (
|
||||
|
@ -1,105 +1,109 @@
|
||||
import { StreamState } from "index"
|
||||
import { StreamState } from "index";
|
||||
import { NostrEvent } from "@snort/system";
|
||||
import { ExternalStore } from "@snort/shared";
|
||||
import { Nip103StreamProvider } from "./nip103";
|
||||
import { ManualProvider } from "./manual";
|
||||
import { OwncastProvider } from "./owncast";
|
||||
|
||||
|
||||
export interface StreamProvider {
|
||||
get name(): string
|
||||
get type(): StreamProviders
|
||||
get name(): string;
|
||||
get type(): StreamProviders;
|
||||
|
||||
/**
|
||||
* Get general info about connected provider to test everything is working
|
||||
*/
|
||||
info(): Promise<StreamProviderInfo>
|
||||
/**
|
||||
* Get general info about connected provider to test everything is working
|
||||
*/
|
||||
info(): Promise<StreamProviderInfo>;
|
||||
|
||||
/**
|
||||
* Create a config object to save in localStorage
|
||||
*/
|
||||
createConfig(): unknown & { type: StreamProviders }
|
||||
/**
|
||||
* Create a config object to save in localStorage
|
||||
*/
|
||||
createConfig(): unknown & { type: StreamProviders };
|
||||
|
||||
/**
|
||||
* Update stream info event
|
||||
*/
|
||||
updateStreamInfo(ev: NostrEvent): Promise<void>
|
||||
/**
|
||||
* Update stream info event
|
||||
*/
|
||||
updateStreamInfo(ev: NostrEvent): Promise<void>;
|
||||
|
||||
/**
|
||||
* Top-up balance with provider
|
||||
*/
|
||||
topup(amount: number): Promise<string>
|
||||
/**
|
||||
* Top-up balance with provider
|
||||
*/
|
||||
topup(amount: number): Promise<string>;
|
||||
}
|
||||
|
||||
export enum StreamProviders {
|
||||
Manual = "manual",
|
||||
Owncast = "owncast",
|
||||
Cloudflare = "cloudflare",
|
||||
NostrType = "nostr"
|
||||
Manual = "manual",
|
||||
Owncast = "owncast",
|
||||
Cloudflare = "cloudflare",
|
||||
NostrType = "nostr",
|
||||
}
|
||||
|
||||
export interface StreamProviderInfo {
|
||||
name: string
|
||||
summary?: string
|
||||
version?: string
|
||||
state: StreamState
|
||||
viewers?: number
|
||||
publishedEvent?: NostrEvent
|
||||
balance?: number
|
||||
endpoints: Array<StreamProviderEndpoint>
|
||||
name: string;
|
||||
summary?: string;
|
||||
version?: string;
|
||||
state: StreamState;
|
||||
viewers?: number;
|
||||
publishedEvent?: NostrEvent;
|
||||
balance?: number;
|
||||
endpoints: Array<StreamProviderEndpoint>;
|
||||
}
|
||||
|
||||
export interface StreamProviderEndpoint {
|
||||
name: string
|
||||
url: string
|
||||
key: string
|
||||
rate?: number
|
||||
unit?: string
|
||||
capabilities?: Array<string>
|
||||
name: string;
|
||||
url: string;
|
||||
key: string;
|
||||
rate?: number;
|
||||
unit?: string;
|
||||
capabilities?: Array<string>;
|
||||
}
|
||||
|
||||
export class ProviderStore extends ExternalStore<Array<StreamProvider>> {
|
||||
#providers: Array<StreamProvider> = []
|
||||
#providers: Array<StreamProvider> = [];
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const cache = window.localStorage.getItem("providers");
|
||||
if (cache) {
|
||||
const cached: Array<{ type: StreamProviders } & Record<string, unknown>> = JSON.parse(cache);
|
||||
for (const c of cached) {
|
||||
switch (c.type) {
|
||||
case StreamProviders.Manual: {
|
||||
this.#providers.push(new ManualProvider());
|
||||
break;
|
||||
}
|
||||
case StreamProviders.NostrType: {
|
||||
this.#providers.push(new Nip103StreamProvider(c.url as string));
|
||||
break;
|
||||
}
|
||||
case StreamProviders.Owncast: {
|
||||
this.#providers.push(new OwncastProvider(c.url as string, c.token as string));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
const cache = window.localStorage.getItem("providers");
|
||||
if (cache) {
|
||||
const cached: Array<{ type: StreamProviders } & Record<string, unknown>> =
|
||||
JSON.parse(cache);
|
||||
for (const c of cached) {
|
||||
switch (c.type) {
|
||||
case StreamProviders.Manual: {
|
||||
this.#providers.push(new ManualProvider());
|
||||
break;
|
||||
}
|
||||
case StreamProviders.NostrType: {
|
||||
this.#providers.push(new Nip103StreamProvider(c.url as string));
|
||||
break;
|
||||
}
|
||||
case StreamProviders.Owncast: {
|
||||
this.#providers.push(
|
||||
new OwncastProvider(c.url as string, c.token as string),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
add(p: StreamProvider) {
|
||||
this.#providers.push(p);
|
||||
this.#save();
|
||||
this.notifyChange();
|
||||
}
|
||||
add(p: StreamProvider) {
|
||||
this.#providers.push(p);
|
||||
this.#save();
|
||||
this.notifyChange();
|
||||
}
|
||||
|
||||
takeSnapshot() {
|
||||
const defaultProvider = new Nip103StreamProvider("https://api.zap.stream/api/nostr/");
|
||||
return [defaultProvider, new ManualProvider(), ...this.#providers];
|
||||
}
|
||||
takeSnapshot() {
|
||||
const defaultProvider = new Nip103StreamProvider(
|
||||
"https://api.zap.stream/api/nostr/",
|
||||
);
|
||||
return [defaultProvider, new ManualProvider(), ...this.#providers];
|
||||
}
|
||||
|
||||
#save() {
|
||||
const cfg = this.#providers.map(a => a.createConfig());
|
||||
window.localStorage.setItem("providers", JSON.stringify(cfg));
|
||||
}
|
||||
#save() {
|
||||
const cfg = this.#providers.map((a) => a.createConfig());
|
||||
window.localStorage.setItem("providers", JSON.stringify(cfg));
|
||||
}
|
||||
}
|
||||
|
||||
export const StreamProviderStore = new ProviderStore();
|
||||
export const StreamProviderStore = new ProviderStore();
|
||||
|
@ -2,82 +2,86 @@ import { StreamState } from "index";
|
||||
import { StreamProvider, StreamProviderInfo, StreamProviders } from "providers";
|
||||
|
||||
export class OwncastProvider implements StreamProvider {
|
||||
#url: string
|
||||
#token: string
|
||||
#url: string;
|
||||
#token: string;
|
||||
|
||||
constructor(url: string, token: string) {
|
||||
this.#url = url;
|
||||
this.#token = token;
|
||||
constructor(url: string, token: string) {
|
||||
this.#url = url;
|
||||
this.#token = token;
|
||||
}
|
||||
|
||||
get name() {
|
||||
return new URL(this.#url).host;
|
||||
}
|
||||
|
||||
get type() {
|
||||
return StreamProviders.Owncast;
|
||||
}
|
||||
|
||||
createConfig() {
|
||||
return {
|
||||
type: StreamProviders.Owncast,
|
||||
url: this.#url,
|
||||
token: this.#token,
|
||||
};
|
||||
}
|
||||
|
||||
updateStreamInfo(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async info() {
|
||||
const info = await this.#getJson<ConfigResponse>("GET", "/api/config");
|
||||
const status = await this.#getJson<StatusResponse>("GET", "/api/status");
|
||||
return {
|
||||
type: StreamProviders.Owncast,
|
||||
name: info.name,
|
||||
summary: info.summary,
|
||||
version: info.version,
|
||||
state: status.online ? StreamState.Live : StreamState.Ended,
|
||||
viewers: status.viewerCount,
|
||||
endpoints: [],
|
||||
} as StreamProviderInfo;
|
||||
}
|
||||
|
||||
topup(): Promise<string> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async #getJson<T>(
|
||||
method: "GET" | "POST",
|
||||
path: string,
|
||||
body?: unknown,
|
||||
): Promise<T> {
|
||||
const rsp = await fetch(`${this.#url}${path}`, {
|
||||
method: method,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
authorization: `Bearer ${this.#token}`,
|
||||
},
|
||||
});
|
||||
const json = await rsp.text();
|
||||
if (!rsp.ok) {
|
||||
throw new Error(json);
|
||||
}
|
||||
|
||||
get name() {
|
||||
return new URL(this.#url).host
|
||||
}
|
||||
|
||||
get type() {
|
||||
return StreamProviders.Owncast
|
||||
}
|
||||
|
||||
createConfig() {
|
||||
return {
|
||||
type: StreamProviders.Owncast,
|
||||
url: this.#url,
|
||||
token: this.#token
|
||||
}
|
||||
}
|
||||
|
||||
updateStreamInfo(): Promise<void> {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
async info() {
|
||||
const info = await this.#getJson<ConfigResponse>("GET", "/api/config");
|
||||
const status = await this.#getJson<StatusResponse>("GET", "/api/status");
|
||||
return {
|
||||
type: StreamProviders.Owncast,
|
||||
name: info.name,
|
||||
summary: info.summary,
|
||||
version: info.version,
|
||||
state: status.online ? StreamState.Live : StreamState.Ended,
|
||||
viewers: status.viewerCount
|
||||
} as StreamProviderInfo
|
||||
}
|
||||
|
||||
topup(): Promise<string> {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
async #getJson<T>(method: "GET" | "POST", path: string, body?: unknown): Promise<T> {
|
||||
const rsp = await fetch(`${this.#url}${path}`, {
|
||||
method: method,
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"authorization": `Bearer ${this.#token}`
|
||||
},
|
||||
});
|
||||
const json = await rsp.text();
|
||||
if (!rsp.ok) {
|
||||
throw new Error(json);
|
||||
}
|
||||
return JSON.parse(json) as T;
|
||||
}
|
||||
|
||||
return JSON.parse(json) as T;
|
||||
}
|
||||
}
|
||||
|
||||
interface ConfigResponse {
|
||||
name?: string,
|
||||
summary?: string,
|
||||
logo?: string,
|
||||
tags?: Array<string>,
|
||||
version?: string
|
||||
name?: string;
|
||||
summary?: string;
|
||||
logo?: string;
|
||||
tags?: Array<string>;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
interface StatusResponse {
|
||||
lastConnectTime?: string
|
||||
lastDisconnectTime?: string
|
||||
online: boolean
|
||||
overallMaxViewerCount: number
|
||||
sessionMaxViewerCount: number
|
||||
viewerCount: number
|
||||
}
|
||||
lastConnectTime?: string;
|
||||
lastDisconnectTime?: string;
|
||||
online: boolean;
|
||||
overallMaxViewerCount: number;
|
||||
sessionMaxViewerCount: number;
|
||||
viewerCount: number;
|
||||
}
|
||||
|
@ -7,6 +7,10 @@ export interface Relays {
|
||||
[key: string]: RelaySettings;
|
||||
}
|
||||
|
||||
export type Tag = string[];
|
||||
|
||||
export type Tags = Tag[];
|
||||
|
||||
export type EmojiTag = ["emoji", string, string];
|
||||
|
||||
export interface Emoji {
|
||||
@ -23,8 +27,8 @@ export interface EmojiPack {
|
||||
|
||||
export interface Badge {
|
||||
name: string;
|
||||
thumb: string;
|
||||
image: string;
|
||||
thumb?: string;
|
||||
image?: string;
|
||||
awardees: Set<string>;
|
||||
accepted: Set<string>;
|
||||
}
|
||||
|
11
src/utils.ts
11
src/utils.ts
@ -1,6 +1,7 @@
|
||||
import { NostrEvent, NostrPrefix, encodeTLV } from "@snort/system";
|
||||
import * as utils from "@noble/curves/abstract/utils";
|
||||
import { bech32 } from "@scure/base";
|
||||
import type { Tag, Tags } from "types";
|
||||
|
||||
export function toAddress(e: NostrEvent): string {
|
||||
if (e.kind && e.kind >= 30000 && e.kind <= 40000) {
|
||||
@ -16,7 +17,7 @@ export function toAddress(e: NostrEvent): string {
|
||||
return e.id;
|
||||
}
|
||||
|
||||
export function toTag(e: NostrEvent): string[] {
|
||||
export function toTag(e: NostrEvent): Tag {
|
||||
if (e.kind && e.kind >= 30000 && e.kind <= 40000) {
|
||||
const dTag = findTag(e, "d");
|
||||
|
||||
@ -105,6 +106,10 @@ export async function openFile(): Promise<File | undefined> {
|
||||
});
|
||||
}
|
||||
|
||||
export function getTagValues(tags: Array<string[]>, tag: string) {
|
||||
return tags.filter((t) => t.at(0) === tag).map((t) => t.at(1));
|
||||
export function getTagValues(tags: Tags, tag: string): Array<string> {
|
||||
return tags
|
||||
.filter((t) => t.at(0) === tag)
|
||||
.map((t) => t.at(1))
|
||||
.filter((t) => t)
|
||||
.map((t) => t as string);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user