34 Commits

Author SHA1 Message Date
ba62f0ef74 feat: link to nests from live streams header 2025-05-07 14:15:07 +01:00
fb844a5969 feat: nests chat / speak 2025-05-07 13:52:03 +01:00
79e2d33e06 feat: filter follow sets 2025-05-07 10:50:56 +01:00
4e5feede23 refactor: replace nip96 with blossom
feat: blossom fallback image loader
2025-05-06 17:24:41 +01:00
d4115e9073 fix: yarn lock 2025-05-06 15:15:12 +01:00
d22ce56ebc feat: follow sets page 2025-05-06 15:09:43 +01:00
91c912a886 feat: media root tab 2025-05-06 13:54:18 +01:00
07474a836e chore: remove lnc / cashu wallets 2025-05-06 12:50:04 +01:00
75324e4862 chore: fix build 2025-05-06 12:46:07 +01:00
948337228e fix: add ws:// relay
closes https://github.com/v0l/snort/issues/600
2025-05-06 12:38:57 +01:00
e4446962ac feat: improve messages:
1. WoT filter
2. React to read status
2025-05-06 12:34:42 +01:00
d442166846 chore: remove nip28 support 2025-05-06 11:35:08 +01:00
fe4e17227e fix: follow by tab 2025-04-30 18:51:17 +01:00
d30aca46e8 chore: lang 2025-04-30 12:50:48 +01:00
18f60681bd chore: update kind name list 2025-04-30 12:44:36 +01:00
3d778f7ec7 fix: handle invalid client tag 2025-04-30 12:22:18 +01:00
992d7f19be feat: application handler note render 2025-04-30 12:15:18 +01:00
1820e7426d fix: test client tag format 2025-04-30 11:22:22 +01:00
e6ca368134 feat: client tags 2025-04-30 11:14:02 +01:00
f028de7c04 chore: cleanup login state 2025-03-12 11:26:46 +00:00
0584089d92 fix: about page 2025-03-12 10:09:45 +00:00
bc1d0512f8 chore: add summoner001 2025-03-12 09:52:09 +00:00
aaa4b0de97 feat: support rendering kind 20,21,22
feat: reply to non-text-note as kind 1111
2025-02-27 15:29:28 +00:00
6977f80652 chore: zapstore release 2025-02-13 15:18:50 +00:00
e3a8495c01 chore: upgrade wasm lib 2025-01-20 16:34:11 +00:00
ea07f91651 fix: import 2025-01-20 14:32:21 +00:00
fb600afabc Revert "chore: disable caches"
This reverts commit 1f0f45e3f9.
2025-01-20 14:27:48 +00:00
68790a4fbb fix: search box 2025-01-20 14:25:55 +00:00
723abea3d9 fix: lock file 2025-01-12 10:08:38 +00:00
d46b5a052b chore: change config 2025-01-12 10:07:02 +00:00
048fdf463b fix PRE and RE query/insert 2025-01-07 17:06:13 +07:00
f8f07a02bb chore: bump pkgs 2024-12-26 14:35:30 +00:00
e9fd593468 fix: note creator tagging 2024-12-21 14:06:06 +00:00
f425830678 fix: build 2024-12-21 13:56:27 +00:00
114 changed files with 2402 additions and 2386 deletions

11
nap.yaml Normal file
View File

@ -0,0 +1,11 @@
id: "social.snort.app"
name: "Snort"
description: ""
icon: "https://snort.social/nostrich_256.png"
images:
- "https://snort.social/nostrich_512.png"
repository: "https://github.com/v0l/snort"
license: "MIT"
tags:
- "social"
- "twitter"

View File

@ -12,10 +12,10 @@
"defaultZapPoolFee": 1,
"features": {
"analytics": true,
"subscriptions": true,
"deck": true,
"zapPool": true,
"communityLeaders": true,
"subscriptions": false,
"deck": false,
"zapPool": false,
"communityLeaders": false,
"nostrAddress": true,
"pushNotifications": true
},
@ -41,17 +41,26 @@
"eventLinkPrefix": "nevent",
"profileLinkPrefix": "nprofile",
"defaultRelays": {
"wss://relay.snort.social/": { "read": true, "write": true },
"wss://nostr.wine/": { "read": true, "write": false },
"wss://relay.damus.io/": { "read": true, "write": true },
"wss://nos.lol/": { "read": true, "write": true }
"wss://relay.snort.social/": {
"read": true,
"write": true
},
"wss://nostr.wine/": {
"read": true,
"write": false
},
"wss://relay.damus.io/": {
"read": true,
"write": true
},
"wss://nos.lol/": {
"read": true,
"write": true
}
},
"alby": {
"clientId": "pohiJjPhQR",
"clientSecret": "GAl1YKLA3FveK1gLBYok"
},
"chatChannels": [
{ "type": "telegram", "value": "https://t.me/irismessenger" },
{ "type": "nip28", "value": "23286a4602ada10cc10200553bff62a110e8dc0eacddf73277395a89ddf26a09" }
]
"chatChannels": []
}

View File

@ -44,10 +44,7 @@
"wss://relay.nostr.band/": { "read": true, "write": true },
"wss://relay.damus.io/": { "read": true, "write": true }
},
"chatChannels": [
{ "type": "telegram", "value": "https://t.me/irismessenger" },
{ "type": "nip28", "value": "23286a4602ada10cc10200553bff62a110e8dc0eacddf73277395a89ddf26a09" }
],
"chatChannels": [{ "type": "telegram", "value": "https://t.me/irismessenger" }],
"alby": {
"clientId": "5rYcHDrlDb",
"clientSecret": "QAI3QmgiaPH3BfTMzzFd"

View File

@ -101,7 +101,7 @@ declare const CONFIG: {
// public chat channels for site
chatChannels?: Array<{
type: "nip28" | "telegram";
type: "telegram";
value: string;
}>;
};

View File

@ -131,7 +131,7 @@
"vite": "^5.2.8",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-pwa": "^0.19.2",
"vite-plugin-version-mark": "^0.0.10",
"vite-plugin-version-mark": "^0.1.4",
"vitest": "^0.34.6"
}
}

View File

@ -1,6 +1,6 @@
import { IMeta } from "@snort/system";
import classNames from "classnames";
import React, { CSSProperties, useEffect, useMemo, useRef } from "react";
import React, { CSSProperties, useEffect, useMemo, useRef, useState } from "react";
import { useInView } from "react-intersection-observer";
import { ProxyImg } from "@/Components/ProxyImg";
@ -45,6 +45,8 @@ const ImageElement = ({ url, meta, onMediaClick, size }: ImageElementProps) => {
return style;
}, [imageRef?.current, meta]);
const [alternatives, setAlternatives] = useState<Array<string>>(meta?.fallback ?? []);
const [currentUrl, setCurrentUrl] = useState<string>(url);
return (
<div
className={classNames("flex items-center -mx-4 md:mx-0 my-2", {
@ -52,8 +54,8 @@ const ImageElement = ({ url, meta, onMediaClick, size }: ImageElementProps) => {
"cursor-pointer": onMediaClick,
})}>
<ProxyImg
key={url}
src={url}
key={currentUrl}
src={currentUrl}
size={size}
sha256={meta?.sha256}
onClick={onMediaClick}
@ -62,6 +64,14 @@ const ImageElement = ({ url, meta, onMediaClick, size }: ImageElementProps) => {
})}
style={style}
ref={imageRef}
onError={() => {
const next = alternatives.at(0);
if (next) {
console.warn("IMG FALLBACK", "Failed to load url, trying next: ", next);
setAlternatives(z => z.filter(y => y !== next));
setCurrentUrl(next);
}
}}
/>
</div>
);

View File

@ -26,7 +26,7 @@ export default function Mention({ link }: { link: NostrLink }) {
return (
<span className="highlight" onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
<ProfileLink pubkey={link.id} link={link} user={profile} onClick={e => e.stopPropagation()}>
<ProfileLink pubkey={link.id} user={profile} onClick={e => e.stopPropagation()}>
@<DisplayName user={profile} pubkey={link.id} />
</ProfileLink>
{isHovering && <ProfileCard pubkey={link.id} user={profile} show={true} />}

View File

@ -12,6 +12,8 @@ import usePreferences from "@/Hooks/usePreferences";
import { dedupe, findTag, getDisplayName, hexToBech32 } from "@/Utils";
import { useWallet } from "@/Wallet";
import { ProxyImg } from "../ProxyImg";
export default function PubkeyList({ ev, className }: { ev: NostrEvent; className?: string }) {
const wallet = useWallet();
const defaultZapAmount = usePreferences(s => s.defaultZapAmount);
@ -62,29 +64,33 @@ export default function PubkeyList({ ev, className }: { ev: NostrEvent; classNam
}
}
const picture = findTag(ev, "image");
return (
<FollowListBase
pubkeys={ids}
className={className}
title={findTag(ev, "title") ?? findTag(ev, "d")}
actions={
<>
<AsyncButton className="mr5 secondary" onClick={() => zapAll()}>
<FormattedMessage
defaultMessage="Zap all {n} sats"
id="IVbtTS"
values={{
n: <FormattedNumber value={defaultZapAmount * ids.length} />,
}}
/>
</AsyncButton>
</>
}
profilePreviewProps={{
options: {
about: true,
},
}}
/>
<>
{picture && <ProxyImg src={picture} className="br max-h-44 w-full object-cover mb-4" />}
<FollowListBase
pubkeys={ids}
className={className}
title={findTag(ev, "title") ?? findTag(ev, "d")}
actions={
<>
<AsyncButton className="mr5 secondary" onClick={() => zapAll()}>
<FormattedMessage
defaultMessage="Zap all {n} sats"
id="IVbtTS"
values={{
n: <FormattedNumber value={defaultZapAmount * ids.length} />,
}}
/>
</AsyncButton>
</>
}
profilePreviewProps={{
options: {
about: true,
},
}}
/>
</>
);
}

View File

@ -0,0 +1,39 @@
import { mapEventToProfile, TaggedNostrEvent } from "@snort/system";
import { FormattedMessage } from "react-intl";
import KindName from "../kind-name";
import Avatar from "../User/Avatar";
import DisplayName from "../User/DisplayName";
import ProfileImage from "../User/ProfileImage";
export function ApplicationHandler({ ev }: { ev: TaggedNostrEvent }) {
const profile = mapEventToProfile(ev);
const kinds = ev.tags
.filter(a => a[0] === "k")
.map(a => Number(a[1]))
.sort((a, b) => a - b);
return (
<div className="p flex gap-2 flex-col">
<div className="flex items-center gap-2 text-xl">
<Avatar user={profile} pubkey={""} size={120} />
<div className="flex flex-col gap-2">
<DisplayName user={profile} pubkey={""} />
<div className="text-sm flex items-center gap-2">
<div className="text-gray-light">
<FormattedMessage defaultMessage="Published by" />
</div>
<ProfileImage className="inline" pubkey={ev.pubkey} size={30} link="" />
</div>
</div>
</div>
<FormattedMessage defaultMessage="Supported Kinds:" />
<div className="flex flex-wrap">
{kinds.map(a => (
<div key={a} className="pill">
<KindName kind={a} />
</div>
))}
</div>
</div>
);
}

View File

@ -1,6 +1,16 @@
/* eslint-disable max-lines */
import { fetchNip05Pubkey, unixNow } from "@snort/shared";
import { EventBuilder, EventKind, NostrLink, NostrPrefix, TaggedNostrEvent, tryParseNostrLink } from "@snort/system";
import {
EventBuilder,
EventKind,
Nip94Tags,
nip94TagsToIMeta,
NostrLink,
NostrPrefix,
readNip94Tags,
TaggedNostrEvent,
tryParseNostrLink,
} from "@snort/system";
import { useUserProfile } from "@snort/system-react";
import { ZapTarget } from "@snort/wallet";
import { Menu, MenuItem } from "@szhsin/react-menu";
@ -28,7 +38,7 @@ import usePreferences from "@/Hooks/usePreferences";
import useRelays from "@/Hooks/useRelays";
import { useNoteCreator } from "@/State/NoteCreator";
import { openFile, trackEvent } from "@/Utils";
import useFileUpload, { addExtensionToNip94Url, nip94TagsToIMeta, readNip94Tags } from "@/Utils/Upload";
import useFileUpload from "@/Utils/Upload";
import { GetPowWorker } from "@/Utils/wasm";
import { OkResponseRow } from "./OkResponseRow";
@ -139,7 +149,6 @@ export function NoteCreator() {
extraTags ??= [];
extraTags.push(["content-warning", note.sensitive]);
}
const kind = note.pollOptions ? EventKind.Polls : EventKind.TextNote;
if (note.pollOptions) {
extraTags ??= [];
extraTags.push(...note.pollOptions.map((a, i) => ["poll_option", i.toString(), a]));
@ -149,15 +158,32 @@ export function NoteCreator() {
extraTags.push(...note.hashTags.map(a => ["t", a.toLowerCase()]));
}
for (const ex of note.otherEvents ?? []) {
const meta = readNip94Tags(ex.tags);
if (!meta.url) continue;
if (!note.note.endsWith("\n")) {
note.note += "\n";
}
note.note += addExtensionToNip94Url(meta);
// attach 1 link and use other duplicates as fallback urls
for (const [, v] of Object.entries(note.attachments ?? {})) {
const at = v[0];
note.note += note.note.length > 0 ? `\n${at.url}` : at.url;
console.debug(at);
const n94 =
(at.nip94?.length ?? 0) > 0
? readNip94Tags(at.nip94!)
: ({
url: at.url,
hash: at.sha256,
size: at.size,
mimeType: at.type,
} as Nip94Tags);
// attach fallbacks
n94.fallback ??= [];
n94.fallback.push(
...v
.slice(1)
.filter(a => a.url)
.map(a => a.url!),
);
extraTags ??= [];
extraTags.push(nip94TagsToIMeta(meta));
extraTags.push(nip94TagsToIMeta(n94));
}
// add quote repost
@ -179,7 +205,9 @@ export function NoteCreator() {
const hk = (eb: EventBuilder) => {
extraTags?.forEach(t => eb.tag(t));
note.extraTags?.forEach(t => eb.tag(t));
eb.kind(kind);
if (note.pollOptions) {
eb.kind(EventKind.Polls);
}
return eb;
};
const ev = note.replyTo
@ -261,20 +289,12 @@ export function NoteCreator() {
async function uploadFile(file: File) {
try {
if (file && uploader) {
const rx = await uploader.upload(file, file.name);
const rx = await uploader.upload(file);
note.update(v => {
if (rx.header) {
v.otherEvents ??= [];
v.otherEvents.push(rx.header);
} else if (rx.url) {
v.note = `${v.note ? `${v.note}\n` : ""}${rx.url}`;
if (rx.metadata) {
v.extraTags ??= [];
const imeta = nip94TagsToIMeta(rx.metadata);
v.extraTags.push(imeta);
}
} else if (rx?.error) {
v.error = rx.error;
if (rx.url) {
v.attachments ??= {};
v.attachments[rx.sha256] ??= [];
v.attachments[rx.sha256].push(rx);
}
});
}
@ -666,31 +686,25 @@ export function NoteCreator() {
</div>
</div>
)}
{(note.otherEvents?.length ?? 0) > 0 && !note.preview && (
{Object.entries(note.attachments ?? {}).length > 0 && !note.preview && (
<div className="flex gap-2 flex-wrap">
{note.otherEvents
?.map(a => ({
event: a,
tags: readNip94Tags(a.tags),
}))
.filter(a => a.tags.url)
.map(a => (
<div key={a.tags.url} className="relative">
<img
className="object-cover w-[80px] h-[80px] !mt-0 rounded-lg"
src={addExtensionToNip94Url(a.tags)}
/>
<Icon
name="x"
className="absolute -top-[0.25rem] -right-[0.25rem] bg-gray rounded-full cursor-pointer"
onClick={() =>
note.update(
n => (n.otherEvents = n.otherEvents?.filter(b => readNip94Tags(b.tags).url !== a.tags.url)),
)
}
/>
</div>
))}
{Object.entries(note.attachments ?? {}).map(([k, v]) => (
<div key={k} className="relative">
<img className="object-cover w-[80px] h-[80px] !mt-0 rounded-lg" src={v[0].url} />
<Icon
name="x"
className="absolute -top-[0.25rem] -right-[0.25rem] bg-gray rounded-full cursor-pointer"
onClick={() =>
note.update(n => {
if (n.attachments?.[k]) {
delete n.attachments[k];
}
return n;
})
}
/>
</div>
))}
</div>
)}
{noteCreatorFooter()}
@ -722,8 +736,11 @@ export function NoteCreator() {
<MediaServerFileList
onPicked={files => {
note.update(n => {
n.otherEvents ??= [];
n.otherEvents?.push(...files);
for (const x of files) {
n.attachments ??= {};
n.attachments[x.sha256] ??= [];
n.attachments[x.sha256].push(x);
}
n.filePicker = "hidden";
});
}}

View File

@ -1,16 +1,3 @@
.note > .header .reply {
font-size: 13px;
color: var(--font-secondary-color);
}
.note > .header .reply a {
color: var(--highlight);
}
.note > .header .reply a:hover {
text-decoration-color: var(--highlight);
}
.note .header .info {
font-size: var(--font-size);
margin-left: 4px;
@ -57,8 +44,7 @@
margin-top: 16px;
}
.note > .header img:hover,
.note > .header .name > .reply:hover {
.note > .header img:hover {
cursor: pointer;
}

View File

@ -1,20 +1,20 @@
import "./EventComponent.css";
import { EventKind, NostrEvent, TaggedNostrEvent } from "@snort/system";
import { EventKind, NostrEvent, parseIMeta, TaggedNostrEvent } from "@snort/system";
import { memo, ReactNode } from "react";
import PubkeyList from "@/Components/Embed/PubkeyList";
import ZapstrEmbed from "@/Components/Embed/ZapstrEmbed";
import ErrorBoundary from "@/Components/ErrorBoundary";
import { ApplicationHandler } from "@/Components/Event/Application";
import { LongFormText } from "@/Components/Event/LongFormText";
import { NostrFileElement } from "@/Components/Event/NostrFileHeader";
import { Note } from "@/Components/Event/Note/Note";
import NoteReaction from "@/Components/Event/NoteReaction";
import { ZapGoal } from "@/Components/Event/ZapGoal";
import { LiveEvent } from "@/Components/LiveStream/LiveEvent";
import ProfilePreview from "@/Components/User/ProfilePreview";
import { LongFormText } from "./LongFormText";
import { Note } from "./Note/Note";
export interface NotePropsOptions {
isRoot?: boolean;
showHeader?: boolean;
@ -62,6 +62,7 @@ export default memo(function EventComponent(props: NoteProps) {
case EventKind.ZapstrTrack:
content = <ZapstrEmbed ev={ev} />;
break;
case EventKind.StarterPackSet:
case EventKind.FollowSet:
case EventKind.ContactList:
content = <PubkeyList ev={ev} className={className} />;
@ -75,6 +76,25 @@ export default memo(function EventComponent(props: NoteProps) {
case 9041: // Assuming 9041 is a valid EventKind
content = <ZapGoal ev={ev} />;
break;
case EventKind.ApplicationHandler: {
content = <ApplicationHandler ev={ev} />;
break;
}
case EventKind.Photo:
case EventKind.Video:
case EventKind.ShortVideo: {
// append media to note as if kind1 post
const media = parseIMeta(ev.tags);
// Sometimes we cann call this twice so check the URL's are not already
// in the content
const urls = Object.entries(media ?? {}).map(([k]) => k);
if (!urls.every(u => ev.content.includes(u))) {
const newContent = ev.content + " " + urls.join("\n");
props.data.content = newContent;
}
content = <Note {...props} />;
break;
}
case EventKind.LongFormTextNote:
content = (
<LongFormText

View File

@ -0,0 +1,21 @@
import { NostrLink, TaggedNostrEvent } from "@snort/system";
import { FormattedMessage } from "react-intl";
import { Link } from "react-router-dom";
export function ClientTag({ ev }: { ev: TaggedNostrEvent }) {
const tag = ev.tags.find(a => a[0] === "client");
if (!tag) return;
const link = tag[2] && tag[2].includes(":") ? NostrLink.tryFromTag(["a", tag[2]]) : undefined;
return (
<span className="text-xs text-gray-light">
{" "}
<FormattedMessage
defaultMessage="via {client}"
description="via {client name} tag"
values={{
client: link ? <Link to={`/${link.encode()}`}>{tag[1]}</Link> : tag[1],
}}
/>
</span>
);
}

View File

@ -32,14 +32,21 @@ const defaultOptions = {
showContextMenu: true,
};
const canRenderAsTextNote = [EventKind.TextNote, EventKind.Polls];
const canRenderAsTextNote = [
EventKind.TextNote,
EventKind.Polls,
EventKind.Photo,
EventKind.Video,
EventKind.ShortVideo,
EventKind.Comment,
];
const translationCache = new LRUCache<string, NoteTranslation>({ maxSize: 300 });
export function Note(props: NoteProps) {
const { data: ev, highlight, options: opt, ignoreModeration = false, className, waitUntilInView } = props;
const baseClassName = classNames("note min-h-[110px] flex flex-col gap-4 card", className ?? "");
const { isEventMuted } = useModeration();
const { ref, inView } = useInView({ triggerOnce: true, rootMargin: "2000px" });
const { ref, inView } = useInView({ triggerOnce: true });
const { ref: setSeenAtRef, inView: setSeenAtInView } = useInView({ rootMargin: "0px", threshold: 1 });
const [showTranslation, setShowTranslation] = useState(true);
const [translated, setTranslated] = useState<NoteTranslation | null>(translationCache.get(ev.id));

View File

@ -9,11 +9,13 @@ import DisplayName from "@/Components/User/DisplayName";
import { ProfileLink } from "@/Components/User/ProfileLink";
import { hexToBech32 } from "@/Utils";
import { ClientTag } from "./ClientTag";
export default function ReplyTag({ ev }: { ev: TaggedNostrEvent }) {
const { formatMessage } = useIntl();
const thread = EventExt.extractThread(ev);
if (thread === undefined) {
return undefined;
return <ClientTag ev={ev} />;
}
const maxMentions = 2;
@ -33,7 +35,7 @@ export default function ReplyTag({ ev }: { ev: TaggedNostrEvent }) {
name: u?.name ?? shortNpub,
link: (
<ProfileLink pubkey={pk} user={u}>
<DisplayName pubkey={pk} user={u} />{" "}
<DisplayName pubkey={pk} user={u} className="text-highlight" />
</ProfileLink>
),
});
@ -53,7 +55,7 @@ export default function ReplyTag({ ev }: { ev: TaggedNostrEvent }) {
const others = mentions.length > maxMentions ? formatMessage(messages.Others, { n: othersLength }) : "";
const link = replyLink?.encode(CONFIG.eventLinkPrefix);
return (
<div className="reply">
<small className="text-xs">
re:&nbsp;
{(mentions?.length ?? 0) > 0 ? (
<>
@ -62,6 +64,7 @@ export default function ReplyTag({ ev }: { ev: TaggedNostrEvent }) {
) : (
replyLink && <Link to={`/${link}`}>{link?.substring(0, 12)}</Link>
)}
</div>
<ClientTag ev={ev} />
</small>
);
}

View File

@ -61,17 +61,6 @@ export function rootTabItems(base: string, pubKey: string | undefined, tags: Arr
</>
),
},
{
tab: "suggested",
path: `${base}/suggested`,
show: Boolean(pubKey),
element: (
<>
<Icon name="thumbs-up" />
<FormattedMessage defaultMessage="Suggested Follows" />
</>
),
},
{
tab: "trending/hashtags",
path: `${base}/trending/hashtags`,
@ -94,6 +83,28 @@ export function rootTabItems(base: string, pubKey: string | undefined, tags: Arr
</>
),
},
{
tab: "media",
path: `${base}/media`,
show: true,
element: (
<>
<Icon name="camera-plus" />
<FormattedMessage defaultMessage="Media" />
</>
),
},
{
tab: "follow-sets",
path: `${base}/follow-sets`,
show: true,
element: (
<>
<Icon name="thumbs-up" />
<FormattedMessage defaultMessage="Follow Sets" />
</>
),
},
] as Array<{
tab: RootTabRoutePath;
path: string;

View File

@ -15,12 +15,16 @@ import { AutoLoadMore } from "../Event/LoadMore";
import TimelineChunk from "./TimelineChunk";
export interface TimelineFollowsProps {
id?: string;
postsOnly: boolean;
liveStreams?: boolean;
noteFilter?: (ev: NostrEvent) => boolean;
noteRenderer?: (ev: NostrEvent) => ReactNode;
noteOnClick?: (ev: NostrEvent) => void;
displayAs?: DisplayAs;
kinds?: Array<EventKind>;
showDisplayAsSelector?: boolean;
firstChunkSize?: number;
windowSize?: number;
}
/**
@ -38,12 +42,15 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
const { isFollowing, followList } = useFollowsControls();
const { chunks, showMore } = useTimelineChunks({
now: openedAt,
firstChunkSize: Hour * 2,
window: props.windowSize,
firstChunkSize: props.firstChunkSize ?? Hour * 2,
});
const builder = useCallback(
(rb: RequestBuilder) => {
rb.withFilter().authors(followList).kinds([EventKind.TextNote, EventKind.Repost, EventKind.Polls]);
rb.withFilter()
.authors(followList)
.kinds(props.kinds ?? [EventKind.TextNote, EventKind.Repost, EventKind.Polls]);
},
[followList],
);
@ -58,11 +65,13 @@ const TimelineFollows = (props: TimelineFollowsProps) => {
return (
<>
<DisplayAsSelector activeSelection={displayAs} onSelect={(displayAs: DisplayAs) => setDisplayAs(displayAs)} />
{(props.showDisplayAsSelector ?? true) && (
<DisplayAsSelector activeSelection={displayAs} onSelect={(displayAs: DisplayAs) => setDisplayAs(displayAs)} />
)}
{chunks.map(c => (
<TimelineChunk
key={c.until}
id="follows"
id={`follows${props.id ? `:${props.id}` : ""}`}
chunk={c}
builder={builder}
noteFilter={filterEvents}

View File

@ -463,6 +463,14 @@
<path d="M14 3C14 2.44772 14.4477 2 15 2H21C21.5523 2 22 2.44772 22 3V9C22 9.55229 21.5523 10 21 10C20.4477 10 20 9.55229 20 9V5.41421L14.7071 10.7071C14.3166 11.0976 13.6834 11.0976 13.2929 10.7071C12.9024 10.3166 12.9024 9.68342 13.2929 9.29289L18.5858 4H15C14.4477 4 14 3.55228 14 3Z" fill="currentColor" />
<path d="M5.41421 20L10.7071 14.7071C11.0976 14.3166 11.0976 13.6834 10.7071 13.2929C10.3166 12.9024 9.68342 12.9024 9.29289 13.2929L4 18.5858L4 15C4 14.4477 3.55229 14 3 14C2.44772 14 2 14.4477 2 15V21C2 21.5523 2.44772 22 3 22H9C9.55228 22 10 21.5523 10 21C10 20.4477 9.55228 20 9 20H5.41421Z" fill="currentColor" />
</symbol>
<symbol id="mic" viewBox="0 0 20 20" fill="none">
<path d="M10 13.4375C11.0771 13.4363 12.1097 13.0078 12.8713 12.2463C13.6328 11.4847 14.0613 10.4521 14.0625 9.375V5C14.0625 3.92256 13.6345 2.88925 12.8726 2.12738C12.1108 1.36551 11.0774 0.9375 10 0.9375C8.92256 0.9375 7.88925 1.36551 7.12738 2.12738C6.36551 2.88925 5.9375 3.92256 5.9375 5V9.375C5.93874 10.4521 6.36715 11.4847 7.12875 12.2463C7.89035 13.0078 8.92294 13.4363 10 13.4375ZM7.8125 5C7.8125 4.41984 8.04297 3.86344 8.4532 3.4532C8.86344 3.04297 9.41984 2.8125 10 2.8125C10.5802 2.8125 11.1366 3.04297 11.5468 3.4532C11.957 3.86344 12.1875 4.41984 12.1875 5V9.375C12.1875 9.95516 11.957 10.5116 11.5468 10.9218C11.1366 11.332 10.5802 11.5625 10 11.5625C9.41984 11.5625 8.86344 11.332 8.4532 10.9218C8.04297 10.5116 7.8125 9.95516 7.8125 9.375V5ZM10.9375 16.5016V18.125C10.9375 18.3736 10.8387 18.6121 10.6629 18.7879C10.4871 18.9637 10.2486 19.0625 10 19.0625C9.75136 19.0625 9.5129 18.9637 9.33709 18.7879C9.16127 18.6121 9.0625 18.3736 9.0625 18.125V16.5016C7.3344 16.2719 5.74838 15.4229 4.59892 14.1122C3.44947 12.8016 2.81471 11.1183 2.8125 9.375C2.8125 9.12636 2.91127 8.8879 3.08709 8.71209C3.2629 8.53627 3.50136 8.4375 3.75 8.4375C3.99864 8.4375 4.2371 8.53627 4.41291 8.71209C4.58873 8.8879 4.6875 9.12636 4.6875 9.375C4.6875 10.784 5.24721 12.1352 6.2435 13.1315C7.23978 14.1278 8.59104 14.6875 10 14.6875C11.409 14.6875 12.7602 14.1278 13.7565 13.1315C14.7528 12.1352 15.3125 10.784 15.3125 9.375C15.3125 9.12636 15.4113 8.8879 15.5871 8.71209C15.7629 8.53627 16.0014 8.4375 16.25 8.4375C16.4986 8.4375 16.7371 8.53627 16.9129 8.71209C17.0887 8.8879 17.1875 9.12636 17.1875 9.375C17.1853 11.1183 16.5505 12.8016 15.4011 14.1122C14.2516 15.4229 12.6656 16.2719 10.9375 16.5016Z" fill="currentColor"/>
</symbol>
<symbol id="mic-off" viewBox="0 0 24 30" fill="none">
<path d="M3.10989 2.99125C2.97816 2.84276 2.81827 2.72189 2.63949 2.63565C2.46071 2.54942 2.26658 2.49952 2.06837 2.48885C1.87016 2.47819 1.67181 2.50697 1.48481 2.57353C1.2978 2.6401 1.12587 2.74311 0.978971 2.87661C0.832073 3.01011 0.713132 3.17143 0.629043 3.35124C0.544953 3.53104 0.497387 3.72575 0.489101 3.92407C0.480814 4.1224 0.511973 4.32039 0.580772 4.50658C0.64957 4.69278 0.754638 4.86346 0.889888 5.00875L5.49989 10.08V14C5.49894 15.0718 5.76306 16.1273 6.26872 17.0723C6.77439 18.0174 7.50591 18.8227 8.39814 19.4166C9.29038 20.0105 10.3156 20.3746 11.3826 20.4764C12.4496 20.5782 13.5252 20.4145 14.5136 20L15.9211 21.5487C14.7105 22.1791 13.3648 22.5055 11.9999 22.5C9.74626 22.4977 7.5856 21.6014 5.99204 20.0078C4.39848 18.4143 3.5022 16.2536 3.49989 14C3.49989 13.6022 3.34185 13.2206 3.06055 12.9393C2.77924 12.658 2.39771 12.5 1.99989 12.5C1.60206 12.5 1.22053 12.658 0.939228 12.9393C0.657924 13.2206 0.499888 13.6022 0.499888 14C0.503423 16.7893 1.51905 19.4825 3.35817 21.5795C5.19729 23.6766 7.73494 25.035 10.4999 25.4025V28C10.4999 28.3978 10.6579 28.7794 10.9392 29.0607C11.2205 29.342 11.6021 29.5 11.9999 29.5C12.3977 29.5 12.7792 29.342 13.0605 29.0607C13.3419 28.7794 13.4999 28.3978 13.4999 28V25.4037C15.0922 25.2004 16.6228 24.66 17.9899 23.8187L20.8899 27.0087C21.1588 27.2977 21.5308 27.4689 21.9252 27.4854C22.3195 27.5019 22.7045 27.3622 22.9966 27.0968C23.2887 26.8313 23.4644 26.4614 23.4856 26.0673C23.5068 25.6731 23.3718 25.2865 23.1099 24.9912L3.10989 2.99125ZM11.9999 17.5C11.0716 17.5 10.1814 17.1313 9.52501 16.4749C8.86864 15.8185 8.49989 14.9283 8.49989 14V13.375L12.2374 17.4862C12.1586 17.5 12.0799 17.5 11.9999 17.5ZM7.33364 4.65875C7.18979 4.52338 7.0741 4.36094 6.9932 4.18075C6.9123 4.00055 6.86778 3.80616 6.86221 3.60871C6.85663 3.41127 6.89011 3.21467 6.96071 3.0302C7.03132 2.84572 7.13766 2.67701 7.27364 2.53375C8.16735 1.58718 9.3247 0.930779 10.5958 0.649565C11.8668 0.368351 13.1931 0.475282 14.4027 0.956509C15.6123 1.43774 16.6495 2.27108 17.38 3.34861C18.1105 4.42614 18.5007 5.69819 18.4999 7V13.0675C18.4999 13.4653 18.3419 13.8469 18.0605 14.1282C17.7792 14.4095 17.3977 14.5675 16.9999 14.5675C16.6021 14.5675 16.2205 14.4095 15.9392 14.1282C15.6579 13.8469 15.4999 13.4653 15.4999 13.0675V7C15.4998 6.29921 15.2894 5.61457 14.8959 5.03471C14.5024 4.45486 13.9438 4.00649 13.2926 3.74766C12.6413 3.48884 11.9274 3.43147 11.2432 3.58298C10.5589 3.7345 9.93597 4.08792 9.45489 4.5975C9.31967 4.74087 9.15752 4.85619 8.97771 4.93687C8.7979 5.01755 8.60395 5.062 8.40696 5.06769C8.20996 5.07337 8.01377 5.04019 7.82961 4.97002C7.64544 4.89985 7.47691 4.79408 7.33364 4.65875ZM19.8749 17.1975C20.2886 16.1823 20.5009 15.0963 20.4999 14C20.4999 13.6022 20.6579 13.2206 20.9392 12.9393C21.2205 12.658 21.6021 12.5 21.9999 12.5C22.3977 12.5 22.7792 12.658 23.0605 12.9393C23.3419 13.2206 23.4999 13.6022 23.4999 14C23.5023 15.4831 23.2161 16.9524 22.6574 18.3262C22.4983 18.6801 22.2082 18.9585 21.8482 19.1031C21.4881 19.2476 21.0861 19.247 20.7264 19.1014C20.3668 18.9558 20.0776 18.6766 19.9195 18.3222C19.7614 17.9679 19.7468 17.5661 19.8786 17.2012L19.8749 17.1975Z" fill="currentColor"/>
</symbol>
<symbol id="hand" viewBox="0 0 24 30" fill="none">
<path d="M19.5 9.49996C19.1627 9.4993 18.8267 9.5413 18.5 9.62496V6.49996C18.5006 5.85364 18.3445 5.2168 18.0452 4.64397C17.7458 4.07114 17.3121 3.57937 16.7812 3.21077C16.2503 2.84216 15.638 2.6077 14.9967 2.52745C14.3553 2.4472 13.7041 2.52355 13.0988 2.74996C12.7035 1.9353 12.0435 1.27886 11.2267 0.887983C10.4099 0.497104 9.48469 0.394917 8.60229 0.598133C7.7199 0.80135 6.93256 1.29794 6.36903 2.00671C5.8055 2.71547 5.49913 3.59447 5.5 4.49996V4.62496C4.90875 4.4723 4.2904 4.45704 3.69233 4.58034C3.09427 4.70363 2.53237 4.96222 2.04971 5.33629C1.56705 5.71035 1.17644 6.18995 0.907819 6.73833C0.639196 7.28672 0.499693 7.88932 0.500001 8.49996V18C0.500001 21.05 1.7116 23.975 3.86827 26.1317C6.02494 28.2884 8.95001 29.5 12 29.5C15.05 29.5 17.9751 28.2884 20.1317 26.1317C22.2884 23.975 23.5 21.05 23.5 18V13.5C23.5 12.4391 23.0786 11.4217 22.3284 10.6715C21.5783 9.92139 20.5609 9.49996 19.5 9.49996ZM20.5 18C20.5 20.2543 19.6045 22.4163 18.0104 24.0104C16.4163 25.6044 14.2543 26.5 12 26.5C9.74566 26.5 7.58365 25.6044 5.98959 24.0104C4.39553 22.4163 3.5 20.2543 3.5 18V8.49996C3.5 8.23475 3.60536 7.98039 3.79289 7.79286C3.98043 7.60532 4.23478 7.49996 4.5 7.49996C4.76522 7.49996 5.01957 7.60532 5.20711 7.79286C5.39464 7.98039 5.5 8.23475 5.5 8.49996V14C5.5 14.3978 5.65804 14.7793 5.93934 15.0606C6.22064 15.3419 6.60218 15.5 7 15.5C7.39783 15.5 7.77936 15.3419 8.06066 15.0606C8.34197 14.7793 8.5 14.3978 8.5 14V4.49996C8.5 4.23475 8.60536 3.98039 8.79289 3.79286C8.98043 3.60532 9.23478 3.49996 9.5 3.49996C9.76522 3.49996 10.0196 3.60532 10.2071 3.79286C10.3946 3.98039 10.5 4.23475 10.5 4.49996V13C10.5 13.3978 10.658 13.7793 10.9393 14.0606C11.2206 14.3419 11.6022 14.5 12 14.5C12.3978 14.5 12.7794 14.3419 13.0607 14.0606C13.342 13.7793 13.5 13.3978 13.5 13V6.49996C13.5 6.23475 13.6054 5.98039 13.7929 5.79286C13.9804 5.60532 14.2348 5.49996 14.5 5.49996C14.7652 5.49996 15.0196 5.60532 15.2071 5.79286C15.3946 5.98039 15.5 6.23475 15.5 6.49996V14.675C14.0773 15.0144 12.8103 15.823 11.9033 16.9705C10.9962 18.1179 10.5019 19.5373 10.5 21C10.5 21.3978 10.658 21.7793 10.9393 22.0606C11.2206 22.3419 11.6022 22.5 12 22.5C12.3978 22.5 12.7794 22.3419 13.0607 22.0606C13.342 21.7793 13.5 21.3978 13.5 21C13.5 20.0717 13.8687 19.1815 14.5251 18.5251C15.1815 17.8687 16.0717 17.5 17 17.5C17.3978 17.5 17.7794 17.3419 18.0607 17.0606C18.342 16.7793 18.5 16.3978 18.5 16V13.5C18.5 13.2347 18.6054 12.9804 18.7929 12.7929C18.9804 12.6053 19.2348 12.5 19.5 12.5C19.7652 12.5 20.0196 12.6053 20.2071 12.7929C20.3946 12.9804 20.5 13.2347 20.5 13.5V18Z" fill="currentColor"/>
</symbol>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 139 KiB

View File

@ -10,6 +10,7 @@ import useLiveStreams from "@/Hooks/useLiveStreams";
import { findTag } from "@/Utils";
import Avatar from "../User/Avatar";
import { NestsParticipants } from "./nests-participants";
export function LiveStreams() {
const streams = useLiveStreams();
@ -17,9 +18,18 @@ export function LiveStreams() {
return (
<div className="flex mx-2 gap-4 overflow-x-auto sm-hide-scrollbar">
{streams.map(v => (
<LiveStreamEvent ev={v} key={`${v.kind}:${v.pubkey}:${findTag(v, "d")}`} className="h-[80px]" />
))}
{streams.map(v => {
const k = `${v.kind}:${v.pubkey}:${findTag(v, "d")}`;
const isVideoStream = v.tags.some(a => a[0] === "streaming" && a[1].includes(".m3u8"));
if (isVideoStream) {
return <LiveStreamEvent ev={v} key={k} className="h-[80px]" />;
}
const isNests = v.tags.some(a => a[0] === "streaming" && a[1].startsWith("wss+livekit://"));
if (isNests) {
return <AudioRoom ev={v} key={k} className="h-[80px]" />;
}
})}
</div>
);
}
@ -66,3 +76,37 @@ export function LiveStreamEvent({ ev, className }: { ev: NostrEvent; className?:
</Link>
);
}
export function AudioRoom({ ev, className }: { ev: NostrEvent; className?: string }) {
const { proxy } = useImgProxy();
const title = findTag(ev, "title");
const image = findTag(ev, "image");
const link = NostrLink.fromEvent(ev).encode();
const imageProxy = proxy(image ?? "");
return (
<Link className={classNames("flex gap-2", className)} to={`/${link}`}>
<div className="relative aspect-video">
<div
className="absolute h-full w-full bg-center bg-cover bg-gray-ultradark rounded-lg flex items-end justify-center"
style={
{
backgroundImage: `url(${imageProxy})`,
} as CSSProperties
}>
<div className="flex items-center gap-1">
<NestsParticipants ev={ev} />
</div>
</div>
<div className="absolute left-0 top-0 w-full overflow-hidden">
<div
className="whitespace-nowrap px-1 text-ellipsis overflow-hidden text-xs font-medium bg-background opacity-70 text-center"
title={title}>
{title}
</div>
</div>
</div>
</Link>
);
}

View File

@ -0,0 +1,77 @@
import { useEffect, useRef } from "react";
export default function VuBar({
track,
full,
width,
height,
className,
}: {
track?: MediaStreamTrack;
full?: boolean;
height?: number;
width?: number;
className?: string;
}) {
const ref = useRef<HTMLCanvasElement | null>(null);
useEffect(() => {
if (ref && track) {
const audioContext = new AudioContext();
const trackClone = track;
const mediaStreamSource = audioContext.createMediaStreamSource(new MediaStream([trackClone]));
const analyser = audioContext.createAnalyser();
const minVU = -60;
const maxVU = 0;
const minFreq = 50;
const maxFreq = 7_000;
analyser.minDecibels = -100;
analyser.maxDecibels = 0;
analyser.smoothingTimeConstant = 0.4;
analyser.fftSize = 1024;
mediaStreamSource.connect(analyser);
const dataArray = new Uint8Array(analyser.frequencyBinCount);
const filteredAudio = (i: Uint8Array) => {
const binFreq = audioContext.sampleRate / 2 / dataArray.length;
return i.subarray(minFreq / binFreq, maxFreq / binFreq);
};
const peakVolume = (data: Uint8Array) => {
const max = data.reduce((acc, v) => (v > acc ? v : acc), 0);
return (maxVU - minVU) * (max / 256) + minVU;
};
const canvas = ref.current!;
const ctx = canvas.getContext("2d")!;
const t = setInterval(() => {
analyser.getByteFrequencyData(dataArray);
const data = filteredAudio(dataArray);
const vol = peakVolume(data);
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (full) {
ctx.fillStyle = "#00FF00";
for (let x = 0; x < data.length; x++) {
const bx = data[x];
const h = canvas.height / data.length;
ctx.fillRect(0, x * h, (bx / 255) * canvas.width, h);
}
}
const barLen = ((vol - minVU) / (maxVU - minVU)) * canvas.height;
ctx.fillStyle = "#fff";
ctx.fillRect(0, canvas.height - barLen, canvas.width, barLen);
}, 50);
return () => {
clearInterval(t);
audioContext.close();
};
}
}, [ref, track, full]);
return <canvas ref={ref} width={width ?? 200} height={height ?? 10} className={className}></canvas>;
}

View File

@ -1,25 +1,43 @@
import { LiveKitRoom as LiveKitRoomContext, RoomAudioRenderer, useParticipants } from "@livekit/components-react";
import { dedupe, unixNow } from "@snort/shared";
import { EventKind, NostrLink, RequestBuilder, TaggedNostrEvent } from "@snort/system";
/* eslint-disable max-lines */
import {
LiveKitRoom as LiveKitRoomContext,
RoomAudioRenderer,
useEnsureRoom,
useParticipantPermissions,
useParticipants,
} from "@livekit/components-react";
import { unixNow } from "@snort/shared";
import { EventKind, EventPublisher, NostrLink, RequestBuilder, SystemInterface, TaggedNostrEvent } from "@snort/system";
import { useRequestBuilder, useUserProfile } from "@snort/system-react";
import { LocalParticipant, RemoteParticipant } from "livekit-client";
import classNames from "classnames";
import { LocalParticipant, LocalTrackPublication, RemoteParticipant, RoomEvent, Track } from "livekit-client";
import { useEffect, useMemo, useState } from "react";
import { FormattedMessage } from "react-intl";
import { FormattedMessage, useIntl } from "react-intl";
import Text from "@/Components/Text/Text";
import useEventPublisher from "@/Hooks/useEventPublisher";
import { extractStreamInfo } from "@/Utils/stream";
import AsyncButton from "../Button/AsyncButton";
import IconButton from "../Button/IconButton";
import { ProxyImg } from "../ProxyImg";
import Avatar from "../User/Avatar";
import { AvatarGroup } from "../User/AvatarGroup";
import DisplayName from "../User/DisplayName";
import ProfileImage from "../User/ProfileImage";
import { NestsParticipants } from "./nests-participants";
import VuBar from "./VU";
enum RoomTab {
Participants,
Chat,
}
export default function LiveKitRoom({ ev, canJoin }: { ev: TaggedNostrEvent; canJoin?: boolean }) {
const { stream, service, id } = extractStreamInfo(ev);
const { publisher } = useEventPublisher();
const { publisher, system } = useEventPublisher();
const [join, setJoin] = useState(false);
const [token, setToken] = useState<string>();
const [tab, setTab] = useState(RoomTab.Participants);
async function getToken() {
if (!service || !publisher) return;
@ -43,6 +61,17 @@ export default function LiveKitRoom({ ev, canJoin }: { ev: TaggedNostrEvent; can
}
}
async function publishPresence(publisher: EventPublisher, system: SystemInterface) {
const e = await publisher.generic(eb => {
const aTag = NostrLink.fromEvent(ev).toEventTag();
return eb
.kind(10_312 as EventKind)
.tag(aTag!)
.tag(["expiration", (unixNow() + 60).toString()]);
});
await system.BroadcastEvent(e);
}
useEffect(() => {
if (join && !token) {
getToken()
@ -51,6 +80,18 @@ export default function LiveKitRoom({ ev, canJoin }: { ev: TaggedNostrEvent; can
}
}, [join]);
useEffect(() => {
if (token && publisher && system) {
publishPresence(publisher, system);
const t = setInterval(async () => {
if (token) {
publishPresence(publisher, system);
}
}, 60_000);
return () => clearInterval(t);
}
}, [token, publisher, system]);
if (!join) {
return (
<div className="p flex flex-col gap-2">
@ -65,8 +106,8 @@ export default function LiveKitRoom({ ev, canJoin }: { ev: TaggedNostrEvent; can
}
return (
<LiveKitRoomContext token={token} serverUrl={stream?.replace("wss+livekit://", "wss://")} connect={true}>
<RoomAudioRenderer volume={1} />
<ParticipantList ev={ev} />
<RoomAudioRenderer volume={1} muted={false} />
<RoomBody ev={ev} tab={tab} onSelectTab={setTab} />
</LiveKitRoomContext>
);
}
@ -75,57 +116,198 @@ function RoomHeader({ ev }: { ev: TaggedNostrEvent }) {
const { image, title } = extractStreamInfo(ev);
return (
<div className="relative rounded-xl h-[140px] w-full overflow-hidden">
{image ? <ProxyImg src={image} className="w-full" /> : <div className="absolute bg-gray-dark w-full h-full" />}
<div className="absolute left-4 top-4 w-full flex justify-between pr-4">
{image ? (
<ProxyImg src={image} className="w-full h-full object-cover object-center" />
) : (
<div className="absolute bg-gray-dark w-full h-full" />
)}
<div className="absolute left-4 top-4 w-full flex justify-between pr-8">
<div className="text-2xl">{title}</div>
<div>
<NostrParticipants ev={ev} />
<div className="flex gap-2 items-center">
<NestsParticipants ev={ev} />
</div>
</div>
</div>
);
}
function ParticipantList({ ev }: { ev: TaggedNostrEvent }) {
const participants = useParticipants();
function RoomBody({ ev, tab, onSelectTab }: { ev: TaggedNostrEvent; tab: RoomTab; onSelectTab: (t: RoomTab) => void }) {
const participants = useParticipants({
updateOnlyOn: [
RoomEvent.ParticipantConnected,
RoomEvent.ParticipantDisconnected,
RoomEvent.ParticipantPermissionsChanged,
RoomEvent.TrackMuted,
RoomEvent.TrackPublished,
RoomEvent.TrackUnmuted,
RoomEvent.TrackUnmuted,
],
});
return (
<div className="p">
<RoomHeader ev={ev} />
<h3>
<FormattedMessage defaultMessage="Participants" />
</h3>
<div className="grid grid-cols-4">
{participants.map(a => (
<LiveKitUser p={a} key={a.identity} />
))}
<MyControls />
<div className="flex text-center items-center text-xl font-medium mb-2">
<div
className={classNames("flex-1 py-2 cursor-pointer select-none border-b border-transparent", {
"!border-highlight": tab === RoomTab.Participants,
})}
onClick={() => onSelectTab(RoomTab.Participants)}>
<FormattedMessage defaultMessage="Participants" />
</div>
<div
className={classNames("flex-1 py-2 cursor-pointer select-none border-b border-transparent", {
"!border-highlight": tab === RoomTab.Chat,
})}
onClick={() => onSelectTab(RoomTab.Chat)}>
<FormattedMessage defaultMessage="Chat" />
</div>
</div>
{tab === RoomTab.Participants && (
<div className="grid grid-cols-4">
{participants.map(a => (
<LiveKitUser p={a} key={a.identity} />
))}
</div>
)}
{tab === RoomTab.Chat && (
<>
<RoomChat ev={ev} />
<WriteChatMessage ev={ev} />
</>
)}
</div>
);
}
function MyControls() {
const room = useEnsureRoom();
const p = room.localParticipant;
const permissions = useParticipantPermissions({
participant: p,
});
useEffect(() => {
if (permissions && p instanceof LocalParticipant) {
const handler = (lt: LocalTrackPublication) => {
lt.mute();
};
p.on("localTrackPublished", handler);
if (permissions.canPublish && p.audioTrackPublications.size === 0) {
p.setMicrophoneEnabled(true);
}
return () => {
p.off("localTrackPublished", handler);
};
}
}, [p, permissions]);
const isMuted = p.getTrackPublication(Track.Source.Microphone)?.isMuted ?? true;
return (
<div className="flex gap-2 items-center mt-2">
{p.permissions?.canPublish && (
<IconButton
icon={{ name: !isMuted ? "mic" : "mic-off", size: 20 }}
onClick={async () => {
if (isMuted) {
await p.setMicrophoneEnabled(true);
} else {
await p.setMicrophoneEnabled(false);
}
}}
/>
)}
{/*<IconButton icon={{ name: "hand", size: 20 }} />*/}
</div>
);
}
function RoomChat({ ev }: { ev: TaggedNostrEvent }) {
const link = NostrLink.fromEvent(ev);
const sub = useMemo(() => {
const sub = new RequestBuilder(`room-chat:${link.tagKey}`);
sub.withOptions({ leaveOpen: true, replaceable: true });
sub.withFilter().replyToLink([link]).kinds([EventKind.LiveEventChat]).limit(100);
return sub;
}, [link.tagKey]);
const chat = useRequestBuilder(sub);
return (
<div className="flex h-[calc(100dvh-370px)] overflow-x-hidden overflow-y-scroll">
<div className="flex flex-col gap-1 flex-col-reverse w-full">
{chat
.sort((a, b) => b.created_at - a.created_at)
.map(e => (
<ChatMessage key={e.id} ev={e} />
))}
</div>
</div>
);
}
function NostrParticipants({ ev }: { ev: TaggedNostrEvent }) {
const link = NostrLink.fromEvent(ev);
const sub = useMemo(() => {
const sub = new RequestBuilder(`livekit-participants:${link.tagKey}`);
sub
.withFilter()
.replyToLink([link])
.kinds([10_312 as EventKind])
.since(unixNow() - 600);
return sub;
}, [link.tagKey]);
function ChatMessage({ ev }: { ev: TaggedNostrEvent }) {
return (
<div className="grid grid-cols-[auto_1fr] items-center gap-2">
<ProfileImage
pubkey={ev.pubkey}
size={20}
showBadges={false}
showFollowDistance={false}
className="text-highlight"
/>
<Text id={ev.id} content={ev.content} creator={ev.pubkey} tags={ev.tags} disableMedia={true} />
</div>
);
}
const presense = useRequestBuilder(sub);
return <AvatarGroup ids={dedupe(presense.map(a => a.pubkey))} size={32} />;
function WriteChatMessage({ ev }: { ev: TaggedNostrEvent }) {
const link = NostrLink.fromEvent(ev);
const [chat, setChat] = useState("");
const { publisher, system } = useEventPublisher();
const { formatMessage } = useIntl();
async function sendMessage() {
if (!publisher || !system || chat.length < 2) return;
const eChat = await publisher.generic(eb => eb.kind(EventKind.LiveEventChat).tag(link.toEventTag()!).content(chat));
await system.BroadcastEvent(eChat);
setChat("");
}
return (
<div className="flex gap-2 mt-2">
<input
type="text"
value={chat}
placeholder={formatMessage({ defaultMessage: "Write message" })}
onChange={e => setChat(e.target.value)}
className="grow"
onKeyDown={e => {
if (e.key === "Enter") {
sendMessage();
}
}}
/>
<IconButton icon={{ name: "arrow-right" }} onClick={sendMessage} />
</div>
);
}
function LiveKitUser({ p }: { p: RemoteParticipant | LocalParticipant }) {
const pubkey = p.identity.startsWith("guest-") ? "anon" : p.identity;
const profile = useUserProfile(pubkey);
const mic = p.getTrackPublication(Track.Source.Microphone);
return (
<div className="flex flex-col gap-2 items-center text-center">
<Avatar pubkey={pubkey} className={p.isSpeaking ? "outline" : ""} user={profile} size={48} />
<DisplayName pubkey={pubkey} user={pubkey === "anon" ? { name: "Anon" } : profile} />
<div className="relative w-[45px] h-[45px] flex items-center justify-center rounded-full overflow-hidden">
{mic?.audioTrack?.mediaStreamTrack && (
<VuBar track={mic.audioTrack?.mediaStreamTrack} className="absolute h-full w-full" />
)}
<Avatar pubkey={pubkey} user={profile} size={40} className="absolute" />
</div>
<div>
<DisplayName pubkey={pubkey} user={pubkey === "anon" ? { name: "Anon" } : profile} />
{p.permissions?.canPublish && <div className="text-highlight">Speaker</div>}
</div>
</div>
);
}

View File

@ -0,0 +1,24 @@
import { dedupe, unixNow } from "@snort/shared";
import { EventKind, NostrLink, RequestBuilder, TaggedNostrEvent } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { useMemo } from "react";
import { AvatarGroup } from "../User/AvatarGroup";
export function NestsParticipants({ ev }: { ev: TaggedNostrEvent }) {
const link = NostrLink.fromEvent(ev);
const sub = useMemo(() => {
const sub = new RequestBuilder(`livekit-participants:${link.tagKey}`);
sub.withOptions({ leaveOpen: true });
sub
.withFilter()
.replyToLink([link])
.kinds([10_312 as EventKind])
.since(unixNow() - 600);
return sub;
}, [link.tagKey]);
const presense = useRequestBuilder(sub);
const filteredPresence = presense.filter(ev => ev.created_at > unixNow() - 600);
return <AvatarGroup ids={dedupe(filteredPresence.map(a => a.pubkey)).slice(0, 5)} size={32} />;
}

View File

@ -1,7 +1,7 @@
import "./SearchBox.css";
import { NostrLink, tryParseNostrLink } from "@snort/system";
import { ChangeEvent, useEffect, useRef, useState } from "react";
import { ChangeEvent, useEffect, useMemo, useRef, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useLocation, useNavigate } from "react-router-dom";
@ -25,7 +25,8 @@ export default function SearchBox() {
const [activeIndex, setActiveIndex] = useState<number>(-1);
const resultListRef = useRef<HTMLDivElement | null>(null);
const results = useProfileSearch(search);
const searchFn = useProfileSearch();
const results = useMemo(() => searchFn(search), [search, searchFn]);
useEffect(() => {
const handleGlobalKeyDown = (e: KeyboardEvent) => {

View File

@ -1,4 +1,4 @@
import { HexKey, NostrPrefix } from "@snort/system";
import { NostrPrefix } from "@snort/system";
import { useState } from "react";
import { FormattedMessage } from "react-intl";
@ -61,7 +61,7 @@ export default function SuggestedProfiles() {
</select>
</div>
<FollowListBase
pubkeys={userList as HexKey[]}
pubkeys={userList}
profilePreviewProps={{
options: {
about: true,

View File

@ -9,7 +9,7 @@ import TextareaAutosize from "react-textarea-autosize";
import Avatar from "@/Components/User/Avatar";
import Nip05 from "@/Components/User/Nip05";
import { FuzzySearchResult } from "@/Db/FuzzySearch";
import { userSearch } from "@/Hooks/useProfileSearch";
import useProfileSearch from "@/Hooks/useProfileSearch";
import searchEmoji from "@/Utils/emoji-search";
import messages from "../messages";
@ -58,6 +58,7 @@ interface TextareaProps {
const Textarea = (props: TextareaProps) => {
const { formatMessage } = useIntl();
const userSearch = useProfileSearch();
const userDataProvider = (token: string) => {
return userSearch(token).slice(0, 10);

View File

@ -1,4 +1,3 @@
import { NostrEvent } from "@snort/system";
import classNames from "classnames";
import { useEffect, useState } from "react";
import { FormattedMessage, FormattedNumber } from "react-intl";
@ -7,8 +6,7 @@ import useEventPublisher from "@/Hooks/useEventPublisher";
import useImgProxy from "@/Hooks/useImgProxy";
import useLogin from "@/Hooks/useLogin";
import { useMediaServerList } from "@/Hooks/useMediaServerList";
import { findTag } from "@/Utils";
import { Nip96Uploader } from "@/Utils/Upload/Nip96";
import { BlobDescriptor, Blossom } from "@/Utils/Upload/blossom";
import AsyncButton from "../Button/AsyncButton";
@ -16,12 +14,12 @@ export function MediaServerFileList({
onPicked,
cols,
}: {
onPicked: (files: Array<NostrEvent>) => void;
onPicked: (files: Array<BlobDescriptor>) => void;
cols?: number;
}) {
const { state } = useLogin(s => ({ v: s.state.version, state: s.state }));
const { publisher } = useEventPublisher();
const [fileList, setFilesList] = useState<Array<NostrEvent>>([]);
const [fileList, setFilesList] = useState<Array<BlobDescriptor>>([]);
const [pickedFiles, setPickedFiles] = useState<Array<string>>([]);
const servers = useMediaServerList();
@ -30,11 +28,9 @@ export function MediaServerFileList({
if (!publisher) return;
for (const s of servers.servers) {
try {
const sx = new Nip96Uploader(s, publisher);
const files = await sx.listFiles();
if (files?.files) {
res.push(...files.files);
}
const sx = new Blossom(s, publisher);
const files = await sx.list(state.pubkey);
res.push(...files);
} catch (e) {
console.error(e);
}
@ -42,14 +38,12 @@ export function MediaServerFileList({
setFilesList(res);
}
function toggleFile(ev: NostrEvent) {
const hash = findTag(ev, "x");
if (!hash) return;
function toggleFile(b: BlobDescriptor) {
setPickedFiles(a => {
if (a.includes(hash)) {
return a.filter(a => a != hash);
if (a.includes(b.sha256)) {
return a.filter(a => a != b.sha256);
} else {
return [...a, hash];
return [...a, b.sha256];
}
});
}
@ -58,6 +52,17 @@ export function MediaServerFileList({
listFiles().catch(console.error);
}, [servers.servers.length, state?.version]);
const finalFileList = fileList
.sort((a, b) => (b.uploaded ?? 0) - (a.uploaded ?? 0))
.reduce(
(acc, v) => {
acc[v.sha256] ??= [];
acc[v.sha256].push(v);
return acc;
},
{} as Record<string, Array<BlobDescriptor>>,
);
return (
<div>
<div
@ -65,33 +70,25 @@ export function MediaServerFileList({
"grid-cols-2": cols === 2 || cols === undefined,
"grid-cols-6": cols === 6,
})}>
{fileList.map(a => (
<Nip96File
key={a.id}
file={a}
onClick={() => toggleFile(a)}
checked={pickedFiles.includes(findTag(a, "x") ?? "")}
/>
{Object.entries(finalFileList).map(([k, v]) => (
<ServerFile key={k} file={v[0]} onClick={() => toggleFile(v[0])} checked={pickedFiles.includes(k)} />
))}
</div>
<AsyncButton
disabled={pickedFiles.length === 0}
onClick={() => onPicked(fileList.filter(a => pickedFiles.includes(findTag(a, "x") ?? "")))}>
onClick={() => onPicked(fileList.filter(a => pickedFiles.includes(a.sha256)))}>
<FormattedMessage defaultMessage="Select" />
</AsyncButton>
</div>
);
}
function Nip96File({ file, checked, onClick }: { file: NostrEvent; checked: boolean; onClick: () => void }) {
const mime = findTag(file, "m");
const url = findTag(file, "url");
const size = findTag(file, "size");
function ServerFile({ file, checked, onClick }: { file: BlobDescriptor; checked: boolean; onClick: () => void }) {
const { proxy } = useImgProxy();
function backgroundImage() {
if (url && (mime?.startsWith("image/") || mime?.startsWith("video/"))) {
return `url(${proxy(url, 512)})`;
if (file.url && (file.type?.startsWith("image/") || file.type?.startsWith("video/"))) {
return `url(${proxy(file.url, 512)})`;
}
}
@ -103,26 +100,25 @@ function Nip96File({ file, checked, onClick }: { file: NostrEvent; checked: bool
backgroundImage: backgroundImage(),
}}>
<div className="absolute w-full h-full opacity-0 bg-black hover:opacity-80 flex flex-col items-center justify-center gap-4">
<div>{file.content.length === 0 ? <FormattedMessage defaultMessage="Untitled" /> : file.content}</div>
<div>
{Number(size) > 1024 * 1024 && (
{file.size > 1024 * 1024 && (
<FormattedMessage
defaultMessage="{n}MiB"
values={{
n: <FormattedNumber value={Number(size) / 1024 / 1024} />,
n: <FormattedNumber value={file.size / 1024 / 1024} />,
}}
/>
)}
{Number(size) < 1024 * 1024 && (
{file.size < 1024 * 1024 && (
<FormattedMessage
defaultMessage="{n}KiB"
values={{
n: <FormattedNumber value={Number(size) / 1024} />,
n: <FormattedNumber value={file.size / 1024} />,
}}
/>
)}
</div>
<div>{new Date(file.created_at * 1000).toLocaleString()}</div>
<div>{file.uploaded && new Date(file.uploaded * 1000).toLocaleString()}</div>
</div>
<div
className={classNames("w-4 h-4 border border-2 rounded-full right-1 top-1 absolute", {

View File

@ -1,16 +1,16 @@
import { HexKey } from "@snort/system";
import classNames from "classnames";
import { ReactNode } from "react";
import { FormattedMessage } from "react-intl";
import ProfilePreview, { ProfilePreviewProps } from "@/Components/User/ProfilePreview";
import useFollowsControls from "@/Hooks/useFollowControls";
import useLogin from "@/Hooks/useLogin";
import useWoT from "@/Hooks/useWoT";
import AsyncButton from "../Button/AsyncButton";
import messages from "../messages";
export interface FollowListBaseProps {
pubkeys: HexKey[];
pubkeys: string[];
title?: ReactNode;
showFollowAll?: boolean;
className?: string;
@ -27,7 +27,8 @@ export default function FollowListBase({
profilePreviewProps,
}: FollowListBaseProps) {
const control = useFollowsControls();
const login = useLogin();
const readonly = useLogin(s => s.readonly);
const wot = useWoT();
async function followAll() {
await control.addFollow(pubkeys);
@ -37,15 +38,17 @@ export default function FollowListBase({
<div className="flex flex-col gap-2">
{(showFollowAll ?? true) && (
<div className="flex items-center">
<div className="grow font-bold">{title}</div>
<div className="grow font-bold text-xl">{title}</div>
{actions}
<AsyncButton className="transparent" type="button" onClick={() => followAll()} disabled={login.readonly}>
<FormattedMessage {...messages.FollowAll} />
<AsyncButton className="transparent" type="button" onClick={() => followAll()} disabled={readonly}>
<FormattedMessage defaultMessage="Follow All" />
</AsyncButton>
</div>
)}
<div className={className}>
{pubkeys?.slice(0, 20).map(a => <ProfilePreview pubkey={a} key={a} {...profilePreviewProps} />)}
<div className={classNames("flex flex-col gap-2", className)}>
{wot.sortPubkeys(pubkeys).map(a => (
<ProfilePreview pubkey={a} key={a} {...profilePreviewProps} />
))}
</div>
</div>
);

View File

@ -13,7 +13,7 @@ export default function FollowedBy({ pubkey }: { pubkey: HexKey }) {
const wot = useWoT();
const followDistance = wot.followDistance(pubkey);
const { followedByFriendsArray, totalFollowedByFriends } = useMemo(() => {
const followedByFriends = wot.followedByCount(pubkey);
const followedByFriends = wot.followedBy(pubkey);
return {
followedByFriendsArray: Array.from(followedByFriends).slice(0, MAX_FOLLOWED_BY_FRIENDS),
totalFollowedByFriends: followedByFriends.size,

View File

@ -118,7 +118,7 @@ export default function ProfileImage({
const classNamesOverInner = classNames(
"min-w-0",
{
"grid grid-cols-[min-content_auto] gap-3 items-center": showUsername,
"grid grid-cols-[min-content_auto] gap-2 items-center": showUsername,
},
className,
);

View File

@ -1,5 +1,10 @@
/* eslint-disable max-lines */
import { FormattedMessage } from "react-intl";
// Take the markdown kinds table and find-replace with following regex:
// FIND: ^\|\s+`([`\d\-]+)`\s+\| ([\w \-\(\)\/]+)[\s]*\|.*$
// REPLACE: case $1:\n\treturn <FormattedMessage defaultMessage="$2" />;
export default function KindName({ kind }: { kind: number }) {
switch (kind) {
case 0:
@ -21,21 +26,37 @@ export default function KindName({ kind }: { kind: number }) {
case 8:
return <FormattedMessage defaultMessage="Badge Award" />;
case 9:
return <FormattedMessage defaultMessage="Group Chat Message" />;
return <FormattedMessage defaultMessage="Chat Message" />;
case 10:
return <FormattedMessage defaultMessage="Group Chat Threaded Reply" />;
case 11:
return <FormattedMessage defaultMessage="Group Thread" />;
return <FormattedMessage defaultMessage="Thread" />;
case 12:
return <FormattedMessage defaultMessage="Group Thread Reply" />;
case 13:
return <FormattedMessage defaultMessage="Seal" />;
case 14:
return <FormattedMessage defaultMessage="Direct Message" />;
case 15:
return <FormattedMessage defaultMessage="File Message" />;
case 16:
return <FormattedMessage defaultMessage="Generic Repost" />;
case 17:
return <FormattedMessage defaultMessage="Reaction to a website" />;
case 20:
return <FormattedMessage defaultMessage="Picture" />;
case 21:
return <FormattedMessage defaultMessage="Video Event" />;
case 22:
return <FormattedMessage defaultMessage="Short-form Portrait Video Event" />;
case 30:
return <FormattedMessage defaultMessage="internal reference" />;
case 31:
return <FormattedMessage defaultMessage="external web reference" />;
case 32:
return <FormattedMessage defaultMessage="hardcopy reference" />;
case 33:
return <FormattedMessage defaultMessage="prompt reference" />;
case 40:
return <FormattedMessage defaultMessage="Channel Creation" />;
case 41:
@ -46,10 +67,14 @@ export default function KindName({ kind }: { kind: number }) {
return <FormattedMessage defaultMessage="Channel Hide Message" />;
case 44:
return <FormattedMessage defaultMessage="Channel Mute User" />;
case 62:
return <FormattedMessage defaultMessage="Request to Vanish" />;
case 64:
return <FormattedMessage defaultMessage="Chess (PGN)" />;
case 818:
return <FormattedMessage defaultMessage="Merge Requests" />;
case 1018:
return <FormattedMessage defaultMessage="Poll Response" />;
case 1021:
return <FormattedMessage defaultMessage="Bid" />;
case 1022:
@ -60,14 +85,20 @@ export default function KindName({ kind }: { kind: number }) {
return <FormattedMessage defaultMessage="Gift Wrap" />;
case 1063:
return <FormattedMessage defaultMessage="File Metadata" />;
case 1068:
return <FormattedMessage defaultMessage="Poll" />;
case 1111:
return <FormattedMessage defaultMessage="Comment" />;
case 1311:
return <FormattedMessage defaultMessage="Live Chat Message" />;
case 1337:
return <FormattedMessage defaultMessage="Code Snippet" />;
case 1617:
return <FormattedMessage defaultMessage="Patches" />;
case 1621:
return <FormattedMessage defaultMessage="Issues" />;
case 1622:
return <FormattedMessage defaultMessage="Replies" />;
return <FormattedMessage defaultMessage="Git Replies (deprecated)" />;
case 1971:
return <FormattedMessage defaultMessage="Problem Tracker" />;
case 1984:
@ -88,8 +119,16 @@ export default function KindName({ kind }: { kind: number }) {
return <FormattedMessage defaultMessage="Community Post Approval" />;
case 7000:
return <FormattedMessage defaultMessage="Job Feedback" />;
case 7374:
return <FormattedMessage defaultMessage="Reserved Cashu Wallet Tokens" />;
case 7375:
return <FormattedMessage defaultMessage="Cashu Wallet Tokens" />;
case 7376:
return <FormattedMessage defaultMessage="Cashu Wallet History" />;
case 9041:
return <FormattedMessage defaultMessage="Zap Goal" />;
case 9321:
return <FormattedMessage defaultMessage="Nutzap" />;
case 9467:
return <FormattedMessage defaultMessage="Tidal login" />;
case 9734:
@ -116,8 +155,12 @@ export default function KindName({ kind }: { kind: number }) {
return <FormattedMessage defaultMessage="Search relays list" />;
case 10009:
return <FormattedMessage defaultMessage="User groups" />;
case 10013:
return <FormattedMessage defaultMessage="Private event relay list" />;
case 10015:
return <FormattedMessage defaultMessage="Interests list" />;
case 10019:
return <FormattedMessage defaultMessage="Nutzap Mint Recommendation" />;
case 10030:
return <FormattedMessage defaultMessage="User emoji list" />;
case 10050:
@ -126,8 +169,12 @@ export default function KindName({ kind }: { kind: number }) {
return <FormattedMessage defaultMessage="User server list" />;
case 10096:
return <FormattedMessage defaultMessage="File storage server list" />;
case 10166:
return <FormattedMessage defaultMessage="Relay Monitor Announcement" />;
case 13194:
return <FormattedMessage defaultMessage="Wallet Info" />;
case 17375:
return <FormattedMessage defaultMessage="Cashu Wallet Event" />;
case 21000:
return <FormattedMessage defaultMessage="Lightning Pub RPC" />;
case 22242:
@ -177,17 +224,23 @@ export default function KindName({ kind }: { kind: number }) {
case 30030:
return <FormattedMessage defaultMessage="Emoji sets" />;
case 30040:
return <FormattedMessage defaultMessage="Modular Article Header" />;
return <FormattedMessage defaultMessage="Curated Publication Index" />;
case 30041:
return <FormattedMessage defaultMessage="Modular Article Content" />;
return <FormattedMessage defaultMessage="Curated Publication Content" />;
case 30063:
return <FormattedMessage defaultMessage="Release artifact sets" />;
case 30078:
return <FormattedMessage defaultMessage="Application-specific Data" />;
case 30166:
return <FormattedMessage defaultMessage="Relay Discovery" />;
case 30267:
return <FormattedMessage defaultMessage="App curation sets" />;
case 30311:
return <FormattedMessage defaultMessage="Live Event" />;
case 30315:
return <FormattedMessage defaultMessage="User Statuses" />;
case 30388:
return <FormattedMessage defaultMessage="Slide Set" />;
case 30402:
return <FormattedMessage defaultMessage="Classified Listing" />;
case 30403:
@ -200,6 +253,10 @@ export default function KindName({ kind }: { kind: number }) {
return <FormattedMessage defaultMessage="Wiki article" />;
case 30819:
return <FormattedMessage defaultMessage="Redirects" />;
case 31234:
return <FormattedMessage defaultMessage="Draft Event" />;
case 31388:
return <FormattedMessage defaultMessage="Link Set" />;
case 31890:
return <FormattedMessage defaultMessage="Feed" />;
case 31922:
@ -214,13 +271,17 @@ export default function KindName({ kind }: { kind: number }) {
return <FormattedMessage defaultMessage="Handler recommendation" />;
case 31990:
return <FormattedMessage defaultMessage="Handler information" />;
case 34235:
return <FormattedMessage defaultMessage="Video Event" />;
case 34236:
return <FormattedMessage defaultMessage="Short-form Portrait Video Event" />;
case 34237:
return <FormattedMessage defaultMessage="Video View Event" />;
case 32267:
return <FormattedMessage defaultMessage="Software Application" />;
case 34550:
return <FormattedMessage defaultMessage="Community Definition" />;
case 38383:
return <FormattedMessage defaultMessage="Peer-to-peer Order events" />;
case 39089:
return <FormattedMessage defaultMessage="Starter Pack" />;
case 39701:
return <FormattedMessage defaultMessage="Web bookmarks" />;
default:
return kind;
}
}

View File

@ -22,7 +22,7 @@ export default function useLoginFeed() {
const { publisher, system } = useEventPublisher();
useEffect(() => {
//system.checkSigs = checkSigs;
system.checkSigs = checkSigs;
}, [system, checkSigs]);
useEffect(() => {
@ -37,6 +37,7 @@ export default function useLoginFeed() {
}, [pubKey]);
useEffect(() => {
console.debug("UserState: start init from LoginFeed", login.state.didInit);
login.state.init(publisher?.signer, system).catch(console.error);
}, [login, publisher, system]);

View File

@ -1,4 +1,4 @@
import { EventKind, Nip10, NostrLink, RequestBuilder } from "@snort/system";
import { EventKind, Nip10, Nip22, NostrLink, RequestBuilder } from "@snort/system";
import { SnortContext, useRequestBuilder } from "@snort/system-react";
import { useContext, useEffect, useMemo, useState } from "react";
@ -34,7 +34,7 @@ export default function useThreadFeed(link: NostrLink) {
for (const v of Object.values(grouped)) {
sub
.withFilter()
.kinds([EventKind.TextNote])
.kinds([EventKind.TextNote, EventKind.Comment])
.replyToLink(v)
.relay(rootRelays ?? []);
}
@ -48,7 +48,9 @@ export default function useThreadFeed(link: NostrLink) {
const links = store
.map(a => [
NostrLink.fromEvent(a),
...a.tags.filter(a => a[0] === "e" || a[0] === "a").map(v => NostrLink.fromTag(v)),
...a.tags
.filter(a => a[0] === "e" || a[0] === "a" || a[0] === "E" || a[0] === "A")
.map(v => NostrLink.fromTag(v)),
])
.flat();
setAllEvents(links);
@ -56,14 +58,19 @@ export default function useThreadFeed(link: NostrLink) {
// load the thread structure from the current note
const current = store.find(a => link.matchesEvent(a));
if (current) {
const t = Nip10.parseThread(current);
if (t) {
const rootOrReplyAsRoot = t?.root ?? t?.replyTo;
if (rootOrReplyAsRoot) {
setRoot(rootOrReplyAsRoot);
if (current.kind === EventKind.TextNote) {
const t = Nip10.parseThread(current);
if (t) {
const rootOrReplyAsRoot = t?.root ?? t?.replyTo;
if (rootOrReplyAsRoot) {
setRoot(rootOrReplyAsRoot);
}
} else {
setRoot(link);
}
} else {
setRoot(link);
const root = Nip22.rootScopeOf(current);
setRoot(NostrLink.fromTag(root));
}
}
}

View File

@ -6,7 +6,7 @@ import { useMemo } from "react";
export default function useDiscoverMediaServers() {
const sub = useMemo(() => {
const rb = new RequestBuilder("media-servers-all");
rb.withFilter().kinds([EventKind.StorageServerList]);
rb.withFilter().kinds([EventKind.BlossomServerList]);
return rb;
}, []);

View File

@ -1,29 +0,0 @@
import { unwrap } from "@snort/shared";
import { decodeTLV, EventKind, NostrPrefix, RequestBuilder, TLVEntryType } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { useMemo } from "react";
import { createEmptyChatObject } from "@/chat";
export function useEmptyChatSystem(id?: string) {
const sub = useMemo(() => {
if (id?.startsWith(NostrPrefix.Chat28)) {
const cx = unwrap(decodeTLV(id).find(a => a.type === TLVEntryType.Special)).value as string;
const rb = new RequestBuilder(`nip28:${id}`);
rb.withFilter().ids([cx]).kinds([EventKind.PublicChatChannel, EventKind.PublicChatMetadata]);
rb.withFilter()
.tag("e", [cx])
.kinds([EventKind.PublicChatChannel, EventKind.PublicChatMessage, EventKind.PublicChatMetadata]);
return rb;
} else {
return new RequestBuilder(id ?? "");
}
}, [id]);
const data = useRequestBuilder(sub);
return useMemo(() => {
if (!id) return;
return createEmptyChatObject(id, data);
}, [id, data.length]);
}

View File

@ -11,7 +11,7 @@ export default function useLiveStreams() {
const rb = new RequestBuilder("streams");
rb.withFilter()
.kinds([EventKind.LiveEvent])
.since(unixNow() - Hour);
.since(unixNow() - 4 * Hour);
return rb;
}, []);

View File

@ -2,23 +2,21 @@ import { removeUndefined, sanitizeRelayUrl } from "@snort/shared";
import { EventKind, UnknownTag } from "@snort/system";
import { useMemo } from "react";
import { Nip96Uploader } from "@/Utils/Upload/Nip96";
import useEventPublisher from "./useEventPublisher";
import useLogin from "./useLogin";
export const DefaultMediaServers = [
//"https://media.zap.stream",
new UnknownTag(["server", "https://nostr.build/"]),
new UnknownTag(["server", "https://nostr.download/"]),
new UnknownTag(["server", "https://blossom.build/"]),
new UnknownTag(["server", "https://nostrcheck.me/"]),
new UnknownTag(["server", "https://files.v0l.io/"]),
new UnknownTag(["server", "https://blossom.primal.net/"]),
];
export function useMediaServerList() {
const { publisher } = useEventPublisher();
const { state } = useLogin(s => ({ v: s.state.version, state: s.state }));
let servers = state?.getList(EventKind.StorageServerList) ?? [];
let servers = state?.getList(EventKind.BlossomServerList) ?? [];
if (servers.length === 0) {
servers = DefaultMediaServers;
}
@ -33,14 +31,12 @@ export function useMediaServerList() {
const u = sanitizeRelayUrl(s);
if (!u) return;
const server = new Nip96Uploader(u, publisher);
await server.loadInfo();
await state?.addToList(EventKind.StorageServerList, new UnknownTag(["server", u]), true);
await state?.addToList(EventKind.BlossomServerList, new UnknownTag(["server", u]), true);
},
removeServer: async (s: string) => {
const u = sanitizeRelayUrl(s);
if (!u) return;
await state?.removeFromList(EventKind.StorageServerList, new UnknownTag(["server", u]), true);
await state?.removeFromList(EventKind.BlossomServerList, new UnknownTag(["server", u]), true);
},
}),
[servers],

View File

@ -1,13 +1,13 @@
import fuzzySearch from "@/Db/FuzzySearch";
import useWoT from "./useWoT";
import useWoT, { WoT } from "./useWoT";
export default function useProfileSearch(search: string | undefined) {
return userSearch(search);
export default function useProfileSearch() {
const wot = useWoT();
return (search: string | undefined) => userSearch(wot, search);
}
export function userSearch(search: string | undefined) {
const wot = useWoT();
function userSearch(wot: WoT, search: string | undefined) {
const searchString = search?.trim() ?? "";
const fuseResults = (searchString?.length ?? 0) > 0 ? fuzzySearch.search(searchString) : [];

View File

@ -1,28 +1,29 @@
import { TaggedNostrEvent } from "@snort/system";
import { SystemInterface, TaggedNostrEvent } from "@snort/system";
import { SnortContext } from "@snort/system-react";
import { useContext, useMemo } from "react";
export interface WoT {
sortEvents: (events: Array<TaggedNostrEvent>) => Array<TaggedNostrEvent>;
sortPubkeys: (events: Array<string>) => Array<string>;
followDistance: (pk: string) => number;
followedByCount: (pk: string) => number;
followedBy: (pk: string) => Set<string>;
}
function wotOnSystem(system: SystemInterface) {
const sgi = system.config.socialGraphInstance;
return {
sortEvents: (events: Array<TaggedNostrEvent>) =>
events.sort((a, b) => sgi.getFollowDistance(a.pubkey) - sgi.getFollowDistance(b.pubkey)),
sortPubkeys: (events: Array<string>) => events.sort((a, b) => sgi.getFollowDistance(a) - sgi.getFollowDistance(b)),
followDistance: (pk: string) => sgi.getFollowDistance(pk),
followedByCount: (pk: string) => sgi.followedByFriendsCount(pk),
followedBy: (pk: string) => sgi.followedByFriends(pk),
instance: sgi,
};
}
export default function useWoT() {
const system = useContext(SnortContext);
return useMemo(
() => ({
sortEvents: (events: Array<TaggedNostrEvent>) =>
events.sort(
(a, b) =>
system.config.socialGraphInstance.getFollowDistance(a.pubkey) -
system.config.socialGraphInstance.getFollowDistance(b.pubkey),
),
sortPubkeys: (events: Array<string>) =>
events.sort(
(a, b) =>
system.config.socialGraphInstance.getFollowDistance(a) -
system.config.socialGraphInstance.getFollowDistance(b),
),
followDistance: (pk: string) => system.config.socialGraphInstance.getFollowDistance(pk),
followedByCount: (pk: string) => system.config.socialGraphInstance.followedByFriendsCount(pk),
followedBy: (pk: string) => system.config.socialGraphInstance.followedByFriends(pk),
instance: system.config.socialGraphInstance,
}),
[system.config.socialGraphInstance],
);
return useMemo<WoT>(() => wotOnSystem(system), [system]);
}

View File

@ -1,29 +1,17 @@
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import Changelog from "@/../CHANGELOG.md";
import Changelog from "@/../CHANGELOG.md?raw";
import { Markdown } from "@/Components/Event/Markdown";
export function AboutPage() {
const [changelog, setChangelog] = useState("");
async function getChangelog() {
const res = await fetch(Changelog);
const content = await res.text();
setChangelog(content);
}
useEffect(() => {
getChangelog().catch(console.error);
}, []);
const version = document.querySelector("meta[name='application-name']")?.getAttribute("content");
return (
<div className="main-content p">
<h1>
<FormattedMessage defaultMessage="About" />
</h1>
Version: <b>{__SNORT_VERSION__}</b>
<Markdown content={changelog} tags={[]} />
Version: <b>{version?.split(":")?.at(1) ?? "unknown version"}</b>
<Markdown content={Changelog} tags={[]} />
</div>
);
}

View File

@ -59,7 +59,6 @@ export function MediaCol({ setThread }: { setThread: (e: NostrLink) => void }) {
</div>
<TimelineFollows
postsOnly={true}
liveStreams={false}
noteFilter={e => {
const parsed = transformTextCached(e.id, e.content, e.tags);
const images = parsed.filter(a => a.type === "media" && a.mimeType?.startsWith("image/"));

View File

@ -1,10 +1,9 @@
import { HexKey } from "@snort/system";
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import { Link, useNavigate } from "react-router-dom";
import { Link } from "react-router-dom";
import Telegram from "@/assets/img/telegram.svg";
import { Nip28ChatSystem } from "@/chat/nip28";
import AsyncButton from "@/Components/Button/AsyncButton";
import Copy from "@/Components/Copy/Copy";
import ZapButton from "@/Components/Event/ZapButton";
@ -22,7 +21,6 @@ const DonatePage = () => {
const [today, setSumToday] = useState<RevenueToday>();
const [onChain, setOnChain] = useState("");
const api = new SnortApi(ApiHost);
const navigate = useNavigate();
async function getOnChainAddress() {
const { address } = await api.onChainDonation();
@ -105,18 +103,6 @@ const DonatePage = () => {
</AsyncButton>
);
}
case "nip28": {
return (
<AsyncButton
onClick={() => {
const id = Nip28ChatSystem.chatId(a.value);
navigate(`/messages/${id}`);
}}>
<img src={CONFIG.icon} width={24} height={24} className="rounded-full" />
<FormattedMessage defaultMessage="Nostr Public Chat" />
</AsyncButton>
);
}
}
})}
</div>

View File

@ -39,5 +39,7 @@ export const Translators = [
bech32ToHex("npub1ust7u0v3qffejwhqee45r49zgcyewrcn99vdwkednd356c9resyqtnn3mj"), // Petri - FI
bech32ToHex("npub1p94p6d4p04mhjt2hdpkhhvkl93v7j7ada4w9lztj0y0fzg2m959sux5h5k"), // Jeremy - SV
bech32ToHex("npub1dnvslq0vvrs8d603suykc4harv94yglcxwna9sl2xu8grt2afm3qgfh0tp"), // summoner001 - HU
];
export const DonateLNURL = "donate@snort.social";

View File

@ -1,5 +1,5 @@
import { unwrap } from "@snort/shared";
import { EventKind, NostrLink, NostrPrefix, parseNostrLink } from "@snort/system";
import { Bech32Regex, unwrap } from "@snort/shared";
import { EventKind, NostrLink, NostrPrefix, tryParseNostrLink } from "@snort/system";
import { useEventFeed } from "@snort/system-react";
import classNames from "classnames";
import React, { useCallback, useMemo } from "react";
@ -9,24 +9,25 @@ import { useLocation, useNavigate } from "react-router-dom";
import { rootTabItems } from "@/Components/Feed/RootTabItems";
import { RootTabs } from "@/Components/Feed/RootTabs";
import Icon from "@/Components/Icons/Icon";
import KindName from "@/Components/kind-name";
import DisplayName from "@/Components/User/DisplayName";
import useLogin from "@/Hooks/useLogin";
import { LogoHeader } from "@/Pages/Layout/LogoHeader";
import NotificationsHeader from "@/Pages/Layout/NotificationsHeader";
import { bech32ToHex } from "@/Utils";
import { bech32ToHex, findTag } from "@/Utils";
export function Header() {
const navigate = useNavigate();
const location = useLocation();
const pageName = decodeURIComponent(location.pathname.split("/")[1]);
const pathSplit = location.pathname.split("/");
const pageName = decodeURIComponent(pathSplit[1]);
const nostrLink = useMemo(() => {
try {
return parseNostrLink(pageName);
} catch (e) {
return undefined;
const nostrEntity = pathSplit.find(a => a.match(Bech32Regex));
if (nostrEntity) {
return tryParseNostrLink(nostrEntity);
}
}, [pageName]);
}, [pathSplit]);
const { publicKey, tags } = useLogin(s => ({
publicKey: s.publicKey,
@ -64,7 +65,11 @@ export function Header() {
</>
);
} else if (nostrLink) {
if (nostrLink.type === NostrPrefix.Event || nostrLink.type === NostrPrefix.Note) {
if (
nostrLink.type === NostrPrefix.Event ||
nostrLink.type === NostrPrefix.Note ||
nostrLink.type === NostrPrefix.Address
) {
title = <NoteTitle link={nostrLink} />;
} else if (nostrLink.type === NostrPrefix.PublicKey || nostrLink.type === NostrPrefix.Profile) {
try {
@ -111,17 +116,20 @@ export function Header() {
function NoteTitle({ link }: { link: NostrLink }) {
const ev = useEventFeed(link);
const values = useMemo(() => {
return { name: <DisplayName pubkey={ev?.pubkey ?? ""} /> };
}, [ev?.pubkey]);
if (!ev?.pubkey) {
return <FormattedMessage defaultMessage="Note" />;
}
const title = findTag(ev, "title");
return (
<>
<FormattedMessage defaultMessage="Note by {name}" values={values} />
<FormattedMessage
defaultMessage="{note_type} by {name}{title}"
values={{
note_type: <KindName kind={ev.kind} />,
name: <DisplayName pubkey={ev.pubkey} />,
title: title ? ` - ${title}` : "",
}}
/>
</>
);
}

View File

@ -1,5 +1,5 @@
import { dedupe, unwrap } from "@snort/shared";
import { EventKind, parseNostrLink } from "@snort/system";
import { parseNostrLink } from "@snort/system";
import { useEventFeed } from "@snort/system-react";
import { useMemo } from "react";
import { FormattedMessage } from "react-intl";
@ -27,7 +27,8 @@ export function ListFeedPage() {
);
if (!data) return <PageSpinner />;
if (data.kind !== EventKind.ContactList && data.kind !== EventKind.FollowSet) {
const hasPTags = data.tags.some(a => a[0] === "p");
if (!hasPTags) {
return (
<b>
<FormattedMessage defaultMessage="Must be a contact list or pubkey list" />

View File

@ -4,7 +4,7 @@ import { useEffect, useState } from "react";
import { useInView } from "react-intersection-observer";
import { FormattedMessage, useIntl } from "react-intl";
import { Chat, ChatMessage, ChatType, setLastReadIn } from "@/chat";
import { Chat, ChatMessage, ChatType } from "@/chat";
import NoteTime from "@/Components/Event/Note/NoteTime";
import messages from "@/Components/messages";
import Text from "@/Components/Text/Text";
@ -31,9 +31,7 @@ export default function DM(props: DMProps) {
if (publisher) {
const decrypted = await msg.decrypt(publisher);
setContent(decrypted || "<ERROR>");
if (!isMe) {
setLastReadIn(msg.id);
}
props.chat.markRead(msg.id);
}
}

View File

@ -4,11 +4,13 @@ import { FormattedMessage, useIntl } from "react-intl";
import { useNavigate, useParams } from "react-router-dom";
import { Chat, ChatType, useChatSystems } from "@/chat";
import { CollapsedSection } from "@/Components/Collapsed";
import NoteTime from "@/Components/Event/Note/NoteTime";
import NoteToSelf from "@/Components/User/NoteToSelf";
import ProfileImage from "@/Components/User/ProfileImage";
import useLogin from "@/Hooks/useLogin";
import usePageDimensions from "@/Hooks/usePageDimensions";
import useWoT from "@/Hooks/useWoT";
import { ChatParticipantProfile } from "@/Pages/Messages/ChatParticipant";
import DmWindow from "@/Pages/Messages/DmWindow";
import NewChatWindow from "@/Pages/Messages/NewChatWindow";
@ -24,8 +26,12 @@ export default function MessagesPage() {
const { width: pageWidth } = usePageDimensions();
const chats = useChatSystems();
const wot = useWoT();
const trustedChats = chats.filter(a => wot.followDistance(a.participants[0].id) <= 2);
const otherChats = chats.filter(a => wot.followDistance(a.participants[0].id) > 2);
const unreadCount = useMemo(() => chats.reduce((p, c) => p + c.unread, 0), [chats]);
const unreadTrustedCount = useMemo(() => trustedChats.reduce((p, c) => p + c.unread, 0), [trustedChats]);
const unreadOtherCount = useMemo(() => otherChats.reduce((p, c) => p + c.unread, 0), [otherChats]);
function openChat(e: React.MouseEvent<HTMLDivElement>, type: ChatType, id: string) {
e.stopPropagation();
@ -81,26 +87,45 @@ export default function MessagesPage() {
);
}
function sortMessages(a: Chat, b: Chat) {
const aSelf = a.participants.length === 1 && a.participants[0].id === login.publicKey;
const bSelf = b.participants.length === 1 && b.participants[0].id === login.publicKey;
if (aSelf || bSelf) {
return aSelf ? -1 : 1;
}
return b.lastMessage > a.lastMessage ? 1 : -1;
}
return (
<div className="flex flex-1 md:h-screen md:overflow-hidden">
{(pageWidth >= TwoCol || !id) && (
<div className="overflow-y-auto md:h-screen p-1 w-full md:w-1/3 flex-shrink-0">
<div className="flex items-center justify-between p-2">
<button disabled={unreadCount <= 0} type="button" className="text-sm font-semibold">
<button
disabled={unreadTrustedCount <= 0}
type="button"
className="text-sm font-semibold"
onClick={() => {
chats.forEach(c => c.markRead());
}}>
<FormattedMessage defaultMessage="Mark all read" />
</button>
<NewChatWindow />
</div>
{chats
.sort((a, b) => {
const aSelf = a.participants.length === 1 && a.participants[0].id === login.publicKey;
const bSelf = b.participants.length === 1 && b.participants[0].id === login.publicKey;
if (aSelf || bSelf) {
return aSelf ? -1 : 1;
}
return b.lastMessage > a.lastMessage ? 1 : -1;
})
.map(conversation)}
{trustedChats.sort(sortMessages).map(conversation)}
{otherChats.sort(sortMessages).length > 0 && (
<>
<CollapsedSection
title={
<div className="text-xl flex items-center gap-4">
<FormattedMessage defaultMessage="Other Chats" />
{unreadOtherCount > 0 && <div className="has-unread" />}
</div>
}>
{otherChats.map(conversation)}
</CollapsedSection>
</>
)}
</div>
)}
{id ? <DmWindow id={id} /> : pageWidth >= TwoCol && <div className="flex-1 rt-border"></div>}

View File

@ -1,21 +1,15 @@
import { decodeTLV, EventKind, NostrPrefix } from "@snort/system";
import { useUserSearch } from "@snort/system-react";
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import { ChatType, createChatLink } from "@/chat";
import { Nip28ChatSystem } from "@/chat/nip28";
import Icon from "@/Components/Icons/Icon";
import Modal from "@/Components/Modal/Modal";
import ProfileImage from "@/Components/User/ProfileImage";
import ProfilePreview from "@/Components/User/ProfilePreview";
import useEventPublisher from "@/Hooks/useEventPublisher";
import useFollowsControls from "@/Hooks/useFollowControls";
import useLogin from "@/Hooks/useLogin";
import Nip28ChatProfile from "@/Pages/Messages/Nip28ChatProfile";
import { appendDedupe, debounce } from "@/Utils";
import { LoginSession, LoginStore } from "@/Utils/Login";
export default function NewChatWindow() {
const [show, setShow] = useState(false);
@ -24,8 +18,6 @@ export default function NewChatWindow() {
const [term, setSearchTerm] = useState("");
const navigate = useNavigate();
const search = useUserSearch();
const login = useLogin();
const { system, publisher } = useEventPublisher();
const { followList } = useFollowsControls();
useEffect(() => {
@ -115,32 +107,6 @@ export default function NewChatWindow() {
/>
);
})}
{results.length === 1 && (
<Nip28ChatProfile
id={results[0]}
onClick={async id => {
setShow(false);
const chats = appendDedupe(login.extraChats, [Nip28ChatSystem.chatId(id)]);
LoginStore.updateSession({
...login,
extraChats: chats,
} as LoginSession);
const evList = await publisher?.generic(eb => {
eb.kind(EventKind.PublicChatsList);
chats.forEach(c => {
if (c.startsWith(NostrPrefix.Chat28)) {
eb.tag(["e", decodeTLV(c)[0].value as string]);
}
});
return eb;
});
if (evList) {
await system.BroadcastEvent(evList);
}
navigate(createChatLink(ChatType.PublicGroupChat, id));
}}
/>
)}
</div>
</div>
</div>

View File

@ -1,24 +0,0 @@
import { NostrLink, UserMetadata } from "@snort/system";
import { useEventFeed } from "@snort/system-react";
import React from "react";
import ProfilePreview from "@/Components/User/ProfilePreview";
export default function Nip28ChatProfile({ id, onClick }: { id: string; onClick: (id: string) => void }) {
const channel = useEventFeed(new NostrLink(CONFIG.eventLinkPrefix, id, 40));
if (channel) {
const meta = JSON.parse(channel.content) as UserMetadata;
return (
<ProfilePreview
pubkey=""
profile={meta}
profileImageProps={{
link: "",
}}
options={{ about: false }}
actions={<></>}
onClick={() => onClick(id)}
/>
);
}
}

View File

@ -74,8 +74,8 @@ export function FollowersTab({ id }: { id: HexKey }) {
const followers = useFollowersFeed(id);
return (
<FollowsList
pubkeys={followers}
className="p"
pubkeys={followers.map(a => a.pubkey)}
className="p flex flex-col gap-1"
profilePreviewProps={{
options: {
about: true,
@ -90,7 +90,7 @@ export function FollowsTab({ id }: { id: HexKey }) {
return (
<FollowsList
pubkeys={follows}
className="p"
className="p flex flex-col gap-1"
profilePreviewProps={{
options: {
about: true,

View File

@ -0,0 +1,100 @@
import { dedupe } from "@snort/shared";
import { EventKind, NostrLink, RequestBuilder } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { Link } from "react-router-dom";
import AsyncButton from "@/Components/Button/AsyncButton";
import { AutoLoadMore } from "@/Components/Event/LoadMore";
import { AvatarGroup } from "@/Components/User/AvatarGroup";
import DisplayName from "@/Components/User/DisplayName";
import { ProfileLink } from "@/Components/User/ProfileLink";
import useFollowsControls from "@/Hooks/useFollowControls";
import useWoT from "@/Hooks/useWoT";
import { findTag } from "@/Utils";
export default function FollowSetsPage() {
const sub = new RequestBuilder("follow-sets");
sub.withFilter().kinds([EventKind.StarterPackSet, EventKind.FollowSet]);
const { formatMessage } = useIntl();
const data = useRequestBuilder(sub);
const wot = useWoT();
const control = useFollowsControls();
const dataSorted = wot.sortEvents(data);
const [showN, setShowN] = useState(10);
const [search, setSearch] = useState("");
const filtered = dataSorted.filter(s => {
if (search) {
const ss = search.toLowerCase();
return s.content.toLowerCase().includes(ss) || s.tags.some(t => t[1].toLowerCase().includes(ss));
} else {
return true;
}
});
return (
<div className="p flex flex-col gap-4">
<input
type="text"
placeholder={formatMessage({ defaultMessage: "Search sets.." })}
value={search}
onChange={e => setSearch(e.target.value)}
/>
{filtered.slice(0, showN).map(a => {
const title = findTag(a, "title") ?? findTag(a, "d") ?? a.content;
const pTags = wot.sortPubkeys(dedupe(a.tags.filter(a => a[0] === "p").map(a => a[1])));
const isFollowingAll = pTags.every(a => control.isFollowing(a));
if (pTags.length === 0) return;
const link = NostrLink.fromEvent(a);
return (
<div key={a.id} className="p br bg-gray-ultradark flex flex-col gap-4">
<div className="flex items-center justify-between">
<div className="flex flex-col">
<div className="text-xl">{title}</div>
<div className="text-gray-medium font-medium flex items-center gap-2">
<Link to={`/${link.encode()}`} state={a}>
<FormattedMessage defaultMessage="{n} people" values={{ n: pTags.length }} />
</Link>
-
<Link to={`/list-feed/${link.encode()}`}>
<FormattedMessage defaultMessage="View Feed" />
</Link>
</div>
</div>
{!isFollowingAll && (
<div className="flex gap-4">
<AsyncButton
className="secondary"
onClick={async () => {
await control.addFollow(pTags);
}}>
<FormattedMessage defaultMessage="Follow All" />
</AsyncButton>
</div>
)}
</div>
<div className="flex gap-2">
<AvatarGroup ids={pTags.slice(0, 10)} size={40} />
</div>
<div>
<FormattedMessage
defaultMessage="<dark>Created by</dark> {name}"
values={{
dark: c => <span className="text-gray-medium">{c}</span>,
name: (
<ProfileLink pubkey={a.pubkey}>
<DisplayName pubkey={a.pubkey} />
</ProfileLink>
),
}}
/>
</div>
</div>
);
})}
{filtered.length > showN && <AutoLoadMore onClick={() => setShowN(n => n + 10)} />}
</div>
);
}

View File

@ -0,0 +1,19 @@
import { EventKind } from "@snort/system";
import TimelineFollows from "@/Components/Feed/TimelineFollows";
import { Day } from "@/Utils/Const";
export default function MediaPosts() {
return (
<div className="p">
<TimelineFollows
id="media"
postsOnly={true}
kinds={[EventKind.Photo, EventKind.Video, EventKind.ShortVideo]}
showDisplayAsSelector={true}
firstChunkSize={Day}
windowSize={Day}
/>
</div>
);
}

View File

@ -1,5 +1,5 @@
import { lazy } from "react";
import { Outlet, RouteObject } from "react-router-dom";
import { Outlet, RouteObject, useLocation } from "react-router-dom";
import { LiveStreams } from "@/Components/LiveStream/LiveStreams";
import { RootTabRoutes } from "@/Pages/Root/RootTabRoutes";
@ -8,9 +8,10 @@ import { getCurrentRefCode } from "@/Utils";
const InviteModal = lazy(() => import("@/Components/Invite"));
export default function RootPage() {
const code = getCurrentRefCode();
const location = useLocation();
return (
<>
<LiveStreams />
{(location.pathname === "/" || location.pathname === "/following") && <LiveStreams />}
<div className="main-content">
<Outlet />
</div>

View File

@ -1,4 +1,3 @@
import SuggestedProfiles from "@/Components/SuggestedProfiles";
import TrendingHashtags from "@/Components/Trending/TrendingHashtags";
import TrendingNotes from "@/Components/Trending/TrendingPosts";
import Discover from "@/Pages/Discover";
@ -6,7 +5,9 @@ import HashTagsPage from "@/Pages/HashTagsPage";
import { ConversationsTab } from "@/Pages/Root/ConversationsTab";
import { DefaultTab } from "@/Pages/Root/DefaultTab";
import { FollowedByFriendsTab } from "@/Pages/Root/FollowedByFriendsTab";
import FollowSetsPage from "@/Pages/Root/FollowSets";
import { ForYouTab } from "@/Pages/Root/ForYouTab";
import MediaPosts from "@/Pages/Root/Media";
import { NotesTab } from "@/Pages/Root/NotesTab";
import { TagsTab } from "@/Pages/Root/TagsTab";
import { TopicsPage } from "@/Pages/TopicsPage";
@ -23,7 +24,9 @@ export type RootTabRoutePath =
| "trending/hashtags"
| "suggested"
| "t/:tag"
| "topics";
| "topics"
| "media"
| "follow-sets";
export type RootTabRoute = {
path: RootTabRoutePath;
@ -67,14 +70,6 @@ export const RootTabRoutes: RootTabRoute[] = [
path: "trending/hashtags",
element: <TrendingHashtags />,
},
{
path: "suggested",
element: (
<div className="p">
<SuggestedProfiles />
</div>
),
},
{
path: "t/:tag",
element: <HashTagsPage />,
@ -83,4 +78,12 @@ export const RootTabRoutes: RootTabRoute[] = [
path: "topics",
element: <TopicsPage />,
},
{
path: "media",
element: <MediaPosts />,
},
{
path: "follow-sets",
element: <FollowSetsPage />,
},
];

View File

@ -15,7 +15,8 @@ const NOTES = 0;
const PROFILES = 1;
const Profiles = ({ keyword }: { keyword: string }) => {
const results = useProfileSearch(keyword);
const searchFn = useProfileSearch();
const results = useMemo(() => searchFn(keyword), [keyword, searchFn]);
const ids = useMemo(() => results.map(r => r.pubkey), [results]);
const content = keyword ? (
<FollowListBase

View File

@ -21,7 +21,7 @@ const RelaySettingsPage = () => {
async function addNewRelay() {
const urls = removeUndefined(
(newRelay?.trim()?.split("\n") ?? []).map(a => {
if (!a.startsWith("wss://")) {
if (!a.startsWith("wss://") && !a.startsWith("ws://")) {
a = `wss://${a}`;
}
return sanitizeRelayUrl(a);

View File

@ -2,10 +2,8 @@ import { ReactNode } from "react";
import { FormattedMessage } from "react-intl";
import { useNavigate } from "react-router-dom";
import LndLogo from "@/assets/img/lnd-logo.png";
import AlbyIcon from "@/Components/Icons/Alby";
import BlueWallet from "@/Components/Icons/BlueWallet";
//import CashuIcon from "@/Components/Icons/Cashu";
import Icon from "@/Components/Icons/Icon";
import NWCIcon from "@/Components/Icons/NWC";
import { getAlbyOAuth } from "@/Pages/settings/wallet/utils";
@ -57,24 +55,12 @@ const WalletSettings = () => {
url="/settings/wallet/nwc"
desc={<FormattedMessage defaultMessage="Native nostr wallet connection" />}
/>
<WalletRow
logo={<img src={LndLogo} />}
name="LND via LNC"
url="/settings/wallet/lnc"
desc={<FormattedMessage defaultMessage="Connect to your own LND node with Lightning Node Connect" />}
/>
<WalletRow
logo={<BlueWallet width={64} height={64} />}
name="LNDHub"
url="/settings/wallet/lndhub"
desc={<FormattedMessage defaultMessage="Generic LNDHub wallet (BTCPayServer / Alby / LNBits)" />}
/>
{/*<WalletRow
logo={<CashuIcon size={64} />}
name="Cashu"
url="/settings/wallet/cashu"
desc={<FormattedMessage defaultMessage="Cashu mint wallet" />}
/>*/}
{CONFIG.alby && (
<WalletRow
logo={<AlbyIcon size={64} />}

View File

@ -1,4 +1,4 @@
import { unwrap } from "@snort/shared";
import { sanitizeRelayUrl, unwrap } from "@snort/shared";
import { EventKind, UnknownTag } from "@snort/system";
import { useState } from "react";
import { FormattedMessage, FormattedNumber } from "react-intl";
@ -8,35 +8,15 @@ import IconButton from "@/Components/Button/IconButton";
import { CollapsedSection } from "@/Components/Collapsed";
import { RelayFavicon } from "@/Components/Relay/RelaysMetadata";
import useDiscoverMediaServers from "@/Hooks/useDiscoverMediaServers";
import useEventPublisher from "@/Hooks/useEventPublisher";
import useLogin from "@/Hooks/useLogin";
import { Nip96Uploader } from "@/Utils/Upload/Nip96";
import { getRelayName } from "@/Utils";
export default function MediaSettingsPage() {
const { state } = useLogin(s => ({ v: s.state.version, state: s.state }));
const { publisher } = useEventPublisher();
const list = state.getList(EventKind.StorageServerList);
const list = state.getList(EventKind.BlossomServerList);
const [newServer, setNewServer] = useState("");
const [error, setError] = useState("");
const knownServers = useDiscoverMediaServers();
async function validateServer(url: string) {
if (!publisher) return;
setError("");
try {
const svc = new Nip96Uploader(url, publisher);
await svc.loadInfo();
return true;
} catch (e) {
if (e instanceof Error) {
setError(e.message);
}
return false;
}
}
return (
<div className="flex flex-col gap-3">
<div className="text-xl">
@ -57,7 +37,7 @@ export default function MediaSettingsPage() {
size: 15,
}}
onClick={async () => {
await state.removeFromList(EventKind.StorageServerList, [new UnknownTag(["server", addr])], true);
await state.removeFromList(EventKind.BlossomServerList, [new UnknownTag(["server", addr])], true);
}}
/>
</div>
@ -83,9 +63,9 @@ export default function MediaSettingsPage() {
/>
<AsyncButton
onClick={async () => {
if (await validateServer(newServer)) {
if (sanitizeRelayUrl(newServer)) {
await state.addToList(
EventKind.StorageServerList,
EventKind.BlossomServerList,
[new UnknownTag(["server", new URL(newServer).toString()])],
true,
);
@ -95,7 +75,6 @@ export default function MediaSettingsPage() {
<FormattedMessage defaultMessage="Add" />
</AsyncButton>
</div>
{error && <b className="text-warning">{error}</b>}
</div>
<CollapsedSection
title={
@ -127,7 +106,7 @@ export default function MediaSettingsPage() {
<tr key={k}>
<td className="flex gap-2 items-center">
<RelayFavicon url={k} />
{k}
{getRelayName(k)}
</td>
<td className="text-center">
<FormattedNumber value={v} />
@ -136,9 +115,7 @@ export default function MediaSettingsPage() {
<AsyncButton
className="!py-1 mb-1"
onClick={async () => {
if (await validateServer(k)) {
await state.addToList(EventKind.StorageServerList, [new UnknownTag(["server", k])], true);
}
await state.addToList(EventKind.BlossomServerList, [new UnknownTag(["server", k])], true);
}}>
<FormattedMessage defaultMessage="Add" />
</AsyncButton>

View File

@ -1,78 +0,0 @@
import { CashuWallet, WalletKind } from "@snort/wallet";
import { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useNavigate } from "react-router-dom";
import { v4 as uuid } from "uuid";
import AsyncButton from "@/Components/Button/AsyncButton";
import { unwrap } from "@/Utils";
import { WalletConfig, Wallets } from "@/Wallet";
const ConnectCashu = () => {
const navigate = useNavigate();
const { formatMessage } = useIntl();
const [mintUrl, setMintUrl] = useState<string>("https://8333.space:3338");
const [error, setError] = useState<string>();
async function tryConnect(config: string) {
try {
if (!mintUrl) {
throw new Error("Mint URL is required");
}
const connection = new CashuWallet({
url: config,
keys: {},
proofs: [],
keysets: [],
});
await connection.login();
const info = await connection.getInfo();
const newWallet = {
id: uuid(),
kind: WalletKind.Cashu,
active: true,
info,
data: JSON.stringify(connection.getConfig()),
} as WalletConfig;
Wallets.add(newWallet);
navigate("/settings/wallet");
} catch (e) {
if (e instanceof Error) {
setError((e as Error).message);
} else {
setError(
formatMessage({
defaultMessage: "Unknown error",
id: "qDwvZ4",
}),
);
}
}
}
return (
<>
<h4>
<FormattedMessage defaultMessage="Enter mint URL" id="KoFlZg" />
</h4>
<div className="flex">
<div className="grow mr10">
<input
type="text"
placeholder="Mint URL"
className="w-max"
value={mintUrl}
onChange={e => setMintUrl(e.target.value)}
/>
</div>
<AsyncButton onClick={() => tryConnect(unwrap(mintUrl))} disabled={!mintUrl}>
<FormattedMessage defaultMessage="Connect" id="+vVZ/G" />
</AsyncButton>
</div>
{error && <b className="error p10">{error}</b>}
</>
);
};
export default ConnectCashu;

View File

@ -1,123 +0,0 @@
import { LNCWallet, LNWallet, WalletInfo, WalletKind } from "@snort/wallet";
import { useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useNavigate } from "react-router-dom";
import { v4 as uuid } from "uuid";
import AsyncButton from "@/Components/Button/AsyncButton";
import { unwrap } from "@/Utils";
import { Wallets } from "@/Wallet";
const ConnectLNC = () => {
const { formatMessage } = useIntl();
const navigate = useNavigate();
const [pairingPhrase, setPairingPhrase] = useState<string>();
const [error, setError] = useState<string>();
const [connectedLNC, setConnectedLNC] = useState<LNWallet & { setPassword(pw: string): void }>();
const [walletInfo, setWalletInfo] = useState<WalletInfo>();
const [walletPassword, setWalletPassword] = useState<string>();
async function tryConnect(cfg: string) {
try {
const lnc = await LNCWallet.Initialize(cfg);
const info = await lnc.getInfo();
// prompt password
setConnectedLNC(lnc);
setWalletInfo(info as WalletInfo);
} catch (e) {
if (e instanceof Error) {
setError(e.message);
} else {
setError(
formatMessage({
defaultMessage: "Unknown error",
id: "qDwvZ4",
}),
);
}
}
}
function setLNCPassword(pw: string) {
connectedLNC?.setPassword(pw);
Wallets.add({
id: uuid(),
kind: WalletKind.LNC,
active: true,
info: unwrap(walletInfo),
});
navigate("/settings/wallet");
}
function flowConnect() {
if (connectedLNC) return null;
return (
<>
<h4>
<FormattedMessage defaultMessage="Enter pairing phrase" />
</h4>
<div className="flex">
<div className="grow mr10">
<input
type="text"
placeholder={formatMessage({ defaultMessage: "Pairing phrase", id: "8v1NN+" })}
className="w-max"
value={pairingPhrase}
onChange={e => setPairingPhrase(e.target.value)}
/>
</div>
<AsyncButton onClick={() => tryConnect(unwrap(pairingPhrase))} disabled={!pairingPhrase}>
<FormattedMessage defaultMessage="Connect" />
</AsyncButton>
</div>
{error && <b className="error p10">{error}</b>}
</>
);
}
function flowSetPassword() {
if (!connectedLNC) return null;
return (
<div className="flex flex-col">
<h3>
<FormattedMessage
defaultMessage="Connected to: {node} 🎉"
id="1c4YST"
values={{
node: walletInfo?.alias,
}}
/>
</h3>
<h4>
<FormattedMessage defaultMessage="Enter password" />
</h4>
<div className="flex w-max">
<div className="grow mr10">
<input
type="password"
placeholder={formatMessage({ defaultMessage: "Wallet password", id: "lTbT3s" })}
className="w-max"
value={walletPassword}
onChange={e => setWalletPassword(e.target.value)}
/>
</div>
<AsyncButton
onClick={() => setLNCPassword(unwrap(walletPassword))}
disabled={(walletPassword?.length ?? 0) < 8}>
<FormattedMessage defaultMessage="Save" />
</AsyncButton>
</div>
</div>
);
}
return (
<>
{flowConnect()}
{flowSetPassword()}
</>
);
};
export default ConnectLNC;

View File

@ -2,8 +2,6 @@ import { RouteObject } from "react-router-dom";
import WalletSettings from "../WalletSettings";
import AlbyOAuth from "./Alby";
//import ConnectCashu from "./Cashu";
import ConnectLNC from "./LNC";
import ConnectLNDHub from "./LNDHub";
import ConnectNostrWallet from "./NWC";
@ -12,10 +10,6 @@ export const WalletSettingsRoutes = [
path: "/settings/wallet",
element: <WalletSettings />,
},
{
path: "/settings/wallet/lnc",
element: <ConnectLNC />,
},
{
path: "/settings/wallet/lndhub",
element: <ConnectLNDHub />,
@ -24,10 +18,6 @@ export const WalletSettingsRoutes = [
path: "/settings/wallet/nwc",
element: <ConnectNostrWallet />,
},
/*{
path: "/settings/wallet/cashu",
element: <ConnectCashu />,
},*/
{
path: "/settings/wallet/alby",
element: <AlbyOAuth />,

View File

@ -3,6 +3,8 @@ import { NostrEvent, TaggedNostrEvent } from "@snort/system";
import { ZapTarget } from "@snort/wallet";
import { useSyncExternalStoreWithSelector } from "use-sync-external-store/with-selector";
import { BlobDescriptor } from "@/Utils/Upload/blossom";
interface NoteCreatorDataSnapshot {
show: boolean;
note: string;
@ -17,6 +19,7 @@ interface NoteCreatorDataSnapshot {
sensitive?: string;
pollOptions?: Array<string>;
otherEvents?: Array<NostrEvent>;
attachments?: Record<string, Array<BlobDescriptor>>;
extraTags?: Array<Array<string>>;
sending?: Array<NostrEvent>;
sendStarted: boolean;

View File

@ -62,6 +62,9 @@ export interface LoginSession {
*/
publicKey?: HexKey;
/**
* Login state for the current user
*/
state: UserState<SnortAppData>;
/**

View File

@ -7,7 +7,6 @@ import {
EventPublisher,
HexKey,
KeyStorage,
NotEncrypted,
RelaySettings,
UserState,
UserStateObject,
@ -63,6 +62,7 @@ const LoggedOut = {
export class MultiAccountStore extends ExternalStore<LoginSession> {
#activeAccount?: HexKey;
#saveDebounce?: ReturnType<typeof setTimeout>;
#accounts: Map<string, LoginSession> = new Map();
#publishers = new Map<string, EventPublisher>();
@ -107,8 +107,12 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
},
stateObj,
);
stateClass.checkIsStandardList(EventKind.StorageServerList); // track nip96 list
stateClass.checkIsStandardList(EventKind.BlossomServerList); // track blossom list
stateClass.on("change", () => this.#save());
if (v.state instanceof UserState) {
v.state.destroy();
}
console.debug("UserState assign = ", stateClass);
v.state = stateClass;
// always activate signer
@ -117,7 +121,6 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
this.#publishers.set(v.id, signer);
}
}
this.#loadIrisKeyIfExists();
}
getSessions() {
@ -191,8 +194,8 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
stalker: stalker ?? false,
} as LoginSession;
newSession.state.checkIsStandardList(EventKind.StorageServerList); // track nip96 list
newSession.state.on("change", () => this.#save());
newSession.state!.checkIsStandardList(EventKind.BlossomServerList); // track blossom list
newSession.state!.on("change", () => this.#save());
const pub = createPublisher(newSession);
if (pub) {
this.#publishers.set(newSession.id, pub);
@ -240,8 +243,8 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
appdataId: "snort",
}),
} as LoginSession;
newSession.state.checkIsStandardList(EventKind.StorageServerList); // track nip96 list
newSession.state.on("change", () => this.#save());
newSession.state!.checkIsStandardList(EventKind.BlossomServerList); // track blossom list
newSession.state!.on("change", () => this.#save());
if ("nostr_os" in window && window?.nostr_os) {
window?.nostr_os.saveKey(key.value);
@ -280,22 +283,6 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
return { ...s };
}
#loadIrisKeyIfExists() {
try {
const irisKeyJSON = window.localStorage.getItem("iris.myKey");
if (irisKeyJSON) {
const irisKeyObj = JSON.parse(irisKeyJSON);
if (irisKeyObj.priv) {
const privateKey = new NotEncrypted(irisKeyObj.priv);
this.loginWithPrivateKey(privateKey);
window.localStorage.removeItem("iris.myKey");
}
}
} catch (e) {
console.error("Failed to load iris key", e);
}
}
#migrate() {
let didMigrate = false;
@ -367,27 +354,33 @@ export class MultiAccountStore extends ExternalStore<LoginSession> {
}
#save() {
if (!this.#activeAccount && this.#accounts.size > 0) {
this.#activeAccount = this.#accounts.keys().next().value;
if (this.#saveDebounce !== undefined) {
clearTimeout(this.#saveDebounce);
}
const toSave = [];
for (const v of this.#accounts.values()) {
if (v.privateKeyData instanceof KeyStorage) {
toSave.push({
...v,
state: v.state instanceof UserState ? v.state.serialize() : v.state,
privateKeyData: v.privateKeyData.toPayload(),
});
} else {
toSave.push({
...v,
state: v.state instanceof UserState ? v.state.serialize() : v.state,
});
}
}
console.debug("Trying to save", toSave);
window.localStorage.setItem(AccountStoreKey, JSON.stringify(toSave));
this.notifyChange();
this.#saveDebounce = setTimeout(() => {
if (!this.#activeAccount && this.#accounts.size > 0) {
this.#activeAccount = this.#accounts.keys().next().value;
}
const toSave = [];
for (const v of this.#accounts.values()) {
if (v.privateKeyData instanceof KeyStorage) {
toSave.push({
...v,
state: v.state instanceof UserState ? v.state.serialize() : v.state,
privateKeyData: v.privateKeyData.toPayload(),
});
} else {
toSave.push({
...v,
state: v.state instanceof UserState ? v.state.serialize() : v.state,
});
}
}
console.debug("Trying to save", toSave);
window.localStorage.setItem(AccountStoreKey, JSON.stringify(toSave));
this.#saveDebounce = undefined;
}, 2000);
}
}

View File

@ -1,4 +1,4 @@
import { Nip10, NostrLink, TaggedNostrEvent } from "@snort/system";
import { EventKind, Nip10, NostrLink, TaggedNostrEvent } from "@snort/system";
/**
* Get the chain key as a reply event
@ -6,9 +6,14 @@ import { Nip10, NostrLink, TaggedNostrEvent } from "@snort/system";
* ie. Get the key for which this event is replying to
*/
export function replyChainKey(ev: TaggedNostrEvent) {
const t = Nip10.parseThread(ev);
const tag = t?.replyTo ?? t?.root;
return tag?.tagKey;
if (ev.kind !== EventKind.Comment) {
const t = Nip10.parseThread(ev);
const tag = t?.replyTo ?? t?.root;
return tag?.tagKey;
} else {
const k = ev.tags.find(t => ["e", "a", "i"].includes(t[0]));
return k?.[1];
}
}
/**

View File

@ -1,115 +0,0 @@
import { base64 } from "@scure/base";
import { throwIfOffline } from "@snort/shared";
import { EventKind, EventPublisher, NostrEvent } from "@snort/system";
import { addExtensionToNip94Url, readNip94Tags, UploadResult } from ".";
export class Nip96Uploader {
#info?: Nip96Info;
constructor(
readonly url: string,
readonly publisher: EventPublisher,
) {
this.url = new URL(this.url).toString();
}
get progress() {
return [];
}
async loadInfo() {
const u = new URL(this.url);
const rsp = await fetch(`${u.protocol}//${u.host}/.well-known/nostr/nip96.json`);
this.#info = (await rsp.json()) as Nip96Info;
return this.#info;
}
async listFiles(page = 0, count = 50) {
const rsp = await this.#req(`?page=${page}&count=${count}`, "GET");
if (rsp.ok) {
return (await rsp.json()) as Nip96FileList;
}
}
async upload(file: File | Blob, filename: string): Promise<UploadResult> {
const fd = new FormData();
fd.append("size", file.size.toString());
fd.append("caption", filename);
fd.append("content_type", file.type);
fd.append("file", file);
const rsp = await this.#req("", "POST", fd);
if (rsp.ok) {
const data = (await rsp.json()) as Nip96Result;
if (data.status === "success") {
const meta = readNip94Tags(data.nip94_event.tags);
return {
url: addExtensionToNip94Url(meta),
header: data.nip94_event,
metadata: meta,
};
}
return {
error: data.message,
};
} else {
const text = await rsp.text();
try {
const obj = JSON.parse(text) as Nip96Result;
return {
error: obj.message,
};
} catch {
return {
error: `Upload failed: ${text}`,
};
}
}
}
async #req(path: string, method: "GET" | "POST" | "DELETE", body?: BodyInit) {
throwIfOffline();
const auth = async (url: string, method: string) => {
const auth = await this.publisher.generic(eb => {
return eb.kind(EventKind.HttpAuthentication).tag(["u", url]).tag(["method", method]);
});
return `Nostr ${base64.encode(new TextEncoder().encode(JSON.stringify(auth)))}`;
};
const info = this.#info ?? (await this.loadInfo());
let u = info.api_url;
if (u.startsWith("/")) {
u = `${this.url}${u.slice(1)}`;
}
u += path;
return await fetch(u, {
method,
body,
headers: {
accept: "application/json",
authorization: await auth(u, method),
},
});
}
}
export interface Nip96Info {
api_url: string;
download_url?: string;
}
export interface Nip96Result {
status: string;
message: string;
processing_url?: string;
nip94_event: NostrEvent;
}
export interface Nip96FileList {
count: number;
total: number;
page: number;
files: Array<NostrEvent>;
}

View File

@ -0,0 +1,134 @@
import { base64, bytesToString } from "@scure/base";
import { throwIfOffline, unixNow } from "@snort/shared";
import { EventKind, EventPublisher } from "@snort/system";
export interface BlobDescriptor {
url?: string;
sha256: string;
size: number;
type?: string;
uploaded?: number;
nip94?: Array<Array<string>>;
}
export class Blossom {
constructor(
readonly url: string,
readonly publisher: EventPublisher,
) {
this.url = new URL(this.url).toString();
}
async upload(file: File) {
const hash = await window.crypto.subtle.digest("SHA-256", await file.arrayBuffer());
const tags = [["x", bytesToString("hex", new Uint8Array(hash))]];
const rsp = await this.#req("upload", "PUT", "upload", file, tags);
if (rsp.ok) {
const ret = (await rsp.json()) as BlobDescriptor;
this.#fixTags(ret);
return ret;
} else {
const text = await rsp.text();
throw new Error(text);
}
}
async media(file: File) {
const hash = await window.crypto.subtle.digest("SHA-256", await file.arrayBuffer());
const tags = [["x", bytesToString("hex", new Uint8Array(hash))]];
const rsp = await this.#req("media", "PUT", "media", file, tags);
if (rsp.ok) {
const ret = (await rsp.json()) as BlobDescriptor;
this.#fixTags(ret);
return ret;
} else {
const text = await rsp.text();
throw new Error(text);
}
}
async mirror(url: string) {
const rsp = await this.#req("mirror", "PUT", "mirror", JSON.stringify({ url }), undefined, {
"content-type": "application/json",
});
if (rsp.ok) {
const ret = (await rsp.json()) as BlobDescriptor;
this.#fixTags(ret);
return ret;
} else {
const text = await rsp.text();
throw new Error(text);
}
}
async list(pk: string) {
const rsp = await this.#req(`list/${pk}`, "GET", "list");
if (rsp.ok) {
const ret = (await rsp.json()) as Array<BlobDescriptor>;
ret.forEach(a => this.#fixTags(a));
return ret;
} else {
const text = await rsp.text();
throw new Error(text);
}
}
async delete(id: string) {
const tags = [["x", id]];
const rsp = await this.#req(id, "DELETE", "delete", undefined, tags);
if (!rsp.ok) {
const text = await rsp.text();
throw new Error(text);
}
}
#fixTags(r: BlobDescriptor) {
if (!r.nip94) return;
if (Array.isArray(r.nip94)) return;
// blossom.band invalid response
if (r.nip94 && "tags" in r.nip94) {
r.nip94 = r.nip94["tags"];
return;
}
r.nip94 = Object.entries(r.nip94 as Record<string, string>);
}
async #req(
path: string,
method: "GET" | "POST" | "DELETE" | "PUT",
term: string,
body?: BodyInit,
tags?: Array<Array<string>>,
headers?: Record<string, string>,
) {
throwIfOffline();
const url = `${this.url}${path}`;
const now = unixNow();
const auth = async (url: string, method: string) => {
const auth = await this.publisher.generic(eb => {
eb.kind(24_242 as EventKind)
.tag(["u", url])
.tag(["method", method.toLowerCase()])
.tag(["t", term])
.tag(["expiration", (now + 10).toString()]);
tags?.forEach(t => eb.tag(t));
return eb;
});
return `Nostr ${base64.encode(new TextEncoder().encode(JSON.stringify(auth)))}`;
};
return await fetch(url, {
method,
body,
headers: {
...headers,
accept: "application/json",
authorization: await auth(url, method),
},
});
}
}

View File

@ -1,27 +1,11 @@
import { EventPublisher, NostrEvent } from "@snort/system";
import { EventPublisher, Nip94Tags, NostrEvent } from "@snort/system";
import useEventPublisher from "@/Hooks/useEventPublisher";
import { useMediaServerList } from "@/Hooks/useMediaServerList";
import { bech32ToHex, randomSample } from "@/Utils";
import { FileExtensionRegex, KieranPubKey } from "@/Utils/Const";
import { KieranPubKey } from "@/Utils/Const";
import { Nip96Uploader } from "./Nip96";
export interface Nip94Tags {
url?: string;
mimeType?: string;
hash?: string;
originalHash?: string;
size?: number;
dimensions?: [number, number];
magnet?: string;
blurHash?: string;
thumb?: string;
image?: Array<string>;
summary?: string;
alt?: string;
fallback?: Array<string>;
}
import { Blossom } from "./blossom";
export interface UploadResult {
url?: string;
@ -81,129 +65,8 @@ export default function useFileUpload(privKey?: string) {
const pub = privKey ? EventPublisher.privateKey(privKey) : publisher;
if (servers.length > 0 && pub) {
const random = randomSample(servers, 1)[0];
return new Nip96Uploader(random, pub);
return new Blossom(random, pub);
} else if (pub) {
return new Nip96Uploader("https://nostr.build", pub);
return new Blossom("https://blossom.build", pub);
}
}
export function addExtensionToNip94Url(meta: Nip94Tags) {
if (!meta.url?.match(FileExtensionRegex) && meta.mimeType) {
switch (meta.mimeType) {
case "image/webp": {
return `${meta.url}.webp`;
}
case "image/jpeg":
case "image/jpg": {
return `${meta.url}.jpg`;
}
case "video/mp4": {
return `${meta.url}.mp4`;
}
}
}
return meta.url;
}
/**
* Read NIP-94 tags from `imeta` tag
*/
export function readNip94TagsFromIMeta(tag: Array<string>) {
const asTags = tag.slice(1).map(a => a.split(" ", 2));
return readNip94Tags(asTags);
}
/**
* Read NIP-94 tags from event tags
*/
export function readNip94Tags(tags: Array<Array<string>>) {
const res: Nip94Tags = {};
for (const tx of tags) {
const [k, v] = tx;
switch (k) {
case "url": {
res.url = v;
break;
}
case "m": {
res.mimeType = v;
break;
}
case "x": {
res.hash = v;
break;
}
case "ox": {
res.originalHash = v;
break;
}
case "size": {
res.size = Number(v);
break;
}
case "dim": {
res.dimensions = v.split("x").map(Number) as [number, number];
break;
}
case "magnet": {
res.magnet = v;
break;
}
case "blurhash": {
res.blurHash = v;
break;
}
case "thumb": {
res.thumb = v;
break;
}
case "image": {
res.image ??= [];
res.image.push(v);
break;
}
case "summary": {
res.summary = v;
break;
}
case "alt": {
res.alt = v;
break;
}
case "fallback": {
res.fallback ??= [];
res.fallback.push(v);
break;
}
}
}
return res;
}
export function nip94TagsToIMeta(meta: Nip94Tags) {
const ret: Array<string> = ["imeta"];
const ifPush = (key: string, value?: string | number) => {
if (value) {
ret.push(`${key} ${value}`);
}
};
ifPush("url", meta.url);
ifPush("m", meta.mimeType);
ifPush("x", meta.hash);
ifPush("ox", meta.originalHash);
ifPush("size", meta.size);
ifPush("dim", meta.dimensions?.join("x"));
ifPush("magnet", meta.magnet);
ifPush("blurhash", meta.blurHash);
ifPush("thumb", meta.thumb);
ifPush("summary", meta.summary);
ifPush("alt", meta.alt);
if (meta.image) {
meta.image.forEach(a => ifPush("image", a));
}
if (meta.fallback) {
meta.fallback.forEach(a => ifPush("fallback", a));
}
return ret;
}

View File

@ -1,8 +1,21 @@
import { TaggedNostrEvent } from "@snort/system";
import { EventKind, ParsedFragment, readNip94TagsFromIMeta, TaggedNostrEvent } from "@snort/system";
import { transformTextCached } from "@/Hooks/useTextTransformCache";
export default function getEventMedia(event: TaggedNostrEvent) {
// emulate parsed media from imeta kinds
const mediaKinds = [EventKind.Photo, EventKind.Video, EventKind.ShortVideo];
if (mediaKinds.includes(event.kind)) {
const meta = event.tags.filter(a => a[0] === "imeta").map(readNip94TagsFromIMeta);
return meta.map(
a =>
({
type: "media",
mimeType: a.mimeType,
content: a.url,
}) as ParsedFragment,
);
}
const parsed = transformTextCached(event.id, event.content, event.tags);
return parsed.filter(
a => a.type === "media" && (a.mimeType?.startsWith("image/") || a.mimeType?.startsWith("video/")),

View File

@ -1,4 +1,4 @@
import { unixNow, unwrap } from "@snort/shared";
import { ExternalStore, unixNow, unwrap } from "@snort/shared";
import {
encodeTLVEntries,
EventKind,
@ -13,9 +13,8 @@ import {
UserMetadata,
} from "@snort/system";
import { useRequestBuilder } from "@snort/system-react";
import { useEffect, useMemo } from "react";
import { useEffect, useMemo, useSyncExternalStore } from "react";
import { useEmptyChatSystem } from "@/Hooks/useEmptyChatSystem";
import useEventPublisher from "@/Hooks/useEventPublisher";
import useLogin from "@/Hooks/useLogin";
import useModeration from "@/Hooks/useModeration";
@ -23,7 +22,6 @@ import { findTag } from "@/Utils";
import { LoginSession } from "@/Utils/Login";
import { Nip17Chats, Nip17ChatSystem } from "./nip17";
import { Nip28Chats, Nip28ChatSystem } from "./nip28";
export enum ChatType {
PublicGroupChat = 2,
@ -57,6 +55,7 @@ export interface Chat {
messages: Array<ChatMessage>;
createMessage(msg: string, pub: EventPublisher): Promise<Array<NostrEvent>>;
sendMessage(ev: Array<NostrEvent>, system: SystemInterface): void | Promise<void>;
markRead(id?: string): void;
}
export interface ChatSystem {
@ -106,10 +105,13 @@ export function lastReadInChat(id: string) {
return parseInt(window.localStorage.getItem(k) ?? "0");
}
export function setLastReadIn(id: string) {
const now = unixNow();
export function setLastReadIn(id: string, time?: number) {
const now = time ?? unixNow();
const k = `dm:seen:${id}`;
window.localStorage.setItem(k, now.toString());
const current = lastReadInChat(id);
if (current < now) {
window.localStorage.setItem(k, now.toString());
}
}
export function createChatLink(type: ChatType, ...params: Array<string>) {
@ -135,41 +137,39 @@ export function createChatLink(type: ChatType, ...params: Array<string>) {
),
)}`;
}
case ChatType.PublicGroupChat: {
return `/messages/${Nip28ChatSystem.chatId(params[0])}`;
}
}
throw new Error("Unknown chat type");
}
export function createEmptyChatObject(id: string, messages?: Array<TaggedNostrEvent>) {
export function createEmptyChatObject(id: string) {
if (id.startsWith(NostrPrefix.Chat17)) {
return Nip17ChatSystem.createChatObj(id, []);
}
if (id.startsWith(NostrPrefix.Chat28)) {
return Nip28ChatSystem.createChatObj(id, messages ?? []);
}
throw new Error("Cant create new empty chat, unknown id");
}
export function useChatSystem(chat: ChatSystem) {
export function useChatSystem<T extends ChatSystem & ExternalStore<Array<Chat>>>(sys: T) {
const login = useLogin();
const { publisher } = useEventPublisher();
const chat = useSyncExternalStore(
s => sys.hook(s),
() => sys.snapshot(),
);
const sub = useMemo(() => {
return chat.subscription(login);
}, [chat, login]);
return sys.subscription(login);
}, [login]);
const data = useRequestBuilder(sub);
const { isMuted } = useModeration();
useEffect(() => {
if (publisher) {
chat.processEvents(publisher, data);
sys.processEvents(publisher, data);
}
}, [data, publisher]);
return useMemo(() => {
if (login.publicKey) {
return chat.listChats(
return sys.listChats(
login.publicKey,
data.filter(a => !isMuted(a.pubkey)),
);
@ -179,23 +179,12 @@ export function useChatSystem(chat: ChatSystem) {
}
export function useChatSystems() {
const nip28 = useChatSystem(Nip28Chats);
const nip17 = useChatSystem(Nip17Chats);
return [...nip28, ...nip17];
return nip17;
}
export function useChat(id: string) {
const getStore = () => {
if (id.startsWith(NostrPrefix.Chat17)) {
return Nip17Chats;
}
if (id.startsWith(NostrPrefix.Chat28)) {
return Nip28Chats;
}
throw new Error("Unsupported chat system");
};
const ret = useChatSystem(getStore()).find(a => a.id === id);
const emptyChat = useEmptyChatSystem(ret === undefined ? id : undefined);
return ret ?? emptyChat;
const ret = useChatSystem(Nip17Chats).find(a => a.id === id);
return ret;
}

View File

@ -14,7 +14,7 @@ import {
import { GiftsCache } from "@/Cache";
import { GiftWrapCache } from "@/Cache/GiftWrapCache";
import { Chat, ChatSystem, ChatType, lastReadInChat } from "@/chat";
import { Chat, ChatSystem, ChatType, lastReadInChat, setLastReadIn } from "@/chat";
import { UnwrappedGift } from "@/Db";
import { LoginSession } from "@/Utils/Login";
import { GetPowWorker } from "@/Utils/wasm";
@ -100,8 +100,8 @@ export class Nip17ChatSystem extends ExternalStore<Array<Chat>> implements ChatS
type: ChatType.PrivateDirectMessage,
id,
title: title.title,
unread: messages.reduce((acc, v) => (v.created_at > last ? acc++ : acc), 0),
lastMessage: messages.reduce((acc, v) => (v.created_at > acc ? v.created_at : acc), 0),
unread: messages.reduce((acc, v) => (v.inner.created_at > last ? acc + 1 : acc), 0),
lastMessage: messages.reduce((acc, v) => (v.inner.created_at > acc ? v.created_at : acc), 0),
participants,
messages: messages.map(m => ({
id: m.id,
@ -128,11 +128,18 @@ export class Nip17ChatSystem extends ExternalStore<Array<Chat>> implements ChatS
messages.push(recvSealedN);
}
messages.push(pub.giftWrap(await pub.sealRumor(gossip, pub.pubKey), pub.pubKey, powTarget, GetPowWorker()));
return await Promise.all(messages);
const ret = await Promise.all(messages);
Nip17Chats.notifyChange();
return ret;
},
sendMessage: (ev, system) => {
ev.forEach(a => system.BroadcastEvent(a));
},
markRead: msgId => {
const msg = messages.find(a => a.id === msgId);
setLastReadIn(id, msg?.inner.created_at);
Nip17Chats.notifyChange();
},
} as Chat;
}

View File

@ -1,144 +0,0 @@
import { unwrap } from "@snort/shared";
import {
decodeTLV,
encodeTLVEntries,
EventKind,
NostrEvent,
NostrPrefix,
RequestBuilder,
SystemInterface,
TaggedNostrEvent,
TLVEntryType,
UserMetadata,
} from "@snort/system";
import { Chat, ChatParticipant, ChatSystem, ChatType, lastReadInChat } from "@/chat";
import { findTag } from "@/Utils";
import { LoginSession } from "@/Utils/Login";
export class Nip28ChatSystem implements ChatSystem {
readonly ChannelKinds = [
EventKind.PublicChatChannel,
EventKind.PublicChatMessage,
EventKind.PublicChatMetadata,
EventKind.PublicChatMuteMessage,
EventKind.PublicChatMuteUser,
];
subscription(session: LoginSession): RequestBuilder {
const chats = (session.extraChats ?? []).filter(a => a.startsWith(NostrPrefix.Chat28));
const chatId = (v: string) => unwrap(decodeTLV(v).find(a => a.type === TLVEntryType.Special)).value as string;
const rb = new RequestBuilder(`nip28:${session.id}`);
if (chats.length > 0) {
rb.withFilter()
.ids(chats.map(v => chatId(v)))
.kinds([EventKind.PublicChatChannel, EventKind.PublicChatMetadata]);
for (const c of chats) {
const id = chatId(c);
rb.withFilter().tag("e", [id]).kinds(this.ChannelKinds);
}
}
return rb;
}
processEvents(): Promise<void> {
// nothing to do
return Promise.resolve();
}
listChats(pk: string, evs: Array<TaggedNostrEvent>): Chat[] {
const chats = this.#chatChannels(evs);
const ret = Object.entries(chats).map(([k, v]) => {
return Nip28ChatSystem.createChatObj(Nip28ChatSystem.chatId(k), v);
});
return ret;
}
static chatId(id: string) {
return encodeTLVEntries(NostrPrefix.Chat28, {
type: TLVEntryType.Special,
value: id,
length: id.length,
});
}
static createChatObj(id: string, messages: Array<NostrEvent>) {
const last = lastReadInChat(id);
const participants = decodeTLV(id)
.filter(v => v.type === TLVEntryType.Special)
.map(
v =>
({
type: "generic",
id: v.value as string,
profile: this.#chatProfileFromMessages(messages),
}) as ChatParticipant,
);
return {
type: ChatType.PublicGroupChat,
id,
unread: messages.reduce((acc, v) => (v.created_at > last ? acc++ : acc), 0),
lastMessage: messages.reduce((acc, v) => (v.created_at > acc ? v.created_at : acc), 0),
participants,
messages: messages
.filter(a => a.kind === EventKind.PublicChatMessage)
.map(m => ({
id: m.id,
created_at: m.created_at,
from: m.pubkey,
tags: m.tags,
content: m.content,
needsDecryption: false,
})),
createMessage: async (msg, pub) => {
return [
await pub.generic(eb => {
return eb.kind(EventKind.PublicChatMessage).content(msg).tag(["e", participants[0].id, "", "root"]);
}),
];
},
sendMessage: (ev, system: SystemInterface) => {
ev.forEach(a => system.BroadcastEvent(a));
},
} as Chat;
}
static #chatProfileFromMessages(messages: Array<NostrEvent>) {
const chatDefs = messages.filter(
a => a.kind === EventKind.PublicChatChannel || a.kind === EventKind.PublicChatMetadata,
);
const chatDef =
chatDefs.length > 0
? chatDefs.reduce((acc, v) => (acc.created_at > v.created_at ? acc : v), chatDefs[0])
: undefined;
return chatDef ? (JSON.parse(chatDef.content) as UserMetadata) : undefined;
}
#chatChannels(evs: Array<TaggedNostrEvent>) {
const chats = evs.reduce(
(acc, v) => {
const k = this.#chatId(v);
if (k) {
acc[k] ??= [];
acc[k].push(v);
}
return acc;
},
{} as Record<string, Array<NostrEvent>>,
);
return chats;
}
#chatId(ev: NostrEvent) {
if (ev.kind === EventKind.PublicChatChannel) {
return ev.id;
} else if (ev.kind === EventKind.PublicChatMetadata) {
return findTag(ev, "e");
} else if (this.ChannelKinds.includes(ev.kind)) {
return ev.tags.find(a => a[0] === "e" && a[3] === "root")?.[1];
}
}
}
export const Nip28Chats = new Nip28ChatSystem();

View File

@ -1,109 +0,0 @@
import { dedupe, ExternalStore, FeedCache, removeUndefined } from "@snort/shared";
import { EventKind, NostrEvent, RequestBuilder, SystemInterface, TaggedNostrEvent } from "@snort/system";
import { Chat, ChatSystem, ChatType, lastReadInChat } from "@/chat";
import { LoginSession } from "@/Utils/Login";
export class Nip29ChatSystem extends ExternalStore<Array<Chat>> implements ChatSystem {
readonly #cache: FeedCache<NostrEvent>;
constructor(cache: FeedCache<NostrEvent>) {
super();
this.#cache = cache;
}
processEvents(): Promise<void> {
return Promise.resolve();
}
takeSnapshot(): Chat[] {
return this.listChats();
}
subscription(session: LoginSession) {
const id = session.publicKey;
if (!id) return;
const gs = id.split("/", 2);
const rb = new RequestBuilder(`nip29:${id}`);
const last = this.listChats().find(a => a.id === id)?.lastMessage;
rb.withFilter()
.relay(`wss://${gs[0]}`)
.kinds([EventKind.SimpleChatMessage])
.tag("g", [`/${gs[1]}`])
.since(last);
rb.withFilter()
.relay(`wss://${gs[0]}`)
.kinds([EventKind.SimpleChatMetadata])
.tag("d", [`/${gs[1]}`]);
return rb;
}
async onEvent(evs: readonly TaggedNostrEvent[]) {
const msg = evs.filter(a => a.kind === EventKind.SimpleChatMessage && a.tags.some(b => b[0] === "g"));
if (msg.length > 0) {
await this.#cache.bulkSet(msg);
this.notifyChange();
}
}
listChats(): Chat[] {
const allMessages = this.#nip29Chats();
const groups = dedupe(
removeUndefined(allMessages.map(a => a.tags.find(b => b[0] === "g"))).map(a => `${a[2]}${a[1]}`),
);
return groups.map(g => {
const [relay, channel] = g.split("/", 2);
const messages = allMessages.filter(
a => `${a.tags.find(b => b[0] === "g")?.[2]}${a.tags.find(b => b[0] === "g")?.[1]}` === g,
);
const lastRead = lastReadInChat(g);
return {
type: ChatType.PublicGroupChat,
id: g,
title: `${relay}/${channel}`,
unread: messages.reduce((acc, v) => (v.created_at > lastRead ? acc++ : acc), 0),
lastMessage: messages.reduce((acc, v) => (v.created_at > acc ? v.created_at : acc), 0),
messages: messages.map(m => ({
id: m.id,
created_at: m.created_at,
from: m.pubkey,
tags: m.tags,
needsDecryption: false,
content: m.content,
decrypt: async () => {
return m.content;
},
})),
participants: [
{
type: "generic",
id: "",
profile: {
name: `${relay}/${channel}`,
},
},
],
createMessage: async (msg, pub) => {
return [
await pub.generic(eb => {
return eb
.kind(EventKind.SimpleChatMessage)
.tag(["g", `/${channel}`, relay])
.content(msg);
}),
];
},
sendMessage: async (ev, system: SystemInterface) => {
ev.forEach(async a => {
system.HandleEvent("*", { ...a, relays: [] });
await system.WriteOnceToRelay(`wss://${relay}`, a);
});
},
} as Chat;
});
}
#nip29Chats() {
return this.#cache.snapshot().filter(a => a.kind === EventKind.SimpleChatMessage);
}
}

View File

@ -179,6 +179,10 @@ a.ext {
-webkit-text-stroke: 1px black;
}
.text-highlight {
color: var(--highlight);
}
.br {
border-radius: 16px;
}

View File

@ -2,16 +2,17 @@ import "./index.css";
import "@szhsin/react-menu/dist/index.css";
import "@/assets/fonts/inter.css";
import { unixNowMs } from "@snort/shared";
import { unixNow, unixNowMs } from "@snort/shared";
import { EventBuilder } from "@snort/system";
import { SnortContext } from "@snort/system-react";
import { StrictMode } from "react";
import * as ReactDOM from "react-dom/client";
import { createBrowserRouter, RouteObject, RouterProvider } from "react-router-dom";
import { preload, UserCache } from "@/Cache";
import { initRelayWorker, preload, Relay, UserCache } from "@/Cache";
import { ThreadRoute } from "@/Components/Event/Thread/ThreadRoute";
import { IntlProvider } from "@/Components/IntlProvider/IntlProvider";
import { db } from "@/Db";
import { addCachedMetadataToFuzzySearch } from "@/Db/FuzzySearch";
import { AboutPage } from "@/Pages/About";
import { DebugPage } from "@/Pages/CacheDebug";
@ -39,6 +40,7 @@ import { WalletSendPage } from "@/Pages/wallet/send";
import ZapPoolPage from "@/Pages/ZapPool/ZapPool";
import { System } from "@/system";
import { storeRefCode, unwrap } from "@/Utils";
import { hasWasm, wasmInit, WasmPath } from "@/Utils/wasm";
import { Wallets } from "@/Wallet";
import { setupWebLNWalletConfig } from "@/Wallet";
@ -52,10 +54,14 @@ async function initSite() {
"31990:84de35e2584d2b144aae823c9ed0b0f3deda09648530b93d1a2a146d1dea9864:app-profile",
];
storeRefCode();
if (hasWasm) {
await wasmInit(WasmPath);
await initRelayWorker();
}
setupWebLNWalletConfig(Wallets);
//db.ready = await db.isAvailable();
db.ready = await db.isAvailable();
const login = LoginStore.snapshot();
preload(login.state.follows).then(async () => {
@ -78,7 +84,7 @@ async function initSite() {
});
// cleanup
//Relay.delete(["REQ", "cleanup", { kinds: [1, 7, 9735], until: unixNow() - Day * 30 }]);
Relay.delete(["REQ", "cleanup", { kinds: [1, 7, 9735], until: unixNow() - Day * 30 }]);
return null;
}

View File

@ -17,9 +17,18 @@
"+Vxixo": {
"defaultMessage": "Secret Group Chat"
},
"+W+Kof": {
"defaultMessage": "Cashu Wallet History"
},
"+aZY2h": {
"defaultMessage": "Zap Type"
},
"+mO9i4": {
"defaultMessage": "Code Snippet"
},
"+sDqqq": {
"defaultMessage": "Peer-to-peer Order events"
},
"+tShPg": {
"defaultMessage": "following"
},
@ -53,9 +62,6 @@
"/Xf4UW": {
"defaultMessage": "Send anonymous usage metrics"
},
"/b1IHW": {
"defaultMessage": "Group Chat Message"
},
"/d6vEc": {
"defaultMessage": "Make your profile easier to find and share"
},
@ -92,6 +98,9 @@
"0mch2Y": {
"defaultMessage": "name has disallowed characters"
},
"0oMk/p": {
"defaultMessage": "Other Chats"
},
"0siT4z": {
"defaultMessage": "Politics"
},
@ -108,6 +117,9 @@
"1/BFEj": {
"defaultMessage": "git stuff"
},
"1GvBMj": {
"defaultMessage": "internal reference"
},
"1Mo59U": {
"defaultMessage": "Are you sure you want to remove this note from bookmarks?"
},
@ -117,9 +129,6 @@
"1UWegE": {
"defaultMessage": "Be sure to back up your keys!"
},
"1c4YST": {
"defaultMessage": "Connected to: {node} 🎉"
},
"1nYUGC": {
"defaultMessage": "{n} Following"
},
@ -156,15 +165,15 @@
"2IFGap": {
"defaultMessage": "Donate"
},
"2LbrkB": {
"defaultMessage": "Enter password"
},
"2O2sfp": {
"defaultMessage": "Finish"
},
"2Qsf9/": {
"defaultMessage": "Generic lists"
},
"2YxhJx": {
"defaultMessage": "Reserved Cashu Wallet Tokens"
},
"2a2YiP": {
"defaultMessage": "{n} Bookmarks"
},
@ -186,9 +195,6 @@
"2z7Kky": {
"defaultMessage": "Latest Articles"
},
"3/onCd": {
"defaultMessage": "Replies"
},
"39AHJm": {
"defaultMessage": "Sign Up"
},
@ -213,9 +219,6 @@
"3gOsZq": {
"defaultMessage": "Translators"
},
"3kbIhS": {
"defaultMessage": "Untitled"
},
"3qnJlS": {
"defaultMessage": "You are voting with {amount} sats"
},
@ -258,6 +261,9 @@
"4OB335": {
"defaultMessage": "Dislike"
},
"4OQuna": {
"defaultMessage": "Draft Event"
},
"4P/kKm": {
"defaultMessage": "Private Key Encryption"
},
@ -270,9 +276,18 @@
"4emo2p": {
"defaultMessage": "Missing Relays"
},
"4oPRxH": {
"defaultMessage": "Supported Kinds:"
},
"4rYCjn": {
"defaultMessage": "Note to Self"
},
"4rxi5n": {
"defaultMessage": "hardcopy reference"
},
"4tKMJS": {
"defaultMessage": "Software Application"
},
"4wgYpI": {
"defaultMessage": "Recommended Application Handlers"
},
@ -333,12 +348,21 @@
"6WWD34": {
"defaultMessage": "Looking for: {noteId}"
},
"6Z3bDG": {
"defaultMessage": "Cashu Wallet Event"
},
"6bgpn+": {
"defaultMessage": "Not all clients support this, you may still receive some zaps as if zap splits was not configured"
},
"6dmn4m": {
"defaultMessage": "Thread"
},
"6ewQqw": {
"defaultMessage": "Likes ({n})"
},
"6m1Zkw": {
"defaultMessage": "Relay Monitor Announcement"
},
"6mr8WU": {
"defaultMessage": "Followed by"
},
@ -384,9 +408,6 @@
"7hp70g": {
"defaultMessage": "NIP-05"
},
"7jfPsW": {
"defaultMessage": "Modular Article Content"
},
"7nAz/z": {
"defaultMessage": "Mute notes from people who are outside your web of trust"
},
@ -402,6 +423,9 @@
"8BDFvJ": {
"defaultMessage": "Conventions for clients' use of e and p tags in text events"
},
"8Cw1Fj": {
"defaultMessage": "Link Set"
},
"8ED/4u": {
"defaultMessage": "Reply To"
},
@ -417,18 +441,12 @@
"8Y6bZQ": {
"defaultMessage": "Invalid zap split: {input}"
},
"8ZGqWl": {
"defaultMessage": "Group Thread"
},
"8g2vyB": {
"defaultMessage": "name too long"
},
"8jmwT8": {
"defaultMessage": "bech32-encoded entities"
},
"8v1NN+": {
"defaultMessage": "Pairing phrase"
},
"8xdDLn": {
"defaultMessage": "Follow sets"
},
@ -438,9 +456,15 @@
"9+Ddtu": {
"defaultMessage": "Next"
},
"92gdbw": {
"defaultMessage": "Relay Discovery"
},
"9HU8vw": {
"defaultMessage": "Reply"
},
"9RNiUn": {
"defaultMessage": "View Feed"
},
"9SvQep": {
"defaultMessage": "Follows {n}"
},
@ -450,6 +474,9 @@
"9WRlF4": {
"defaultMessage": "Send"
},
"9WTAKy": {
"defaultMessage": "Search sets.."
},
"9kO0VQ": {
"defaultMessage": "Hide muted notes"
},
@ -469,9 +496,6 @@
"defaultMessage": "Parent",
"description": "Link to parent note in thread"
},
"ALdW69": {
"defaultMessage": "Note by {name}"
},
"AN0Z7Q": {
"defaultMessage": "Muted Words"
},
@ -508,6 +532,9 @@
"AyGauy": {
"defaultMessage": "Login"
},
"Ayx8rG": {
"defaultMessage": "Curated Publication Index"
},
"B4C47Y": {
"defaultMessage": "name too short"
},
@ -560,15 +587,15 @@
"C8FsOr": {
"defaultMessage": "Popular Servers"
},
"C8HhVE": {
"defaultMessage": "Suggested Follows"
},
"CA1efg": {
"defaultMessage": "Video sets"
},
"CHTbO3": {
"defaultMessage": "Failed to load invoice"
},
"CJ0biq": {
"defaultMessage": "Poll Response"
},
"CJx5Nd": {
"defaultMessage": "Profile Zaps"
},
@ -578,6 +605,9 @@
"CM0k0d": {
"defaultMessage": "Prune follow list"
},
"CSOaM+": {
"defaultMessage": "{note_type} by {name}{title}"
},
"CVWeJ6": {
"defaultMessage": "Trending People"
},
@ -755,6 +785,12 @@
"GL8aXW": {
"defaultMessage": "Bookmarks ({n})"
},
"GLdw/8": {
"defaultMessage": "File Message"
},
"GQo+OV": {
"defaultMessage": "prompt reference"
},
"GSye7T": {
"defaultMessage": "Lightning Address"
},
@ -767,12 +803,22 @@
"GpkNYn": {
"defaultMessage": "Torrent"
},
"GqKcVm": {
"defaultMessage": "App curation sets"
},
"GqQeu/": {
"defaultMessage": "Invalid Lightning Address"
},
"GrDnue": {
"defaultMessage": "Nutzap Mint Recommendation"
},
"GspYR7": {
"defaultMessage": "{n} Dislike"
},
"GtIxzZ": {
"defaultMessage": "via {client}",
"description": "via {client name} tag"
},
"Gxcr08": {
"defaultMessage": "Broadcast Event"
},
@ -819,6 +865,9 @@
"HqRNN8": {
"defaultMessage": "Support"
},
"Hqo/rL": {
"defaultMessage": "Curated Publication Content"
},
"HzSFeV": {
"defaultMessage": "Expiration Timestamp"
},
@ -1120,6 +1169,9 @@
"P8JC58": {
"defaultMessage": "Distance"
},
"P8zI6H": {
"defaultMessage": "Slide Set"
},
"PCSt5T": {
"defaultMessage": "Preferences"
},
@ -1156,6 +1208,9 @@
"R/6nsx": {
"defaultMessage": "Subscription"
},
"R7x0mX": {
"defaultMessage": "Chat Message"
},
"R81upa": {
"defaultMessage": "People you follow"
},
@ -1228,6 +1283,12 @@
"SmuYUd": {
"defaultMessage": "What should we call you?"
},
"So+Y+A": {
"defaultMessage": "Nutzap"
},
"SopQOK": {
"defaultMessage": "Web bookmarks"
},
"Ss0sWu": {
"defaultMessage": "Pay Now"
},
@ -1286,6 +1347,9 @@
"TwyMau": {
"defaultMessage": "Account"
},
"U/fbvs": {
"defaultMessage": "Git Replies (deprecated)"
},
"U1aPPi": {
"defaultMessage": "Stop listening"
},
@ -1298,6 +1362,9 @@
"ULXFfP": {
"defaultMessage": "Receive"
},
"ULsJTk": {
"defaultMessage": "Published by"
},
"UNjfWJ": {
"defaultMessage": "Check all event signatures received from relays"
},
@ -1331,6 +1398,9 @@
"V20Og0": {
"defaultMessage": "Labeling"
},
"V93INS": {
"defaultMessage": "<dark>Created by</dark> {name}"
},
"VOjC1i": {
"defaultMessage": "Pick which upload service you want to upload attachments to"
},
@ -1361,6 +1431,9 @@
"W9355R": {
"defaultMessage": "Unmute"
},
"WTrOy3": {
"defaultMessage": "Chat"
},
"WeLEuL": {
"defaultMessage": "From Server"
},
@ -1415,6 +1488,9 @@
"YH2RKk": {
"defaultMessage": "Popular media servers."
},
"YLGfQn": {
"defaultMessage": "Write message"
},
"YQZY/S": {
"defaultMessage": "It looks like you dont follow enough people, take a look at {newUsersPage} to discover people to follow!"
},
@ -1436,9 +1512,6 @@
"Z48UEo": {
"defaultMessage": "Channel Metadata"
},
"Z4BMCZ": {
"defaultMessage": "Enter pairing phrase"
},
"Z7kkeJ": {
"defaultMessage": "Delegated Event Signing"
},
@ -1454,6 +1527,9 @@
"ZS+jRE": {
"defaultMessage": "Send zap splits to"
},
"ZT17bG": {
"defaultMessage": "Cashu Wallet Tokens"
},
"Zff6lu": {
"defaultMessage": "Username iris.to/<b>{name}</b> is reserved for you!"
},
@ -1481,9 +1557,6 @@
"aRex7h": {
"defaultMessage": "Paid {amount} sats, fee {fee} sats"
},
"aSGz4J": {
"defaultMessage": "Connect to your own LND node with Lightning Node Connect"
},
"aWpBzj": {
"defaultMessage": "Show more"
},
@ -1544,6 +1617,12 @@
"c3g2hL": {
"defaultMessage": "Broadcast Again"
},
"c6BMLV": {
"defaultMessage": "Starter Pack"
},
"cF3ruj": {
"defaultMessage": "Follow All"
},
"cFbU1B": {
"defaultMessage": "Using Alby? Go to {link} to get your NWC config!"
},
@ -1605,9 +1684,6 @@
"dOQCL8": {
"defaultMessage": "Display name"
},
"dZZIGe": {
"defaultMessage": "Modular Article Header"
},
"ddd3JX": {
"defaultMessage": "Popular Hashtags"
},
@ -1725,6 +1801,9 @@
"gDzDRs": {
"defaultMessage": "Emoji to send when reactiong to a note"
},
"gPxSgn": {
"defaultMessage": "Follow Sets"
},
"gXgY3+": {
"defaultMessage": "Not all clients support this yet"
},
@ -1749,9 +1828,15 @@
"grQ+mI": {
"defaultMessage": "Proof of Work"
},
"grRQTM": {
"defaultMessage": "{n} people"
},
"gtNjNP": {
"defaultMessage": "Basic protocol flow description"
},
"h1gtUi": {
"defaultMessage": "Poll"
},
"h7jvCs": {
"defaultMessage": "{site} is more fun together!"
},
@ -1929,9 +2014,6 @@
"lPWASz": {
"defaultMessage": "Snort nostr address"
},
"lTbT3s": {
"defaultMessage": "Wallet password"
},
"lbr3Lq": {
"defaultMessage": "Copy link"
},
@ -2124,6 +2206,9 @@
"qtWLmt": {
"defaultMessage": "Like"
},
"qx+v3H": {
"defaultMessage": "Request to Vanish"
},
"qyJtWy": {
"defaultMessage": "Show less"
},
@ -2190,6 +2275,9 @@
"saorw+": {
"defaultMessage": "Event Deletion Request"
},
"sbSCT3": {
"defaultMessage": "Private event relay list"
},
"sfL/O+": {
"defaultMessage": "Muted notes will not be shown"
},
@ -2214,9 +2302,6 @@
"tU0ADf": {
"defaultMessage": "Unknown NIP-{x}"
},
"tVuVg9": {
"defaultMessage": "Video View Event"
},
"tf1lIh": {
"defaultMessage": "Free"
},
@ -2265,6 +2350,9 @@
"uc0din": {
"defaultMessage": "Send sats splits to"
},
"uex/ui": {
"defaultMessage": "external web reference"
},
"ufvXH1": {
"defaultMessage": "Found {n} events"
},
@ -2325,9 +2413,6 @@
"wc9st7": {
"defaultMessage": "Media Attachments"
},
"whSrs+": {
"defaultMessage": "Nostr Public Chat"
},
"wih7iJ": {
"defaultMessage": "name is blocked"
},
@ -2343,6 +2428,9 @@
"wtLjP6": {
"defaultMessage": "Copy ID"
},
"wvoA3H": {
"defaultMessage": "Picture"
},
"x+3fl6": {
"defaultMessage": "My Relays"
},

View File

@ -1,24 +1,25 @@
import { removeUndefined, throwIfOffline } from "@snort/shared";
import { mapEventToProfile, NostrEvent, NostrSystem } from "@snort/system";
import { EventsCache, Relay, RelayMetrics, SystemDb, UserCache, UserFollows, UserRelays } from "@/Cache";
import { addEventToFuzzySearch } from "@/Db/FuzzySearch";
import { LoginStore } from "@/Utils/Login";
import { hasWasm, WasmOptimizer } from "@/Utils/wasm";
/**
* Singleton nostr system
*/
export const System = new NostrSystem({
//relays: UserRelays,
//events: EventsCache,
//profiles: UserCache,
//relayMetrics: RelayMetrics,
//cachingRelay: Relay,
//contactLists: UserFollows,
//optimizer: hasWasm ? WasmOptimizer : undefined,
//db: SystemDb,
relays: UserRelays,
events: EventsCache,
profiles: UserCache,
relayMetrics: RelayMetrics,
cachingRelay: Relay,
contactLists: UserFollows,
optimizer: hasWasm ? WasmOptimizer : undefined,
db: SystemDb,
buildFollowGraph: true,
automaticOutboxModel: true,
checkSigs: false,
});
System.on("auth", async (c, r, cb) => {
@ -30,8 +31,8 @@ System.on("auth", async (c, r, cb) => {
});
System.on("event", (_, ev) => {
//EventsCache.discover(ev);
//UserCache.discover(ev);
EventsCache.discover(ev);
UserCache.discover(ev);
addEventToFuzzySearch(ev);
});

View File

@ -5,7 +5,10 @@
"+QMdsy": "Relay Stats",
"+UjDmN": "Logged in with write access",
"+Vxixo": "Secret Group Chat",
"+W+Kof": "Cashu Wallet History",
"+aZY2h": "Zap Type",
"+mO9i4": "Code Snippet",
"+sDqqq": "Peer-to-peer Order events",
"+tShPg": "following",
"+vA//S": "Logins",
"+vIQlC": "Please make sure to save the following password in order to manage your handle in the future",
@ -17,7 +20,6 @@
"/JE/X+": "Account Support",
"/T7HId": "HTTP File Storage Integration",
"/Xf4UW": "Send anonymous usage metrics",
"/b1IHW": "Group Chat Message",
"/d6vEc": "Make your profile easier to find and share",
"/ioUrF": "From File",
"/n5KSF": "{n} ms",
@ -30,15 +32,16 @@
"0jOEtS": "Invalid LNURL",
"0kOBMu": "Handling Mentions",
"0mch2Y": "name has disallowed characters",
"0oMk/p": "Other Chats",
"0siT4z": "Politics",
"0uoY11": "Show Status",
"0yO7wF": "{n} secs",
"0zASjL": "Go",
"1/BFEj": "git stuff",
"1GvBMj": "internal reference",
"1Mo59U": "Are you sure you want to remove this note from bookmarks?",
"1R43+L": "Enter Nostr Wallet Connect config",
"1UWegE": "Be sure to back up your keys!",
"1c4YST": "Connected to: {node} 🎉",
"1nYUGC": "{n} Following",
"1o2BgB": "Check Signatures",
"1ozeyg": "Nature",
@ -51,9 +54,9 @@
"2BBGxX": "Subject tag in text events",
"2HIqeO": "User emoji list",
"2IFGap": "Donate",
"2LbrkB": "Enter password",
"2O2sfp": "Finish",
"2Qsf9/": "Generic lists",
"2YxhJx": "Reserved Cashu Wallet Tokens",
"2a2YiP": "{n} Bookmarks",
"2k0Cv+": "Dislikes ({n})",
"2mcwT8": "New Note",
@ -61,7 +64,6 @@
"2raFAu": "Application-specific data",
"2ukA4d": "{n} hours",
"2z7Kky": "Latest Articles",
"3/onCd": "Replies",
"39AHJm": "Sign Up",
"3GWu6/": "User Statuses",
"3KNMbJ": "Articles",
@ -70,7 +72,6 @@
"3adEeb": "{n} viewers",
"3cc4Ct": "Light",
"3gOsZq": "Translators",
"3kbIhS": "Untitled",
"3qnJlS": "You are voting with {amount} sats",
"3t3kok": "{n,plural,=1{{n} new note} other{{n} new notes}}",
"3tVy+Z": "{n} Followers",
@ -85,11 +86,15 @@
"4L2vUY": "Your new NIP-05 handle is:",
"4MjsHk": "Life",
"4OB335": "Dislike",
"4OQuna": "Draft Event",
"4P/kKm": "Private Key Encryption",
"4Vmpt4": "Nostr Plebs is one of the first NIP-05 providers in the space and offers a good collection of domains at reasonable prices",
"4Z3t5i": "Use imgproxy to compress images",
"4emo2p": "Missing Relays",
"4oPRxH": "Supported Kinds:",
"4rYCjn": "Note to Self",
"4rxi5n": "hardcopy reference",
"4tKMJS": "Software Application",
"4wgYpI": "Recommended Application Handlers",
"5BVs2e": "zap",
"5CB6zB": "Zap Splits",
@ -110,8 +115,11 @@
"6KGebm": "Seal",
"6OSOXl": "Reason: <i>{reason}</i>",
"6WWD34": "Looking for: {noteId}",
"6Z3bDG": "Cashu Wallet Event",
"6bgpn+": "Not all clients support this, you may still receive some zaps as if zap splits was not configured",
"6dmn4m": "Thread",
"6ewQqw": "Likes ({n})",
"6m1Zkw": "Relay Monitor Announcement",
"6mr8WU": "Followed by",
"6pdxsi": "Extra metadata fields and tags",
"6uMqL1": "Unpaid",
@ -127,35 +135,35 @@
"7YkSA2": "Community Leader",
"7gMmSL": "Reaction",
"7hp70g": "NIP-05",
"7jfPsW": "Modular Article Content",
"7nAz/z": "Mute notes from people who are outside your web of trust",
"7pFGAQ": "Close Relays",
"8/vBbP": "Reposts ({n})",
"89q5wc": "Confirm Reposts",
"8BDFvJ": "Conventions for clients' use of e and p tags in text events",
"8Cw1Fj": "Link Set",
"8ED/4u": "Reply To",
"8HJxXG": "Sign up",
"8QDesP": "Zap {n} sats",
"8Rkoyb": "Recipient",
"8Y6bZQ": "Invalid zap split: {input}",
"8ZGqWl": "Group Thread",
"8g2vyB": "name too long",
"8jmwT8": "bech32-encoded entities",
"8v1NN+": "Pairing phrase",
"8xdDLn": "Follow sets",
"8za9Pq": "Draft Classified Listing",
"9+Ddtu": "Next",
"92gdbw": "Relay Discovery",
"9HU8vw": "Reply",
"9RNiUn": "View Feed",
"9SvQep": "Follows {n}",
"9V0wg3": "Calendar Event RSVP",
"9WRlF4": "Send",
"9WTAKy": "Search sets..",
"9kO0VQ": "Hide muted notes",
"9kSari": "Retry publishing",
"9pMqYs": "Nostr Address",
"9wO4wJ": "Lightning Invoice",
"A86fJ+": "Generic Repost",
"ADmfQT": "Parent",
"ALdW69": "Note by {name}",
"AN0Z7Q": "Muted Words",
"ASRK0S": "This author has been muted",
"AedFVZ": "Create or update a product",
@ -168,6 +176,7 @@
"Awq32I": "Push notifications",
"AxDOiG": "Months",
"AyGauy": "Login",
"Ayx8rG": "Curated Publication Index",
"B4C47Y": "name too short",
"B6+XJy": "zapped",
"B6H7eJ": "nsec, npub, nip-05, hex",
@ -185,12 +194,13 @@
"C7642/": "Quote Repost",
"C81/uG": "Logout",
"C8FsOr": "Popular Servers",
"C8HhVE": "Suggested Follows",
"CA1efg": "Video sets",
"CHTbO3": "Failed to load invoice",
"CJ0biq": "Poll Response",
"CJx5Nd": "Profile Zaps",
"CM+Cfj": "Follow List",
"CM0k0d": "Prune follow list",
"CSOaM+": "{note_type} by {name}{title}",
"CVWeJ6": "Trending People",
"CYkOCI": "and {count} others you follow",
"Cdxwi0": "Repository announcements",
@ -250,12 +260,17 @@
"GFOoEE": "Salt",
"GIqktu": "Supported NIPs",
"GL8aXW": "Bookmarks ({n})",
"GLdw/8": "File Message",
"GQo+OV": "prompt reference",
"GSye7T": "Lightning Address",
"GUlSVG": "Claim your included Snort nostr address",
"Gcn9NQ": "Magnet Link",
"GpkNYn": "Torrent",
"GqKcVm": "App curation sets",
"GqQeu/": "Invalid Lightning Address",
"GrDnue": "Nutzap Mint Recommendation",
"GspYR7": "{n} Dislike",
"GtIxzZ": "via {client}",
"Gxcr08": "Broadcast Event",
"H+vHiz": "Hex Key..",
"H/oroO": "Dealing with Unknown Events",
@ -271,6 +286,7 @@
"HhcAVH": "You don't follow this person, click here to load media from <i>{link}</i>, or update <a><i>your preferences</i></a> to always load media from everybody.",
"HpAmQZ": "Relay reviews",
"HqRNN8": "Support",
"Hqo/rL": "Curated Publication Content",
"HzSFeV": "Expiration Timestamp",
"I0tYZf": "Create or update a stall",
"I1AoOu": "Last post {time}",
@ -371,6 +387,7 @@
"P7FD0F": "System (Default)",
"P7nJT9": "Total today (UTC): {amount} sats",
"P8JC58": "Distance",
"P8zI6H": "Slide Set",
"PCSt5T": "Preferences",
"PXQ0z0": "Receiving to <b>{wallet}</b>",
"PamNxw": "Unknown file header: {name}",
@ -383,6 +400,7 @@
"Qxv0B2": "You currently have {number} sats in your zap pool.",
"Qy6/Ft": "Private Direct Messages",
"R/6nsx": "Subscription",
"R7x0mX": "Chat Message",
"R81upa": "People you follow",
"RDha9y": "Service Worker Not Running",
"RRz1cA": "Repository state announcements",
@ -407,6 +425,8 @@
"ShdEie": "Mark all read",
"Sjo1P4": "Custom",
"SmuYUd": "What should we call you?",
"So+Y+A": "Nutzap",
"SopQOK": "Web bookmarks",
"Ss0sWu": "Pay Now",
"SsUQnC": "Application-specific Data",
"StKzTE": "The author has marked this note as a <i>sensitive topic</i>",
@ -426,10 +446,12 @@
"Tpy00S": "People",
"TvKqBp": "liked",
"TwyMau": "Account",
"U/fbvs": "Git Replies (deprecated)",
"U1aPPi": "Stop listening",
"U30H69": "Community Definition",
"UJTWqI": "Remove from my relays",
"ULXFfP": "Receive",
"ULsJTk": "Published by",
"UNjfWJ": "Check all event signatures received from relays",
"UT7Nkj": "New Chat",
"UUPFlt": "Users must accept the content warning to show the content of your note.",
@ -441,6 +463,7 @@
"UsCzPc": "Share a personalized invitation with friends!",
"UxgyeY": "Your referral code is {code}",
"V20Og0": "Labeling",
"V93INS": "<dark>Created by</dark> {name}",
"VOjC1i": "Pick which upload service you want to upload attachments to",
"VR5eHw": "Public key (npub/nprofile)",
"VcwrfF": "Yes please",
@ -451,6 +474,7 @@
"W2PiAr": "{n} Blocked",
"W4SaxY": "Local",
"W9355R": "Unmute",
"WTrOy3": "Chat",
"WeLEuL": "From Server",
"Wj5TbN": "Issues",
"WmZhfL": "Automatically translate notes to your local language",
@ -469,6 +493,7 @@
"YDMrKK": "Users",
"YDURw6": "Service URL",
"YH2RKk": "Popular media servers.",
"YLGfQn": "Write message",
"YQZY/S": "It looks like you dont follow enough people, take a look at {newUsersPage} to discover people to follow!",
"YR2I9M": "No keys, no {app}, There is no way to reset it if you don't back up. It only takes a minute.",
"YU7ZYp": "Public Chat",
@ -476,12 +501,12 @@
"Yf3DwC": "Connect a wallet to send instant payments",
"YuoEb9": "Try another relay",
"Z48UEo": "Channel Metadata",
"Z4BMCZ": "Enter pairing phrase",
"Z7kkeJ": "Delegated Event Signing",
"ZFe9tl": "Compose a note",
"ZKORll": "Activate Now",
"ZLmyG9": "Contributors",
"ZS+jRE": "Send zap splits to",
"ZT17bG": "Cashu Wallet Tokens",
"Zff6lu": "Username iris.to/<b>{name}</b> is reserved for you!",
"ZlIh4/": "Encrypted Direct Messages",
"ZlmK/p": "{name} invited you to {app}",
@ -491,7 +516,6 @@
"aHje0o": "Name or nym",
"aMaLBK": "Supported Extensions",
"aRex7h": "Paid {amount} sats, fee {fee} sats",
"aSGz4J": "Connect to your own LND node with Lightning Node Connect",
"aWpBzj": "Show more",
"abbGKq": "{n} km",
"ak3MTf": "Invite Friends",
@ -512,6 +536,8 @@
"c35bj2": "If you have an enquiry about your NIP-05 order please DM {link}",
"c3LlRO": "{n}KiB",
"c3g2hL": "Broadcast Again",
"c6BMLV": "Starter Pack",
"cF3ruj": "Follow All",
"cFbU1B": "Using Alby? Go to {link} to get your NWC config!",
"cG/bKQ": "Native nostr wallet connection",
"cHCwbF": "Photography",
@ -532,7 +558,6 @@
"d7d0/x": "LN Address",
"dK2CcV": "The public key is like your username, you can share it with anyone.",
"dOQCL8": "Display name",
"dZZIGe": "Modular Article Header",
"ddd3JX": "Popular Hashtags",
"deEeEI": "Register",
"djLctd": "Amount in sats",
@ -572,6 +597,7 @@
"g5pX+a": "About",
"g985Wp": "Failed to send vote",
"gDzDRs": "Emoji to send when reactiong to a note",
"gPxSgn": "Follow Sets",
"gXgY3+": "Not all clients support this yet",
"gczcC5": "Subscribe",
"geppt8": "{count} ({count2} in memory)",
@ -580,7 +606,9 @@
"gl1NeW": "Lists",
"go2/QF": "User server list",
"grQ+mI": "Proof of Work",
"grRQTM": "{n} people",
"gtNjNP": "Basic protocol flow description",
"h1gtUi": "Poll",
"h7jvCs": "{site} is more fun together!",
"h8XMJL": "Badges",
"h9M0rW": "User Metadata",
@ -640,7 +668,6 @@
"lD3+8a": "Pay",
"lEnclp": "My events: {n}",
"lPWASz": "Snort nostr address",
"lTbT3s": "Wallet password",
"lbr3Lq": "Copy link",
"lfOesV": "Non-Zap",
"lgg1KN": "account page",
@ -705,6 +732,7 @@
"qkvYUb": "Add to Profile",
"qmJ8kD": "Translation failed",
"qtWLmt": "Like",
"qx+v3H": "Request to Vanish",
"qyJtWy": "Show less",
"qydxOd": "Science",
"qz9fty": "Incorrect pin",
@ -727,6 +755,7 @@
"sZQzjQ": "Failed to parse zap split: {input}",
"saInmO": "The relay name shown is not the same as the full URL entered.",
"saorw+": "Event Deletion Request",
"sbSCT3": "Private event relay list",
"sfL/O+": "Muted notes will not be shown",
"t79a6U": "Connection Success:",
"tDDiRL": "Interests list",
@ -735,7 +764,6 @@
"tOdNiY": "Dark",
"tRGdV1": "Versioned Encryption",
"tU0ADf": "Unknown NIP-{x}",
"tVuVg9": "Video View Event",
"tf1lIh": "Free",
"th5lxp": "Send note to a subset of your write relays",
"thnRpU": "Getting NIP-05 verified can help:",
@ -752,6 +780,7 @@
"uJaMkO": "Relay list to receive DMs",
"uSV4Ti": "Reposts need to be manually confirmed",
"uc0din": "Send sats splits to",
"uex/ui": "external web reference",
"ufvXH1": "Found {n} events",
"uhu5aG": "Public",
"un1nGw": "{n} notes",
@ -772,12 +801,12 @@
"wOyDTB": "File storage server list",
"wSZR47": "Submit",
"wc9st7": "Media Attachments",
"whSrs+": "Nostr Public Chat",
"wih7iJ": "name is blocked",
"wlWMuh": "Patches",
"wofVHy": "Moderation",
"wqyN/i": "Find out more info about {service} at {link}",
"wtLjP6": "Copy ID",
"wvoA3H": "Picture",
"x+3fl6": "My Relays",
"x/Fx2P": "Fund the services that you use by splitting a portion of all your zaps into a pool of funds!",
"x82IOl": "Mute",

View File

@ -35,7 +35,7 @@ export default defineConfig({
name: "snort",
ifGitSHA: true,
command: "git describe --always --tags",
ifMeta: false,
ifMeta: true,
ifLog: false,
ifGlobal: false,
}),

View File

@ -238,11 +238,13 @@ export function isOffline() {
return !("navigator" in globalThis && globalThis.navigator.onLine);
}
export function isHex(s: string) {
export function isHex(s?: string) {
if (!s) return false;
// 48-57 = 0-9
// 65-90 = A-Z
// 97-122 = a-z
return [...s]
.map(v => v.charCodeAt(0))
.every(v => (v >= 48 && v <= 57) || (v >= 65 && v <= 90) || v >= 97 || v <= 122);
return (
s.length % 2 == 0 &&
[...s].map(v => v.charCodeAt(0)).every(v => (v >= 48 && v <= 57) || (v >= 65 && v <= 90) || v >= 97 || v <= 122)
);
}

View File

@ -1,6 +1,6 @@
{
"name": "@snort/system-react",
"version": "1.6.0",
"version": "1.6.1",
"description": "React hooks for @snort/system",
"main": "dist/index.js",
"module": "src/index.ts",
@ -17,7 +17,7 @@
],
"dependencies": {
"@snort/shared": "^1.0.17",
"@snort/system": "^1.6.0",
"@snort/system": "^1.6.1",
"react": "^18.2.0"
},
"devDependencies": {

View File

@ -31,7 +31,7 @@ export function useReactions(
}
others?.(rb);
return rb;
}, [ids]);
}, [ids, others]);
return useRequestBuilder(sub);
}

View File

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
version = 4
[[package]]
name = "aho-corasick"
@ -19,15 +19,21 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
[[package]]
name = "anstyle"
version = "1.0.8"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "arrayvec"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "async-trait"
version = "0.1.82"
version = "0.1.85"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a27b8a3a6e1a44fa4c8baf1f653e4172e81486d4941f2237e20dc2d0cf4ddff1"
checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056"
dependencies = [
"proc-macro2",
"quote",
@ -36,9 +42,25 @@ dependencies = [
[[package]]
name = "autocfg"
version = "1.3.0"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "bitcoin-io"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b47c4ab7a93edb0c7198c5535ed9b52b63095f4e9b45279c6736cec4b856baf"
[[package]]
name = "bitcoin_hashes"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb18c03d0db0247e147a21a6faafd5a7eb851c743db062de72018b6b7e8e4d16"
dependencies = [
"bitcoin-io",
"hex-conservative",
]
[[package]]
name = "block-buffer"
@ -63,9 +85,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.7.1"
version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50"
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
[[package]]
name = "cast"
@ -75,9 +97,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
[[package]]
name = "cc"
version = "1.1.18"
version = "1.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b62ac837cdb5cb22e10a256099b4fc502b1dfe560cb282963a974d7abd80e476"
checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229"
dependencies = [
"shlex",
]
@ -117,18 +139,18 @@ dependencies = [
[[package]]
name = "clap"
version = "4.5.17"
version = "4.5.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac"
checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.17"
version = "4.5.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73"
checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121"
dependencies = [
"anstyle",
"clap_lex",
@ -136,9 +158,9 @@ dependencies = [
[[package]]
name = "clap_lex"
version = "0.7.2"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "console_error_panic_hook"
@ -152,9 +174,9 @@ dependencies = [
[[package]]
name = "cpufeatures"
version = "0.2.14"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0"
checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
dependencies = [
"libc",
]
@ -197,9 +219,9 @@ dependencies = [
[[package]]
name = "crossbeam-deque"
version = "0.8.5"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
dependencies = [
"crossbeam-epoch",
"crossbeam-utils",
@ -216,9 +238,9 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
version = "0.8.20"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80"
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
[[package]]
name = "crunchy"
@ -295,6 +317,15 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hex-conservative"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5313b072ce3c597065a808dbf612c4c8e8590bdbf8b579508bf7a762c5eae6cd"
dependencies = [
"arrayvec",
]
[[package]]
name = "is-terminal"
version = "0.4.13"
@ -317,39 +348,40 @@ dependencies = [
[[package]]
name = "itertools"
version = "0.13.0"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]]
name = "itoa"
version = "1.0.11"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "js-sys"
version = "0.3.70"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
dependencies = [
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.158"
version = "0.2.169"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
[[package]]
name = "log"
version = "0.4.22"
version = "0.4.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f"
[[package]]
name = "memchr"
@ -359,9 +391,9 @@ checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "minicov"
version = "0.3.5"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c71e683cd655513b99affab7d317deb690528255a0d5f717f1024093c12b169"
checksum = "f27fe9f1cc3c22e1687f9446c2083c4c5fc7f0bcf1c7a86bdbded14985895b4b"
dependencies = [
"cc",
"walkdir",
@ -378,9 +410,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.19.0"
version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
[[package]]
name = "oorandom"
@ -427,18 +459,18 @@ dependencies = [
[[package]]
name = "proc-macro2"
version = "1.0.86"
version = "1.0.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.37"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
dependencies = [
"proc-macro2",
]
@ -495,9 +527,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.10.6"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
@ -507,9 +539,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.7"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
@ -518,9 +550,15 @@ dependencies = [
[[package]]
name = "regex-syntax"
version = "0.8.4"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "rustversion"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
[[package]]
name = "ryu"
@ -537,18 +575,14 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294"
[[package]]
name = "secp256k1"
version = "0.29.1"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113"
checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252"
dependencies = [
"bitcoin_hashes",
"rand",
"secp256k1-sys",
]
@ -563,9 +597,9 @@ dependencies = [
[[package]]
name = "serde"
version = "1.0.210"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70"
dependencies = [
"serde_derive",
]
@ -583,9 +617,9 @@ dependencies = [
[[package]]
name = "serde_derive"
version = "1.0.210"
version = "1.0.217"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [
"proc-macro2",
"quote",
@ -594,9 +628,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.128"
version = "1.0.137"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b"
dependencies = [
"itoa",
"memchr",
@ -635,9 +669,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.77"
version = "2.0.96"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80"
dependencies = [
"proc-macro2",
"quote",
@ -651,7 +685,7 @@ dependencies = [
"console_error_panic_hook",
"criterion",
"hex",
"itertools 0.13.0",
"itertools 0.14.0",
"rand",
"secp256k1",
"serde",
@ -680,9 +714,9 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
[[package]]
name = "unicode-ident"
version = "1.0.13"
version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
[[package]]
name = "version_check"
@ -708,24 +742,24 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.93"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.93"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
@ -734,21 +768,22 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.43"
version = "0.4.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed"
checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
dependencies = [
"cfg-if",
"js-sys",
"once_cell",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.93"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@ -756,9 +791,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.93"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [
"proc-macro2",
"quote",
@ -769,20 +804,21 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.93"
version = "0.2.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-bindgen-test"
version = "0.3.43"
version = "0.3.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68497a05fb21143a08a7d24fc81763384a3072ee43c44e86aad1744d6adef9d9"
checksum = "66c8d5e33ca3b6d9fa3b4676d774c5778031d27a578c2b007f905acf816152c3"
dependencies = [
"console_error_panic_hook",
"js-sys",
"minicov",
"scoped-tls",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-bindgen-test-macro",
@ -790,9 +826,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-test-macro"
version = "0.3.43"
version = "0.3.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4b8220be1fa9e4c889b30fd207d4906657e7e90b12e0e6b0c8b8d8709f5de021"
checksum = "17d5042cc5fa009658f9a7333ef24291b1291a25b6382dd68862a7f3b969f69b"
dependencies = [
"proc-macro2",
"quote",
@ -801,9 +837,9 @@ dependencies = [
[[package]]
name = "web-sys"
version = "0.3.70"
version = "0.3.77"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
dependencies = [
"js-sys",
"wasm-bindgen",

View File

@ -10,8 +10,8 @@ crate-type = ["cdylib", "rlib"]
[dependencies]
console_error_panic_hook = "0.1.7"
hex = { version = "0.4.3", features = [], default-features = false }
itertools = "0.13.0"
secp256k1 = { version = "0.29.0", features = ["global-context"] }
itertools = "0.14.0"
secp256k1 = { version = "0.30.0", features = ["global-context"] }
serde = { version = "1.0.188", features = ["derive"], default-features = false }
serde-wasm-bindgen = "0.6.5"
serde_json = "1.0.105"

View File

@ -1,72 +1,38 @@
/* tslint:disable */
/* eslint-disable */
/**
* @param {any} prev
* @param {any} next
* @returns {any}
*/
export function diff_filters(prev: any, next: any): any;
/**
* @param {any} val
* @returns {any}
*/
export function expand_filter(val: any): any;
/**
* @param {any} prev
* @param {any} next
* @returns {any}
*/
export function get_diff(prev: any, next: any): any;
/**
* @param {any} val
* @returns {any}
*/
export function flat_merge(val: any): any;
/**
* @param {any} val
* @returns {any}
*/
export function compress(val: any): any;
/**
* @param {any} val
* @param {any} target
* @returns {any}
*/
export function pow(val: any, target: any): any;
/**
* @param {any} hash
* @param {any} sig
* @param {any} pub_key
* @returns {boolean}
*/
export function schnorr_verify(hash: any, sig: any, pub_key: any): boolean;
/**
* @param {any} event
* @returns {boolean}
*/
export function schnorr_verify_event(event: any): boolean;
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly diff_filters: (a: number, b: number, c: number) => void;
readonly expand_filter: (a: number, b: number) => void;
readonly get_diff: (a: number, b: number, c: number) => void;
readonly flat_merge: (a: number, b: number) => void;
readonly compress: (a: number, b: number) => void;
readonly pow: (a: number, b: number, c: number) => void;
readonly schnorr_verify: (a: number, b: number, c: number, d: number) => void;
readonly schnorr_verify_event: (a: number, b: number) => void;
readonly diff_filters: (a: any, b: any) => [number, number, number];
readonly expand_filter: (a: any) => [number, number, number];
readonly get_diff: (a: any, b: any) => [number, number, number];
readonly flat_merge: (a: any) => [number, number, number];
readonly compress: (a: any) => [number, number, number];
readonly pow: (a: any, b: any) => [number, number, number];
readonly schnorr_verify: (a: any, b: any, c: any) => [number, number, number];
readonly schnorr_verify_event: (a: any) => [number, number, number];
readonly rustsecp256k1_v0_10_0_context_create: (a: number) => number;
readonly rustsecp256k1_v0_10_0_context_destroy: (a: number) => void;
readonly rustsecp256k1_v0_10_0_default_illegal_callback_fn: (a: number, b: number) => void;
readonly rustsecp256k1_v0_10_0_default_error_callback_fn: (a: number, b: number) => void;
readonly __wbindgen_exn_store: (a: number) => void;
readonly __externref_table_alloc: () => number;
readonly __wbindgen_export_2: WebAssembly.Table;
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
readonly __wbindgen_exn_store: (a: number) => void;
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
readonly __externref_table_dealloc: (a: number) => void;
readonly __wbindgen_start: () => void;
}
export type SyncInitInput = BufferSource | WebAssembly.Module;

View File

@ -1,28 +1,32 @@
let wasm;
const heap = new Array(128).fill(undefined);
heap.push(undefined, null, true, false);
function getObject(idx) {
return heap[idx];
function addToExternrefTable0(obj) {
const idx = wasm.__externref_table_alloc();
wasm.__wbindgen_export_2.set(idx, obj);
return idx;
}
let heap_next = heap.length;
function dropObject(idx) {
if (idx < 132) return;
heap[idx] = heap_next;
heap_next = idx;
function handleError(f, args) {
try {
return f.apply(this, args);
} catch (e) {
const idx = addToExternrefTable0(e);
wasm.__wbindgen_exn_store(idx);
}
}
function takeObject(idx) {
const ret = getObject(idx);
dropObject(idx);
return ret;
}
const cachedTextDecoder =
typeof TextDecoder !== "undefined"
? new TextDecoder("utf-8", { ignoreBOM: true, fatal: true })
: {
decode: () => {
throw Error("TextDecoder not available");
},
};
let WASM_VECTOR_LEN = 0;
if (typeof TextDecoder !== "undefined") {
cachedTextDecoder.decode();
}
let cachedUint8ArrayMemory0 = null;
@ -33,6 +37,13 @@ function getUint8ArrayMemory0() {
return cachedUint8ArrayMemory0;
}
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
}
let WASM_VECTOR_LEN = 0;
const cachedTextEncoder =
typeof TextEncoder !== "undefined"
? new TextEncoder("utf-8")
@ -96,10 +107,6 @@ function passStringToWasm0(arg, malloc, realloc) {
return ptr;
}
function isLikeNone(x) {
return x === undefined || x === null;
}
let cachedDataViewMemory0 = null;
function getDataViewMemory0() {
@ -113,31 +120,8 @@ function getDataViewMemory0() {
return cachedDataViewMemory0;
}
function addHeapObject(obj) {
if (heap_next === heap.length) heap.push(heap.length + 1);
const idx = heap_next;
heap_next = heap[idx];
heap[idx] = obj;
return idx;
}
const cachedTextDecoder =
typeof TextDecoder !== "undefined"
? new TextDecoder("utf-8", { ignoreBOM: true, fatal: true })
: {
decode: () => {
throw Error("TextDecoder not available");
},
};
if (typeof TextDecoder !== "undefined") {
cachedTextDecoder.decode();
}
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
function isLikeNone(x) {
return x === undefined || x === null;
}
function debugString(val) {
@ -181,7 +165,7 @@ function debugString(val) {
// Test for built-in
const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val));
let className;
if (builtInMatches.length > 1) {
if (builtInMatches && builtInMatches.length > 1) {
className = builtInMatches[1];
} else {
// Failed to match the standard '[object ClassName]'
@ -204,25 +188,23 @@ function debugString(val) {
// TODO we could test for more things here, like `Set`s and `Map`s.
return className;
}
function takeFromExternrefTable0(idx) {
const value = wasm.__wbindgen_export_2.get(idx);
wasm.__externref_table_dealloc(idx);
return value;
}
/**
* @param {any} prev
* @param {any} next
* @returns {any}
*/
export function diff_filters(prev, next) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
wasm.diff_filters(retptr, addHeapObject(prev), addHeapObject(next));
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
if (r2) {
throw takeObject(r1);
}
return takeObject(r0);
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
const ret = wasm.diff_filters(prev, next);
if (ret[2]) {
throw takeFromExternrefTable0(ret[1]);
}
return takeFromExternrefTable0(ret[0]);
}
/**
@ -230,19 +212,11 @@ export function diff_filters(prev, next) {
* @returns {any}
*/
export function expand_filter(val) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
wasm.expand_filter(retptr, addHeapObject(val));
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
if (r2) {
throw takeObject(r1);
}
return takeObject(r0);
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
const ret = wasm.expand_filter(val);
if (ret[2]) {
throw takeFromExternrefTable0(ret[1]);
}
return takeFromExternrefTable0(ret[0]);
}
/**
@ -251,19 +225,11 @@ export function expand_filter(val) {
* @returns {any}
*/
export function get_diff(prev, next) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
wasm.get_diff(retptr, addHeapObject(prev), addHeapObject(next));
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
if (r2) {
throw takeObject(r1);
}
return takeObject(r0);
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
const ret = wasm.get_diff(prev, next);
if (ret[2]) {
throw takeFromExternrefTable0(ret[1]);
}
return takeFromExternrefTable0(ret[0]);
}
/**
@ -271,19 +237,11 @@ export function get_diff(prev, next) {
* @returns {any}
*/
export function flat_merge(val) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
wasm.flat_merge(retptr, addHeapObject(val));
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
if (r2) {
throw takeObject(r1);
}
return takeObject(r0);
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
const ret = wasm.flat_merge(val);
if (ret[2]) {
throw takeFromExternrefTable0(ret[1]);
}
return takeFromExternrefTable0(ret[0]);
}
/**
@ -291,19 +249,11 @@ export function flat_merge(val) {
* @returns {any}
*/
export function compress(val) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
wasm.compress(retptr, addHeapObject(val));
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
if (r2) {
throw takeObject(r1);
}
return takeObject(r0);
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
const ret = wasm.compress(val);
if (ret[2]) {
throw takeFromExternrefTable0(ret[1]);
}
return takeFromExternrefTable0(ret[0]);
}
/**
@ -312,19 +262,11 @@ export function compress(val) {
* @returns {any}
*/
export function pow(val, target) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
wasm.pow(retptr, addHeapObject(val), addHeapObject(target));
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
if (r2) {
throw takeObject(r1);
}
return takeObject(r0);
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
const ret = wasm.pow(val, target);
if (ret[2]) {
throw takeFromExternrefTable0(ret[1]);
}
return takeFromExternrefTable0(ret[0]);
}
/**
@ -334,19 +276,11 @@ export function pow(val, target) {
* @returns {boolean}
*/
export function schnorr_verify(hash, sig, pub_key) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
wasm.schnorr_verify(retptr, addHeapObject(hash), addHeapObject(sig), addHeapObject(pub_key));
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
if (r2) {
throw takeObject(r1);
}
return r0 !== 0;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
const ret = wasm.schnorr_verify(hash, sig, pub_key);
if (ret[2]) {
throw takeFromExternrefTable0(ret[1]);
}
return ret[0] !== 0;
}
/**
@ -354,27 +288,11 @@ export function schnorr_verify(hash, sig, pub_key) {
* @returns {boolean}
*/
export function schnorr_verify_event(event) {
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
wasm.schnorr_verify_event(retptr, addHeapObject(event));
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
var r2 = getDataViewMemory0().getInt32(retptr + 4 * 2, true);
if (r2) {
throw takeObject(r1);
}
return r0 !== 0;
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
}
}
function handleError(f, args) {
try {
return f.apply(this, args);
} catch (e) {
wasm.__wbindgen_exn_store(addHeapObject(e));
const ret = wasm.schnorr_verify_event(event);
if (ret[2]) {
throw takeFromExternrefTable0(ret[1]);
}
return ret[0] !== 0;
}
async function __wbg_load(module, imports) {
@ -385,7 +303,7 @@ async function __wbg_load(module, imports) {
} catch (e) {
if (module.headers.get("Content-Type") != "application/wasm") {
console.warn(
"`WebAssembly.instantiateStreaming` failed because your server does not serve wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",
"`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n",
e,
);
} else {
@ -410,196 +328,21 @@ async function __wbg_load(module, imports) {
function __wbg_get_imports() {
const imports = {};
imports.wbg = {};
imports.wbg.__wbindgen_object_drop_ref = function (arg0) {
takeObject(arg0);
};
imports.wbg.__wbindgen_string_get = function (arg0, arg1) {
const obj = getObject(arg1);
const ret = typeof obj === "string" ? obj : undefined;
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
};
imports.wbg.__wbindgen_is_object = function (arg0) {
const val = getObject(arg0);
const ret = typeof val === "object" && val !== null;
imports.wbg.__wbg_buffer_609cc3eee51ed158 = function (arg0) {
const ret = arg0.buffer;
return ret;
};
imports.wbg.__wbindgen_is_undefined = function (arg0) {
const ret = getObject(arg0) === undefined;
return ret;
};
imports.wbg.__wbindgen_in = function (arg0, arg1) {
const ret = getObject(arg0) in getObject(arg1);
return ret;
};
imports.wbg.__wbindgen_is_bigint = function (arg0) {
const ret = typeof getObject(arg0) === "bigint";
return ret;
};
imports.wbg.__wbindgen_bigint_from_u64 = function (arg0) {
const ret = BigInt.asUintN(64, arg0);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_jsval_eq = function (arg0, arg1) {
const ret = getObject(arg0) === getObject(arg1);
return ret;
};
imports.wbg.__wbindgen_error_new = function (arg0, arg1) {
const ret = new Error(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
};
imports.wbg.__wbindgen_jsval_loose_eq = function (arg0, arg1) {
const ret = getObject(arg0) == getObject(arg1);
return ret;
};
imports.wbg.__wbindgen_boolean_get = function (arg0) {
const v = getObject(arg0);
const ret = typeof v === "boolean" ? (v ? 1 : 0) : 2;
return ret;
};
imports.wbg.__wbindgen_number_get = function (arg0, arg1) {
const obj = getObject(arg1);
const ret = typeof obj === "number" ? obj : undefined;
getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
};
imports.wbg.__wbindgen_as_number = function (arg0) {
const ret = +getObject(arg0);
return ret;
};
imports.wbg.__wbindgen_number_new = function (arg0) {
const ret = arg0;
return addHeapObject(ret);
};
imports.wbg.__wbindgen_object_clone_ref = function (arg0) {
const ret = getObject(arg0);
return addHeapObject(ret);
};
imports.wbg.__wbindgen_string_new = function (arg0, arg1) {
const ret = getStringFromWasm0(arg0, arg1);
return addHeapObject(ret);
};
imports.wbg.__wbg_getwithrefkey_edc2c8960f0f1191 = function (arg0, arg1) {
const ret = getObject(arg0)[getObject(arg1)];
return addHeapObject(ret);
};
imports.wbg.__wbg_set_f975102236d3c502 = function (arg0, arg1, arg2) {
getObject(arg0)[takeObject(arg1)] = takeObject(arg2);
};
imports.wbg.__wbg_get_3baa728f9d58d3f6 = function (arg0, arg1) {
const ret = getObject(arg0)[arg1 >>> 0];
return addHeapObject(ret);
};
imports.wbg.__wbg_length_ae22078168b726f5 = function (arg0) {
const ret = getObject(arg0).length;
return ret;
};
imports.wbg.__wbg_new_a220cf903aa02ca2 = function () {
const ret = new Array();
return addHeapObject(ret);
};
imports.wbg.__wbindgen_is_function = function (arg0) {
const ret = typeof getObject(arg0) === "function";
return ret;
};
imports.wbg.__wbg_next_de3e9db4440638b2 = function (arg0) {
const ret = getObject(arg0).next;
return addHeapObject(ret);
};
imports.wbg.__wbg_next_f9cb570345655b9a = function () {
return handleError(function (arg0) {
const ret = getObject(arg0).next();
return addHeapObject(ret);
}, arguments);
};
imports.wbg.__wbg_done_bfda7aa8f252b39f = function (arg0) {
const ret = getObject(arg0).done;
return ret;
};
imports.wbg.__wbg_value_6d39332ab4788d86 = function (arg0) {
const ret = getObject(arg0).value;
return addHeapObject(ret);
};
imports.wbg.__wbg_iterator_888179a48810a9fe = function () {
const ret = Symbol.iterator;
return addHeapObject(ret);
};
imports.wbg.__wbg_get_224d16597dbbfd96 = function () {
imports.wbg.__wbg_call_672a4d21634d4a24 = function () {
return handleError(function (arg0, arg1) {
const ret = Reflect.get(getObject(arg0), getObject(arg1));
return addHeapObject(ret);
const ret = arg0.call(arg1);
return ret;
}, arguments);
};
imports.wbg.__wbg_call_1084a111329e68ce = function () {
return handleError(function (arg0, arg1) {
const ret = getObject(arg0).call(getObject(arg1));
return addHeapObject(ret);
}, arguments);
};
imports.wbg.__wbg_new_525245e2b9901204 = function () {
const ret = new Object();
return addHeapObject(ret);
};
imports.wbg.__wbg_set_673dda6c73d19609 = function (arg0, arg1, arg2) {
getObject(arg0)[arg1 >>> 0] = takeObject(arg2);
};
imports.wbg.__wbg_isArray_8364a5371e9737d8 = function (arg0) {
const ret = Array.isArray(getObject(arg0));
imports.wbg.__wbg_done_769e5ede4b31c67b = function (arg0) {
const ret = arg0.done;
return ret;
};
imports.wbg.__wbg_instanceof_ArrayBuffer_61dfc3198373c902 = function (arg0) {
let result;
try {
result = getObject(arg0) instanceof ArrayBuffer;
} catch (_) {
result = false;
}
const ret = result;
return ret;
};
imports.wbg.__wbg_isSafeInteger_7f1ed56200d90674 = function (arg0) {
const ret = Number.isSafeInteger(getObject(arg0));
return ret;
};
imports.wbg.__wbg_buffer_b7b08af79b0b0974 = function (arg0) {
const ret = getObject(arg0).buffer;
return addHeapObject(ret);
};
imports.wbg.__wbg_new_ea1883e1e5e86686 = function (arg0) {
const ret = new Uint8Array(getObject(arg0));
return addHeapObject(ret);
};
imports.wbg.__wbg_set_d1e79e2388520f18 = function (arg0, arg1, arg2) {
getObject(arg0).set(getObject(arg1), arg2 >>> 0);
};
imports.wbg.__wbg_length_8339fcf5d8ecd12e = function (arg0) {
const ret = getObject(arg0).length;
return ret;
};
imports.wbg.__wbg_instanceof_Uint8Array_247a91427532499e = function (arg0) {
let result;
try {
result = getObject(arg0) instanceof Uint8Array;
} catch (_) {
result = false;
}
const ret = result;
return ret;
};
imports.wbg.__wbg_new_abda76e883ba8a5f = function () {
const ret = new Error();
return addHeapObject(ret);
};
imports.wbg.__wbg_stack_658279fe44541cf6 = function (arg0, arg1) {
const ret = getObject(arg1).stack;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
};
imports.wbg.__wbg_error_f851667af71bcfc6 = function (arg0, arg1) {
imports.wbg.__wbg_error_7534b8e9a36f1ab4 = function (arg0, arg1) {
let deferred0_0;
let deferred0_1;
try {
@ -610,25 +353,202 @@ function __wbg_get_imports() {
wasm.__wbindgen_free(deferred0_0, deferred0_1, 1);
}
};
imports.wbg.__wbindgen_bigint_get_as_i64 = function (arg0, arg1) {
const v = getObject(arg1);
const ret = typeof v === "bigint" ? v : undefined;
getDataViewMemory0().setBigInt64(arg0 + 8 * 1, isLikeNone(ret) ? BigInt(0) : ret, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
imports.wbg.__wbg_get_67b2ba62fc30de12 = function () {
return handleError(function (arg0, arg1) {
const ret = Reflect.get(arg0, arg1);
return ret;
}, arguments);
};
imports.wbg.__wbindgen_debug_string = function (arg0, arg1) {
const ret = debugString(getObject(arg1));
imports.wbg.__wbg_get_b9b93047fe3cf45b = function (arg0, arg1) {
const ret = arg0[arg1 >>> 0];
return ret;
};
imports.wbg.__wbg_getwithrefkey_1dc361bd10053bfe = function (arg0, arg1) {
const ret = arg0[arg1];
return ret;
};
imports.wbg.__wbg_instanceof_ArrayBuffer_e14585432e3737fc = function (arg0) {
let result;
try {
result = arg0 instanceof ArrayBuffer;
} catch (_) {
result = false;
}
const ret = result;
return ret;
};
imports.wbg.__wbg_instanceof_Uint8Array_17156bcf118086a9 = function (arg0) {
let result;
try {
result = arg0 instanceof Uint8Array;
} catch (_) {
result = false;
}
const ret = result;
return ret;
};
imports.wbg.__wbg_isArray_a1eab7e0d067391b = function (arg0) {
const ret = Array.isArray(arg0);
return ret;
};
imports.wbg.__wbg_isSafeInteger_343e2beeeece1bb0 = function (arg0) {
const ret = Number.isSafeInteger(arg0);
return ret;
};
imports.wbg.__wbg_iterator_9a24c88df860dc65 = function () {
const ret = Symbol.iterator;
return ret;
};
imports.wbg.__wbg_length_a446193dc22c12f8 = function (arg0) {
const ret = arg0.length;
return ret;
};
imports.wbg.__wbg_length_e2d2a49132c1b256 = function (arg0) {
const ret = arg0.length;
return ret;
};
imports.wbg.__wbg_new_405e22f390576ce2 = function () {
const ret = new Object();
return ret;
};
imports.wbg.__wbg_new_78feb108b6472713 = function () {
const ret = new Array();
return ret;
};
imports.wbg.__wbg_new_8a6f238a6ece86ea = function () {
const ret = new Error();
return ret;
};
imports.wbg.__wbg_new_a12002a7f91c75be = function (arg0) {
const ret = new Uint8Array(arg0);
return ret;
};
imports.wbg.__wbg_next_25feadfc0913fea9 = function (arg0) {
const ret = arg0.next;
return ret;
};
imports.wbg.__wbg_next_6574e1a8a62d1055 = function () {
return handleError(function (arg0) {
const ret = arg0.next();
return ret;
}, arguments);
};
imports.wbg.__wbg_set_37837023f3d740e8 = function (arg0, arg1, arg2) {
arg0[arg1 >>> 0] = arg2;
};
imports.wbg.__wbg_set_3f1d0b984ed272ed = function (arg0, arg1, arg2) {
arg0[arg1] = arg2;
};
imports.wbg.__wbg_set_65595bdd868b3009 = function (arg0, arg1, arg2) {
arg0.set(arg1, arg2 >>> 0);
};
imports.wbg.__wbg_stack_0ed75d68575b0f3c = function (arg0, arg1) {
const ret = arg1.stack;
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
};
imports.wbg.__wbindgen_throw = function (arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
imports.wbg.__wbg_value_cd1ffa7b1ab794f1 = function (arg0) {
const ret = arg0.value;
return ret;
};
imports.wbg.__wbindgen_as_number = function (arg0) {
const ret = +arg0;
return ret;
};
imports.wbg.__wbindgen_bigint_from_u64 = function (arg0) {
const ret = BigInt.asUintN(64, arg0);
return ret;
};
imports.wbg.__wbindgen_bigint_get_as_i64 = function (arg0, arg1) {
const v = arg1;
const ret = typeof v === "bigint" ? v : undefined;
getDataViewMemory0().setBigInt64(arg0 + 8 * 1, isLikeNone(ret) ? BigInt(0) : ret, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
};
imports.wbg.__wbindgen_boolean_get = function (arg0) {
const v = arg0;
const ret = typeof v === "boolean" ? (v ? 1 : 0) : 2;
return ret;
};
imports.wbg.__wbindgen_debug_string = function (arg0, arg1) {
const ret = debugString(arg1);
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
};
imports.wbg.__wbindgen_error_new = function (arg0, arg1) {
const ret = new Error(getStringFromWasm0(arg0, arg1));
return ret;
};
imports.wbg.__wbindgen_in = function (arg0, arg1) {
const ret = arg0 in arg1;
return ret;
};
imports.wbg.__wbindgen_init_externref_table = function () {
const table = wasm.__wbindgen_export_2;
const offset = table.grow(4);
table.set(0, undefined);
table.set(offset + 0, undefined);
table.set(offset + 1, null);
table.set(offset + 2, true);
table.set(offset + 3, false);
};
imports.wbg.__wbindgen_is_bigint = function (arg0) {
const ret = typeof arg0 === "bigint";
return ret;
};
imports.wbg.__wbindgen_is_function = function (arg0) {
const ret = typeof arg0 === "function";
return ret;
};
imports.wbg.__wbindgen_is_object = function (arg0) {
const val = arg0;
const ret = typeof val === "object" && val !== null;
return ret;
};
imports.wbg.__wbindgen_is_undefined = function (arg0) {
const ret = arg0 === undefined;
return ret;
};
imports.wbg.__wbindgen_jsval_eq = function (arg0, arg1) {
const ret = arg0 === arg1;
return ret;
};
imports.wbg.__wbindgen_jsval_loose_eq = function (arg0, arg1) {
const ret = arg0 == arg1;
return ret;
};
imports.wbg.__wbindgen_memory = function () {
const ret = wasm.memory;
return addHeapObject(ret);
return ret;
};
imports.wbg.__wbindgen_number_get = function (arg0, arg1) {
const obj = arg1;
const ret = typeof obj === "number" ? obj : undefined;
getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
};
imports.wbg.__wbindgen_number_new = function (arg0) {
const ret = arg0;
return ret;
};
imports.wbg.__wbindgen_string_get = function (arg0, arg1) {
const obj = arg1;
const ret = typeof obj === "string" ? obj : undefined;
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
};
imports.wbg.__wbindgen_string_new = function (arg0, arg1) {
const ret = getStringFromWasm0(arg0, arg1);
return ret;
};
imports.wbg.__wbindgen_throw = function (arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};
return imports;
@ -642,14 +562,20 @@ function __wbg_finalize_init(instance, module) {
cachedDataViewMemory0 = null;
cachedUint8ArrayMemory0 = null;
wasm.__wbindgen_start();
return wasm;
}
function initSync(module) {
if (wasm !== undefined) return wasm;
if (typeof module !== "undefined" && Object.getPrototypeOf(module) === Object.prototype) ({ module } = module);
else console.warn("using deprecated parameters for `initSync()`; pass a single object instead");
if (typeof module !== "undefined") {
if (Object.getPrototypeOf(module) === Object.prototype) {
({ module } = module);
} else {
console.warn("using deprecated parameters for `initSync()`; pass a single object instead");
}
}
const imports = __wbg_get_imports();
@ -667,9 +593,13 @@ function initSync(module) {
async function __wbg_init(module_or_path) {
if (wasm !== undefined) return wasm;
if (typeof module_or_path !== "undefined" && Object.getPrototypeOf(module_or_path) === Object.prototype)
({ module_or_path } = module_or_path);
else console.warn("using deprecated parameters for the initialization function; pass a single object instead");
if (typeof module_or_path !== "undefined") {
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
({ module_or_path } = module_or_path);
} else {
console.warn("using deprecated parameters for the initialization function; pass a single object instead");
}
}
if (typeof module_or_path === "undefined") {
module_or_path = new URL("system_wasm_bg.wasm", import.meta.url);

View File

@ -1,20 +1,23 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export function diff_filters(a: number, b: number, c: number): void;
export function expand_filter(a: number, b: number): void;
export function get_diff(a: number, b: number, c: number): void;
export function flat_merge(a: number, b: number): void;
export function compress(a: number, b: number): void;
export function pow(a: number, b: number, c: number): void;
export function schnorr_verify(a: number, b: number, c: number, d: number): void;
export function schnorr_verify_event(a: number, b: number): void;
export function rustsecp256k1_v0_10_0_context_create(a: number): number;
export function rustsecp256k1_v0_10_0_context_destroy(a: number): void;
export function rustsecp256k1_v0_10_0_default_illegal_callback_fn(a: number, b: number): void;
export function rustsecp256k1_v0_10_0_default_error_callback_fn(a: number, b: number): void;
export function __wbindgen_malloc(a: number, b: number): number;
export function __wbindgen_realloc(a: number, b: number, c: number, d: number): number;
export function __wbindgen_add_to_stack_pointer(a: number): number;
export function __wbindgen_exn_store(a: number): void;
export function __wbindgen_free(a: number, b: number, c: number): void;
export const diff_filters: (a: any, b: any) => [number, number, number];
export const expand_filter: (a: any) => [number, number, number];
export const get_diff: (a: any, b: any) => [number, number, number];
export const flat_merge: (a: any) => [number, number, number];
export const compress: (a: any) => [number, number, number];
export const pow: (a: any, b: any) => [number, number, number];
export const schnorr_verify: (a: any, b: any, c: any) => [number, number, number];
export const schnorr_verify_event: (a: any) => [number, number, number];
export const rustsecp256k1_v0_10_0_context_create: (a: number) => number;
export const rustsecp256k1_v0_10_0_context_destroy: (a: number) => void;
export const rustsecp256k1_v0_10_0_default_illegal_callback_fn: (a: number, b: number) => void;
export const rustsecp256k1_v0_10_0_default_error_callback_fn: (a: number, b: number) => void;
export const __wbindgen_exn_store: (a: number) => void;
export const __externref_table_alloc: () => number;
export const __wbindgen_export_2: WebAssembly.Table;
export const __wbindgen_free: (a: number, b: number, c: number) => void;
export const __wbindgen_malloc: (a: number, b: number) => number;
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
export const __externref_table_dealloc: (a: number) => void;
export const __wbindgen_start: () => void;

View File

@ -1,9 +1,9 @@
extern crate console_error_panic_hook;
use secp256k1::{Message, XOnlyPublicKey, SECP256K1};
use crate::filter::{FlatReqFilter, ReqFilter};
use secp256k1::{XOnlyPublicKey, SECP256K1};
use serde::{Deserialize, Serialize};
use serde_json::json;
use crate::filter::{FlatReqFilter, ReqFilter};
use wasm_bindgen::prelude::*;
pub mod diff;
@ -96,10 +96,11 @@ pub fn schnorr_verify(hash: JsValue, sig: JsValue, pub_key: JsValue) -> Result<b
let sig_hex: String = serde_wasm_bindgen::from_value(sig)?;
let pub_key_hex: String = serde_wasm_bindgen::from_value(pub_key)?;
let msg = Message::from_digest_slice(&hex::decode(msg_hex).unwrap()).unwrap();
let key = XOnlyPublicKey::from_slice(&hex::decode(pub_key_hex).unwrap()).unwrap();
let sig = secp256k1::schnorr::Signature::from_slice(&hex::decode(sig_hex).unwrap()).unwrap();
Ok(SECP256K1.verify_schnorr(&sig, &msg, &key).is_ok())
Ok(SECP256K1
.verify_schnorr(&sig, &hex::decode(msg_hex).unwrap(), &key)
.is_ok())
}
#[wasm_bindgen]
@ -107,13 +108,23 @@ pub fn schnorr_verify_event(event: JsValue) -> Result<bool, JsValue> {
console_error_panic_hook::set_once();
let event_obj: Event = serde_wasm_bindgen::from_value(event)?;
let json = json!([0, event_obj.pubkey, event_obj.created_at, event_obj.kind, event_obj.tags, event_obj.content]);
let json = json!([
0,
event_obj.pubkey,
event_obj.created_at,
event_obj.kind,
event_obj.tags,
event_obj.content
]);
let id = sha256::digest(json.to_string().as_bytes());
let msg = Message::from_digest_slice(&hex::decode(id).unwrap()).unwrap();
let key = XOnlyPublicKey::from_slice(&hex::decode(&event_obj.pubkey).unwrap()).unwrap();
let sig = secp256k1::schnorr::Signature::from_slice(&hex::decode(&event_obj.sig.unwrap()).unwrap()).unwrap();
Ok(SECP256K1.verify_schnorr(&sig, &msg, &key).is_ok())
let sig =
secp256k1::schnorr::Signature::from_slice(&hex::decode(&event_obj.sig.unwrap()).unwrap())
.unwrap();
Ok(SECP256K1
.verify_schnorr(&sig, &hex::decode(id).unwrap(), &key)
.is_ok())
}
#[cfg(test)]
mod tests {

View File

@ -1,6 +1,6 @@
{
"name": "@snort/system",
"version": "1.6.0",
"version": "1.6.1",
"description": "Snort nostr system package",
"type": "module",
"main": "dist/index.js",

View File

@ -7,8 +7,8 @@ export class UserProfileCache extends FeedCache<CachedMetadata> {
constructor(table?: DexieTableLike<CachedMetadata>) {
super("UserCache", table);
this.#processZapperQueue();
this.#processNip5Queue();
//this.#processZapperQueue();
//this.#processNip5Queue();
}
key(of: CachedMetadata): string {

View File

@ -12,12 +12,16 @@ const enum EventKind {
SimpleChatMessage = 9, // NIP-29
SealedRumor = 13, // NIP-59
ChatRumor = 14, // NIP-24
Photo = 20, // NIP-68
Video = 21, // NIP-71
ShortVideo = 22, // NIP-71
PublicChatChannel = 40, // NIP-28
PublicChatMetadata = 41, // NIP-28
PublicChatMessage = 42, // NIP-28
PublicChatMuteMessage = 43, // NIP-28
PublicChatMuteUser = 44, // NIP-28
SnortSubscriptions = 1000, // NIP-XX
Comment = 1111, // NIP-22
Polls = 6969, // NIP-69
GiftWrap = 1059, // NIP-59
FileHeader = 1063, // NIP-94
@ -34,6 +38,7 @@ const enum EventKind {
SearchRelaysList = 10_007, // NIP-51
InterestsList = 10_015, // NIP-51
EmojisList = 10_030, // NIP-51
BlossomServerList = 10_063,
StorageServerList = 10_096, // NIP-96 server list
FollowSet = 30_000, // NIP-51
@ -42,15 +47,18 @@ const enum EventKind {
CurationSet = 30_004, // NIP-51
InterestSet = 30_015, // NIP-15
EmojiSet = 30_030, // NIP-51
StarterPackSet = 39_089, // NIP-51
Badge = 30009, // NIP-58
ProfileBadges = 30008, // NIP-58
LongFormTextNote = 30023, // NIP-23
AppData = 30_078, // NIP-78
LiveEvent = 30311, // NIP-102
LiveEvent = 30311, // NIP-53
LiveEventChat = 1311, // NIP-53
UserStatus = 30315, // NIP-38
ZapstrTrack = 31337,
ApplicationHandler = 31_990,
SimpleChatMetadata = 39_000, // NIP-29
ZapRequest = 9734, // NIP 57
ZapReceipt = 9735, // NIP 57

View File

@ -25,6 +25,7 @@ import { EventBuilder } from "./event-builder";
import { findTag } from "./utils";
import { Nip7Signer } from "./impl/nip7";
import { Nip10 } from "./impl/nip10";
import { Nip22 } from "./impl/nip22";
type EventBuilderHook = (ev: EventBuilder) => EventBuilder;
@ -195,12 +196,19 @@ export class EventPublisher {
/**
* Reply to a note
*
* Replies to kind 1 notes are kind 1, otherwise kind 1111
*/
async reply(replyTo: TaggedNostrEvent, msg: string, fnExtra?: EventBuilderHook) {
const eb = this.#eb(EventKind.TextNote);
const kind = replyTo.kind === EventKind.TextNote ? EventKind.TextNote : EventKind.Comment;
const eb = this.#eb(kind);
eb.content(msg);
Nip10.replyTo(replyTo, eb);
if (kind === EventKind.TextNote) {
Nip10.replyTo(replyTo, eb);
} else {
Nip22.replyTo(replyTo, eb);
}
eb.processContent();
fnExtra?.(eb);
return await this.#sign(eb);
@ -211,6 +219,7 @@ export class EventPublisher {
eb.content(content);
eb.tag(unwrap(NostrLink.fromEvent(evRef).toEventTag()));
eb.tag(["p", evRef.pubkey]);
eb.tag(["k", evRef.kind.toString()]);
return await this.#sign(eb);
}

View File

@ -0,0 +1,35 @@
import { findTag } from "../utils";
import { EventBuilder, NostrEvent, NostrLink, NostrPrefix } from "../index";
export class Nip22 {
/**
* Get the root scope tag (E/A/I) or
* create a root scope tag from the provided event
*/
static rootScopeOf(other: NostrEvent) {
const linkOther = NostrLink.fromEvent(other);
return other.tags.find(t => ["E", "A", "I"].includes(t[0])) ?? linkOther.toEventTagNip22(true)!;
}
static replyTo(other: NostrEvent, eb: EventBuilder) {
const linkOther = NostrLink.fromEvent(other);
const rootScope = Nip22.rootScopeOf(other);
const rootKind = ["K", findTag(other, "K") ?? other.kind.toString()];
const rootAuthor = ["P", findTag(other, "P") ?? other.pubkey];
const replyScope = linkOther.toEventTagNip22(false);
const replyKind = ["k", other.kind.toString()];
const replyAuthor = ["p", other.pubkey];
if (rootScope === undefined || replyScope === undefined) {
throw new Error("RootScope or ReplyScope are undefined!");
}
eb.tag(rootScope);
eb.tag(rootKind);
eb.tag(rootAuthor);
eb.tag(replyScope);
eb.tag(replyKind);
eb.tag(replyAuthor);
}
}

View File

@ -0,0 +1,42 @@
import { Nip94Tags, readNip94Tags } from "./nip94";
/**
* Read NIP-94 tags from `imeta` tag
*/
export function readNip94TagsFromIMeta(tag: Array<string>) {
const asTags = tag.slice(1).map(a => a.split(" ", 2));
return readNip94Tags(asTags);
}
export function nip94TagsToIMeta(meta: Nip94Tags) {
if (!meta.url) {
throw new Error("URL is required!");
}
const ret: Array<string> = ["imeta"];
const ifPush = (key: string, value?: string | number) => {
if (value) {
ret.push(`${key} ${value}`);
}
};
ifPush("url", meta.url);
ifPush("m", meta.mimeType);
ifPush("x", meta.hash);
ifPush("ox", meta.originalHash);
ifPush("size", meta.size);
ifPush("dim", meta.dimensions?.join("x"));
ifPush("magnet", meta.magnet);
ifPush("blurhash", meta.blurHash);
ifPush("thumb", meta.thumb);
ifPush("summary", meta.summary);
ifPush("alt", meta.alt);
ifPush("duration", meta.duration);
ifPush("bitrate", meta.bitrate);
if (meta.image) {
meta.image.forEach(a => ifPush("image", a));
}
if (meta.fallback) {
meta.fallback.forEach(a => ifPush("fallback", a));
}
return ret;
}

View File

@ -0,0 +1,112 @@
import { FileExtensionRegex } from "../const";
export interface Nip94Tags {
url?: string;
mimeType?: string;
hash?: string;
originalHash?: string;
size?: number;
dimensions?: [number, number];
magnet?: string;
blurHash?: string;
thumb?: string;
image?: Array<string>;
summary?: string;
alt?: string;
fallback?: Array<string>;
duration?: number;
bitrate?: number;
}
/**
* Read NIP-94 tags from event tags
*/
export function readNip94Tags(tags: Array<Array<string>>) {
const res: Nip94Tags = {};
for (const tx of tags) {
const [k, v] = tx;
switch (k) {
case "url": {
res.url = v;
break;
}
case "m": {
res.mimeType = v;
break;
}
case "x": {
res.hash = v;
break;
}
case "ox": {
res.originalHash = v;
break;
}
case "size": {
res.size = Number(v);
break;
}
case "dim": {
res.dimensions = v.split("x").map(Number) as [number, number];
break;
}
case "magnet": {
res.magnet = v;
break;
}
case "blurhash": {
res.blurHash = v;
break;
}
case "thumb": {
res.thumb = v;
break;
}
case "image": {
res.image ??= [];
res.image.push(v);
break;
}
case "summary": {
res.summary = v;
break;
}
case "alt": {
res.alt = v;
break;
}
case "fallback": {
res.fallback ??= [];
res.fallback.push(v);
break;
}
case "duration": {
res.duration = Number(v);
break;
}
case "bitrate": {
res.bitrate = Number(v);
break;
}
}
}
return res;
}
export function addExtensionToNip94Url(meta: Nip94Tags) {
if (!meta.url?.match(FileExtensionRegex) && meta.mimeType) {
switch (meta.mimeType) {
case "image/webp": {
return `${meta.url}.webp`;
}
case "image/jpeg":
case "image/jpg": {
return `${meta.url}.jpg`;
}
case "video/mp4": {
return `${meta.url}.mp4`;
}
}
}
return meta.url;
}

View File

@ -30,10 +30,13 @@ export * from "./encryption";
export * from "./impl/nip4";
export * from "./impl/nip7";
export * from "./impl/nip10";
export * from "./impl/nip22";
export * from "./impl/nip44";
export * from "./impl/nip46";
export * from "./impl/nip57";
export * from "./impl/nip55";
export * from "./impl/nip94";
export * from "./impl/nip92";
export * from "./cache/index";
export * from "./cache/user-relays";

View File

@ -14,7 +14,6 @@ export enum NostrPrefix {
Address = "naddr",
Req = "nreq",
Chat17 = "nchat17",
Chat28 = "nchat28",
}
export enum TLVEntryType {

View File

@ -55,7 +55,7 @@ export class NostrLink implements ToNostrEventTag {
readonly marker?: string,
) {
if (type !== NostrPrefix.Address && !isHex(id)) {
throw new Error("ID must be hex");
throw new Error(`ID must be hex: ${JSON.stringify(id)}`);
}
}
@ -114,6 +114,32 @@ export class NostrLink implements ToNostrEventTag {
}
}
/**
* Create an event tag from this link as per NIP-22 (no marker position)
*/
toEventTagNip22(root?: boolean) {
// emulate root flag by root marker
root ??= this.marker === "root";
const suffix: Array<string> = [];
if (this.relays && this.relays.length > 0) {
suffix.push(this.relays[0]);
}
if (this.type === NostrPrefix.PublicKey || this.type === NostrPrefix.Profile) {
return [root ? "P" : "p", this.id, ...suffix];
} else if (this.type === NostrPrefix.Note || this.type === NostrPrefix.Event) {
if (this.author) {
if (suffix[0] === undefined) {
suffix.push(""); // empty relay hint
}
suffix.push(this.author);
}
return [root ? "E" : "e", this.id, ...suffix];
} else if (this.type === NostrPrefix.Address) {
return [root ? "A" : "a", `${this.kind}:${this.author}:${this.id}`, ...suffix];
}
}
matchesEvent(ev: NostrEvent) {
if (this.type === NostrPrefix.Address) {
const dTag = findTag(ev, "d");
@ -124,7 +150,11 @@ export class NostrLink implements ToNostrEventTag {
const ifSetCheck = <T>(a: T | undefined, b: T) => {
return !Boolean(a) || a === b;
};
return ifSetCheck(this.id, ev.id) && ifSetCheck(this.author, ev.pubkey) && ifSetCheck(this.kind, ev.kind);
return (
(EventExt.isReplaceable(ev.kind) || ifSetCheck(this.id, ev.id)) &&
ifSetCheck(this.author, ev.pubkey) &&
ifSetCheck(this.kind, ev.kind)
);
}
return false;
@ -194,20 +224,41 @@ export class NostrLink implements ToNostrEventTag {
static fromTag(tag: Array<string>, author?: string, kind?: number) {
const relays = tag.length > 2 ? [tag[2]] : undefined;
switch (tag[0]) {
case "E": {
return new NostrLink(NostrPrefix.Event, tag[1], kind, author ?? tag[3], relays, "root");
}
case "e": {
return new NostrLink(NostrPrefix.Event, tag[1], kind, author ?? tag[4], relays, tag[3]);
}
case "p": {
return new NostrLink(NostrPrefix.Profile, tag[1], kind, author, relays);
}
case "A": {
const [kind, author, dTag] = tag[1].split(":");
if (!isHex(author)) {
throw new Error(`Invalid author in A tag: ${tag[1]}`);
}
return new NostrLink(NostrPrefix.Address, dTag, Number(kind), author, relays, "root");
}
case "a": {
const [kind, author, dTag] = tag[1].split(":");
if (!isHex(author)) {
throw new Error(`Invalid author in a tag: ${tag[1]}`);
}
return new NostrLink(NostrPrefix.Address, dTag, Number(kind), author, relays, tag[3]);
}
}
throw new Error("Unknown tag!");
}
static tryFromTag(tag: Array<string>, author?: string, kind?: number) {
try {
return NostrLink.fromTag(tag, author, kind);
} catch (e) {
// ignored
}
}
static fromTags(tags: ReadonlyArray<Array<string>>) {
return removeUndefined(
tags.map(a => {
@ -242,7 +293,7 @@ export class NostrLink implements ToNostrEventTag {
let relays = "relays" in ev ? ev.relays : undefined;
const eventRelays = removeUndefined(
ev.tags
.filter(a => a[0] === "relays" || a[0] === "relay" || a[0] === "r")
.filter(a => a[0] === "relays" || a[0] === "relay" || (a[0] === "r" && ev.kind == EventKind.Relays))
.flatMap(a => a.slice(1).map(b => sanitizeRelayUrl(b))),
);
relays = appendDedupe(relays, eventRelays);

View File

@ -14,7 +14,7 @@ export interface TaggedNostrEvent extends NostrEvent {
/**
* A list of relays this event was seen on
*/
relays: Array<string>;
relays?: Array<string>;
/**
* Additional context

Some files were not shown because too many files have changed in this diff Show More